Files
leaudit-platform-frontend/app/components/dify-dataset-manager/area-dataset-config.tsx
T
PingChuan 5bee9288b9 feat:替换 Dify 为自建 RAG去实现
1、修复了若干无权限时的失败提示语
2、新增了一个生成后续建议问题的功能
3、重构了知识问答部分的权限管理模块
4、修复了若干渲染不恰当的样式渲染
2026-04-10 16:20:32 +08:00

594 lines
16 KiB
TypeScript

/**
* 知识库配置管理组件
*
* 提供地区-知识库绑定管理功能,包括增删改查
*
* @author 开发团队
* @version 1.0.0
*/
import { useEffect } from 'react';
import {
Card,
Table,
Button,
Tag,
Space,
Modal,
Form,
Input,
Select,
Switch,
InputNumber,
message,
Flex,
Typography,
Popconfirm,
Tooltip,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
InfoCircleOutlined,
CheckCircleOutlined,
} from '@ant-design/icons';
import { useAreaDatasetConfig } from '~/hooks/use-area-dataset-config';
import { usePermission } from '~/hooks/usePermission';
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中
// 筛选 - 多选
filterAreas,
setFilterAreas,
page,
setPage,
pageSize,
// 表单状态
modalVisible,
setModalVisible,
editingId,
setEditingId,
submitLoading,
// 操作方法
loadDatasets,
loadAreas,
handleCreate,
handleUpdate,
handleDelete,
// 权限
canManageDataset,
} = useAreaDatasetConfig();
const { userRole: rawUserRole, userArea: rawUserArea } = usePermission();
const isProvincialAdmin = rawUserRole === 'provincial_admin';
// 内部状态
const [form] = Form.useForm();
// ==================== Effects ====================
// 当编辑的ID变化时,加载表单数据
useEffect(() => {
if (editingId && modalVisible) {
const record = datasets.find((item) => item.id === editingId);
if (record) {
form.setFieldsValue({
area: record.area,
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();
// 非省级管理员自动填充地区
if (!isProvincialAdmin && rawUserArea) {
form.setFieldValue('area', rawUserArea);
}
}
}, [editingId, modalVisible, datasets, form]);
// ==================== Event Handlers ====================
/**
* 处理新建按钮点击
*/
const handleCreateClick = () => {
if (!canManageDataset) {
message.error('您没有创建知识库的权限');
return;
}
setEditingId(null);
setModalVisible(true);
form.resetFields();
};
/**
* 处理编辑按钮点击
*/
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) => {
// 编辑时检查 is_default 是否从 false 变为 true
if (editingId && values.is_default) {
const record = datasets.find((item) => item.id === editingId);
if (record && !record.is_default) {
Modal.confirm({
title: '切换默认知识库',
content: '确认将此知识库设为默认?该地区的对话助手将自动绑定此知识库进行问答。',
okText: '确认',
cancelText: '取消',
onOk: () => doSubmit(values),
});
return;
}
}
await doSubmit(values);
};
const doSubmit = 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 = (values: string[]) => {
setFilterAreas(values);
setPage(1); // 重置到第一页
};
// ==================== Render ====================
// 用户角色已经在 hook 中处理好了,直接使用 userRole
const userRoleLabel = 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,
align: 'center',
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,
align: 'center',
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: '#00684a' }}>
</Tag>
)}
</Space>
),
},
{
title: '排序',
dataIndex: 'sort_order',
key: 'sort_order',
width: 170,
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) => {
// 市级管理员只能编辑自己地区的知识库
const canEdit = isProvincialAdmin || record.area === rawUserArea;
return (
<Space size="small">
{canEdit && (
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => handleEditClick(record)}
>
</Button>
)}
{isProvincialAdmin && (
<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
mode="multiple"
style={{ minWidth: '200px', maxWidth: '400px' }}
placeholder="请选择地区(可多选)"
allowClear
value={filterAreas}
onChange={handleAreaFilterChange}
maxTagCount={3}
maxTagPlaceholder={(omittedValues) => `+${omittedValues.length}`}
options={Array.isArray(areas) ? 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={
isProvincialAdmin
? (Array.isArray(areas) ? areas.map((area) => ({ label: area, value: area })) : [])
: (rawUserArea ? [{ label: rawUserArea, value: rawUserArea }] : [])
}
/>
</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>
);
}