922 lines
26 KiB
TypeScript
922 lines
26 KiB
TypeScript
/**
|
||
* IIT 项目配置详情页
|
||
*
|
||
* 包含4个Tab:基本配置、质控规则、用户映射、知识库
|
||
*/
|
||
|
||
import React, { useState, useEffect, useCallback } from 'react';
|
||
import { useParams, useNavigate } from 'react-router-dom';
|
||
import {
|
||
Card,
|
||
Tabs,
|
||
Button,
|
||
Form,
|
||
Input,
|
||
Select,
|
||
Table,
|
||
Modal,
|
||
message,
|
||
Popconfirm,
|
||
Typography,
|
||
Space,
|
||
Tag,
|
||
Spin,
|
||
Alert,
|
||
Descriptions,
|
||
Empty,
|
||
Tooltip,
|
||
Badge,
|
||
} from 'antd';
|
||
import {
|
||
ArrowLeftOutlined,
|
||
SaveOutlined,
|
||
PlusOutlined,
|
||
DeleteOutlined,
|
||
EditOutlined,
|
||
CheckCircleOutlined,
|
||
CloseCircleOutlined,
|
||
SyncOutlined,
|
||
LinkOutlined,
|
||
DisconnectOutlined,
|
||
ExclamationCircleOutlined,
|
||
BookOutlined,
|
||
ThunderboltOutlined,
|
||
BarChartOutlined,
|
||
DashboardOutlined,
|
||
} from '@ant-design/icons';
|
||
import * as iitProjectApi from '../api/iitProjectApi';
|
||
import type {
|
||
IitProject,
|
||
UpdateProjectRequest,
|
||
QCRule,
|
||
CreateRuleRequest,
|
||
IitUserMapping,
|
||
CreateUserMappingRequest,
|
||
RoleOption,
|
||
KnowledgeBaseOption,
|
||
} from '../types/iitProject';
|
||
|
||
const { Title, Text, Paragraph } = Typography;
|
||
const { TextArea } = Input;
|
||
|
||
// ==================== 常量定义 ====================
|
||
|
||
const SEVERITY_MAP = {
|
||
error: { color: 'error', text: '错误' },
|
||
warning: { color: 'warning', text: '警告' },
|
||
info: { color: 'processing', text: '信息' },
|
||
};
|
||
|
||
const CATEGORY_MAP = {
|
||
inclusion: { color: '#52c41a', text: '纳入标准' },
|
||
exclusion: { color: '#ff4d4f', text: '排除标准' },
|
||
lab_values: { color: '#1890ff', text: '变量范围' },
|
||
logic_check: { color: '#722ed1', text: '逻辑检查' },
|
||
};
|
||
|
||
// ==================== 主组件 ====================
|
||
|
||
const IitProjectDetailPage: React.FC = () => {
|
||
const { id } = useParams<{ id: string }>();
|
||
const navigate = useNavigate();
|
||
|
||
const [project, setProject] = useState<IitProject | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [activeTab, setActiveTab] = useState('basic');
|
||
const [batchQcLoading, setBatchQcLoading] = useState(false);
|
||
const [batchSummaryLoading, setBatchSummaryLoading] = useState(false);
|
||
|
||
// 加载项目详情
|
||
const loadProject = useCallback(async () => {
|
||
if (!id) return;
|
||
setLoading(true);
|
||
try {
|
||
const data = await iitProjectApi.getProject(id);
|
||
setProject(data);
|
||
} catch (error) {
|
||
message.error('加载项目详情失败');
|
||
navigate('/admin/iit-projects');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [id, navigate]);
|
||
|
||
useEffect(() => {
|
||
loadProject();
|
||
}, [loadProject]);
|
||
|
||
// ⭐ 一键全量质控
|
||
const handleBatchQc = async () => {
|
||
if (!id) return;
|
||
setBatchQcLoading(true);
|
||
try {
|
||
const result = await iitProjectApi.batchQualityCheck(id);
|
||
message.success(`质控完成!共 ${result.stats.totalRecords} 条记录,通过率 ${result.stats.passRate}`);
|
||
} catch (error: any) {
|
||
message.error(error.message || '质控失败');
|
||
} finally {
|
||
setBatchQcLoading(false);
|
||
}
|
||
};
|
||
|
||
// ⭐ 一键全量数据汇总
|
||
const handleBatchSummary = async () => {
|
||
if (!id) return;
|
||
setBatchSummaryLoading(true);
|
||
try {
|
||
const result = await iitProjectApi.batchSummary(id);
|
||
message.success(`汇总完成!共 ${result.stats.totalRecords} 条记录,平均完成率 ${result.stats.avgCompletionRate}`);
|
||
} catch (error: any) {
|
||
message.error(error.message || '汇总失败');
|
||
} finally {
|
||
setBatchSummaryLoading(false);
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 400 }}>
|
||
<Spin size="large" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!project) {
|
||
return <Empty description="项目不存在" />;
|
||
}
|
||
|
||
const tabItems = [
|
||
{
|
||
key: 'basic',
|
||
label: 'REDCap 配置',
|
||
children: <BasicConfigTab project={project} onUpdate={loadProject} />,
|
||
},
|
||
{
|
||
key: 'rules',
|
||
label: '质控规则',
|
||
children: <QCRulesTab projectId={project.id} />,
|
||
},
|
||
{
|
||
key: 'users',
|
||
label: '通知设置',
|
||
children: <UserMappingTab projectId={project.id} />,
|
||
},
|
||
{
|
||
key: 'kb',
|
||
label: '知识库',
|
||
children: <KnowledgeBaseTab project={project} onUpdate={loadProject} />,
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div style={{ padding: 24 }}>
|
||
{/* 页面标题 */}
|
||
<div style={{ marginBottom: 24 }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||
<Space align="center">
|
||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/admin/iit-projects')}>
|
||
返回列表
|
||
</Button>
|
||
<Title level={3} style={{ margin: 0 }}>
|
||
{project.name}
|
||
</Title>
|
||
<Badge status={project.status === 'active' ? 'success' : 'warning'} />
|
||
</Space>
|
||
{/* ⭐ 批量操作按钮 */}
|
||
<Space>
|
||
{/* ⭐ 质控全览图按钮 - 导航到驾驶舱页面 */}
|
||
<Button
|
||
type="primary"
|
||
icon={<DashboardOutlined />}
|
||
onClick={() => navigate(`/admin/iit-projects/${id}/cockpit`)}
|
||
style={{ background: '#722ed1' }}
|
||
>
|
||
质控全览图
|
||
</Button>
|
||
<Button
|
||
icon={<ThunderboltOutlined />}
|
||
loading={batchQcLoading}
|
||
onClick={handleBatchQc}
|
||
>
|
||
一键全量质控
|
||
</Button>
|
||
<Button
|
||
icon={<BarChartOutlined />}
|
||
loading={batchSummaryLoading}
|
||
onClick={handleBatchSummary}
|
||
>
|
||
一键全量汇总
|
||
</Button>
|
||
</Space>
|
||
</div>
|
||
{project.description && (
|
||
<Paragraph type="secondary" style={{ marginTop: 8, marginBottom: 0 }}>
|
||
{project.description}
|
||
</Paragraph>
|
||
)}
|
||
</div>
|
||
|
||
{/* Tab 页 */}
|
||
<Card>
|
||
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
|
||
</Card>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ==================== Tab 1: 基本配置 ====================
|
||
|
||
interface BasicConfigTabProps {
|
||
project: IitProject;
|
||
onUpdate: () => void;
|
||
}
|
||
|
||
const BasicConfigTab: React.FC<BasicConfigTabProps> = ({ project, onUpdate }) => {
|
||
const [form] = Form.useForm();
|
||
const [saving, setSaving] = useState(false);
|
||
const [testing, setTesting] = useState(false);
|
||
const [syncing, setSyncing] = useState(false);
|
||
|
||
useEffect(() => {
|
||
form.setFieldsValue({
|
||
name: project.name,
|
||
description: project.description,
|
||
redcapUrl: project.redcapUrl,
|
||
redcapProjectId: project.redcapProjectId,
|
||
redcapApiToken: project.redcapApiToken,
|
||
});
|
||
}, [form, project]);
|
||
|
||
const handleSave = async (values: UpdateProjectRequest) => {
|
||
setSaving(true);
|
||
try {
|
||
await iitProjectApi.updateProject(project.id, values);
|
||
message.success('保存成功');
|
||
onUpdate();
|
||
} catch (error) {
|
||
message.error('保存失败');
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleTestConnection = async () => {
|
||
setTesting(true);
|
||
try {
|
||
const result = await iitProjectApi.testProjectConnection(project.id);
|
||
if (result.success) {
|
||
message.success(`连接成功!REDCap 版本: ${result.version},记录数: ${result.recordCount}`);
|
||
} else {
|
||
message.error(`连接失败: ${result.error}`);
|
||
}
|
||
} catch (error) {
|
||
message.error('测试连接失败');
|
||
} finally {
|
||
setTesting(false);
|
||
}
|
||
};
|
||
|
||
const handleSyncMetadata = async () => {
|
||
setSyncing(true);
|
||
try {
|
||
const result = await iitProjectApi.syncMetadata(project.id);
|
||
message.success(`同步成功!共 ${result.fieldCount} 个字段`);
|
||
} catch (error) {
|
||
message.error('同步失败');
|
||
} finally {
|
||
setSyncing(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div>
|
||
<Form form={form} layout="vertical" onFinish={handleSave} style={{ maxWidth: 600 }}>
|
||
<Form.Item name="name" label="项目名称" rules={[{ required: true }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
|
||
<Form.Item name="description" label="项目描述">
|
||
<TextArea rows={2} />
|
||
</Form.Item>
|
||
|
||
<Form.Item name="redcapUrl" label="REDCap URL" rules={[{ required: true }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
|
||
<Form.Item name="redcapProjectId" label="REDCap 项目 ID (PID)" rules={[{ required: true }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="redcapApiToken"
|
||
label="API Token"
|
||
rules={[{ required: true, message: '请输入 API Token' }]}
|
||
>
|
||
<Input.Password placeholder="REDCap 项目的 API Token" />
|
||
</Form.Item>
|
||
|
||
<Form.Item>
|
||
<Space>
|
||
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={saving}>
|
||
保存配置
|
||
</Button>
|
||
<Button icon={<CheckCircleOutlined />} onClick={handleTestConnection} loading={testing}>
|
||
测试连接
|
||
</Button>
|
||
<Button icon={<SyncOutlined />} onClick={handleSyncMetadata} loading={syncing}>
|
||
同步元数据
|
||
</Button>
|
||
</Space>
|
||
</Form.Item>
|
||
</Form>
|
||
|
||
<Descriptions title="项目信息" style={{ marginTop: 24 }} column={1} bordered>
|
||
<Descriptions.Item label="项目 ID">{project.id}</Descriptions.Item>
|
||
<Descriptions.Item label="创建时间">
|
||
{new Date(project.createdAt).toLocaleString()}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="最后更新">
|
||
{new Date(project.updatedAt).toLocaleString()}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="最后同步">
|
||
{project.lastSyncAt ? new Date(project.lastSyncAt).toLocaleString() : '从未同步'}
|
||
</Descriptions.Item>
|
||
</Descriptions>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ==================== Tab 2: 质控规则 ====================
|
||
|
||
interface QCRulesTabProps {
|
||
projectId: string;
|
||
}
|
||
|
||
const QCRulesTab: React.FC<QCRulesTabProps> = ({ projectId }) => {
|
||
const [rules, setRules] = useState<QCRule[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
const [editingRule, setEditingRule] = useState<QCRule | null>(null);
|
||
const [form] = Form.useForm();
|
||
|
||
const loadRules = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
const data = await iitProjectApi.listRules(projectId);
|
||
setRules(data);
|
||
} catch (error) {
|
||
message.error('加载质控规则失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [projectId]);
|
||
|
||
useEffect(() => {
|
||
loadRules();
|
||
}, [loadRules]);
|
||
|
||
const handleAdd = () => {
|
||
setEditingRule(null);
|
||
form.resetFields();
|
||
setModalOpen(true);
|
||
};
|
||
|
||
const handleEdit = (rule: QCRule) => {
|
||
setEditingRule(rule);
|
||
form.setFieldsValue({
|
||
...rule,
|
||
field: Array.isArray(rule.field) ? rule.field.join(', ') : rule.field,
|
||
logic: JSON.stringify(rule.logic, null, 2),
|
||
});
|
||
setModalOpen(true);
|
||
};
|
||
|
||
const handleDelete = async (ruleId: string) => {
|
||
try {
|
||
await iitProjectApi.deleteRule(projectId, ruleId);
|
||
message.success('删除成功');
|
||
loadRules();
|
||
} catch (error) {
|
||
message.error('删除失败');
|
||
}
|
||
};
|
||
|
||
const handleSubmit = async (values: Record<string, unknown>) => {
|
||
try {
|
||
const fieldValue = values.field as string;
|
||
const logicValue = values.logic as string;
|
||
|
||
const ruleData: CreateRuleRequest = {
|
||
name: values.name as string,
|
||
field: fieldValue.includes(',') ? fieldValue.split(',').map((s) => s.trim()) : fieldValue,
|
||
logic: JSON.parse(logicValue),
|
||
message: values.message as string,
|
||
severity: values.severity as 'error' | 'warning' | 'info',
|
||
category: values.category as 'inclusion' | 'exclusion' | 'lab_values' | 'logic_check',
|
||
};
|
||
|
||
if (editingRule) {
|
||
await iitProjectApi.updateRule(projectId, editingRule.id, ruleData);
|
||
message.success('更新成功');
|
||
} else {
|
||
await iitProjectApi.addRule(projectId, ruleData);
|
||
message.success('添加成功');
|
||
}
|
||
|
||
setModalOpen(false);
|
||
loadRules();
|
||
} catch (error) {
|
||
message.error('操作失败,请检查 JSON Logic 格式');
|
||
}
|
||
};
|
||
|
||
const columns = [
|
||
{
|
||
title: '规则名称',
|
||
dataIndex: 'name',
|
||
key: 'name',
|
||
width: 200,
|
||
},
|
||
{
|
||
title: '分类',
|
||
dataIndex: 'category',
|
||
key: 'category',
|
||
width: 100,
|
||
render: (category: keyof typeof CATEGORY_MAP) => {
|
||
const cat = CATEGORY_MAP[category];
|
||
return <Tag color={cat?.color}>{cat?.text || category}</Tag>;
|
||
},
|
||
},
|
||
{
|
||
title: '严重程度',
|
||
dataIndex: 'severity',
|
||
key: 'severity',
|
||
width: 100,
|
||
render: (severity: keyof typeof SEVERITY_MAP) => {
|
||
const sev = SEVERITY_MAP[severity];
|
||
return <Tag color={sev?.color}>{sev?.text || severity}</Tag>;
|
||
},
|
||
},
|
||
{
|
||
title: '字段',
|
||
dataIndex: 'field',
|
||
key: 'field',
|
||
width: 150,
|
||
render: (field: string | string[]) => (
|
||
<Text code>{Array.isArray(field) ? field.join(', ') : field}</Text>
|
||
),
|
||
},
|
||
{
|
||
title: '错误信息',
|
||
dataIndex: 'message',
|
||
key: 'message',
|
||
ellipsis: true,
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 120,
|
||
render: (_: unknown, record: QCRule) => (
|
||
<Space>
|
||
<Tooltip title="编辑">
|
||
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
|
||
</Tooltip>
|
||
<Popconfirm title="确认删除此规则?" onConfirm={() => handleDelete(record.id)}>
|
||
<Button type="text" danger icon={<DeleteOutlined />} />
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
|
||
<Alert
|
||
message="质控规则使用 JSON Logic 格式定义,用于数据质量检查"
|
||
type="info"
|
||
showIcon
|
||
style={{ flex: 1, marginRight: 16 }}
|
||
/>
|
||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
||
添加规则
|
||
</Button>
|
||
</div>
|
||
|
||
<Table
|
||
columns={columns}
|
||
dataSource={rules}
|
||
rowKey="id"
|
||
loading={loading}
|
||
pagination={false}
|
||
size="small"
|
||
/>
|
||
|
||
<Modal
|
||
title={editingRule ? '编辑规则' : '添加规则'}
|
||
open={modalOpen}
|
||
onCancel={() => setModalOpen(false)}
|
||
footer={null}
|
||
width={700}
|
||
>
|
||
<Form form={form} layout="vertical" onFinish={handleSubmit} style={{ marginTop: 24 }}>
|
||
<Form.Item name="name" label="规则名称" rules={[{ required: true }]}>
|
||
<Input placeholder="例如:年龄范围检查" />
|
||
</Form.Item>
|
||
|
||
<Form.Item name="category" label="分类" rules={[{ required: true }]}>
|
||
<Select
|
||
options={Object.entries(CATEGORY_MAP).map(([value, { text }]) => ({
|
||
value,
|
||
label: text,
|
||
}))}
|
||
/>
|
||
</Form.Item>
|
||
|
||
<Form.Item name="severity" label="严重程度" rules={[{ required: true }]}>
|
||
<Select
|
||
options={Object.entries(SEVERITY_MAP).map(([value, { text }]) => ({
|
||
value,
|
||
label: text,
|
||
}))}
|
||
/>
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="field"
|
||
label="字段名"
|
||
rules={[{ required: true }]}
|
||
extra="多个字段用逗号分隔"
|
||
>
|
||
<Input placeholder="例如:age 或 age, gender" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="logic"
|
||
label="JSON Logic 表达式"
|
||
rules={[
|
||
{ required: true },
|
||
{
|
||
validator: (_, value) => {
|
||
try {
|
||
JSON.parse(value);
|
||
return Promise.resolve();
|
||
} catch {
|
||
return Promise.reject('请输入有效的 JSON');
|
||
}
|
||
},
|
||
},
|
||
]}
|
||
>
|
||
<TextArea
|
||
rows={6}
|
||
placeholder={'{\n "and": [\n {">=": [{"var": "age"}, 16]},\n {"<=": [{"var": "age"}, 35]}\n ]\n}'}
|
||
style={{ fontFamily: 'monospace' }}
|
||
/>
|
||
</Form.Item>
|
||
|
||
<Form.Item name="message" label="错误信息" rules={[{ required: true }]}>
|
||
<Input placeholder="例如:年龄必须在 16-35 岁之间" />
|
||
</Form.Item>
|
||
|
||
<Form.Item>
|
||
<Space>
|
||
<Button onClick={() => setModalOpen(false)}>取消</Button>
|
||
<Button type="primary" htmlType="submit">
|
||
{editingRule ? '更新' : '添加'}
|
||
</Button>
|
||
</Space>
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ==================== Tab 3: 用户映射 ====================
|
||
|
||
interface UserMappingTabProps {
|
||
projectId: string;
|
||
}
|
||
|
||
const UserMappingTab: React.FC<UserMappingTabProps> = ({ projectId }) => {
|
||
const [mappings, setMappings] = useState<IitUserMapping[]>([]);
|
||
const [roleOptions, setRoleOptions] = useState<RoleOption[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
const [editingMapping, setEditingMapping] = useState<IitUserMapping | null>(null);
|
||
const [form] = Form.useForm();
|
||
|
||
const loadData = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
const [mappingsData, rolesData] = await Promise.all([
|
||
iitProjectApi.listUserMappings(projectId),
|
||
iitProjectApi.getRoleOptions(),
|
||
]);
|
||
setMappings(mappingsData);
|
||
setRoleOptions(rolesData);
|
||
} catch (error) {
|
||
message.error('加载数据失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [projectId]);
|
||
|
||
useEffect(() => {
|
||
loadData();
|
||
}, [loadData]);
|
||
|
||
const handleAdd = () => {
|
||
setEditingMapping(null);
|
||
form.resetFields();
|
||
setModalOpen(true);
|
||
};
|
||
|
||
const handleEdit = (mapping: IitUserMapping) => {
|
||
setEditingMapping(mapping);
|
||
form.setFieldsValue(mapping);
|
||
setModalOpen(true);
|
||
};
|
||
|
||
const handleDelete = async (mappingId: string) => {
|
||
try {
|
||
await iitProjectApi.deleteUserMapping(projectId, mappingId);
|
||
message.success('删除成功');
|
||
loadData();
|
||
} catch (error) {
|
||
message.error('删除失败');
|
||
}
|
||
};
|
||
|
||
const handleSubmit = async (values: CreateUserMappingRequest) => {
|
||
try {
|
||
if (editingMapping) {
|
||
await iitProjectApi.updateUserMapping(projectId, editingMapping.id, values);
|
||
message.success('更新成功');
|
||
} else {
|
||
await iitProjectApi.createUserMapping(projectId, values);
|
||
message.success('添加成功');
|
||
}
|
||
setModalOpen(false);
|
||
loadData();
|
||
} catch (error) {
|
||
message.error('操作失败');
|
||
}
|
||
};
|
||
|
||
const columns = [
|
||
{
|
||
title: '企业微信用户 ID',
|
||
dataIndex: 'wecomUserId',
|
||
key: 'wecomUserId',
|
||
render: (id: string | undefined) => id || <Text type="secondary">未配置</Text>,
|
||
},
|
||
{
|
||
title: '角色',
|
||
dataIndex: 'role',
|
||
key: 'role',
|
||
width: 120,
|
||
render: (role: string) => {
|
||
const option = roleOptions.find((r) => r.value === role);
|
||
return <Tag>{option?.label || role}</Tag>;
|
||
},
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 100,
|
||
render: (_: unknown, record: IitUserMapping) => (
|
||
<Space>
|
||
<Tooltip title="编辑">
|
||
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
|
||
</Tooltip>
|
||
<Popconfirm title="确认删除此通知接收人?" onConfirm={() => handleDelete(record.id)}>
|
||
<Button type="text" danger icon={<DeleteOutlined />} />
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
||
添加通知接收人
|
||
</Button>
|
||
</div>
|
||
|
||
<Table
|
||
columns={columns}
|
||
dataSource={mappings}
|
||
rowKey="id"
|
||
loading={loading}
|
||
pagination={false}
|
||
size="small"
|
||
/>
|
||
|
||
<Modal
|
||
title={editingMapping ? '编辑通知接收人' : '添加通知接收人'}
|
||
open={modalOpen}
|
||
onCancel={() => setModalOpen(false)}
|
||
footer={null}
|
||
>
|
||
<Form form={form} layout="vertical" onFinish={handleSubmit} style={{ marginTop: 24 }}>
|
||
<Form.Item
|
||
name="wecomUserId"
|
||
label="企业微信用户 ID"
|
||
rules={[{ required: true, message: '请输入企业微信用户 ID' }]}
|
||
extra="用于接收质控通知,如:FengZhiBo"
|
||
>
|
||
<Input placeholder="企业微信通讯录中的用户 ID" />
|
||
</Form.Item>
|
||
|
||
<Form.Item name="role" label="角色">
|
||
<Select options={roleOptions} placeholder="选择角色(可选)" allowClear />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="systemUserId"
|
||
label="系统用户 ID"
|
||
extra="可选,用于系统内部标识"
|
||
>
|
||
<Input placeholder="例如:user_001(可选)" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="redcapUsername"
|
||
label="REDCap 用户名"
|
||
extra="可选,REDCap 中的用户名"
|
||
>
|
||
<Input placeholder="REDCap 用户名(可选)" />
|
||
</Form.Item>
|
||
|
||
<Form.Item>
|
||
<Space>
|
||
<Button onClick={() => setModalOpen(false)}>取消</Button>
|
||
<Button type="primary" htmlType="submit">
|
||
{editingMapping ? '更新' : '添加'}
|
||
</Button>
|
||
</Space>
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ==================== Tab 4: 知识库 ====================
|
||
|
||
interface KnowledgeBaseTabProps {
|
||
project: IitProject;
|
||
onUpdate: () => void;
|
||
}
|
||
|
||
const KnowledgeBaseTab: React.FC<KnowledgeBaseTabProps> = ({ project, onUpdate }) => {
|
||
const [kbList, setKbList] = useState<KnowledgeBaseOption[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [linking, setLinking] = useState(false);
|
||
const navigate = useNavigate();
|
||
|
||
useEffect(() => {
|
||
const loadKbList = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const data = await iitProjectApi.listKnowledgeBases();
|
||
setKbList(data);
|
||
} catch (error) {
|
||
message.error('加载知识库列表失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
loadKbList();
|
||
}, []);
|
||
|
||
const handleLink = async (kbId: string) => {
|
||
setLinking(true);
|
||
try {
|
||
await iitProjectApi.linkKnowledgeBase(project.id, kbId);
|
||
message.success('关联成功');
|
||
onUpdate();
|
||
} catch (error) {
|
||
message.error('关联失败');
|
||
} finally {
|
||
setLinking(false);
|
||
}
|
||
};
|
||
|
||
const handleUnlink = async () => {
|
||
setLinking(true);
|
||
try {
|
||
await iitProjectApi.unlinkKnowledgeBase(project.id);
|
||
message.success('解除关联成功');
|
||
onUpdate();
|
||
} catch (error) {
|
||
message.error('操作失败');
|
||
} finally {
|
||
setLinking(false);
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return <Spin />;
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
{project.knowledgeBase ? (
|
||
<div>
|
||
<Alert
|
||
message="已关联知识库"
|
||
description={
|
||
<div style={{ marginTop: 8 }}>
|
||
<Space direction="vertical">
|
||
<Text>
|
||
<BookOutlined style={{ marginRight: 8 }} />
|
||
{project.knowledgeBase.name}
|
||
</Text>
|
||
<Text type="secondary">
|
||
文档数量: {project.knowledgeBase.documentCount}
|
||
</Text>
|
||
</Space>
|
||
</div>
|
||
}
|
||
type="success"
|
||
showIcon
|
||
action={
|
||
<Space direction="vertical">
|
||
<Button
|
||
size="small"
|
||
onClick={() => navigate(`/admin/system-kb/${project.knowledgeBase?.id}`)}
|
||
>
|
||
查看知识库
|
||
</Button>
|
||
<Popconfirm title="确认解除关联?" onConfirm={handleUnlink}>
|
||
<Button size="small" danger icon={<DisconnectOutlined />}>
|
||
解除关联
|
||
</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
}
|
||
/>
|
||
|
||
<div style={{ marginTop: 24 }}>
|
||
<Title level={5}>知识库内容分类</Title>
|
||
<Text type="secondary">
|
||
上传研究方案、CRF、入排标准等文档到知识库,AI 将基于这些内容回答问题。
|
||
</Text>
|
||
<div style={{ marginTop: 16 }}>
|
||
<Space wrap>
|
||
<Tag color="blue">研究方案</Tag>
|
||
<Tag color="green">CRF</Tag>
|
||
<Tag color="orange">入排标准</Tag>
|
||
<Tag color="purple">知情同意</Tag>
|
||
<Tag color="cyan">伦理审查</Tag>
|
||
<Tag color="magenta">研究流程</Tag>
|
||
<Tag>其他</Tag>
|
||
</Space>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div>
|
||
<Alert
|
||
message="未关联知识库"
|
||
description="请选择或创建一个知识库,用于存储研究方案、入排标准等文档。"
|
||
type="warning"
|
||
showIcon
|
||
icon={<ExclamationCircleOutlined />}
|
||
/>
|
||
|
||
<Title level={5} style={{ marginTop: 24 }}>
|
||
选择知识库
|
||
</Title>
|
||
|
||
{kbList.length === 0 ? (
|
||
<Empty description="暂无可用知识库">
|
||
<Button type="primary" onClick={() => navigate('/admin/system-kb')}>
|
||
创建知识库
|
||
</Button>
|
||
</Empty>
|
||
) : (
|
||
<Select
|
||
style={{ width: 400 }}
|
||
placeholder="选择要关联的知识库"
|
||
loading={linking}
|
||
onChange={handleLink}
|
||
options={kbList.map((kb) => ({
|
||
value: kb.id,
|
||
label: kb.name,
|
||
}))}
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default IitProjectDetailPage;
|