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:
139
apps/web/src/components/ai/AiAnalysisCard.tsx
Normal file
139
apps/web/src/components/ai/AiAnalysisCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user