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