feat(web): CopilotBadge + CopilotCard 组件 + hooks
- CopilotBadge: 风险评分标签(低/中/高/危急) - CopilotCard: 洞察列表卡片(支持忽略操作) - useCopilotRisk / useCopilotInsights: 数据获取 hooks
This commit is contained in:
28
apps/web/src/components/Copilot/CopilotBadge.tsx
Normal file
28
apps/web/src/components/Copilot/CopilotBadge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
76
apps/web/src/components/Copilot/CopilotCard.tsx
Normal file
76
apps/web/src/components/Copilot/CopilotCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
4
apps/web/src/components/Copilot/index.ts
Normal file
4
apps/web/src/components/Copilot/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { CopilotBadge } from './CopilotBadge';
|
||||
export { CopilotCard } from './CopilotCard';
|
||||
export { useCopilotRisk } from './useCopilotRisk';
|
||||
export { useCopilotInsights } from './useCopilotInsights';
|
||||
30
apps/web/src/components/Copilot/useCopilotInsights.ts
Normal file
30
apps/web/src/components/Copilot/useCopilotInsights.ts
Normal 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 };
|
||||
}
|
||||
29
apps/web/src/components/Copilot/useCopilotRisk.ts
Normal file
29
apps/web/src/components/Copilot/useCopilotRisk.ts
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user