import { useEffect, useState, useCallback, useRef } from 'react'; import { Row, Col, Spin, Empty } from 'antd'; import { UserOutlined, SafetyCertificateOutlined, FileTextOutlined, BellOutlined, ThunderboltOutlined, SettingOutlined, PartitionOutlined, ClockCircleOutlined, ApartmentOutlined, CheckCircleOutlined, RightOutlined, } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import client from '../api/client'; import { useThemeMode } from '../hooks/useThemeMode'; import { useMessageStore } from '../stores/message'; import { listAuditLogs, type AuditLogItem } from '../api/auditLogs'; import { listPendingTasks, type TaskInfo } from '../api/workflowTasks'; interface DashboardStats { userCount: number; roleCount: number; processInstanceCount: number; unreadMessages: number; } interface StatCardConfig { key: string; title: string; value: number; icon: React.ReactNode; gradient: string; iconBg: string; delay: string; onClick?: () => void; } function useCountUp(end: number, duration = 800) { const [count, setCount] = useState(0); const prevEnd = useRef(end); useEffect(() => { if (end === prevEnd.current && count > 0) return; prevEnd.current = end; if (end === 0) { setCount(0); return; } const startTime = performance.now(); const startVal = 0; function tick(now: number) { const elapsed = now - startTime; const progress = Math.min(elapsed / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); setCount(Math.round(startVal + (end - startVal) * eased)); if (progress < 1) requestAnimationFrame(tick); } requestAnimationFrame(tick); }, [end, duration]); return count; } function StatValue({ value, loading }: { value: number; loading: boolean }) { const animatedValue = useCountUp(value); if (loading) return ; return {animatedValue.toLocaleString()}; } const ACTION_LABELS: Record = { create: '创建', update: '更新', delete: '删除', login: '登录', user_login: '登录', 'user.login': '登录', 'user.create': '创建', 'user.update': '更新', 'user.delete': '删除', 'role.create': '创建', 'role.update': '更新', 'role.delete': '删除', 'patient.create': '创建', 'patient.update': '更新', 'appointment.create': '创建', 'appointment.update': '更新', }; const RESOURCE_LABELS: Record = { user: '用户', role: '角色', permission: '权限', organization: '组织', department: '部门', position: '岗位', dictionary: '字典', menu: '菜单', setting: '设置', process_definition: '流程定义', process_instance: '流程实例', message: '消息', plugin: '插件', patient: '患者', doctor: '医护', appointment: '预约', follow_up_task: '随访', consultation_session: '咨询', auth: '认证', }; const RESOURCE_ICONS: Record = { user: , role: , organization: , process_definition: , process_instance: , message: , patient: , doctor: , }; 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} 天前`; } function formatActionLabel(action: string): string { if (ACTION_LABELS[action]) return ACTION_LABELS[action]; const lastPart = action.split('.').pop() || action; return ACTION_LABELS[lastPart] || lastPart; } function formatResourceLabel(resource: string): string { if (RESOURCE_LABELS[resource]) return RESOURCE_LABELS[resource]; const lastPart = resource.split('.').pop() || resource; return RESOURCE_LABELS[lastPart] || lastPart; } export default function Home() { const [stats, setStats] = useState({ userCount: 0, roleCount: 0, processInstanceCount: 0, unreadMessages: 0, }); const [loading, setLoading] = useState(true); const [pendingTasks, setPendingTasks] = useState([]); const [recentActivities, setRecentActivities] = useState([]); const [activitiesLoading, setActivitiesLoading] = useState(true); const unreadCount = useMessageStore((s) => s.unreadCount); const fetchUnreadCount = useMessageStore((s) => s.fetchUnreadCount); const navigate = useNavigate(); const isDark = useThemeMode(); useEffect(() => { let cancelled = false; async function loadStats() { setLoading(true); try { const [usersRes, rolesRes, instancesRes] = await Promise.allSettled([ client.get('/users', { params: { page: 1, page_size: 1 } }), client.get('/roles', { params: { page: 1, page_size: 1 } }), client.get('/workflow/instances', { params: { page: 1, page_size: 1 } }), ]); if (cancelled) return; const extractTotal = (res: PromiseSettledResult<{ data: { data?: { total?: number } } }>) => { if (res.status !== 'fulfilled') return 0; const body = res.value.data; if (body && typeof body === 'object' && 'data' in body) { const inner = (body as { data?: { total?: number } }).data; return inner?.total ?? 0; } return 0; }; setStats({ userCount: extractTotal(usersRes), roleCount: extractTotal(rolesRes), processInstanceCount: extractTotal(instancesRes), unreadMessages: unreadCount, }); } catch { // 静默处理 } finally { if (!cancelled) setLoading(false); } } async function loadTasks() { try { const result = await listPendingTasks(1, 5); if (!cancelled) setPendingTasks(result.data); } catch { // 静默处理 } } async function loadActivities() { setActivitiesLoading(true); try { const result = await listAuditLogs({ page: 1, page_size: 5 }); if (!cancelled) setRecentActivities(result.data); } catch { // 静默处理 } finally { if (!cancelled) setActivitiesLoading(false); } } fetchUnreadCount(); loadStats(); loadTasks(); loadActivities(); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleNavigate = useCallback((path: string) => { navigate(path); }, [navigate]); const statCards: StatCardConfig[] = [ { key: 'users', title: '用户总数', value: stats.userCount, icon: , gradient: 'linear-gradient(135deg, #2563eb, #60a5fa)', iconBg: 'rgba(79, 70, 229, 0.12)', delay: 'erp-fade-in erp-fade-in-delay-1', onClick: () => handleNavigate('/users'), }, { key: 'roles', title: '角色数量', value: stats.roleCount, icon: , gradient: 'linear-gradient(135deg, #059669, #10B981)', iconBg: 'rgba(5, 150, 105, 0.12)', delay: 'erp-fade-in erp-fade-in-delay-2', onClick: () => handleNavigate('/roles'), }, { key: 'processes', title: '流程实例', value: stats.processInstanceCount, icon: , gradient: 'linear-gradient(135deg, #d97706, #F59E0B)', iconBg: 'rgba(217, 119, 6, 0.12)', delay: 'erp-fade-in erp-fade-in-delay-3', onClick: () => handleNavigate('/workflow'), }, { key: 'messages', title: '未读消息', value: stats.unreadMessages, icon: , gradient: 'linear-gradient(135deg, #E11D48, #F43F5E)', iconBg: 'rgba(225, 29, 72, 0.12)', delay: 'erp-fade-in erp-fade-in-delay-4', onClick: () => handleNavigate('/messages'), }, ]; const quickActions = [ { icon: , label: '用户管理', path: '/users', color: '#2563eb' }, { icon: , label: '权限管理', path: '/roles', color: '#059669' }, { icon: , label: '组织架构', path: '/organizations', color: '#d97706' }, { icon: , label: '工作流', path: '/workflow', color: '#7C3AED' }, { icon: , label: '消息中心', path: '/messages', color: '#E11D48' }, { icon: , label: '系统设置', path: '/settings', color: '#475569' }, ]; return (
{/* 欢迎语 */}

工作台

欢迎回来,这是您的系统概览

{/* 统计卡片行 */} {statCards.map((card) => (
{ if (e.key === 'Enter') card.onClick?.(); }} >
{card.title}
{card.icon}
))} {/* 待办任务 + 最近活动 */} {/* 待办任务 */}
待办任务 {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}
))}
系统信息
{[ { label: '系统版本', value: 'v0.1.0' }, { label: '后端框架', value: 'Axum 0.8 + Tokio' }, { label: '数据库', value: 'PostgreSQL 18' }, { label: '缓存', value: 'Redis 7' }, { label: '前端框架', value: 'React 19 + Ant Design 6' }, { label: '模块数量', value: '6 个业务模块' }, ].map((item) => (
{item.label} {item.value}
))}
); }