Files
hms/apps/web/src/pages/health/AiPromptList.tsx
iven 5621dbe273
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
feat(web): AI 管理端 3 页面 — Prompt/分析历史/用量统计
- API 封装: prompts.ts / analysis.ts / usage.ts
- AiPromptList: CRUD + 激活/回滚 + AuthButton 权限
- AiAnalysisList: 历史列表 + 行展开查看结果
- AiUsageDashboard: 总次数/类型分布统计卡片
- 菜单注册 + 路由配置 (MainLayout + App.tsx)
2026-04-25 23:44:15 +08:00

275 lines
7.5 KiB
TypeScript

import { useEffect, useState, useCallback } from 'react';
import {
Table,
Button,
Space,
Modal,
Form,
Input,
Select,
Tag,
message,
} from 'antd';
import { PlusOutlined, UndoOutlined, CheckOutlined } from '@ant-design/icons';
import { promptApi, type PromptItem, type CreatePromptReq } from '../../api/ai/prompts';
import { AuthButton } from '../../components/AuthButton';
import { useThemeMode } from '../../hooks/useThemeMode';
const CATEGORIES = [
{ value: 'lab_report_interpretation', label: '化验单解读' },
{ value: 'health_trend_analysis', label: '趋势分析' },
{ value: 'personalized_checkup_plan', label: '体检方案' },
{ value: 'report_summary_generation', label: '报告摘要' },
];
const CATEGORY_MAP: Record<string, string> = Object.fromEntries(
CATEGORIES.map((c) => [c.value, c.label]),
);
export default function AiPromptList() {
const [data, setData] = useState<PromptItem[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [categoryFilter, setCategoryFilter] = useState<string | undefined>();
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
const isDark = useThemeMode();
const fetchData = useCallback(
async (p = page) => {
setLoading(true);
try {
const result = await promptApi.list({
page: p,
page_size: 20,
category: categoryFilter,
});
setData(result.data);
setTotal(result.total);
} catch {
message.error('加载 Prompt 列表失败');
} finally {
setLoading(false);
}
},
[page, categoryFilter],
);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleCreate = async (values: CreatePromptReq) => {
try {
await promptApi.create(values);
message.success('Prompt 创建成功');
setModalOpen(false);
form.resetFields();
fetchData();
} catch {
message.error('创建失败');
}
};
const handleActivate = async (id: string) => {
try {
await promptApi.activate(id);
message.success('已激活');
fetchData();
} catch {
message.error('激活失败');
}
};
const handleRollback = async (id: string) => {
try {
await promptApi.rollback(id);
message.success('已回滚');
fetchData();
} catch {
message.error('回滚失败');
}
};
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 180,
render: (name: string) => <span style={{ fontWeight: 500 }}>{name}</span>,
},
{
title: '类别',
dataIndex: 'category',
key: 'category',
width: 120,
render: (v: string) => (
<Tag color="blue">{CATEGORY_MAP[v] || v}</Tag>
),
},
{
title: '版本',
dataIndex: 'version',
key: 'version',
width: 70,
render: (v: number) => `v${v}`,
},
{
title: '状态',
dataIndex: 'is_active',
key: 'is_active',
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('zh-CN') : '-'),
},
{
title: '操作',
key: 'actions',
width: 160,
render: (_: unknown, record: PromptItem) => (
<AuthButton code="ai.prompt.manage">
<Space size={4}>
{!record.is_active && (
<Button
type="link"
size="small"
icon={<CheckOutlined />}
onClick={() => handleActivate(record.id)}
>
</Button>
)}
<Button
type="link"
size="small"
icon={<UndoOutlined />}
onClick={() => handleRollback(record.id)}
>
</Button>
</Space>
</AuthButton>
),
},
];
return (
<div>
<div className="erp-page-header">
<div>
<h4>AI Prompt </h4>
<div className="erp-page-subtitle"> AI </div>
</div>
<Space size={8}>
<Select
placeholder="筛选类别"
value={categoryFilter}
onChange={(v) => {
setCategoryFilter(v);
setPage(1);
}}
options={CATEGORIES}
allowClear
style={{ width: 150 }}
/>
<AuthButton code="ai.prompt.manage">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
form.resetFields();
setModalOpen(true);
}}
>
Prompt
</Button>
</AuthButton>
</Space>
</div>
<div
style={{
background: isDark ? '#111827' : '#FFFFFF',
borderRadius: 12,
border: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
overflow: 'hidden',
}}
>
<Table
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={{
current: page,
total,
pageSize: 20,
onChange: (p) => {
setPage(p);
fetchData(p);
},
showTotal: (t) => `${t} 条记录`,
style: { padding: '12px 16px', margin: 0 },
}}
/>
</div>
<Modal
title="新建 Prompt"
open={modalOpen}
onCancel={() => setModalOpen(false)}
onOk={() => form.submit()}
width={600}
destroyOnClose
>
<Form form={form} onFinish={handleCreate} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item
name="name"
label="名称"
rules={[{ required: true, message: '请输入 Prompt 名称' }]}
>
<Input placeholder="如:化验单解读 V2" />
</Form.Item>
<Form.Item name="category" label="类别" rules={[{ required: true, message: '请选择类别' }]}>
<Select options={CATEGORIES} placeholder="选择类别" />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={2} placeholder="Prompt 用途说明" />
</Form.Item>
<Form.Item
name="system_prompt"
label="System Prompt"
rules={[{ required: true, message: '请输入 System Prompt' }]}
>
<Input.TextArea rows={4} placeholder="系统提示词" />
</Form.Item>
<Form.Item
name="user_prompt_template"
label="User Prompt 模板"
rules={[{ required: true, message: '请输入 User Prompt 模板' }]}
>
<Input.TextArea rows={4} placeholder="用户提示词模板,可用 {{变量}} 占位" />
</Form.Item>
<Form.Item
name="model_config"
label="模型配置 (JSON)"
initialValue={{ model: 'deepseek-chat', temperature: 0.7 }}
>
<Input.TextArea rows={3} placeholder='{"model": "deepseek-chat", "temperature": 0.7}' />
</Form.Item>
</Form>
</Modal>
</div>
);
}