From f54fb336dcad1cbcf57f7f909ff48f3c746a9d6c Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 4 May 2026 17:48:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=8A=A4=E5=A3=AB=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8F=B0=20Phase=201=20=E5=89=8D=E7=AB=AF=20=E2=80=94?= =?UTF-8?q?=20NurseWorkbench=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 NurseWorkbench 组件:问候行 + 统计卡片 + 班次患者 + 待办 + 右面板 - actionInbox API 客户端:新增 assigned_to_me/patient_id 参数 + myPatients 端点 - Home.tsx 护士角色路由到 NurseWorkbench(其他角色不受影响) - 班次患者列表:显示今日分配给护士的患者 + 风险优先级色点 - 快捷操作面板:随访/体征/AI分析/咨询入口 - 今日进度条:完成百分比可视化 --- apps/web/src/api/health/actionInbox.ts | 21 +- apps/web/src/pages/Home.tsx | 24 +- .../components/workbench/NurseWorkbench.tsx | 258 ++++++++++++++++++ 3 files changed, 280 insertions(+), 23 deletions(-) create mode 100644 apps/web/src/pages/health/components/workbench/NurseWorkbench.tsx diff --git a/apps/web/src/api/health/actionInbox.ts b/apps/web/src/api/health/actionInbox.ts index 2d73b88..951ba63 100644 --- a/apps/web/src/api/health/actionInbox.ts +++ b/apps/web/src/api/health/actionInbox.ts @@ -50,6 +50,13 @@ export interface WorkbenchStats { completion_rate: number | null; } +export interface NursePatientSummary { + patient_id: string; + patient_name: string; + pending_actions: number; + highest_priority: ActionPriority; +} + export interface TeamMemberOverview { user_id: string; name: string; @@ -77,6 +84,8 @@ export const actionInboxApi = { type?: string; page?: number; page_size?: number; + assigned_to_me?: boolean; + patient_id?: string; }) => { const { data } = await client.get<{ success: boolean; @@ -93,11 +102,19 @@ export const actionInboxApi = { return data.data; }, - stats: async () => { + stats: async (params?: { assigned_to_me?: boolean }) => { const { data } = await client.get<{ success: boolean; data: WorkbenchStats; - }>('/health/action-inbox/stats'); + }>('/health/action-inbox/stats', { params }); + return data.data; + }, + + myPatients: async () => { + const { data } = await client.get<{ + success: boolean; + data: NursePatientSummary[]; + }>('/health/action-inbox/my-patients'); return data.data; }, diff --git a/apps/web/src/pages/Home.tsx b/apps/web/src/pages/Home.tsx index 746771d..1b7cc69 100644 --- a/apps/web/src/pages/Home.tsx +++ b/apps/web/src/pages/Home.tsx @@ -36,6 +36,7 @@ import ActionDetailDrawer from './health/components/workbench/ActionDetailDrawer import TaskQueue from './health/components/workbench/TaskQueue'; import TaskDetail from './health/components/workbench/TaskDetail'; import DoctorWorkbench from './health/components/workbench/DoctorWorkbench'; +import NurseWorkbench from './health/components/workbench/NurseWorkbench'; import OperatorWorkbench from './health/components/workbench/OperatorWorkbench'; import AdminDashboard from './health/components/workbench/AdminDashboard'; import type { ActionItem } from '../api/health/actionInbox'; @@ -321,28 +322,9 @@ export default function Home() { })} - {/* 双栏布局 — nurse */} + {/* 护士专属工作台 */} {role === 'nurse' ? ( -
-
-
- 待办事项 -
- { setDrawerItem(item); setDrawerOpen(true); }} /> -
- -
+ ) : ( diff --git a/apps/web/src/pages/health/components/workbench/NurseWorkbench.tsx b/apps/web/src/pages/health/components/workbench/NurseWorkbench.tsx new file mode 100644 index 0000000..3df0122 --- /dev/null +++ b/apps/web/src/pages/health/components/workbench/NurseWorkbench.tsx @@ -0,0 +1,258 @@ +import { useEffect, useState, useCallback } from 'react'; +import { Card, Spin, Empty, Progress, Tag } from 'antd'; +import { + TeamOutlined, + BellOutlined, + WarningOutlined, + ThunderboltOutlined, + RobotOutlined, + CheckCircleOutlined, +} from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; +import TodoList from './TodoList'; +import AiInsightPanel from './AiInsightPanel'; +import ActionDetailDrawer from './ActionDetailDrawer'; +import { + actionInboxApi, + type ActionItem, + type WorkbenchStats, + type NursePatientSummary, +} from '../../../../api/health/actionInbox'; + +const PRIORITY_COLORS: Record = { + urgent: '#DC2626', + high: '#D97706', + medium: '#2563EB', + low: '#94A3B8', +}; + +export default function NurseWorkbench() { + const navigate = useNavigate(); + const [stats, setStats] = useState(null); + const [patients, setPatients] = useState([]); + const [loadingStats, setLoadingStats] = useState(true); + const [loadingPatients, setLoadingPatients] = useState(true); + const [drawerItem, setDrawerItem] = useState(null); + const [drawerOpen, setDrawerOpen] = useState(false); + + const fetchStats = useCallback(async () => { + try { + const data = await actionInboxApi.stats({ assigned_to_me: true }); + setStats(data); + } finally { + setLoadingStats(false); + } + }, []); + + const fetchPatients = useCallback(async () => { + try { + const data = await actionInboxApi.myPatients(); + setPatients(data); + } finally { + setLoadingPatients(false); + } + }, []); + + useEffect(() => { + fetchStats(); + fetchPatients(); + }, [fetchStats, fetchPatients]); + + const completedToday = stats + ? stats.total_pending > 0 + ? Math.round(((stats.total_pending - (stats.followup_due + stats.ai_suggestion_pending + stats.urgent_alerts)) / stats.total_pending) * 100) + : 100 + : 0; + + const greeting = () => { + const h = new Date().getHours(); + if (h < 6) return '夜班辛苦了'; + if (h < 12) return '早上好'; + if (h < 14) return '中午好'; + if (h < 18) return '下午好'; + return '晚上好'; + }; + + const shiftLabel = () => { + const h = new Date().getHours(); + if (h >= 8 && h < 16) return '白班'; + if (h >= 16 && h < 24) return '晚班'; + return '夜班'; + }; + + const statCards = [ + { label: '今日待办', value: stats?.total_pending ?? 0, icon: , color: '#2563EB' }, + { label: 'AI建议待审', value: stats?.ai_suggestion_pending ?? 0, icon: , color: '#7C3AED' }, + { label: '危急告警', value: stats?.urgent_alerts ?? 0, icon: , color: '#DC2626' }, + { label: '我的随访', value: stats?.followup_due ?? 0, icon: , color: '#0284C7' }, + ]; + + return ( +
+ {/* 问候行 */} +
+ {greeting()},护理团队 + {shiftLabel()} + + {new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'long' })} + +
+ + {/* 统计卡片 */} +
+ {statCards.map((card) => ( + +
+
+ {card.icon} +
+
+
{card.value}
+
{card.label}
+
+
+
+ ))} +
+ + {/* 双栏布局 */} +
+ {/* 左栏 */} +
+ {/* 班次患者 */} + 我的班次患者} + size="small" + style={{ borderRadius: 10 }} + > + {loadingPatients ? ( +
+ ) : patients.length === 0 ? ( + + ) : ( +
+ {patients.map((p) => ( +
navigate(`/health/patients/${p.patient_id}`)} + style={{ + display: 'flex', alignItems: 'center', padding: '10px 12px', + borderRadius: 8, cursor: 'pointer', + border: '1px solid var(--erp-border, #E2E8F0)', + transition: 'border-color 0.2s', + }} + onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--erp-primary)'; }} + onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--erp-border, #E2E8F0)'; }} + role="button" + tabIndex={0} + onKeyDown={(e) => { if (e.key === 'Enter') navigate(`/health/patients/${p.patient_id}`); }} + > + + {p.patient_name} + 0 ? 'blue' : 'default'} style={{ margin: 0 }}> + {p.pending_actions} 项待办 + +
+ ))} +
+ )} +
+ + {/* 待办事项 */} + 待办事项} + size="small" + style={{ borderRadius: 10 }} + > + { setDrawerItem(item); setDrawerOpen(true); }} /> + +
+ + {/* 右栏 */} +
+ {/* 快捷操作 */} + 快捷操作} + size="small" + style={{ borderRadius: 10 }} + > +
+ {[ + { label: '开始随访', icon: '📋', path: '/health/follow-up' }, + { label: '录入体征', icon: '📊', path: '/health/vital-signs' }, + { label: '查看AI分析', icon: '🤖', path: '/health/action-inbox' }, + { label: '联系患者', icon: '📞', path: '/health/consultation' }, + ].map((action) => ( +
navigate(action.path)} + style={{ + padding: '10px 12px', borderRadius: 8, cursor: 'pointer', + border: '1px solid var(--erp-border, #E2E8F0)', + display: 'flex', alignItems: 'center', gap: 8, + transition: 'background 0.2s', + }} + onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--erp-bg-hover, #F8FAFC)'; }} + onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }} + role="button" + tabIndex={0} + onKeyDown={(e) => { if (e.key === 'Enter') navigate(action.path); }} + > + {action.icon} + {action.label} +
+ ))} +
+
+ + {/* 今日进度 */} + + + 今日进度 +
+ } + size="small" + style={{ borderRadius: 10 }} + > + +
+ {stats ? `${stats.total_pending} 项待处理` : '加载中...'} +
+ + + {/* AI 洞察 */} + +
+
+ + {/* Action Detail Drawer */} + { setDrawerOpen(false); setDrawerItem(null); }} + onRefresh={() => { fetchStats(); }} + /> + + ); +}