- API 封装: prompts.ts / analysis.ts / usage.ts - AiPromptList: CRUD + 激活/回滚 + AuthButton 权限 - AiAnalysisList: 历史列表 + 行展开查看结果 - AiUsageDashboard: 总次数/类型分布统计卡片 - 菜单注册 + 路由配置 (MainLayout + App.tsx)
202 lines
6.1 KiB
TypeScript
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>
|
|
);
|
|
}
|