feat(web): AiAnalysisCard 通用组件 — 封装 SSE 流式分析 + 加载/错误/成功态

- 新增 AiAnalysisCard: 支持 4 种分析类型, 自动组装 request body
- 4 种状态: idle(按钮) / loading(骨架屏) / error(重试) / success(结果卡片)
- 权限控制通过 AuthButton 集成, 供多触点复用

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iven
2026-05-18 22:58:51 +08:00
parent bf37acc681
commit 281c71ebfc

View File

@@ -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<AnalysisState>('idle');
const [content, setContent] = useState('');
const [errorMsg, setErrorMsg] = useState('');
const handleStart = useCallback(async () => {
setState('loading');
setContent('');
setErrorMsg('');
const body: Record<string, unknown> = {};
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 ? (
<AuthButton
code={permission}
icon={<ThunderboltOutlined />}
loading={state === 'loading'}
onClick={handleStart}
size="small"
>
{triggerLabel}
</AuthButton>
) : (
<Button
icon={<ThunderboltOutlined />}
loading={state === 'loading'}
onClick={handleStart}
size="small"
>
{triggerLabel}
</Button>
);
if (state === 'idle') {
return TriggerButton;
}
if (state === 'loading') {
return (
<Card size="small" style={{ marginTop: 12 }}>
<div style={{ textAlign: 'center', padding: '24px 0' }}>
<Spin indicator={<ThunderboltOutlined spin style={{ fontSize: 24 }} />} />
<div style={{ marginTop: 8, color: 'var(--ant-color-text-secondary)' }}>
AI ...
</div>
</div>
</Card>
);
}
if (state === 'error') {
return (
<Card size="small" style={{ marginTop: 12 }}>
<Alert
type="error"
message={errorMsg}
showIcon
action={
<Button size="small" icon={<ReloadOutlined />} onClick={handleStart}>
</Button>
}
/>
</Card>
);
}
if (!content) {
return <Empty description="分析结果为空" />;
}
return (
<Card
title={<><ThunderboltOutlined /> {triggerLabel}</>}
size="small"
style={{ marginTop: 12 }}
extra={
<Button size="small" onClick={handleReset}></Button>
}
>
<div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.8, fontSize: 14 }}>
{content}
</div>
</Card>
);
}