27aff59152
新增地区-知识库绑定管理功能,支持增删改查操作 - 添加 V3 API 路由层:area-datasets 相关接口 - 添加 API 客户端:area-datasets.ts - 添加自定义 Hook:use-area-dataset-config.ts - 添加管理组件:area-dataset-config.tsx - 修复路由冲突问题,删除重复的 .ts 路由文件 - 更新 dataset-manager 页面,添加 Tabs 导航 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
663 lines
18 KiB
TypeScript
663 lines
18 KiB
TypeScript
/**
|
|
* 知识库配置管理组件
|
|
*
|
|
* 提供地区-知识库绑定管理功能,包括增删改查
|
|
*
|
|
* @author 开发团队
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import {
|
|
Card,
|
|
Table,
|
|
Button,
|
|
Tag,
|
|
Space,
|
|
Modal,
|
|
Form,
|
|
Input,
|
|
Select,
|
|
Switch,
|
|
InputNumber,
|
|
message,
|
|
Flex,
|
|
Typography,
|
|
Popconfirm,
|
|
Spin,
|
|
Tooltip,
|
|
} from 'antd';
|
|
import {
|
|
PlusOutlined,
|
|
EditOutlined,
|
|
DeleteOutlined,
|
|
InfoCircleOutlined,
|
|
CheckCircleOutlined,
|
|
} from '@ant-design/icons';
|
|
import { useAreaDatasetConfig } from '~/hooks/use-area-dataset-config';
|
|
import { fetchDatasets } from '~/api/dify-dataset/api/datasetApi';
|
|
import type { Dataset as DifyDataset } from '~/api/dify-dataset/type';
|
|
import type { AreaDataset } from '~/api/v3/dify/area-datasets';
|
|
|
|
const { Title, Text } = Typography;
|
|
|
|
// 颜色常量
|
|
const colors = {
|
|
bgLayout: '#f5f5f5',
|
|
border: '#e8e8e8',
|
|
text: '#262626',
|
|
textSecondary: '#8c8c8c',
|
|
primary: '#00684a',
|
|
};
|
|
|
|
/**
|
|
* 知识库配置管理组件
|
|
*/
|
|
export default function AreaDatasetConfig() {
|
|
// 使用自定义hook获取数据和操作方法
|
|
const {
|
|
// 数据
|
|
datasets,
|
|
loading,
|
|
total,
|
|
userArea,
|
|
userRole,
|
|
areas,
|
|
// areasLoading, // 地区列表已加载hook中
|
|
|
|
// 筛选
|
|
filterArea,
|
|
setFilterArea,
|
|
page,
|
|
setPage,
|
|
pageSize,
|
|
|
|
// 表单状态
|
|
modalVisible,
|
|
setModalVisible,
|
|
editingId,
|
|
setEditingId,
|
|
submitLoading,
|
|
|
|
// 操作方法
|
|
loadDatasets,
|
|
loadAreas,
|
|
handleCreate,
|
|
handleUpdate,
|
|
handleDelete,
|
|
|
|
// 权限
|
|
canManageDataset,
|
|
} = useAreaDatasetConfig();
|
|
|
|
// 内部状态
|
|
const [form] = Form.useForm();
|
|
const [difyDatasets, setDifyDatasets] = useState<DifyDataset[]>([]);
|
|
const [difyDatasetsLoading, setDifyDatasetsLoading] = useState<boolean>(false);
|
|
const [difyDatasetsTotal, setDifyDatasetsTotal] = useState<number>(0);
|
|
const [difyDatasetsPage, setDifyDatasetsPage] = useState<number>(1);
|
|
const [isLoadingDifyDatasets, setIsLoadingDifyDatasets] = useState<boolean>(false);
|
|
|
|
// ==================== Effects ====================
|
|
|
|
// 当编辑的ID变化时,加载表单数据
|
|
useEffect(() => {
|
|
if (editingId && modalVisible) {
|
|
const record = datasets.find((item) => item.id === editingId);
|
|
if (record) {
|
|
form.setFieldsValue({
|
|
area: record.area,
|
|
dataset_id: record.dataset_id,
|
|
dataset_name: record.dataset_name,
|
|
dataset_description: record.dataset_description,
|
|
is_public: record.is_public,
|
|
is_default: record.is_default,
|
|
sort_order: record.sort_order,
|
|
});
|
|
}
|
|
} else if (!editingId && modalVisible) {
|
|
// 新增时重置表单
|
|
form.resetFields();
|
|
loadDifyDatasets(); // 加载Dify知识库列表
|
|
}
|
|
}, [editingId, modalVisible, datasets, form]);
|
|
|
|
// ==================== Dify Datasets Loading ====================
|
|
|
|
/**
|
|
* 从Dify API加载知识库列表
|
|
*/
|
|
const loadDifyDatasets = async (pageNum: number = 1) => {
|
|
if (isLoadingDifyDatasets) return;
|
|
|
|
setIsLoadingDifyDatasets(true);
|
|
try {
|
|
const response = await fetchDatasets(pageNum, 20);
|
|
setDifyDatasets(response.data);
|
|
setDifyDatasetsTotal(response.total);
|
|
setDifyDatasetsPage(pageNum);
|
|
setDifyDatasetsLoading(false);
|
|
} catch (error: any) {
|
|
console.error('加载Dify知识库列表失败:', error);
|
|
message.error('加载Dify知识库列表失败');
|
|
setDifyDatasetsLoading(false);
|
|
} finally {
|
|
setIsLoadingDifyDatasets(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Dify数据集选择器滚动加载
|
|
*/
|
|
const handleDatasetSelectScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
|
const { target } = e;
|
|
const { scrollTop, scrollHeight, clientHeight } = target as HTMLDivElement;
|
|
|
|
// 滚动到底部且还有更多数据时加载下一页
|
|
if (scrollHeight - scrollTop === clientHeight &&
|
|
difyDatasets.length < difyDatasetsTotal &&
|
|
!difyDatasetsLoading) {
|
|
loadDifyDatasets(difyDatasetsPage + 1);
|
|
}
|
|
};
|
|
|
|
// ==================== Event Handlers ====================
|
|
|
|
/**
|
|
* 处理新建按钮点击
|
|
*/
|
|
const handleCreateClick = () => {
|
|
if (!canManageDataset) {
|
|
message.error('您没有创建知识库绑定的权限');
|
|
return;
|
|
}
|
|
setEditingId(null);
|
|
setModalVisible(true);
|
|
form.resetFields();
|
|
loadDifyDatasets();
|
|
};
|
|
|
|
/**
|
|
* 处理编辑按钮点击
|
|
*/
|
|
const handleEditClick = (record: AreaDataset) => {
|
|
if (!canManageDataset) {
|
|
message.error('您没有编辑知识库绑定的权限');
|
|
return;
|
|
}
|
|
setEditingId(record.id);
|
|
setModalVisible(true);
|
|
};
|
|
|
|
/**
|
|
* 处理删除按钮点击
|
|
*/
|
|
const handleDeleteClick = async (id: number) => {
|
|
if (!canManageDataset) {
|
|
message.error('您没有删除知识库绑定的权限');
|
|
return;
|
|
}
|
|
|
|
const success = await handleDelete(id);
|
|
if (success) {
|
|
message.success('删除成功');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 处理表单提交
|
|
*/
|
|
const handleFormSubmit = async (values: any) => {
|
|
let success = false;
|
|
|
|
if (editingId) {
|
|
success = await handleUpdate(editingId, values);
|
|
} else {
|
|
success = await handleCreate(values);
|
|
}
|
|
|
|
if (success) {
|
|
setModalVisible(false);
|
|
form.resetFields();
|
|
setEditingId(null);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 处理表单取消
|
|
*/
|
|
const handleFormCancel = () => {
|
|
setModalVisible(false);
|
|
form.resetFields();
|
|
setEditingId(null);
|
|
};
|
|
|
|
/**
|
|
* 处理地区筛选变化
|
|
*/
|
|
const handleAreaFilterChange = (value: string) => {
|
|
setFilterArea(value);
|
|
setPage(1); // 重置到第一页
|
|
};
|
|
|
|
// ==================== Render ====================
|
|
|
|
// 计算用户角色标签
|
|
const userRoleLabel = (() => {
|
|
const labels: Record<string, string> = {
|
|
common: '普通用户',
|
|
admin: '市级管理员',
|
|
provincial_admin: '省级管理员',
|
|
};
|
|
return labels[userRole] || '未知角色';
|
|
})();
|
|
|
|
// 表格列定义
|
|
const columns = [
|
|
{
|
|
title: '序号',
|
|
key: 'index',
|
|
width: 60,
|
|
render: (_: any, __: any, index: number) =>
|
|
(page - 1) * pageSize + index + 1,
|
|
},
|
|
{
|
|
title: '地区',
|
|
dataIndex: 'area',
|
|
key: 'area',
|
|
width: 100,
|
|
render: (area: string) => (
|
|
<Tag color={area === '省级' ? 'gold' : 'blue'}>{area}</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '知识库名称',
|
|
dataIndex: 'dataset_name',
|
|
key: 'dataset_name',
|
|
width: 200,
|
|
ellipsis: true,
|
|
render: (text: string) => (
|
|
<Tooltip title={text}>
|
|
<Text style={{ color: colors.text }} strong>
|
|
{text}
|
|
</Text>
|
|
</Tooltip>
|
|
),
|
|
},
|
|
{
|
|
title: '知识库ID',
|
|
dataIndex: 'dataset_id',
|
|
key: 'dataset_id',
|
|
width: 200,
|
|
ellipsis: true,
|
|
render: (text: string) => (
|
|
<Tooltip title={text}>
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
{text.substring(0, 8)}...{text.substring(text.length - 4)}
|
|
</Text>
|
|
</Tooltip>
|
|
),
|
|
},
|
|
{
|
|
title: '描述',
|
|
dataIndex: 'dataset_description',
|
|
key: 'dataset_description',
|
|
ellipsis: true,
|
|
render: (text: string) =>
|
|
text ? (
|
|
<Tooltip title={text}>
|
|
<Text style={{ color: colors.text }}>
|
|
{text.length > 30 ? text.substring(0, 30) + '...' : text}
|
|
</Text>
|
|
</Tooltip>
|
|
) : (
|
|
<Text type="secondary">-</Text>
|
|
),
|
|
},
|
|
{
|
|
title: '标签',
|
|
key: 'tags',
|
|
width: 120,
|
|
render: (_: any, record: AreaDataset) => (
|
|
<Space size="small">
|
|
{record.is_public && (
|
|
<Tag color="success" icon={<CheckCircleOutlined />}>
|
|
公共
|
|
</Tag>
|
|
)}
|
|
{record.is_default && (
|
|
<Tag color={colors.primary} style={{ color: '#fff' }}>
|
|
默认
|
|
</Tag>
|
|
)}
|
|
</Space>
|
|
),
|
|
},
|
|
{
|
|
title: '排序',
|
|
dataIndex: 'sort_order',
|
|
key: 'sort_order',
|
|
width: 70,
|
|
align: 'center' as const,
|
|
sorter: (a: AreaDataset, b: AreaDataset) => a.sort_order - b.sort_order,
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
key: 'status',
|
|
width: 80,
|
|
render: (status: number) => (
|
|
<Tag color={status === 1 ? 'success' : 'default'}>
|
|
{status === 1 ? '启用' : '禁用'}
|
|
</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '创建时间',
|
|
dataIndex: 'created_at',
|
|
key: 'created_at',
|
|
width: 150,
|
|
render: (text: string) => (
|
|
<Text style={{ color: colors.textSecondary, fontSize: '12px' }}>
|
|
{new Date(text).toLocaleString('zh-CN')}
|
|
</Text>
|
|
),
|
|
},
|
|
// 操作列(仅管理员可见)
|
|
...(canManageDataset
|
|
? [
|
|
{
|
|
title: '操作',
|
|
key: 'actions',
|
|
width: 120,
|
|
fixed: 'right' as const,
|
|
render: (_: any, record: AreaDataset) => (
|
|
<Space size="small">
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
icon={<EditOutlined />}
|
|
onClick={() => handleEditClick(record)}
|
|
>
|
|
编辑
|
|
</Button>
|
|
<Popconfirm
|
|
title="确定删除?"
|
|
description="删除后该地区的用户将无法访问此知识库"
|
|
onConfirm={() => handleDeleteClick(record.id)}
|
|
okText="确定"
|
|
cancelText="取消"
|
|
>
|
|
<Button type="link" danger size="small" icon={<DeleteOutlined />}>
|
|
删除
|
|
</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
]
|
|
: []),
|
|
];
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
height: '100%',
|
|
background: colors.bgLayout,
|
|
padding: '16px',
|
|
}}
|
|
>
|
|
{/* 页面头部 */}
|
|
<Card
|
|
style={{
|
|
marginBottom: '16px',
|
|
borderRadius: '8px',
|
|
}}
|
|
>
|
|
<Flex justify="space-between" align="center">
|
|
<div>
|
|
<Title level={4} style={{ margin: 0, marginBottom: '8px', color: colors.text }}>
|
|
知识库配置管理
|
|
</Title>
|
|
<Flex gap="16px" align="center">
|
|
<Text type="secondary">
|
|
地区: <Text strong>{userArea || '-'}</Text>
|
|
</Text>
|
|
<Text type="secondary">
|
|
角色: <Text strong>{userRoleLabel}</Text>
|
|
</Text>
|
|
<Text type="secondary">
|
|
总数: <Text strong>{total}</Text>
|
|
</Text>
|
|
</Flex>
|
|
</div>
|
|
|
|
{/* 仅管理员显示新增按钮 */}
|
|
{canManageDataset && (
|
|
<Button
|
|
type="primary"
|
|
icon={<PlusOutlined />}
|
|
onClick={handleCreateClick}
|
|
>
|
|
新增绑定
|
|
</Button>
|
|
)}
|
|
</Flex>
|
|
</Card>
|
|
|
|
{/* 筛选区域(仅管理员可见) */}
|
|
{canManageDataset && (
|
|
<Card
|
|
style={{
|
|
marginBottom: '16px',
|
|
borderRadius: '8px',
|
|
}}
|
|
>
|
|
<Flex gap="16px" align="center">
|
|
<Text style={{ color: colors.text }}>地区筛选:</Text>
|
|
<Select
|
|
style={{ width: '150px' }}
|
|
placeholder="全部地区"
|
|
allowClear
|
|
value={filterArea || undefined}
|
|
onChange={handleAreaFilterChange}
|
|
options={[
|
|
{ label: '全部', value: '' },
|
|
{ label: '省级', value: '省级' },
|
|
...areas.map((area) => ({ label: area, value: area })),
|
|
]}
|
|
/>
|
|
</Flex>
|
|
</Card>
|
|
)}
|
|
|
|
{/* 数据表格 */}
|
|
<Card
|
|
style={{
|
|
borderRadius: '8px',
|
|
}}
|
|
>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={datasets}
|
|
loading={loading}
|
|
rowKey="id"
|
|
pagination={
|
|
canManageDataset
|
|
? {
|
|
current: page,
|
|
pageSize: pageSize,
|
|
total: total,
|
|
onChange: (newPage) => setPage(newPage),
|
|
showSizeChanger: false,
|
|
showQuickJumper: true,
|
|
showTotal: (total) => `共 ${total} 条`,
|
|
}
|
|
: false
|
|
}
|
|
scroll={{ x: 'max-content' }}
|
|
locale={{
|
|
emptyText: (
|
|
<Flex vertical align="center" justify="center" style={{ padding: '48px' }}>
|
|
<InfoCircleOutlined style={{ fontSize: '48px', color: colors.textSecondary }} />
|
|
<Text style={{ marginTop: '16px', color: colors.textSecondary }}>
|
|
暂无知识库数据
|
|
</Text>
|
|
</Flex>
|
|
),
|
|
}}
|
|
/>
|
|
</Card>
|
|
|
|
{/* 新增/编辑对话框 */}
|
|
<Modal
|
|
title={editingId ? '编辑知识库绑定' : '新增知识库绑定'}
|
|
open={modalVisible}
|
|
onOk={() => form.submit()}
|
|
onCancel={handleFormCancel}
|
|
confirmLoading={submitLoading}
|
|
width={600}
|
|
destroyOnHidden
|
|
>
|
|
<Form
|
|
form={form}
|
|
layout="vertical"
|
|
onFinish={handleFormSubmit}
|
|
initialValues={{
|
|
is_public: false,
|
|
is_default: false,
|
|
sort_order: 0,
|
|
}}
|
|
>
|
|
{/* 地区选择(仅新增时可选) */}
|
|
<Form.Item
|
|
name="area"
|
|
label="地区"
|
|
rules={[{ required: true, message: '请选择地区' }]}
|
|
>
|
|
<Select
|
|
placeholder="请选择地区"
|
|
disabled={!!editingId} // 编辑时禁用
|
|
options={areas.map((area) => ({
|
|
label: area,
|
|
value: area,
|
|
}))}
|
|
/>
|
|
</Form.Item>
|
|
|
|
{/* Dify知识库选择(仅新增时可选) */}
|
|
<Form.Item
|
|
name="dataset_id"
|
|
label="Dify知识库"
|
|
rules={[{ required: true, message: '请选择Dify知识库' }]}
|
|
>
|
|
<Select
|
|
placeholder="请选择或输入知识库ID"
|
|
disabled={!!editingId}
|
|
loading={difyDatasetsLoading}
|
|
onPopupScroll={handleDatasetSelectScroll}
|
|
dropdownRender={(menu) => (
|
|
<div>
|
|
{menu}
|
|
{difyDatasets.length < difyDatasetsTotal && (
|
|
<div style={{ textAlign: 'center', padding: '8px' }}>
|
|
<Spin size="small" />
|
|
<Text style={{ marginLeft: '8px' }} type="secondary">
|
|
加载中...
|
|
</Text>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
onDropdownVisibleChange={(open) => {
|
|
if (open && !editingId) {
|
|
loadDifyDatasets();
|
|
}
|
|
}}
|
|
options={difyDatasets.map((ds) => ({
|
|
label: (
|
|
<Flex vertical>
|
|
<Text strong>{ds.name}</Text>
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
ID: {ds.id}
|
|
</Text>
|
|
</Flex>
|
|
),
|
|
value: ds.id,
|
|
}))}
|
|
dropdownStyle={{ maxHeight: '300px' }}
|
|
/>
|
|
</Form.Item>
|
|
|
|
{/* 知识库名称 */}
|
|
<Form.Item
|
|
name="dataset_name"
|
|
label="知识库名称"
|
|
rules={[
|
|
{ required: true, message: '请输入知识库名称' },
|
|
{ max: 255, message: '最多255个字符' },
|
|
]}
|
|
>
|
|
<Input placeholder="请输入知识库名称" />
|
|
</Form.Item>
|
|
|
|
{/* 知识库描述 */}
|
|
<Form.Item
|
|
name="dataset_description"
|
|
label="知识库描述"
|
|
>
|
|
<Input.TextArea
|
|
placeholder="请输入知识库描述(可选)"
|
|
rows={3}
|
|
maxLength={500}
|
|
/>
|
|
</Form.Item>
|
|
|
|
{/* 高级设置折叠面板 */}
|
|
<div style={{ marginTop: '24px' }}>
|
|
<Text strong style={{ color: colors.text, display: 'block', marginBottom: '16px' }}>
|
|
高级设置
|
|
</Text>
|
|
|
|
<Flex gap="24px">
|
|
{/* 是否公开 */}
|
|
<Form.Item
|
|
name="is_public"
|
|
label="公开知识库"
|
|
valuePropName="checked"
|
|
tooltip="公开后所有地区的用户都可以访问此知识库"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
|
|
{/* 是否默认 */}
|
|
<Form.Item
|
|
name="is_default"
|
|
label="默认知识库"
|
|
valuePropName="checked"
|
|
tooltip="设置为该地区的默认知识库,用户优先使用"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
|
|
{/* 排序顺序 */}
|
|
<Form.Item
|
|
name="sort_order"
|
|
label="排序顺序"
|
|
tooltip="数值越小越靠前,范围0-100"
|
|
>
|
|
<InputNumber
|
|
min={0}
|
|
max={100}
|
|
style={{ width: '100%' }}
|
|
placeholder="0"
|
|
/>
|
|
</Form.Item>
|
|
</Flex>
|
|
</div>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|