Files
hms/apps/web/src/components/ai/AiAnalysisCard.tsx
iven f3bf8b3b1d fix: DTO 输入校验补全 + 编译修复 + AuthButton 类型修复
- erp-auth/config/workflow/message/plugin/health: 44 处 DTO 校验缺失修复
- erp-plugin/data_dto: utoipa derive 宏 import 修复
- erp-server/main: tracing 宏类型推断修复
- web AuthButton: AiAnalysisCard/VitalSignsTab Button 包裹在 children 内

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 06:58:54 +08:00

146 lines
3.6 KiB
TypeScript

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[];
taskId?: string;
}
type AnalysisState = 'idle' | 'loading' | 'success' | 'error';
export function AiAnalysisCard({
analysisType,
sourceRef,
triggerLabel = 'AI 分析',
permission = 'ai.analysis.manage',
metrics,
taskId,
}: 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 (analysisType === 'follow-up-summary') {
body.source_id = taskId || 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, taskId]);
const handleReset = useCallback(() => {
setState('idle');
setContent('');
setErrorMsg('');
}, []);
const TriggerButton = permission ? (
<AuthButton code={permission}>
<Button
icon={<ThunderboltOutlined />}
loading={state === 'loading'}
onClick={handleStart}
size="small"
>
{triggerLabel}
</Button>
</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>
);
}