From 22ef9b32d6be89780219d7070a894530f7bd58b8 Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 12 May 2026 22:20:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20CopilotBadge=20+=20CopilotCard=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=20+=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CopilotBadge: 风险评分标签(低/中/高/危急) - CopilotCard: 洞察列表卡片(支持忽略操作) - useCopilotRisk / useCopilotInsights: 数据获取 hooks --- .../src/components/Copilot/CopilotBadge.tsx | 28 +++++++ .../src/components/Copilot/CopilotCard.tsx | 76 +++++++++++++++++++ apps/web/src/components/Copilot/index.ts | 4 + .../components/Copilot/useCopilotInsights.ts | 30 ++++++++ .../src/components/Copilot/useCopilotRisk.ts | 29 +++++++ 5 files changed, 167 insertions(+) create mode 100644 apps/web/src/components/Copilot/CopilotBadge.tsx create mode 100644 apps/web/src/components/Copilot/CopilotCard.tsx create mode 100644 apps/web/src/components/Copilot/index.ts create mode 100644 apps/web/src/components/Copilot/useCopilotInsights.ts create mode 100644 apps/web/src/components/Copilot/useCopilotRisk.ts diff --git a/apps/web/src/components/Copilot/CopilotBadge.tsx b/apps/web/src/components/Copilot/CopilotBadge.tsx new file mode 100644 index 0000000..827a50f --- /dev/null +++ b/apps/web/src/components/Copilot/CopilotBadge.tsx @@ -0,0 +1,28 @@ +import { Tag, Tooltip } from 'antd'; +import type { RiskLevel } from '../../api/copilot'; +import { useCopilotRisk } from './useCopilotRisk'; + +const levelConfig: Record = { + low: { color: 'green', label: '低风险' }, + medium: { color: 'orange', label: '中风险' }, + high: { color: 'red', label: '高风险' }, + critical: { color: '#cf1322', label: '危急' }, +}; + +interface CopilotBadgeProps { + patientId: string | undefined; +} + +export function CopilotBadge({ patientId }: CopilotBadgeProps) { + const { data, loading } = useCopilotRisk(patientId); + + if (!data || loading) return null; + + const config = levelConfig[data.level as RiskLevel] ?? levelConfig.low; + + return ( + + {config.label} {data.score}/10 + + ); +} diff --git a/apps/web/src/components/Copilot/CopilotCard.tsx b/apps/web/src/components/Copilot/CopilotCard.tsx new file mode 100644 index 0000000..24abc5f --- /dev/null +++ b/apps/web/src/components/Copilot/CopilotCard.tsx @@ -0,0 +1,76 @@ +import { Card, List, Tag, Button, Empty, Spin, Typography } from 'antd'; +import { CloseOutlined } from '@ant-design/icons'; +import { useState } from 'react'; +import { dismissInsight } from '../../api/copilot'; +import type { CopilotInsight } from '../../api/copilot'; +import { useCopilotInsights } from './useCopilotInsights'; + +const severityColor: Record = { + info: 'blue', + warning: 'orange', + critical: 'red', +}; + +interface CopilotCardProps { + patientId: string | undefined; +} + +export function CopilotCard({ patientId }: CopilotCardProps) { + const { data, loading, refresh } = useCopilotInsights(patientId); + const [dismissing, setDismissing] = useState(null); + + const handleDismiss = async (id: string) => { + setDismissing(id); + try { + await dismissInsight(id); + refresh(); + } finally { + setDismissing(null); + } + }; + + return ( + + {loading ? ( + + ) : data.length === 0 ? ( + + ) : ( + ( + } + loading={dismissing === item.id} + onClick={() => handleDismiss(item.id)} + />, + ]} + > + + {item.severity} + {item.title} + + } + description={ + item.llm_supplement ? ( + + {item.llm_supplement} + + ) : undefined + } + /> + + )} + /> + )} + + ); +} diff --git a/apps/web/src/components/Copilot/index.ts b/apps/web/src/components/Copilot/index.ts new file mode 100644 index 0000000..9f06213 --- /dev/null +++ b/apps/web/src/components/Copilot/index.ts @@ -0,0 +1,4 @@ +export { CopilotBadge } from './CopilotBadge'; +export { CopilotCard } from './CopilotCard'; +export { useCopilotRisk } from './useCopilotRisk'; +export { useCopilotInsights } from './useCopilotInsights'; diff --git a/apps/web/src/components/Copilot/useCopilotInsights.ts b/apps/web/src/components/Copilot/useCopilotInsights.ts new file mode 100644 index 0000000..e194a21 --- /dev/null +++ b/apps/web/src/components/Copilot/useCopilotInsights.ts @@ -0,0 +1,30 @@ +import { useState, useEffect, useCallback } from 'react'; +import { listInsights } from '../../api/copilot'; +import type { CopilotInsight } from '../../api/copilot'; + +export function useCopilotInsights(patientId: string | undefined) { + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + + const fetch = useCallback(async () => { + if (!patientId) return; + setLoading(true); + try { + const res = await listInsights({ patient_id: patientId, page_size: 20 }); + const result = res.data as unknown as { items: CopilotInsight[]; total: number }; + setData(result.items ?? []); + setTotal(result.total ?? 0); + } catch { + // 静默失败 + } finally { + setLoading(false); + } + }, [patientId]); + + useEffect(() => { + fetch(); + }, [fetch]); + + return { data, total, loading, refresh: fetch }; +} diff --git a/apps/web/src/components/Copilot/useCopilotRisk.ts b/apps/web/src/components/Copilot/useCopilotRisk.ts new file mode 100644 index 0000000..c258127 --- /dev/null +++ b/apps/web/src/components/Copilot/useCopilotRisk.ts @@ -0,0 +1,29 @@ +import { useState, useEffect, useCallback } from 'react'; +import { getPatientRisk } from '../../api/copilot'; +import type { RiskScore } from '../../api/copilot'; + +export function useCopilotRisk(patientId: string | undefined) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetch = useCallback(async () => { + if (!patientId) return; + setLoading(true); + setError(null); + try { + const res = await getPatientRisk(patientId); + setData(res.data as unknown as RiskScore); + } catch (err) { + setError(err instanceof Error ? err.message : '加载风险评分失败'); + } finally { + setLoading(false); + } + }, [patientId]); + + useEffect(() => { + fetch(); + }, [fetch]); + + return { data, loading, error, refresh: fetch }; +}