feat(web): 知识库页面(蚕病百科 + AI 解读)
This commit is contained in:
@@ -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<Disease[]>('/knowledge/diseases', { params });
|
||||||
|
export const getDisease = (id: string) => get<Disease>(`/knowledge/diseases/${id}`);
|
||||||
|
export const createDisease = (data: Partial<Disease>) => post<Disease>('/knowledge/diseases', data);
|
||||||
|
export const updateDisease = (id: string, data: Partial<Disease>) => patch<Disease>(`/knowledge/diseases/${id}`, data);
|
||||||
|
export const deleteDisease = (id: string) => del(`/knowledge/diseases/${id}`);
|
||||||
|
|
||||||
|
export const listArticles = (params?: any) => get<KnowledgeArticle[]>('/knowledge/articles', { params });
|
||||||
|
export const createArticle = (data: Partial<KnowledgeArticle>) => post<KnowledgeArticle>('/knowledge/articles', data);
|
||||||
|
export const updateArticle = (id: string, data: Partial<KnowledgeArticle>) => patch<KnowledgeArticle>(`/knowledge/articles/${id}`, data);
|
||||||
|
export const deleteArticle = (id: string) => del(`/knowledge/articles/${id}`);
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
BellOutlined,
|
BellOutlined,
|
||||||
VideoCameraOutlined,
|
VideoCameraOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
|
BookOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
@@ -30,6 +31,7 @@ const menuData = [
|
|||||||
{ path: '/alerts', name: '告警中心', icon: <BellOutlined />, permission: 'alarm:read' },
|
{ path: '/alerts', name: '告警中心', icon: <BellOutlined />, permission: 'alarm:read' },
|
||||||
{ path: '/videos', name: '视频监控', icon: <VideoCameraOutlined />, permission: 'video:read' },
|
{ path: '/videos', name: '视频监控', icon: <VideoCameraOutlined />, permission: 'video:read' },
|
||||||
{ path: '/logs', name: '控制日志', icon: <FileTextOutlined />, permission: 'log:read' },
|
{ path: '/logs', name: '控制日志', icon: <FileTextOutlined />, permission: 'log:read' },
|
||||||
|
{ path: '/knowledge', name: '知识库', icon: <BookOutlined />, permission: 'knowledge:read' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 角色中文名
|
// 角色中文名
|
||||||
|
|||||||
@@ -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<string, string> = {
|
||||||
|
viral: '病毒性',
|
||||||
|
fungal: '真菌性',
|
||||||
|
bacterial: '细菌性',
|
||||||
|
protozoan: '原虫性',
|
||||||
|
other: '其他',
|
||||||
|
};
|
||||||
|
|
||||||
|
const KIND_LABELS: Record<string, string> = {
|
||||||
|
ai_guide: 'AI 结果解读',
|
||||||
|
lamp_guide: 'LAMP 教程',
|
||||||
|
sers_guide: 'SERS 教程',
|
||||||
|
seasonal_tip: '季节性提醒',
|
||||||
|
};
|
||||||
|
|
||||||
|
const canWrite = () => authService.hasPermission('knowledge:write');
|
||||||
|
|
||||||
|
function DiseaseTab() {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [form] = Form.useForm<Partial<Disease>>();
|
||||||
|
const [editing, setEditing] = useState<Disease | null>(null);
|
||||||
|
|
||||||
|
const columns: ProColumns<Disease>[] = [
|
||||||
|
{ 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) => <Tag>{CATEGORY_LABELS[r.category] || r.category}</Tag>,
|
||||||
|
},
|
||||||
|
{ 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 ? <Tag color="default">停用</Tag> : <Tag color="green">启用</Tag>),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
valueType: 'option',
|
||||||
|
render: (_, r) => [
|
||||||
|
<a
|
||||||
|
key="edit"
|
||||||
|
onClick={() => {
|
||||||
|
setEditing(r);
|
||||||
|
form.setFieldsValue(r);
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</a>,
|
||||||
|
...(canWrite()
|
||||||
|
? [
|
||||||
|
<Popconfirm
|
||||||
|
key="del"
|
||||||
|
title="确认删除该病种?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteDisease(r.id);
|
||||||
|
message.success('已删除');
|
||||||
|
actionRef.current?.reload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a style={{ color: '#ff4d4f' }}>删除</a>
|
||||||
|
</Popconfirm>,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
setEditing(null);
|
||||||
|
form.resetFields();
|
||||||
|
form.setFieldsValue({ category: 'viral', enabled: true, sortOrder: 0 });
|
||||||
|
setModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProTable<Disease>
|
||||||
|
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()
|
||||||
|
? [
|
||||||
|
<Button key="new" type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||||
|
新建病种
|
||||||
|
</Button>,
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
title={editing ? '编辑病种' : '新建病种'}
|
||||||
|
open={modalOpen}
|
||||||
|
width={720}
|
||||||
|
onCancel={() => 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();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
|
||||||
|
<Input placeholder="如:核型多角体病" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="类别" name="category" rules={[{ required: true, message: '请选择类别' }]}>
|
||||||
|
<Select
|
||||||
|
options={Object.entries(CATEGORY_LABELS).map(([value, label]) => ({ value, label }))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="病原" name="pathogen">
|
||||||
|
<Input.TextArea rows={2} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="传播途径" name="transmission">
|
||||||
|
<Input.TextArea rows={2} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="传染媒介" name="medium">
|
||||||
|
<Input.TextArea rows={2} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="潜伏期" name="incubation">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="主要症状" name="symptoms">
|
||||||
|
<Input.TextArea rows={3} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="高发阶段" name="highRiskStage">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="高发条件" name="highRiskCondition">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="致死时间" name="lethalTime">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="传播趋势" name="spreadTrend">
|
||||||
|
<Input.TextArea rows={2} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="连发性特征" name="recurrence">
|
||||||
|
<Input.TextArea rows={2} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="检测方法" name="detection">
|
||||||
|
<Input.TextArea rows={2} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="防治指南" name="prevention">
|
||||||
|
<Input.TextArea rows={4} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="排序" name="sortOrder">
|
||||||
|
<InputNumber min={0} style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="启用" name="enabled" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ArticleTab() {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [form] = Form.useForm<Partial<KnowledgeArticle>>();
|
||||||
|
const [editing, setEditing] = useState<KnowledgeArticle | null>(null);
|
||||||
|
|
||||||
|
const columns: ProColumns<KnowledgeArticle>[] = [
|
||||||
|
{ 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) => <Tag>{KIND_LABELS[r.kind] || r.kind}</Tag>,
|
||||||
|
},
|
||||||
|
{ title: '摘要', dataIndex: 'summary', search: false, ellipsis: true, render: (_, r) => r.summary || '-' },
|
||||||
|
{ title: '排序', dataIndex: 'sortOrder', search: false, width: 70 },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
valueType: 'option',
|
||||||
|
render: (_, r) => [
|
||||||
|
<a
|
||||||
|
key="edit"
|
||||||
|
onClick={() => {
|
||||||
|
setEditing(r);
|
||||||
|
form.setFieldsValue(r);
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</a>,
|
||||||
|
...(canWrite()
|
||||||
|
? [
|
||||||
|
<Popconfirm
|
||||||
|
key="del"
|
||||||
|
title="确认删除该文章?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteArticle(r.id);
|
||||||
|
message.success('已删除');
|
||||||
|
actionRef.current?.reload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a style={{ color: '#ff4d4f' }}>删除</a>
|
||||||
|
</Popconfirm>,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
setEditing(null);
|
||||||
|
form.resetFields();
|
||||||
|
form.setFieldsValue({ kind: 'ai_guide', enabled: true, sortOrder: 0 });
|
||||||
|
setModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProTable<KnowledgeArticle>
|
||||||
|
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()
|
||||||
|
? [
|
||||||
|
<Button key="new" type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||||
|
新建文章
|
||||||
|
</Button>,
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
title={editing ? '编辑文章' : '新建文章'}
|
||||||
|
open={modalOpen}
|
||||||
|
width={720}
|
||||||
|
onCancel={() => 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();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item label="类型" name="kind" rules={[{ required: true, message: '请选择类型' }]}>
|
||||||
|
<Select
|
||||||
|
options={Object.entries(KIND_LABELS).map(([value, label]) => ({ value, label }))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题' }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="摘要" name="summary">
|
||||||
|
<Input.TextArea rows={2} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="正文" name="content">
|
||||||
|
<Input.TextArea rows={8} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="排序" name="sortOrder">
|
||||||
|
<InputNumber min={0} style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="启用" name="enabled" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function KnowledgePage() {
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey="diseases"
|
||||||
|
items={[
|
||||||
|
{ key: 'diseases', label: '蚕病百科', children: <DiseaseTab /> },
|
||||||
|
{ key: 'articles', label: 'AI 结果解读', children: <ArticleTab /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import Threshold from './pages/Threshold';
|
|||||||
import Alert from './pages/Alert';
|
import Alert from './pages/Alert';
|
||||||
import Video from './pages/Video';
|
import Video from './pages/Video';
|
||||||
import Log from './pages/Log';
|
import Log from './pages/Log';
|
||||||
|
import Knowledge from './pages/Knowledge';
|
||||||
import NotFound from './pages/NotFound';
|
import NotFound from './pages/NotFound';
|
||||||
import { BasicLayout } from './layout/BasicLayout';
|
import { BasicLayout } from './layout/BasicLayout';
|
||||||
import { authService } from './services/auth';
|
import { authService } from './services/auth';
|
||||||
@@ -46,6 +47,7 @@ export const router = createBrowserRouter([
|
|||||||
{ path: 'alerts', element: <RequirePermission permission="alarm:read"><Alert /></RequirePermission> },
|
{ path: 'alerts', element: <RequirePermission permission="alarm:read"><Alert /></RequirePermission> },
|
||||||
{ path: 'videos', element: <RequirePermission permission="video:read"><Video /></RequirePermission> },
|
{ path: 'videos', element: <RequirePermission permission="video:read"><Video /></RequirePermission> },
|
||||||
{ path: 'logs', element: <RequirePermission permission="log:read"><Log /></RequirePermission> },
|
{ path: 'logs', element: <RequirePermission permission="log:read"><Log /></RequirePermission> },
|
||||||
|
{ path: 'knowledge', element: <RequirePermission permission="knowledge:read"><Knowledge /></RequirePermission> },
|
||||||
{ path: '404', element: <NotFound /> },
|
{ path: '404', element: <NotFound /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user