From 598c06885fd39ef1e8b42876a707193f91c0dab1 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 1 May 2026 09:19:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=82=A3=E8=80=85=20AI=20?= =?UTF-8?q?=E5=BB=BA=E8=AE=AE=E6=A0=87=E7=AD=BE=E9=A1=B5=20=E2=80=94=20?= =?UTF-8?q?=E5=BE=85=E5=AE=A1=E6=89=B9=E5=BB=BA=E8=AE=AE=E5=88=97=E8=A1=A8?= =?UTF-8?q?+=E5=AE=A1=E6=89=B9=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AiSuggestionTab 组件(风险等级+类型+状态+审批按钮) - PatientDetail 添加「AI 建议」标签页 - 复用 suggestions API 层 --- apps/web/src/pages/health/PatientDetail.tsx | 14 ++ .../health/components/AiSuggestionTab.tsx | 163 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 apps/web/src/pages/health/components/AiSuggestionTab.tsx diff --git a/apps/web/src/pages/health/PatientDetail.tsx b/apps/web/src/pages/health/PatientDetail.tsx index 29d2277..4b35286 100644 --- a/apps/web/src/pages/health/PatientDetail.tsx +++ b/apps/web/src/pages/health/PatientDetail.tsx @@ -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: }, { key: 'lab', label: '化验报告', children: }, { key: 'records', label: '健康档案', children: }, + { key: 'daily', label: '日常监测', children: }, ]} /> ) : null, @@ -289,6 +293,16 @@ export default function PatientDetail() { label: '随访记录', children: id ? : null, }, + { + key: 'points', + label: '积分账户', + children: id ? : null, + }, + { + key: 'ai', + label: 'AI 建议', + children: id ? : null, + }, ]} /> diff --git a/apps/web/src/pages/health/components/AiSuggestionTab.tsx b/apps/web/src/pages/health/components/AiSuggestionTab.tsx new file mode 100644 index 0000000..7d161da --- /dev/null +++ b/apps/web/src/pages/health/components/AiSuggestionTab.tsx @@ -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 = { + low: { color: 'green', text: '低风险', icon: }, + medium: { color: 'orange', text: '中风险', icon: }, + high: { color: 'red', text: '高风险', icon: }, +}; + +const TYPE_MAP: Record = { + followup: '随访建议', + appointment: '预约建议', + alert: '预警通知', +}; + +const STATUS_CONFIG: Record = { + 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([]); + const [loading, setLoading] = useState(false); + const [actionLoading, setActionLoading] = useState(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 {cfg.icon} {cfg.text}; + }, + }, + { + title: '类型', + dataIndex: 'suggestion_type', + key: 'suggestion_type', + width: 100, + render: (v: string) => {TYPE_MAP[v] || v}, + }, + { + title: '建议原因', + key: 'reason', + render: (_: unknown, record: SuggestionItem) => { + const params = record.params as Record | null; + return ( + + {(params?.reason as string) || (params?.message as string) || '-'} + + ); + }, + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 90, + render: (v: string) => { + const cfg = STATUS_CONFIG[v] || { color: 'default', text: v }; + return {cfg.text}; + }, + }, + { + 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 ( + + + + + ); + }, + }, + ]; + + return ( +
+ `共 ${t} 条` }} + /> + + ); +}