import { useEffect, useState, useCallback, useRef } from 'react'; import { Row, Col, Spin, theme } from 'antd'; import { UserOutlined, SafetyCertificateOutlined, FileTextOutlined, BellOutlined, ThunderboltOutlined, SettingOutlined, PartitionOutlined, ClockCircleOutlined, ApartmentOutlined, CheckCircleOutlined, TeamOutlined, FileProtectOutlined, RiseOutlined, FallOutlined, RightOutlined, } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import client from '../api/client'; import { useMessageStore } from '../stores/message'; interface DashboardStats { userCount: number; roleCount: number; processInstanceCount: number; unreadMessages: number; } interface TrendData { value: string; direction: 'up' | 'down' | 'neutral'; label: string; } interface StatCardConfig { key: string; title: string; value: number; icon: React.ReactNode; gradient: string; iconBg: string; delay: string; trend: TrendData; sparkline: number[]; onClick?: () => void; } interface TaskItem { id: string; title: string; priority: 'high' | 'medium' | 'low'; assignee: string; dueText: string; color: string; icon: React.ReactNode; path: string; } interface ActivityItem { id: string; text: string; time: string; icon: React.ReactNode; } 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()}; } export default function Home() { const [stats, setStats] = useState({ userCount: 0, roleCount: 0, processInstanceCount: 0, unreadMessages: 0, }); const [loading, setLoading] = useState(true); const unreadCount = useMessageStore((s) => s.unreadCount); const fetchUnreadCount = useMessageStore((s) => s.fetchUnreadCount); const { token } = theme.useToken(); const navigate = useNavigate(); const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)'; 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); } } fetchUnreadCount(); loadStats(); 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, #4F46E5, #6366F1)', iconBg: 'rgba(79, 70, 229, 0.12)', delay: 'erp-fade-in erp-fade-in-delay-1', trend: { value: '+2', direction: 'up', label: '较上周' }, sparkline: [30, 45, 35, 50, 40, 55, 60, 50, 65, 70], 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', trend: { value: '+1', direction: 'up', label: '较上月' }, sparkline: [20, 25, 30, 28, 35, 40, 38, 42, 45, 50], 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', trend: { value: '0', direction: 'neutral', label: '较昨日' }, sparkline: [10, 15, 12, 20, 18, 25, 22, 28, 24, 20], 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', trend: { value: '0', direction: 'neutral', label: '全部已读' }, sparkline: [5, 8, 3, 10, 6, 12, 8, 4, 7, 5], onClick: () => handleNavigate('/messages'), }, ]; const quickActions = [ { icon: , label: '用户管理', path: '/users', color: '#4F46E5' }, { 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: '#64748B' }, ]; const pendingTasks: TaskItem[] = [ { id: '1', title: '审核新用户注册申请', priority: 'high', assignee: '系统', dueText: '待处理', color: '#DC2626', icon: , path: '/users' }, { id: '2', title: '配置工作流审批节点', priority: 'medium', assignee: '管理员', dueText: '进行中', color: '#D97706', icon: , path: '/workflow' }, { id: '3', title: '更新角色权限策略', priority: 'low', assignee: '管理员', dueText: '计划中', color: '#059669', icon: , path: '/roles' }, ]; const recentActivities: ActivityItem[] = [ { id: '1', text: '系统管理员 创建了 管理员角色', time: '刚刚', icon: }, { id: '2', text: '系统管理员 配置了 工作流模板', time: '5 分钟前', icon: }, { id: '3', text: '系统管理员 更新了 组织架构', time: '10 分钟前', icon: }, { id: '4', text: '系统管理员 设置了 消息通知偏好', time: '30 分钟前', icon: }, ]; const priorityLabel: Record = { high: '紧急', medium: '一般', low: '低' }; return ( {/* 欢迎语 */} 工作台 欢迎回来,这是您的系统概览 {/* 统计卡片行 */} {statCards.map((card) => { const maxSpark = Math.max(...card.sparkline, 1); return ( { if (e.key === 'Enter') card.onClick?.(); }} > {card.title} {card.trend.direction === 'up' && } {card.trend.direction === 'down' && } {card.trend.value} {card.trend.label} {card.icon} {card.sparkline.map((v, i) => ( ))} ); })} {/* 待办任务 + 最近活动 */} {/* 待办任务 */} 待办任务 {pendingTasks.length} 项待处理 {pendingTasks.map((task) => ( handleNavigate(task.path)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate(task.path); }} > {task.icon} {task.title} {task.assignee} {task.dueText} {priorityLabel[task.priority]} ))} {/* 最近活动 */} 最近动态 {recentActivities.map((activity) => ( {activity.icon} {activity.text} {activity.time} ))} {/* 快捷入口 + 系统信息 */} 快捷入口 {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 16' }, { label: '缓存', value: 'Redis 7' }, { label: '前端框架', value: 'React 19 + Ant Design 6' }, { label: '模块数量', value: '5 个业务模块' }, ].map((item) => ( {item.label} {item.value} ))} ); }
欢迎回来,这是您的系统概览