refactor(ai): 用知识库 V2 替换旧版 — 删除旧页面/API,菜单路径不变
- 删除旧版 AiKnowledgePage.tsx 和 api/ai/knowledge.ts - V2 页面接管 /health/ai-knowledge 路由(不再用 -v2 后缀) - 迁移 168 改为 UPDATE 旧菜单名称+component(而非新增菜单) - routeConfig 和 App.tsx 路由声明同步更新
This commit is contained in:
@@ -46,7 +46,6 @@ const AiPromptList = lazy(() => import('./pages/health/AiPromptList'));
|
|||||||
const AiAnalysisList = lazy(() => import('./pages/health/AiAnalysisList'));
|
const AiAnalysisList = lazy(() => import('./pages/health/AiAnalysisList'));
|
||||||
const AiUsageDashboard = lazy(() => import('./pages/health/AiUsageDashboard'));
|
const AiUsageDashboard = lazy(() => import('./pages/health/AiUsageDashboard'));
|
||||||
const AiConfigPage = lazy(() => import('./pages/health/AiConfigPage'));
|
const AiConfigPage = lazy(() => import('./pages/health/AiConfigPage'));
|
||||||
const AiKnowledgePage = lazy(() => import('./pages/health/AiKnowledgePage'));
|
|
||||||
const KnowledgeV2Page = lazy(() => import('./pages/ai/KnowledgeV2Page'));
|
const KnowledgeV2Page = lazy(() => import('./pages/ai/KnowledgeV2Page'));
|
||||||
const AiChatPage = lazy(() => import('./pages/ai/ChatPage'));
|
const AiChatPage = lazy(() => import('./pages/ai/ChatPage'));
|
||||||
const AlertList = lazy(() => import('./pages/health/AlertList'));
|
const AlertList = lazy(() => import('./pages/health/AlertList'));
|
||||||
@@ -258,7 +257,7 @@ export default function App() {
|
|||||||
"/health/follow-up-records", "/health/consultations",
|
"/health/follow-up-records", "/health/consultations",
|
||||||
"/health/points-rules", "/health/points-products", "/health/points-orders",
|
"/health/points-rules", "/health/points-products", "/health/points-orders",
|
||||||
"/health/offline-events", "/health/ai-prompts", "/health/ai-analysis",
|
"/health/offline-events", "/health/ai-prompts", "/health/ai-analysis",
|
||||||
"/health/ai-usage", "/health/ai-config", "/health/ai-knowledge", "/health/ai-knowledge-v2", "/health/alerts", "/health/alert-dashboard",
|
"/health/ai-usage", "/health/ai-config", "/health/ai-knowledge", "/health/alerts", "/health/alert-dashboard",
|
||||||
"/ai/chat",
|
"/ai/chat",
|
||||||
"/health/alert-rules", "/health/devices", "/health/realtime-monitor",
|
"/health/alert-rules", "/health/devices", "/health/realtime-monitor",
|
||||||
"/health/oauth-clients", "/health/dialysis", "/health/action-inbox",
|
"/health/oauth-clients", "/health/dialysis", "/health/action-inbox",
|
||||||
@@ -331,8 +330,7 @@ export default function App() {
|
|||||||
<Route path="/health/ai-analysis" element={<AiAnalysisList />} />
|
<Route path="/health/ai-analysis" element={<AiAnalysisList />} />
|
||||||
<Route path="/health/ai-usage" element={<AiUsageDashboard />} />
|
<Route path="/health/ai-usage" element={<AiUsageDashboard />} />
|
||||||
<Route path="/health/ai-config" element={<AiConfigPage />} />
|
<Route path="/health/ai-config" element={<AiConfigPage />} />
|
||||||
<Route path="/health/ai-knowledge" element={<AiKnowledgePage />} />
|
<Route path="/health/ai-knowledge" element={<KnowledgeV2Page />} />
|
||||||
<Route path="/health/ai-knowledge-v2" element={<KnowledgeV2Page />} />
|
|
||||||
<Route path="/ai/chat" element={<AiChatPage />} />
|
<Route path="/ai/chat" element={<AiChatPage />} />
|
||||||
<Route path="/health/alerts" element={<AlertList />} />
|
<Route path="/health/alerts" element={<AlertList />} />
|
||||||
<Route path="/health/alert-dashboard" element={<AlertDashboard />} />
|
<Route path="/health/alert-dashboard" element={<AlertDashboard />} />
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import client from '../client';
|
|
||||||
|
|
||||||
// === Types ===
|
|
||||||
|
|
||||||
export interface KnowledgeReference {
|
|
||||||
id: string;
|
|
||||||
tenant_id: string;
|
|
||||||
title: string;
|
|
||||||
analysis_type: string;
|
|
||||||
source_name: string;
|
|
||||||
content_summary: string;
|
|
||||||
tags: Record<string, unknown> | null;
|
|
||||||
is_enabled: boolean;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KnowledgeGuide {
|
|
||||||
id: string;
|
|
||||||
tenant_id: string;
|
|
||||||
title: string;
|
|
||||||
analysis_type: string;
|
|
||||||
content: string;
|
|
||||||
category: string | null;
|
|
||||||
is_enabled: boolean;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateReferenceReq {
|
|
||||||
title: string;
|
|
||||||
analysis_type: string;
|
|
||||||
source_name: string;
|
|
||||||
content_summary: string;
|
|
||||||
tags?: Record<string, unknown>;
|
|
||||||
is_enabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateReferenceReq {
|
|
||||||
title?: string;
|
|
||||||
analysis_type?: string;
|
|
||||||
source_name?: string;
|
|
||||||
content_summary?: string;
|
|
||||||
tags?: Record<string, unknown>;
|
|
||||||
is_enabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateGuideReq {
|
|
||||||
title: string;
|
|
||||||
analysis_type: string;
|
|
||||||
content: string;
|
|
||||||
category?: string;
|
|
||||||
is_enabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateGuideReq {
|
|
||||||
title?: string;
|
|
||||||
analysis_type?: string;
|
|
||||||
content?: string;
|
|
||||||
category?: string;
|
|
||||||
is_enabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// === API ===
|
|
||||||
|
|
||||||
export const knowledgeApi = {
|
|
||||||
// References
|
|
||||||
listReferences: async (params?: { analysis_type?: string }) => {
|
|
||||||
const resp = await client.get('/ai/knowledge/references', { params });
|
|
||||||
return resp.data.data as { data: KnowledgeReference[]; total: number };
|
|
||||||
},
|
|
||||||
createReference: async (data: CreateReferenceReq) => {
|
|
||||||
const resp = await client.post('/ai/knowledge/references', data);
|
|
||||||
return resp.data.data as { id: string };
|
|
||||||
},
|
|
||||||
updateReference: async (id: string, data: UpdateReferenceReq) => {
|
|
||||||
const resp = await client.put(`/ai/knowledge/references/${id}`, data);
|
|
||||||
return resp.data.data as { id: string };
|
|
||||||
},
|
|
||||||
deleteReference: async (id: string) => {
|
|
||||||
const resp = await client.delete(`/ai/knowledge/references/${id}`);
|
|
||||||
return resp.data.data as { id: string };
|
|
||||||
},
|
|
||||||
reEmbedReference: async (id: string) => {
|
|
||||||
const resp = await client.post(`/ai/knowledge/references/${id}/re-embed`);
|
|
||||||
return resp.data.data as { id: string };
|
|
||||||
},
|
|
||||||
|
|
||||||
// Guides
|
|
||||||
listGuides: async (params?: { analysis_type?: string }) => {
|
|
||||||
const resp = await client.get('/ai/knowledge/guides', { params });
|
|
||||||
return resp.data.data as { data: KnowledgeGuide[]; total: number };
|
|
||||||
},
|
|
||||||
createGuide: async (data: CreateGuideReq) => {
|
|
||||||
const resp = await client.post('/ai/knowledge/guides', data);
|
|
||||||
return resp.data.data as { id: string };
|
|
||||||
},
|
|
||||||
updateGuide: async (id: string, data: UpdateGuideReq) => {
|
|
||||||
const resp = await client.put(`/ai/knowledge/guides/${id}`, data);
|
|
||||||
return resp.data.data as { id: string };
|
|
||||||
},
|
|
||||||
deleteGuide: async (id: string) => {
|
|
||||||
const resp = await client.delete(`/ai/knowledge/guides/${id}`);
|
|
||||||
return resp.data.data as { id: string };
|
|
||||||
},
|
|
||||||
reEmbedGuide: async (id: string) => {
|
|
||||||
const resp = await client.post(`/ai/knowledge/guides/${id}/re-embed`);
|
|
||||||
return resp.data.data as { id: string };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,508 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
Table,
|
|
||||||
Button,
|
|
||||||
Space,
|
|
||||||
Modal,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
Switch,
|
|
||||||
message,
|
|
||||||
Popconfirm,
|
|
||||||
Tabs,
|
|
||||||
Tag,
|
|
||||||
Tooltip,
|
|
||||||
} from 'antd';
|
|
||||||
import {
|
|
||||||
PlusOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
ThunderboltOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
knowledgeApi,
|
|
||||||
type KnowledgeReference,
|
|
||||||
type KnowledgeGuide,
|
|
||||||
type CreateReferenceReq,
|
|
||||||
type UpdateReferenceReq,
|
|
||||||
type CreateGuideReq,
|
|
||||||
type UpdateGuideReq,
|
|
||||||
} from '../../api/ai/knowledge';
|
|
||||||
import { AuthButton } from '../../components/AuthButton';
|
|
||||||
|
|
||||||
const ANALYSIS_TYPES = [
|
|
||||||
{ value: 'lab_report', label: '化验报告' },
|
|
||||||
{ value: 'trend', label: '趋势分析' },
|
|
||||||
{ value: 'report_summary', label: '报告摘要' },
|
|
||||||
{ value: 'dialysis_risk', label: '透析风险' },
|
|
||||||
{ value: 'checkup_plan', label: '体检计划' },
|
|
||||||
{ value: 'follow_up', label: '随访总结' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function AiKnowledgePage() {
|
|
||||||
return (
|
|
||||||
<Card title="AI 知识库管理">
|
|
||||||
<Tabs
|
|
||||||
items={[
|
|
||||||
{ key: 'references', label: '参考资料', children: <ReferencesTab /> },
|
|
||||||
{ key: 'guides', label: '临床指南', children: <GuidesTab /> },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// === References Tab ===
|
|
||||||
|
|
||||||
function ReferencesTab() {
|
|
||||||
const [data, setData] = useState<KnowledgeReference[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
|
||||||
const [editing, setEditing] = useState<KnowledgeReference | null>(null);
|
|
||||||
const [filterType, setFilterType] = useState<string | undefined>();
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const result = await knowledgeApi.listReferences(
|
|
||||||
filterType ? { analysis_type: filterType } : undefined,
|
|
||||||
);
|
|
||||||
setData(result.data);
|
|
||||||
} catch {
|
|
||||||
message.error('加载参考资料失败');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [filterType]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
const openCreate = () => {
|
|
||||||
setEditing(null);
|
|
||||||
form.resetFields();
|
|
||||||
form.setFieldsValue({ is_enabled: true });
|
|
||||||
setModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openEdit = (record: KnowledgeReference) => {
|
|
||||||
setEditing(record);
|
|
||||||
form.setFieldsValue({
|
|
||||||
title: record.title,
|
|
||||||
analysis_type: record.analysis_type,
|
|
||||||
source_name: record.source_name,
|
|
||||||
content_summary: record.content_summary,
|
|
||||||
is_enabled: record.is_enabled,
|
|
||||||
});
|
|
||||||
setModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
const values = await form.validateFields();
|
|
||||||
try {
|
|
||||||
if (editing) {
|
|
||||||
const req: UpdateReferenceReq = {
|
|
||||||
title: values.title,
|
|
||||||
analysis_type: values.analysis_type,
|
|
||||||
source_name: values.source_name,
|
|
||||||
content_summary: values.content_summary,
|
|
||||||
is_enabled: values.is_enabled,
|
|
||||||
};
|
|
||||||
await knowledgeApi.updateReference(editing.id, req);
|
|
||||||
message.success('更新成功');
|
|
||||||
} else {
|
|
||||||
const req: CreateReferenceReq = {
|
|
||||||
title: values.title,
|
|
||||||
analysis_type: values.analysis_type,
|
|
||||||
source_name: values.source_name,
|
|
||||||
content_summary: values.content_summary,
|
|
||||||
is_enabled: values.is_enabled,
|
|
||||||
};
|
|
||||||
await knowledgeApi.createReference(req);
|
|
||||||
message.success('创建成功');
|
|
||||||
}
|
|
||||||
setModalOpen(false);
|
|
||||||
fetchData();
|
|
||||||
} catch {
|
|
||||||
message.error(editing ? '更新失败' : '创建失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await knowledgeApi.deleteReference(id);
|
|
||||||
message.success('删除成功');
|
|
||||||
fetchData();
|
|
||||||
} catch {
|
|
||||||
message.error('删除失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReEmbed = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await knowledgeApi.reEmbedReference(id);
|
|
||||||
message.success('向量重新生成已触发');
|
|
||||||
} catch {
|
|
||||||
message.error('向量重新生成失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ title: '标题', dataIndex: 'title', key: 'title', ellipsis: true },
|
|
||||||
{
|
|
||||||
title: '分析类型',
|
|
||||||
dataIndex: 'analysis_type',
|
|
||||||
key: 'analysis_type',
|
|
||||||
width: 120,
|
|
||||||
render: (v: string) => {
|
|
||||||
const found = ANALYSIS_TYPES.find((t) => t.value === v);
|
|
||||||
return <Tag>{found?.label ?? v}</Tag>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ title: '来源', dataIndex: 'source_name', key: 'source_name', width: 150, ellipsis: true },
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
dataIndex: 'is_enabled',
|
|
||||||
key: 'is_enabled',
|
|
||||||
width: 80,
|
|
||||||
render: (v: boolean) => (
|
|
||||||
<Tag color={v ? 'green' : 'default'}>{v ? '启用' : '禁用'}</Tag>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '更新时间',
|
|
||||||
dataIndex: 'updated_at',
|
|
||||||
key: 'updated_at',
|
|
||||||
width: 170,
|
|
||||||
render: (v: string) => (v ? new Date(v).toLocaleString() : '-'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'actions',
|
|
||||||
width: 200,
|
|
||||||
render: (_: unknown, record: KnowledgeReference) => (
|
|
||||||
<Space size="small">
|
|
||||||
<Tooltip title="编辑">
|
|
||||||
<Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} />
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="重新生成向量">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<ThunderboltOutlined />}
|
|
||||||
onClick={() => handleReEmbed(record.id)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Popconfirm
|
|
||||||
title="确定删除此参考资料?"
|
|
||||||
onConfirm={() => handleDelete(record.id)}
|
|
||||||
okText="删除"
|
|
||||||
cancelText="取消"
|
|
||||||
>
|
|
||||||
<Button type="text" danger icon={<DeleteOutlined />} />
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Space style={{ marginBottom: 16 }}>
|
|
||||||
<Select
|
|
||||||
allowClear
|
|
||||||
placeholder="按分析类型过滤"
|
|
||||||
style={{ width: 180 }}
|
|
||||||
options={ANALYSIS_TYPES}
|
|
||||||
value={filterType}
|
|
||||||
onChange={setFilterType}
|
|
||||||
/>
|
|
||||||
<AuthButton code="ai.knowledge.manage">
|
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
|
||||||
新增参考资料
|
|
||||||
</Button>
|
|
||||||
</AuthButton>
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={fetchData}>
|
|
||||||
刷新
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
|
|
||||||
<Table
|
|
||||||
rowKey="id"
|
|
||||||
columns={columns}
|
|
||||||
dataSource={data}
|
|
||||||
loading={loading}
|
|
||||||
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 条` }}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title={editing ? '编辑参考资料' : '新增参考资料'}
|
|
||||||
open={modalOpen}
|
|
||||||
onOk={handleSubmit}
|
|
||||||
onCancel={() => setModalOpen(false)}
|
|
||||||
width={600}
|
|
||||||
destroyOnClose
|
|
||||||
>
|
|
||||||
<Form form={form} layout="vertical">
|
|
||||||
<Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入标题' }]}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="analysis_type"
|
|
||||||
label="分析类型"
|
|
||||||
rules={[{ required: true, message: '请选择分析类型' }]}
|
|
||||||
>
|
|
||||||
<Select options={ANALYSIS_TYPES} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="source_name" label="来源名称" rules={[{ required: true }]}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="content_summary"
|
|
||||||
label="内容摘要"
|
|
||||||
rules={[{ required: true, message: '请输入内容摘要' }]}
|
|
||||||
>
|
|
||||||
<Input.TextArea rows={4} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="is_enabled" label="启用" valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Guides Tab ===
|
|
||||||
|
|
||||||
function GuidesTab() {
|
|
||||||
const [data, setData] = useState<KnowledgeGuide[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
|
||||||
const [editing, setEditing] = useState<KnowledgeGuide | null>(null);
|
|
||||||
const [filterType, setFilterType] = useState<string | undefined>();
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const result = await knowledgeApi.listGuides(
|
|
||||||
filterType ? { analysis_type: filterType } : undefined,
|
|
||||||
);
|
|
||||||
setData(result.data);
|
|
||||||
} catch {
|
|
||||||
message.error('加载临床指南失败');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [filterType]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
const openCreate = () => {
|
|
||||||
setEditing(null);
|
|
||||||
form.resetFields();
|
|
||||||
form.setFieldsValue({ is_enabled: true });
|
|
||||||
setModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openEdit = (record: KnowledgeGuide) => {
|
|
||||||
setEditing(record);
|
|
||||||
form.setFieldsValue({
|
|
||||||
title: record.title,
|
|
||||||
analysis_type: record.analysis_type,
|
|
||||||
content: record.content,
|
|
||||||
category: record.category,
|
|
||||||
is_enabled: record.is_enabled,
|
|
||||||
});
|
|
||||||
setModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
const values = await form.validateFields();
|
|
||||||
try {
|
|
||||||
if (editing) {
|
|
||||||
const req: UpdateGuideReq = {
|
|
||||||
title: values.title,
|
|
||||||
analysis_type: values.analysis_type,
|
|
||||||
content: values.content,
|
|
||||||
category: values.category,
|
|
||||||
is_enabled: values.is_enabled,
|
|
||||||
};
|
|
||||||
await knowledgeApi.updateGuide(editing.id, req);
|
|
||||||
message.success('更新成功');
|
|
||||||
} else {
|
|
||||||
const req: CreateGuideReq = {
|
|
||||||
title: values.title,
|
|
||||||
analysis_type: values.analysis_type,
|
|
||||||
content: values.content,
|
|
||||||
category: values.category,
|
|
||||||
is_enabled: values.is_enabled,
|
|
||||||
};
|
|
||||||
await knowledgeApi.createGuide(req);
|
|
||||||
message.success('创建成功');
|
|
||||||
}
|
|
||||||
setModalOpen(false);
|
|
||||||
fetchData();
|
|
||||||
} catch {
|
|
||||||
message.error(editing ? '更新失败' : '创建失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await knowledgeApi.deleteGuide(id);
|
|
||||||
message.success('删除成功');
|
|
||||||
fetchData();
|
|
||||||
} catch {
|
|
||||||
message.error('删除失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReEmbed = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await knowledgeApi.reEmbedGuide(id);
|
|
||||||
message.success('向量重新生成已触发');
|
|
||||||
} catch {
|
|
||||||
message.error('向量重新生成失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ title: '标题', dataIndex: 'title', key: 'title', ellipsis: true },
|
|
||||||
{
|
|
||||||
title: '分析类型',
|
|
||||||
dataIndex: 'analysis_type',
|
|
||||||
key: 'analysis_type',
|
|
||||||
width: 120,
|
|
||||||
render: (v: string) => {
|
|
||||||
const found = ANALYSIS_TYPES.find((t) => t.value === v);
|
|
||||||
return <Tag>{found?.label ?? v}</Tag>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '分类',
|
|
||||||
dataIndex: 'category',
|
|
||||||
key: 'category',
|
|
||||||
width: 100,
|
|
||||||
render: (v: string | null) => v || '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
dataIndex: 'is_enabled',
|
|
||||||
key: 'is_enabled',
|
|
||||||
width: 80,
|
|
||||||
render: (v: boolean) => (
|
|
||||||
<Tag color={v ? 'green' : 'default'}>{v ? '启用' : '禁用'}</Tag>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '更新时间',
|
|
||||||
dataIndex: 'updated_at',
|
|
||||||
key: 'updated_at',
|
|
||||||
width: 170,
|
|
||||||
render: (v: string) => (v ? new Date(v).toLocaleString() : '-'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'actions',
|
|
||||||
width: 200,
|
|
||||||
render: (_: unknown, record: KnowledgeGuide) => (
|
|
||||||
<Space size="small">
|
|
||||||
<Tooltip title="编辑">
|
|
||||||
<Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} />
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="重新生成向量">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<ThunderboltOutlined />}
|
|
||||||
onClick={() => handleReEmbed(record.id)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Popconfirm
|
|
||||||
title="确定删除此临床指南?"
|
|
||||||
onConfirm={() => handleDelete(record.id)}
|
|
||||||
okText="删除"
|
|
||||||
cancelText="取消"
|
|
||||||
>
|
|
||||||
<Button type="text" danger icon={<DeleteOutlined />} />
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Space style={{ marginBottom: 16 }}>
|
|
||||||
<Select
|
|
||||||
allowClear
|
|
||||||
placeholder="按分析类型过滤"
|
|
||||||
style={{ width: 180 }}
|
|
||||||
options={ANALYSIS_TYPES}
|
|
||||||
value={filterType}
|
|
||||||
onChange={setFilterType}
|
|
||||||
/>
|
|
||||||
<AuthButton code="ai.knowledge.manage">
|
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
|
||||||
新增临床指南
|
|
||||||
</Button>
|
|
||||||
</AuthButton>
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={fetchData}>
|
|
||||||
刷新
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
|
|
||||||
<Table
|
|
||||||
rowKey="id"
|
|
||||||
columns={columns}
|
|
||||||
dataSource={data}
|
|
||||||
loading={loading}
|
|
||||||
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 条` }}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title={editing ? '编辑临床指南' : '新增临床指南'}
|
|
||||||
open={modalOpen}
|
|
||||||
onOk={handleSubmit}
|
|
||||||
onCancel={() => setModalOpen(false)}
|
|
||||||
width={700}
|
|
||||||
destroyOnClose
|
|
||||||
>
|
|
||||||
<Form form={form} layout="vertical">
|
|
||||||
<Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入标题' }]}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="analysis_type"
|
|
||||||
label="分析类型"
|
|
||||||
rules={[{ required: true, message: '请选择分析类型' }]}
|
|
||||||
>
|
|
||||||
<Select options={ANALYSIS_TYPES} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="category" label="分类">
|
|
||||||
<Input placeholder="如:心血管、内分泌" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="content"
|
|
||||||
label="指南内容"
|
|
||||||
rules={[{ required: true, message: '请输入指南内容' }]}
|
|
||||||
>
|
|
||||||
<Input.TextArea rows={8} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="is_enabled" label="启用" valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -151,10 +151,6 @@ const ENTRIES: RoutePermissionEntry[] = [
|
|||||||
path: "/health/ai-knowledge",
|
path: "/health/ai-knowledge",
|
||||||
permissions: ["ai.knowledge.list", "ai.knowledge.manage"],
|
permissions: ["ai.knowledge.list", "ai.knowledge.manage"],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/health/ai-knowledge-v2",
|
|
||||||
permissions: ["ai.knowledge.list", "ai.knowledge.manage"],
|
|
||||||
},
|
|
||||||
|
|
||||||
// ===== 健康管理 — 积分商城 =====
|
// ===== 健康管理 — 积分商城 =====
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,41 +6,17 @@ pub struct Migration;
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MigrationTrait for Migration {
|
impl MigrationTrait for Migration {
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
// 安全插入:仅在 sys_menu 表存在且有 ai-knowledge 菜单时添加 V2 菜单
|
// 将旧版 AI 知识库菜单更新为 V2 版本
|
||||||
let sql = r#"
|
let sql = r#"
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'sys_menu') THEN
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'sys_menu') THEN
|
||||||
INSERT INTO sys_menu (id, parent_id, name, path, icon, sort, permission, component, is_external, is_cached, status, visible, created_at, updated_at, deleted_at)
|
UPDATE sys_menu
|
||||||
SELECT
|
SET name = '知识库管理',
|
||||||
gen_random_uuid(),
|
icon = 'DatabaseOutlined',
|
||||||
parent_id,
|
component = 'ai/KnowledgeV2Page',
|
||||||
'知识库 V2',
|
updated_at = now()
|
||||||
'/health/ai-knowledge-v2',
|
WHERE path = '/health/ai-knowledge' AND deleted_at IS NULL;
|
||||||
'DatabaseOutlined',
|
|
||||||
54,
|
|
||||||
'ai.knowledge.list',
|
|
||||||
'ai/KnowledgeV2Page',
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
'active',
|
|
||||||
true,
|
|
||||||
now(),
|
|
||||||
now(),
|
|
||||||
NULL
|
|
||||||
FROM sys_menu
|
|
||||||
WHERE path = '/health/ai-knowledge' AND deleted_at IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
|
|
||||||
IF FOUND THEN
|
|
||||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
|
||||||
SELECT r.id, m.id
|
|
||||||
FROM sys_role r, sys_menu m
|
|
||||||
WHERE r.code = 'admin' AND r.deleted_at IS NULL
|
|
||||||
AND m.path = '/health/ai-knowledge-v2' AND m.deleted_at IS NULL
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
END IF;
|
|
||||||
END IF;
|
END IF;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql
|
$$ LANGUAGE plpgsql
|
||||||
@@ -59,7 +35,12 @@ impl MigrationTrait for Migration {
|
|||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'sys_menu') THEN
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'sys_menu') THEN
|
||||||
DELETE FROM sys_menu WHERE path = '/health/ai-knowledge-v2';
|
UPDATE sys_menu
|
||||||
|
SET name = 'AI 知识库',
|
||||||
|
icon = 'BookOutlined',
|
||||||
|
component = 'health/AiKnowledgePage',
|
||||||
|
updated_at = now()
|
||||||
|
WHERE path = '/health/ai-knowledge' AND deleted_at IS NULL;
|
||||||
END IF;
|
END IF;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql
|
$$ LANGUAGE plpgsql
|
||||||
|
|||||||
Reference in New Issue
Block a user