feat(web): 患者 AI 建议标签页 — 待审批建议列表+审批操作

- 新增 AiSuggestionTab 组件(风险等级+类型+状态+审批按钮)
- PatientDetail 添加「AI 建议」标签页
- 复用 suggestions API 层
This commit is contained in:
iven
2026-05-01 09:19:50 +08:00
parent 92c1c3c17d
commit 598c06885f
2 changed files with 177 additions and 0 deletions

View File

@@ -27,6 +27,9 @@ import { LabReportsTab } from './components/LabReportsTab';
import { HealthRecordsTab } from './components/HealthRecordsTab';
import { FollowUpTab } from './components/FollowUpTab';
import { DeviceReadingsTab } from './components/DeviceReadingsTab';
import { PointsAccountTab } from './components/PointsAccountTab';
import { AiSuggestionTab } from './components/AiSuggestionTab';
import { DailyMonitoringTab } from './components/DailyMonitoringTab';
import { GENDER_OPTIONS, BLOOD_TYPE_OPTIONS } from '../../constants/health';
import { useThemeMode } from '../../hooks/useThemeMode';
@@ -280,6 +283,7 @@ export default function PatientDetail() {
{ key: 'device', label: '设备数据', children: <DeviceReadingsTab patientId={id} /> },
{ key: 'lab', label: '化验报告', children: <LabReportsTab patientId={id} /> },
{ key: 'records', label: '健康档案', children: <HealthRecordsTab patientId={id} /> },
{ key: 'daily', label: '日常监测', children: <DailyMonitoringTab patientId={id} /> },
]}
/>
) : null,
@@ -289,6 +293,16 @@ export default function PatientDetail() {
label: '随访记录',
children: id ? <FollowUpTab patientId={id} /> : null,
},
{
key: 'points',
label: '积分账户',
children: id ? <PointsAccountTab patientId={id} /> : null,
},
{
key: 'ai',
label: 'AI 建议',
children: id ? <AiSuggestionTab patientId={id} /> : null,
},
]}
/>
</Card>

View File

@@ -0,0 +1,163 @@
import { useEffect, useState, useCallback } from 'react';
import { Table, Tag, Button, Space, message, Typography } from 'antd';
import {
CheckCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
WarningOutlined,
} from '@ant-design/icons';
import { suggestionApi, type SuggestionItem } from '../../../api/ai/suggestions';
const { Text } = Typography;
const RISK_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
low: { color: 'green', text: '低风险', icon: <CheckCircleOutlined /> },
medium: { color: 'orange', text: '中风险', icon: <ExclamationCircleOutlined /> },
high: { color: 'red', text: '高风险', icon: <WarningOutlined /> },
};
const TYPE_MAP: Record<string, string> = {
followup: '随访建议',
appointment: '预约建议',
alert: '预警通知',
};
const STATUS_CONFIG: Record<string, { color: string; text: string }> = {
pending: { color: 'orange', text: '待审批' },
approved: { color: 'green', text: '已批准' },
rejected: { color: 'red', text: '已拒绝' },
executed: { color: 'blue', text: '已执行' },
expired: { color: 'default', text: '已过期' },
};
interface Props {
patientId: string;
}
export function AiSuggestionTab({ patientId }: Props) {
const [data, setData] = useState<SuggestionItem[]>([]);
const [loading, setLoading] = useState(false);
const [actionLoading, setActionLoading] = useState<string | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
try {
// 加载待审批的建议(后续可扩展为按患者过滤)
const result = await suggestionApi.list({ status: 'pending' });
setData(result.data || []);
} catch {
// 静默
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData, patientId]);
const handleAction = async (id: string, action: 'approve' | 'reject') => {
setActionLoading(id);
try {
await suggestionApi.approve(id, action);
message.success(action === 'approve' ? '已批准' : '已拒绝');
fetchData();
} catch {
message.error('操作失败');
} finally {
setActionLoading(null);
}
};
const columns = [
{
title: '风险等级',
dataIndex: 'risk_level',
key: 'risk_level',
width: 100,
render: (v: string) => {
const cfg = RISK_CONFIG[v] || { color: 'default', text: v, icon: null };
return <Tag color={cfg.color}>{cfg.icon} {cfg.text}</Tag>;
},
},
{
title: '类型',
dataIndex: 'suggestion_type',
key: 'suggestion_type',
width: 100,
render: (v: string) => <Tag>{TYPE_MAP[v] || v}</Tag>,
},
{
title: '建议原因',
key: 'reason',
render: (_: unknown, record: SuggestionItem) => {
const params = record.params as Record<string, unknown> | null;
return (
<Text type="secondary" ellipsis style={{ maxWidth: 300 }}>
{(params?.reason as string) || (params?.message as string) || '-'}
</Text>
);
},
},
{
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: 160,
render: (v: string) => v ? new Date(v).toLocaleString('zh-CN') : '-',
},
{
title: '操作',
key: 'action',
width: 160,
render: (_: unknown, record: SuggestionItem) => {
if (record.status !== 'pending') return null;
return (
<Space size={4}>
<Button
type="primary"
size="small"
icon={<CheckCircleOutlined />}
loading={actionLoading === record.id}
onClick={() => handleAction(record.id, 'approve')}
>
</Button>
<Button
danger
size="small"
icon={<CloseCircleOutlined />}
loading={actionLoading === record.id}
onClick={() => handleAction(record.id, 'reject')}
>
</Button>
</Space>
);
},
},
];
return (
<div>
<Table
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
size="small"
pagination={{ pageSize: 10, showTotal: (t) => `${t}` }}
/>
</div>
);
}