feat(web): 工作台前端 API 客户端 + TodoList 组件
- actionInbox.ts 新增 WorkbenchStats/TeamOverview 类型和 stats()/team() API - 新建 workbench/TodoList.tsx 待办列表组件(分页 + 类型/优先级标签)
This commit is contained in:
@@ -42,6 +42,35 @@ export interface ThreadResponse {
|
|||||||
available_actions: ActionDefinition[];
|
available_actions: ActionDefinition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkbenchStats {
|
||||||
|
total_pending: number;
|
||||||
|
ai_suggestion_pending: number;
|
||||||
|
urgent_alerts: number;
|
||||||
|
followup_due: number;
|
||||||
|
completion_rate: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamMemberOverview {
|
||||||
|
user_id: string;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
pending_count: number;
|
||||||
|
completed_count: number;
|
||||||
|
overdue_count: number;
|
||||||
|
completion_rate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamOverview {
|
||||||
|
members: TeamMemberOverview[];
|
||||||
|
risk_distribution: {
|
||||||
|
high: number;
|
||||||
|
medium: number;
|
||||||
|
low: number;
|
||||||
|
};
|
||||||
|
total_pending: number;
|
||||||
|
total_completed: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const actionInboxApi = {
|
export const actionInboxApi = {
|
||||||
list: async (params?: {
|
list: async (params?: {
|
||||||
status?: string;
|
status?: string;
|
||||||
@@ -63,4 +92,20 @@ export const actionInboxApi = {
|
|||||||
}>(`/health/action-inbox/${encodeURIComponent(sourceRef)}/thread`);
|
}>(`/health/action-inbox/${encodeURIComponent(sourceRef)}/thread`);
|
||||||
return data.data;
|
return data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
stats: async () => {
|
||||||
|
const { data } = await client.get<{
|
||||||
|
success: boolean;
|
||||||
|
data: WorkbenchStats;
|
||||||
|
}>('/health/action-inbox/stats');
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
team: async () => {
|
||||||
|
const { data } = await client.get<{
|
||||||
|
success: boolean;
|
||||||
|
data: TeamOverview;
|
||||||
|
}>('/health/action-inbox/team');
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
103
apps/web/src/pages/health/components/workbench/TodoList.tsx
Normal file
103
apps/web/src/pages/health/components/workbench/TodoList.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { List, Tag, Empty, Spin, Button, Space } from 'antd';
|
||||||
|
import {
|
||||||
|
BellOutlined,
|
||||||
|
RobotOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
WarningOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { actionInboxApi, type ActionItem, type ActionType } from '../../../../api/health/actionInbox';
|
||||||
|
|
||||||
|
const TYPE_CONFIG: Record<ActionType, { label: string; color: string; icon: React.ReactNode }> = {
|
||||||
|
ai_suggestion: { label: 'AI 建议', color: 'blue', icon: <RobotOutlined /> },
|
||||||
|
alert: { label: '告警', color: 'red', icon: <WarningOutlined /> },
|
||||||
|
followup: { label: '随访', color: 'green', icon: <TeamOutlined /> },
|
||||||
|
data_anomaly: { label: '数据异常', color: 'orange', icon: <BellOutlined /> },
|
||||||
|
};
|
||||||
|
|
||||||
|
const PRIORITY_COLOR: Record<string, string> = {
|
||||||
|
urgent: 'red',
|
||||||
|
high: 'volcano',
|
||||||
|
medium: 'orange',
|
||||||
|
low: 'default',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TodoListProps {
|
||||||
|
onItemClick?: (item: ActionItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TodoList({ onItemClick }: TodoListProps) {
|
||||||
|
const [items, setItems] = useState<ActionItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
|
||||||
|
const fetchItems = useCallback(async (p: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await actionInboxApi.list({
|
||||||
|
status: 'pending',
|
||||||
|
page: p,
|
||||||
|
page_size: 10,
|
||||||
|
});
|
||||||
|
setItems(res.items);
|
||||||
|
setTotal(res.total);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchItems(page);
|
||||||
|
}, [page, fetchItems]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div style={{ textAlign: 'center', padding: 40 }}><Spin /></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return <Empty description="暂无待处理事项" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
dataSource={items}
|
||||||
|
pagination={{
|
||||||
|
current: page,
|
||||||
|
total,
|
||||||
|
pageSize: 10,
|
||||||
|
size: 'small',
|
||||||
|
onChange: setPage,
|
||||||
|
}}
|
||||||
|
renderItem={(item) => {
|
||||||
|
const cfg = TYPE_CONFIG[item.action_type];
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
style={{ cursor: 'pointer', padding: '8px 12px' }}
|
||||||
|
onClick={() => onItemClick?.(item)}
|
||||||
|
>
|
||||||
|
<List.Item.Meta
|
||||||
|
avatar={<span style={{ fontSize: 18 }}>{cfg.icon}</span>}
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<span>{item.title}</span>
|
||||||
|
<Tag color={cfg.color}>{cfg.label}</Tag>
|
||||||
|
<Tag color={PRIORITY_COLOR[item.priority]}>{item.priority}</Tag>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<Space direction="vertical" size={2}>
|
||||||
|
<span>{item.summary}</span>
|
||||||
|
<span style={{ color: '#999', fontSize: 12 }}>
|
||||||
|
{item.patient_name} · {new Date(item.created_at).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button type="link" size="small">处理</Button>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user