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[];
|
||||
}
|
||||
|
||||
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 = {
|
||||
list: async (params?: {
|
||||
status?: string;
|
||||
@@ -63,4 +92,20 @@ export const actionInboxApi = {
|
||||
}>(`/health/action-inbox/${encodeURIComponent(sourceRef)}/thread`);
|
||||
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