feat(web): 患者 AI 建议标签页 — 待审批建议列表+审批操作
- 新增 AiSuggestionTab 组件(风险等级+类型+状态+审批按钮) - PatientDetail 添加「AI 建议」标签页 - 复用 suggestions API 层
This commit is contained in:
@@ -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>
|
||||
|
||||
163
apps/web/src/pages/health/components/AiSuggestionTab.tsx
Normal file
163
apps/web/src/pages/health/components/AiSuggestionTab.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user