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