feat(web): CopilotBadge + CopilotCard 组件 + hooks

- CopilotBadge: 风险评分标签(低/中/高/危急)
- CopilotCard: 洞察列表卡片(支持忽略操作)
- useCopilotRisk / useCopilotInsights: 数据获取 hooks
This commit is contained in:
iven
2026-05-12 22:20:56 +08:00
parent cba8c8306d
commit 22ef9b32d6
5 changed files with 167 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
import { Tag, Tooltip } from 'antd';
import type { RiskLevel } from '../../api/copilot';
import { useCopilotRisk } from './useCopilotRisk';
const levelConfig: Record<RiskLevel, { color: string; label: string }> = {
low: { color: 'green', label: '低风险' },
medium: { color: 'orange', label: '中风险' },
high: { color: 'red', label: '高风险' },
critical: { color: '#cf1322', label: '危急' },
};
interface CopilotBadgeProps {
patientId: string | undefined;
}
export function CopilotBadge({ patientId }: CopilotBadgeProps) {
const { data, loading } = useCopilotRisk(patientId);
if (!data || loading) return null;
const config = levelConfig[data.level as RiskLevel] ?? levelConfig.low;
return (
<Tooltip title={`Copilot 风险评分: ${data.score}/10 — ${config.label}`}>
<Tag color={config.color}>{config.label} {data.score}/10</Tag>
</Tooltip>
);
}

View File

@@ -0,0 +1,76 @@
import { Card, List, Tag, Button, Empty, Spin, Typography } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import { useState } from 'react';
import { dismissInsight } from '../../api/copilot';
import type { CopilotInsight } from '../../api/copilot';
import { useCopilotInsights } from './useCopilotInsights';
const severityColor: Record<string, string> = {
info: 'blue',
warning: 'orange',
critical: 'red',
};
interface CopilotCardProps {
patientId: string | undefined;
}
export function CopilotCard({ patientId }: CopilotCardProps) {
const { data, loading, refresh } = useCopilotInsights(patientId);
const [dismissing, setDismissing] = useState<string | null>(null);
const handleDismiss = async (id: string) => {
setDismissing(id);
try {
await dismissInsight(id);
refresh();
} finally {
setDismissing(null);
}
};
return (
<Card title="Copilot 洞察" size="small" style={{ marginBottom: 16 }}>
{loading ? (
<Spin />
) : data.length === 0 ? (
<Empty description="暂无洞察" image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : (
<List
size="small"
dataSource={data}
renderItem={(item: CopilotInsight) => (
<List.Item
actions={[
<Button
key="dismiss"
type="text"
size="small"
icon={<CloseOutlined />}
loading={dismissing === item.id}
onClick={() => handleDismiss(item.id)}
/>,
]}
>
<List.Item.Meta
title={
<span>
<Tag color={severityColor[item.severity] ?? 'default'}>{item.severity}</Tag>
{item.title}
</span>
}
description={
item.llm_supplement ? (
<Typography.Paragraph type="secondary" ellipsis={{ rows: 2 }}>
{item.llm_supplement}
</Typography.Paragraph>
) : undefined
}
/>
</List.Item>
)}
/>
)}
</Card>
);
}

View File

@@ -0,0 +1,4 @@
export { CopilotBadge } from './CopilotBadge';
export { CopilotCard } from './CopilotCard';
export { useCopilotRisk } from './useCopilotRisk';
export { useCopilotInsights } from './useCopilotInsights';

View File

@@ -0,0 +1,30 @@
import { useState, useEffect, useCallback } from 'react';
import { listInsights } from '../../api/copilot';
import type { CopilotInsight } from '../../api/copilot';
export function useCopilotInsights(patientId: string | undefined) {
const [data, setData] = useState<CopilotInsight[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const fetch = useCallback(async () => {
if (!patientId) return;
setLoading(true);
try {
const res = await listInsights({ patient_id: patientId, page_size: 20 });
const result = res.data as unknown as { items: CopilotInsight[]; total: number };
setData(result.items ?? []);
setTotal(result.total ?? 0);
} catch {
// 静默失败
} finally {
setLoading(false);
}
}, [patientId]);
useEffect(() => {
fetch();
}, [fetch]);
return { data, total, loading, refresh: fetch };
}

View File

@@ -0,0 +1,29 @@
import { useState, useEffect, useCallback } from 'react';
import { getPatientRisk } from '../../api/copilot';
import type { RiskScore } from '../../api/copilot';
export function useCopilotRisk(patientId: string | undefined) {
const [data, setData] = useState<RiskScore | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetch = useCallback(async () => {
if (!patientId) return;
setLoading(true);
setError(null);
try {
const res = await getPatientRisk(patientId);
setData(res.data as unknown as RiskScore);
} catch (err) {
setError(err instanceof Error ? err.message : '加载风险评分失败');
} finally {
setLoading(false);
}
}, [patientId]);
useEffect(() => {
fetch();
}, [fetch]);
return { data, loading, error, refresh: fetch };
}