From b8af17d5f5f95d3977a84802cbf86e370266b07d Mon Sep 17 00:00:00 2001 From: weijuesen Date: Wed, 12 Aug 2026 10:51:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E7=9F=A5=E8=AF=86=E5=BA=93?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=EF=BC=88=E8=9A=95=E7=97=85=E7=99=BE=E7=A7=91?= =?UTF-8?q?=20+=20AI=20=E8=A7=A3=E8=AF=BB=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/dal/knowledge.ts | 47 +++++ web/src/layout/BasicLayout.tsx | 2 + web/src/pages/Knowledge.tsx | 330 +++++++++++++++++++++++++++++++++ web/src/router.tsx | 2 + 4 files changed, 381 insertions(+) create mode 100644 web/src/dal/knowledge.ts create mode 100644 web/src/pages/Knowledge.tsx diff --git a/web/src/dal/knowledge.ts b/web/src/dal/knowledge.ts new file mode 100644 index 0000000..9a589df --- /dev/null +++ b/web/src/dal/knowledge.ts @@ -0,0 +1,47 @@ +import { get, post, patch, del } from '../api/http'; + +export interface Disease { + id: string; + name: string; + category: string; + pathogen?: string; + transmission?: string; + medium?: string; + incubation?: string; + symptoms?: string; + highRiskStage?: string; + highRiskCondition?: string; + lethalTime?: string; + spreadTrend?: string; + recurrence?: string; + detection?: string; + prevention?: string; + imageUrl?: string; + sortOrder?: number; + enabled?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface KnowledgeArticle { + id: string; + kind: string; + title: string; + summary?: string; + content?: string; + sortOrder?: number; + enabled?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export const listDiseases = (params?: any) => get('/knowledge/diseases', { params }); +export const getDisease = (id: string) => get(`/knowledge/diseases/${id}`); +export const createDisease = (data: Partial) => post('/knowledge/diseases', data); +export const updateDisease = (id: string, data: Partial) => patch(`/knowledge/diseases/${id}`, data); +export const deleteDisease = (id: string) => del(`/knowledge/diseases/${id}`); + +export const listArticles = (params?: any) => get('/knowledge/articles', { params }); +export const createArticle = (data: Partial) => post('/knowledge/articles', data); +export const updateArticle = (id: string, data: Partial) => patch(`/knowledge/articles/${id}`, data); +export const deleteArticle = (id: string) => del(`/knowledge/articles/${id}`); diff --git a/web/src/layout/BasicLayout.tsx b/web/src/layout/BasicLayout.tsx index 9ebea79..e513de1 100644 --- a/web/src/layout/BasicLayout.tsx +++ b/web/src/layout/BasicLayout.tsx @@ -7,6 +7,7 @@ import { BellOutlined, VideoCameraOutlined, FileTextOutlined, + BookOutlined, SettingOutlined, LogoutOutlined, UserOutlined, @@ -30,6 +31,7 @@ const menuData = [ { path: '/alerts', name: '告警中心', icon: , permission: 'alarm:read' }, { path: '/videos', name: '视频监控', icon: , permission: 'video:read' }, { path: '/logs', name: '控制日志', icon: , permission: 'log:read' }, + { path: '/knowledge', name: '知识库', icon: , permission: 'knowledge:read' }, ]; // 角色中文名 diff --git a/web/src/pages/Knowledge.tsx b/web/src/pages/Knowledge.tsx new file mode 100644 index 0000000..ca9f16d --- /dev/null +++ b/web/src/pages/Knowledge.tsx @@ -0,0 +1,330 @@ +import { useRef, useState } from 'react'; +import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Switch, Tabs, Tag, message } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components'; +import { + listDiseases, + createDisease, + updateDisease, + deleteDisease, + listArticles, + createArticle, + updateArticle, + deleteArticle, + type Disease, + type KnowledgeArticle, +} from '../dal/knowledge'; +import { authService } from '../services/auth'; + +const CATEGORY_LABELS: Record = { + viral: '病毒性', + fungal: '真菌性', + bacterial: '细菌性', + protozoan: '原虫性', + other: '其他', +}; + +const KIND_LABELS: Record = { + ai_guide: 'AI 结果解读', + lamp_guide: 'LAMP 教程', + sers_guide: 'SERS 教程', + seasonal_tip: '季节性提醒', +}; + +const canWrite = () => authService.hasPermission('knowledge:write'); + +function DiseaseTab() { + const actionRef = useRef(); + const [modalOpen, setModalOpen] = useState(false); + const [form] = Form.useForm>(); + const [editing, setEditing] = useState(null); + + const columns: ProColumns[] = [ + { title: '序号', valueType: 'indexBorder', search: false, width: 60 }, + { title: '名称', dataIndex: 'name' }, + { + title: '类别', + dataIndex: 'category', + valueType: 'select', + valueEnum: Object.fromEntries( + Object.entries(CATEGORY_LABELS).map(([k, v]) => [k, { text: v }]), + ), + render: (_, r) => {CATEGORY_LABELS[r.category] || r.category}, + }, + { title: '高发阶段', dataIndex: 'highRiskStage', search: false, ellipsis: true, render: (_, r) => r.highRiskStage || '-' }, + { title: '排序', dataIndex: 'sortOrder', search: false, width: 70 }, + { + title: '状态', + dataIndex: 'enabled', + search: false, + width: 80, + render: (_, r) => (r.enabled === false ? 停用 : 启用), + }, + { + title: '操作', + valueType: 'option', + render: (_, r) => [ + { + setEditing(r); + form.setFieldsValue(r); + setModalOpen(true); + }} + > + 编辑 + , + ...(canWrite() + ? [ + { + await deleteDisease(r.id); + message.success('已删除'); + actionRef.current?.reload(); + }} + > + 删除 + , + ] + : []), + ], + }, + ]; + + const openCreate = () => { + setEditing(null); + form.resetFields(); + form.setFieldsValue({ category: 'viral', enabled: true, sortOrder: 0 }); + setModalOpen(true); + }; + + return ( + <> + + actionRef={actionRef} + rowKey="id" + columns={columns} + search={{ labelWidth: 'auto' }} + request={async (params) => { + const res = await listDiseases({ + category: params.category, + keyword: params.name, + }); + return { data: res, total: res.length, success: true }; + }} + toolBarRender={() => + canWrite() + ? [ + , + ] + : [] + } + /> + setModalOpen(false)} + onOk={async () => { + const v = await form.validateFields(); + if (editing) await updateDisease(editing.id, v); + else await createDisease(v); + message.success('保存成功'); + setModalOpen(false); + actionRef.current?.reload(); + }} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + ); +} + +function ArticleTab() { + const actionRef = useRef(); + const [modalOpen, setModalOpen] = useState(false); + const [form] = Form.useForm>(); + const [editing, setEditing] = useState(null); + + const columns: ProColumns[] = [ + { title: '序号', valueType: 'indexBorder', search: false, width: 60 }, + { title: '标题', dataIndex: 'title' }, + { + title: '类型', + dataIndex: 'kind', + valueType: 'select', + valueEnum: Object.fromEntries( + Object.entries(KIND_LABELS).map(([k, v]) => [k, { text: v }]), + ), + render: (_, r) => {KIND_LABELS[r.kind] || r.kind}, + }, + { title: '摘要', dataIndex: 'summary', search: false, ellipsis: true, render: (_, r) => r.summary || '-' }, + { title: '排序', dataIndex: 'sortOrder', search: false, width: 70 }, + { + title: '操作', + valueType: 'option', + render: (_, r) => [ + { + setEditing(r); + form.setFieldsValue(r); + setModalOpen(true); + }} + > + 编辑 + , + ...(canWrite() + ? [ + { + await deleteArticle(r.id); + message.success('已删除'); + actionRef.current?.reload(); + }} + > + 删除 + , + ] + : []), + ], + }, + ]; + + const openCreate = () => { + setEditing(null); + form.resetFields(); + form.setFieldsValue({ kind: 'ai_guide', enabled: true, sortOrder: 0 }); + setModalOpen(true); + }; + + return ( + <> + + actionRef={actionRef} + rowKey="id" + columns={columns} + search={{ labelWidth: 'auto' }} + request={async (params) => { + const res = await listArticles({ kind: params.kind }); + return { data: res, total: res.length, success: true }; + }} + toolBarRender={() => + canWrite() + ? [ + , + ] + : [] + } + /> + setModalOpen(false)} + onOk={async () => { + const v = await form.validateFields(); + if (editing) await updateArticle(editing.id, v); + else await createArticle(v); + message.success('保存成功'); + setModalOpen(false); + actionRef.current?.reload(); + }} + > +
+ + + + + + + + + + + + + + + +
+
+ + ); +} + +export default function KnowledgePage() { + return ( + }, + { key: 'articles', label: 'AI 结果解读', children: }, + ]} + /> + ); +} diff --git a/web/src/router.tsx b/web/src/router.tsx index 0fc0264..9e280b1 100644 --- a/web/src/router.tsx +++ b/web/src/router.tsx @@ -10,6 +10,7 @@ import Threshold from './pages/Threshold'; import Alert from './pages/Alert'; import Video from './pages/Video'; import Log from './pages/Log'; +import Knowledge from './pages/Knowledge'; import NotFound from './pages/NotFound'; import { BasicLayout } from './layout/BasicLayout'; import { authService } from './services/auth'; @@ -46,6 +47,7 @@ export const router = createBrowserRouter([ { path: 'alerts', element: }, { path: 'videos', element: }, { path: 'logs', element: }, + { path: 'knowledge', element: }, { path: '404', element: }, ], },