From 620af8988b066e9b18ff20c010a2e34f82b4a373 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 1 May 2026 21:17:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=B7=A5=E4=BD=9C=E5=8F=B0?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=20API=20=E5=AE=A2=E6=88=B7=E7=AB=AF=20+=20To?= =?UTF-8?q?doList=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - actionInbox.ts 新增 WorkbenchStats/TeamOverview 类型和 stats()/team() API - 新建 workbench/TodoList.tsx 待办列表组件(分页 + 类型/优先级标签) --- apps/web/src/api/health/actionInbox.ts | 45 ++++++++ .../health/components/workbench/TodoList.tsx | 103 ++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 apps/web/src/pages/health/components/workbench/TodoList.tsx diff --git a/apps/web/src/api/health/actionInbox.ts b/apps/web/src/api/health/actionInbox.ts index ca92bc7..2d73b88 100644 --- a/apps/web/src/api/health/actionInbox.ts +++ b/apps/web/src/api/health/actionInbox.ts @@ -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; + }, }; diff --git a/apps/web/src/pages/health/components/workbench/TodoList.tsx b/apps/web/src/pages/health/components/workbench/TodoList.tsx new file mode 100644 index 0000000..14decba --- /dev/null +++ b/apps/web/src/pages/health/components/workbench/TodoList.tsx @@ -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 = { + ai_suggestion: { label: 'AI 建议', color: 'blue', icon: }, + alert: { label: '告警', color: 'red', icon: }, + followup: { label: '随访', color: 'green', icon: }, + data_anomaly: { label: '数据异常', color: 'orange', icon: }, +}; + +const PRIORITY_COLOR: Record = { + urgent: 'red', + high: 'volcano', + medium: 'orange', + low: 'default', +}; + +interface TodoListProps { + onItemClick?: (item: ActionItem) => void; +} + +export default function TodoList({ onItemClick }: TodoListProps) { + const [items, setItems] = useState([]); + 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
; + } + + if (items.length === 0) { + return ; + } + + return ( + { + const cfg = TYPE_CONFIG[item.action_type]; + return ( + onItemClick?.(item)} + > + {cfg.icon}} + title={ + + {item.title} + {cfg.label} + {item.priority} + + } + description={ + + {item.summary} + + {item.patient_name} · {new Date(item.created_at).toLocaleDateString()} + + + } + /> + + + ); + }} + /> + ); +}