import { useEffect, useState, useCallback } from 'react'; import { Row, Col, Spin, Empty } from 'antd'; import { UserOutlined, TeamOutlined, CalendarOutlined, HeartOutlined, MedicineBoxOutlined, SafetyCertificateOutlined, MessageOutlined, BellOutlined, AlertOutlined, TrophyOutlined, ShoppingOutlined, FileTextOutlined, RightOutlined, PartitionOutlined, ClockCircleOutlined, CheckCircleOutlined, ThunderboltOutlined, SettingOutlined, ApartmentOutlined, } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import { useThemeMode } from '../hooks/useThemeMode'; import { useDashboardRole, type DashboardRole } from '../hooks/useDashboardRole'; import { useMessageStore } from '../stores/message'; import { listAuditLogs, type AuditLogItem } from '../api/auditLogs'; import { listPendingTasks, type TaskInfo } from '../api/workflowTasks'; import { pointsApi, type PersonalStats } from '../api/health/points'; import { useStatsData } from './health/StatisticsDashboard/useStatsData'; import { useCountUp } from '../hooks/useCountUp'; 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'; // --- Shared utilities --- function formatTimeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); const minutes = Math.floor(diff / 60000); if (minutes < 1) return '刚刚'; if (minutes < 60) return `${minutes} 分钟前`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours} 小时前`; const days = Math.floor(hours / 24); return `${days} 天前`; } const ACTION_LABELS: Record = { create: '创建', created: '创建', update: '更新', updated: '更新', delete: '删除', deleted: '删除', login: '登录', 'user.create': '创建', 'user.update': '更新', 'user.delete': '删除', 'patient.create': '创建', 'patient.update': '更新', 'appointment.create': '创建', }; const RESOURCE_LABELS: Record = { user: '用户', role: '角色', patient: '患者', doctor: '医护', appointment: '预约', follow_up_task: '随访', consultation_session: '咨询', message: '消息', plugin: '插件', process_instance: '流程实例', organization: '组织', }; const RESOURCE_ICONS: Record = { user: , role: , patient: , organization: , process_instance: , message: , }; function formatActionLabel(action: string): string { return ACTION_LABELS[action] || ACTION_LABELS[action.split('.').pop() || ''] || action; } function formatResourceLabel(resource: string): string { return RESOURCE_LABELS[resource] || RESOURCE_LABELS[resource.split('.').pop() || ''] || resource; } // --- Role configs --- interface StatCardDef { key: string; title: string; getValue: (p: PersonalStats | null, s: ReturnType) => number; getDiff?: (p: PersonalStats | null) => number | undefined; icon: React.ReactNode; suffix?: string; path: string; } interface QuickActionDef { icon: React.ReactNode; label: string; path: string; } const ROLE_WELCOME: Record = { doctor: { title: '今日工作台', subtitle: '患者概览与待办事项' }, health_manager: { title: '任务工作台', subtitle: '待处理任务与患者管理' }, nurse: { title: '随访监控台', subtitle: '今日随访与体征上报' }, admin: { title: '管理中心', subtitle: '平台运营数据概览' }, operator: { title: '运营中心', subtitle: '积分、内容与活动' }, }; const STAT_BAR_COLORS: string[] = [ 'linear-gradient(90deg, #2563EB, #60A5FA)', 'linear-gradient(90deg, #7C3AED, #A78BFA)', 'linear-gradient(90deg, #DC2626, #F87171)', 'linear-gradient(90deg, #D97706, #FBBF24)', ]; const STAT_TEXT_COLORS: string[] = ['#2563EB', '#7C3AED', '#DC2626', '#D97706']; const ROLE_STATS: Record = { doctor: [ { key: 'my-patients', title: '我的患者', getValue: (p) => p?.my_patients ?? 0, getDiff: (p) => { const c = p?.my_patients, y = p?.yesterday_my_patients; return c != null && y != null ? c - y : undefined; }, icon: , path: '/health/patients' }, { key: 'today-appointments', title: '今日预约', getValue: (p) => p?.today_appointments ?? 0, getDiff: (p) => { const c = p?.today_appointments, y = p?.yesterday_today_appointments; return c != null && y != null ? c - y : undefined; }, icon: , path: '/health/appointments' }, { key: 'consultations', title: '本月咨询', getValue: (p) => p?.consultations_this_month ?? 0, getDiff: (p) => { const c = p?.consultations_this_month, y = p?.yesterday_consultations_this_month; return c != null && y != null ? c - y : undefined; }, icon: , path: '/health/consultations' }, { key: 'followup-rate', title: '随访完成率', getValue: (p) => p?.follow_up_rate ?? 0, icon: , suffix: '%', path: '/health/follow-ups' }, ], health_manager: [ { key: 'today-followups', title: '今日随访', getValue: (p) => p?.today_follow_ups ?? 0, getDiff: (p) => { const c = p?.today_follow_ups, y = p?.yesterday_today_follow_ups; return c != null && y != null ? c - y : undefined; }, icon: , path: '/health/follow-up-tasks' }, { key: 'vital-anomaly', title: '体征异常', getValue: (p) => p?.overdue_follow_ups ?? 0, icon: , path: '/health/alert-dashboard' }, { key: 'ai-pending', title: 'AI 建议待审', getValue: (p) => p?.consultations_this_month ?? 0, icon: , path: '/health/ai-analysis' }, { key: 'followup-rate', title: '处理率', getValue: (p) => p?.follow_up_rate ?? 0, icon: , suffix: '%', path: '/health/follow-up-tasks' }, ], nurse: [ { key: 'today-appointments', title: '今日预约', getValue: (p) => p?.today_appointments ?? 0, getDiff: (p) => { const c = p?.today_appointments, y = p?.yesterday_today_appointments; return c != null && y != null ? c - y : undefined; }, icon: , path: '/health/appointments' }, { key: 'today-followups', title: '今日随访', getValue: (p) => p?.today_follow_ups ?? 0, getDiff: (p) => { const c = p?.today_follow_ups, y = p?.yesterday_today_follow_ups; return c != null && y != null ? c - y : undefined; }, icon: , path: '/health/follow-ups' }, { key: 'overdue', title: '逾期随访', getValue: (p) => p?.overdue_follow_ups ?? 0, getDiff: (p) => { const c = p?.overdue_follow_ups, y = p?.yesterday_overdue_follow_ups; return c != null && y != null ? c - y : undefined; }, icon: , path: '/health/follow-ups' }, { key: 'vital-rate', title: '体征上报率', getValue: (p) => p?.vital_signs_report_rate ?? 0, icon: , suffix: '%', path: '/health/vital-signs' }, ], admin: [ { key: 'patients', title: '患者总数', getValue: (_p, s) => s.patientStats?.total_patients ?? 0, icon: , path: '/health/patients' }, { key: 'appointments', title: '本月预约', getValue: (_p, s) => s.healthDataStats?.appointments?.this_month ?? 0, icon: , path: '/health/appointments' }, { key: 'followup-rate', title: '随访完成率', getValue: (_p, s) => s.followUpStats?.completion_rate ?? 0, icon: , suffix: '%', path: '/health/follow-ups' }, { key: 'vital-rate', title: '体征上报率', getValue: (_p, s) => s.healthDataStats?.vital_signs_report_rate?.report_rate ?? 0, icon: , suffix: '%', path: '/health/vital-signs' }, ], operator: [ { key: 'issued', title: '积分发放', getValue: (_p, s) => s.pointsStats?.total_issued ?? 0, icon: , path: '/health/points' }, { key: 'spent', title: '积分消费', getValue: (_p, s) => s.pointsStats?.total_spent ?? 0, icon: , path: '/health/mall' }, { key: 'active', title: '活跃账户', getValue: (_p, s) => s.pointsStats?.active_accounts ?? 0, icon: , path: '/health/points' }, { key: 'articles', title: '内容发布', getValue: (_p, s) => s.patientStats?.total_patients ?? 0, icon: , path: '/health/content' }, ], }; const ROLE_ACTIONS: Record = { doctor: [ { icon: , label: '患者管理', path: '/health/patients' }, { icon: , label: '预约管理', path: '/health/appointments' }, { icon: , label: '随访管理', path: '/health/follow-ups' }, { icon: , label: '咨询管理', path: '/health/consultations' }, { icon: , label: '告警中心', path: '/health/alert-dashboard' }, { icon: , label: '健康数据', path: '/health/statistics' }, ], health_manager: [ { icon: , label: '随访管理', path: '/health/follow-up-tasks' }, { icon: , label: '体征监测', path: '/health/alert-dashboard' }, { icon: , label: '患者咨询', path: '/health/consultations' }, { icon: , label: '患者管理', path: '/health/patients' }, { icon: , label: '积分商城', path: '/health/points-products' }, { icon: , label: '统计报表', path: '/health/statistics' }, ], nurse: [ { icon: , label: '随访管理', path: '/health/follow-ups' }, { icon: , label: '健康数据', path: '/health/vital-signs' }, { icon: , label: '预约管理', path: '/health/appointments' }, { icon: , label: '告警中心', path: '/health/alert-dashboard' }, { icon: , label: '患者管理', path: '/health/patients' }, { icon: , label: '健康统计', path: '/health/statistics' }, ], admin: [ { icon: , label: '患者管理', path: '/health/patients' }, { icon: , label: '预约管理', path: '/health/appointments' }, { icon: , label: '随访管理', path: '/health/follow-ups' }, { icon: , label: '健康数据', path: '/health/vital-signs' }, { icon: , label: '积分商城', path: '/health/points' }, { icon: , label: '系统设置', path: '/settings' }, ], operator: [ { icon: , label: '积分管理', path: '/health/points' }, { icon: , label: '内容管理', path: '/health/content' }, { icon: , label: '线下活动', path: '/health/events' }, { icon: , label: '患者管理', path: '/health/patients' }, { icon: , label: '健康统计', path: '/health/statistics' }, { icon: , label: '系统设置', path: '/settings' }, ], }; // --- Components --- function StatValue({ value, loading }: { value: number; loading: boolean }) { const animatedValue = useCountUp(value); if (loading) return ; return {animatedValue.toLocaleString()}; } export default function Home() { const navigate = useNavigate(); const role = useDashboardRole(); const isDark = useThemeMode(); const fetchUnreadCount = useMessageStore((s) => s.fetchUnreadCount); const [personalStats, setPersonalStats] = useState(null); const [personalLoading, setPersonalLoading] = useState(true); const [pendingTasks, setPendingTasks] = useState([]); const [recentActivities, setRecentActivities] = useState([]); const [activitiesLoading, setActivitiesLoading] = useState(true); const [drawerItem, setDrawerItem] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const statsData = useStatsData(); const loading = personalLoading || statsData.loading; const welcome = ROLE_WELCOME[role]; const statDefs = ROLE_STATS[role]; const quickActions = ROLE_ACTIONS[role]; useEffect(() => { let cancelled = false; fetchUnreadCount(); if (role === 'doctor' || role === 'nurse') { pointsApi.getPersonalStats() .then((data) => { if (!cancelled) setPersonalStats(data); }) .catch(() => {}) .finally(() => { if (!cancelled) setPersonalLoading(false); }); } else { setPersonalLoading(false); } listPendingTasks(1, 5) .then((result) => { if (!cancelled) setPendingTasks(result.data); }) .catch(() => {}); listAuditLogs({ page: 1, page_size: 5 }) .then((result) => { if (!cancelled) setRecentActivities(result.data.filter((a) => a.action !== 'login_failed')); }) .catch(() => {}) .finally(() => { if (!cancelled) setActivitiesLoading(false); }); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [role]); const handleNavigate = useCallback((path: string) => { navigate(path); }, [navigate]); return (
{/* 角色工作台路由 */} {role === 'doctor' ? ( ) : role === 'health_manager' ? (
) : role === 'operator' ? ( ) : role === 'admin' ? ( ) : ( <> {/* 欢迎语 */}

{welcome.title}

{welcome.subtitle}

{/* 统计卡片行 */}
{statDefs.map((def, i) => { const value = def.getValue(personalStats, statsData); const diff = def.getDiff?.(personalStats); return (
handleNavigate(def.path)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate(def.path); }} onMouseEnter={(e) => { e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.06)'; e.currentTarget.style.transform = 'translateY(-1px)'; }} onMouseLeave={(e) => { e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.transform = 'none'; }} >
{def.title}
{def.suffix && {def.suffix}}
{diff != null && (
0 ? '#16A34A' : diff < 0 ? '#DC2626' : '#94A3B8' }}> {diff === 0 ? '与昨日持平' : `较昨日 ${diff > 0 ? '+' : ''}${diff}`}
)}
); })}
{/* 护士专属工作台 */} {role === 'nurse' ? ( ) : (
待办任务 {pendingTasks.length} 项待处理
{pendingTasks.length === 0 ? ( ) : ( pendingTasks.map((task) => (
handleNavigate('/workflow')} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate('/workflow'); }} >
{task.node_name || task.definition_name || '流程任务'}
{task.definition_name || '工作流'} {task.status === 'pending' ? '待处理' : task.status}
一般
)) )}
最近动态
{activitiesLoading ? (
) : recentActivities.length === 0 ? ( ) : ( recentActivities.map((log) => (
{RESOURCE_ICONS[log.resource_type] || }
{formatActionLabel(log.action)}了{formatResourceLabel(log.resource_type)}
{formatTimeAgo(log.created_at)}
)) )}
)} {/* 快捷入口 */}
快捷入口
{quickActions.map((action) => (
handleNavigate(action.path)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate(action.path); }} >
{action.icon}
{action.label}
))}
{/* 行动详情抽屉 */} { setDrawerOpen(false); setDrawerItem(null); }} onActionComplete={() => { setDrawerOpen(false); setDrawerItem(null); }} /> )}
); }