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