From 281c71ebfc34c59edcbfdb65ff421b67ebfe008f Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 18 May 2026 22:58:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20AiAnalysisCard=20=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E7=BB=84=E4=BB=B6=20=E2=80=94=20=E5=B0=81=E8=A3=85=20?= =?UTF-8?q?SSE=20=E6=B5=81=E5=BC=8F=E5=88=86=E6=9E=90=20+=20=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD/=E9=94=99=E8=AF=AF/=E6=88=90=E5=8A=9F=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AiAnalysisCard: 支持 4 种分析类型, 自动组装 request body - 4 种状态: idle(按钮) / loading(骨架屏) / error(重试) / success(结果卡片) - 权限控制通过 AuthButton 集成, 供多触点复用 Co-Authored-By: Claude Opus 4.7 --- apps/web/src/components/ai/AiAnalysisCard.tsx | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 apps/web/src/components/ai/AiAnalysisCard.tsx diff --git a/apps/web/src/components/ai/AiAnalysisCard.tsx b/apps/web/src/components/ai/AiAnalysisCard.tsx new file mode 100644 index 0000000..ccfb8d6 --- /dev/null +++ b/apps/web/src/components/ai/AiAnalysisCard.tsx @@ -0,0 +1,139 @@ +import { useState, useCallback } from 'react'; +import { Button, Card, Spin, Empty, Alert } from 'antd'; +import { ThunderboltOutlined, ReloadOutlined } from '@ant-design/icons'; +import { startAnalysis, type AnalysisType } from '../../api/ai/analysisSse'; +import { AuthButton } from '../AuthButton'; + +export interface AiAnalysisCardProps { + analysisType: AnalysisType; + sourceRef: string; + patientId?: string; + triggerLabel?: string; + permission?: string; + metrics?: string[]; +} + +type AnalysisState = 'idle' | 'loading' | 'success' | 'error'; + +export function AiAnalysisCard({ + analysisType, + sourceRef, + triggerLabel = 'AI 分析', + permission = 'ai.analysis.manage', + metrics, +}: AiAnalysisCardProps) { + const [state, setState] = useState('idle'); + const [content, setContent] = useState(''); + const [errorMsg, setErrorMsg] = useState(''); + + const handleStart = useCallback(async () => { + setState('loading'); + setContent(''); + setErrorMsg(''); + + const body: Record = {}; + if (analysisType === 'lab-report' || analysisType === 'report-summary') { + body.report_id = sourceRef; + } + if (analysisType === 'trends' || analysisType === 'checkup-plan') { + body.patient_id = sourceRef; + } + if (metrics) { + body.metrics = metrics; + } + + try { + await startAnalysis(analysisType, body, { + onChunk: (chunk) => setContent(prev => prev + chunk), + onError: (msg) => { + setErrorMsg(msg); + setState('error'); + }, + onDone: () => setState('success'), + }); + } catch { + setErrorMsg('分析请求失败'); + setState('error'); + } + }, [analysisType, sourceRef, metrics]); + + const handleReset = useCallback(() => { + setState('idle'); + setContent(''); + setErrorMsg(''); + }, []); + + const TriggerButton = permission ? ( + } + loading={state === 'loading'} + onClick={handleStart} + size="small" + > + {triggerLabel} + + ) : ( + + ); + + if (state === 'idle') { + return TriggerButton; + } + + if (state === 'loading') { + return ( + +
+ } /> +
+ AI 正在分析... +
+
+
+ ); + } + + if (state === 'error') { + return ( + + } onClick={handleStart}> + 重试 + + } + /> + + ); + } + + if (!content) { + return ; + } + + return ( + {triggerLabel}结果} + size="small" + style={{ marginTop: 12 }} + extra={ + + } + > +
+ {content} +
+
+ ); +}