Files
hms/apps/web/src/pages/health/AiAnalysisList.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

202 lines
6.1 KiB
TypeScript

import { useEffect, useState, useCallback } from 'react';
import { Table, Select, Tag, Space, message, Typography } from 'antd';
import { useThemeMode } from '../../hooks/useThemeMode';
import { analysisApi, type AnalysisItem } from '../../api/ai/analysis';
const ANALYSIS_TYPE_MAP: Record<string, string> = {
lab_report_interpretation: '化验单解读',
health_trend_analysis: '趋势分析',
personalized_checkup_plan: '体检方案',
report_summary_generation: '报告摘要',
};
const STATUS_CONFIG: Record<string, { color: string; text: string }> = {
completed: { color: 'green', text: '已完成' },
failed: { color: 'red', text: '失败' },
streaming: { color: 'blue', text: '进行中' },
pending: { color: 'orange', text: '等待中' },
};
const TYPE_OPTIONS = Object.entries(ANALYSIS_TYPE_MAP).map(([value, label]) => ({
value,
label,
}));
export default function AiAnalysisList() {
const [data, setData] = useState<AnalysisItem[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState<{ page: number; page_size: number; analysis_type?: string }>({
page: 1,
page_size: 20,
});
const [expandedId, setExpandedId] = useState<string | null>(null);
const [detail, setDetail] = useState<AnalysisItem | null>(null);
const isDark = useThemeMode();
const fetchData = useCallback(
async (params: { page: number; page_size: number; analysis_type?: string }) => {
setLoading(true);
try {
const result = await analysisApi.list(params);
setData(result.data);
setTotal(result.total);
} catch {
message.error('加载分析历史失败');
} finally {
setLoading(false);
}
},
[],
);
useEffect(() => {
fetchData(query);
}, [query, fetchData]);
const handleExpand = async (expanded: boolean, record: AnalysisItem) => {
if (expanded && record.id !== expandedId) {
try {
const item = await analysisApi.get(record.id);
setDetail(item);
setExpandedId(record.id);
} catch {
// 展开失败不阻塞
}
} else if (!expanded) {
setExpandedId(null);
setDetail(null);
}
};
const columns = [
{
title: '分析类型',
dataIndex: 'analysis_type',
key: 'analysis_type',
width: 130,
render: (v: string) => (
<Tag color="blue">{ANALYSIS_TYPE_MAP[v] || v}</Tag>
),
},
{
title: '患者 ID',
dataIndex: 'patient_id',
key: 'patient_id',
width: 120,
render: (v: string) => (
<span style={{ fontFamily: 'monospace', fontSize: 12 }}>{v.slice(0, 8)}</span>
),
},
{
title: '模型',
dataIndex: 'model_used',
key: 'model_used',
width: 130,
render: (v: string) => v || '-',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 90,
render: (v: string) => {
const cfg = STATUS_CONFIG[v] || { color: 'default', text: v };
return <Tag color={cfg.color}>{cfg.text}</Tag>;
},
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 170,
render: (v: string) => (v ? new Date(v).toLocaleString('zh-CN') : '-'),
},
];
return (
<div>
<div className="erp-page-header">
<div>
<h4>AI </h4>
<div className="erp-page-subtitle"> AI </div>
</div>
<Space size={8}>
<Select
placeholder="筛选类型"
value={query.analysis_type}
onChange={(v) => setQuery((prev) => ({ ...prev, analysis_type: v, page: 1 }))}
options={TYPE_OPTIONS}
allowClear
style={{ width: 150 }}
/>
</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}
expandable={{
expandedRowKeys: expandedId ? [expandedId] : [],
onExpand: handleExpand,
expandedRowRender: () => {
if (!detail) return null;
return (
<div style={{ padding: '8px 0' }}>
{detail.error_message && (
<div style={{ marginBottom: 12 }}>
<Typography.Text type="danger">: {detail.error_message}</Typography.Text>
</div>
)}
{detail.result_content && (
<div>
<Typography.Text strong style={{ display: 'block', marginBottom: 8 }}>
</Typography.Text>
<div
style={{
background: isDark ? '#0f172a' : '#f8fafc',
padding: 16,
borderRadius: 8,
whiteSpace: 'pre-wrap',
fontSize: 13,
lineHeight: 1.8,
maxHeight: 400,
overflow: 'auto',
}}
>
{detail.result_content}
</div>
</div>
)}
{!detail.result_content && !detail.error_message && (
<Typography.Text type="secondary"></Typography.Text>
)}
</div>
);
},
}}
pagination={{
current: query.page,
total,
pageSize: query.page_size,
showSizeChanger: true,
showTotal: (t) => `${t}`,
onChange: (p, ps) => setQuery((prev) => ({ ...prev, page: p, page_size: ps })),
}}
/>
</div>
</div>
);
}