import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Layout, Menu, Button, Avatar, Dropdown, Drawer, Grid } from 'antd'; import { DashboardOutlined, TeamOutlined, HomeOutlined, SwapOutlined, DollarOutlined, FileTextOutlined, LogoutOutlined, UserOutlined, MenuFoldOutlined, MenuUnfoldOutlined, AppstoreOutlined, AuditOutlined, SettingOutlined, WalletOutlined, ReadOutlined, TagsOutlined, FileProtectOutlined, CalendarOutlined, SafetyOutlined, KeyOutlined, CheckCircleOutlined, LaptopOutlined, BellOutlined, ApiOutlined, } from '@ant-design/icons'; import { usePermission } from '../hooks/usePermission'; import NotificationBell from '../components/NotificationBell'; const { Header, Sider, Content } = Layout; interface MenuItemType { key: string; icon: React.ReactNode; label: string; permission?: string; children?: MenuItemType[]; } const allMenuItems: MenuItemType[] = [ { key: '/dashboard', icon: , label: '数据面板', permission: 'dashboard:view', }, { key: 'dorm-group', icon: , label: '宿舍运营', permission: 'room:view', children: [ { key: '/room-visual', icon: , label: '宿舍总览', permission: 'room:view' }, { key: '/rooms', icon: , label: '宿舍管理', permission: 'room:view' }, { key: '/occupancies', icon: , label: '入住管理', permission: 'occupancy:view' }, ], }, { key: 'student-group', icon: , label: '学员管理', permission: 'student:view', children: [ { key: '/students', icon: , label: '学生管理', permission: 'student:view' }, { key: '/classes', icon: , label: '班级管理', permission: 'class:view' }, { key: '/attendance', icon: , label: '考勤管理', permission: 'attendance:view' }, ], }, { key: 'academic-group', icon: , label: '教务管理', permission: 'schedule:view', children: [ { key: '/schedules', icon: , label: '排课管理', permission: 'schedule:view' }, { key: '/teacher-workspace', icon: , label: '教师工作台', permission: 'class:view' }, ], }, { key: 'classroom-group', icon: , label: '教室管理', permission: 'classroom:view', children: [ { key: '/classroom-schedule', icon: , label: '排期总览', permission: 'classroom:view' }, { key: '/classrooms', icon: , label: '教室列表', permission: 'classroom:view' }, { key: '/classroom-rentals', icon: , label: '租赁订单', permission: 'rental:view' }, { key: '/tenants', icon: , label: '租赁方', permission: 'tenant:view' }, ], }, { key: 'finance-group', icon: , label: '财务管理', permission: 'expense:view', children: [ { key: '/expenses', icon: , label: '费用录入', permission: 'expense:view' }, { key: '/deposits', icon: , label: '押金管理', permission: 'deposit:view' }, { key: '/bills', icon: , label: '账单管理', permission: 'bill:view' }, ], }, { key: 'system-group', icon: , label: '系统管理', permission: 'log:view', children: [ { key: '/notifications', icon: , label: '通知中心', permission: 'notification:view' }, { key: '/operation-logs', icon: , label: '操作日志', permission: 'log:view' }, { key: '/roles', icon: , label: '角色管理', permission: 'role:view' }, { key: '/permissions', icon: , label: '权限一览', permission: 'role:view' }, { key: '/integration-config', icon: , label: '钉钉集成配置', permission: 'integration:read' }, { key: '/users', icon: , label: '账号管理', permission: 'user:view' }, ], }, ]; const MainLayout: React.FC = () => { const [collapsed, setCollapsed] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false); const [openKeys, setOpenKeys] = useState([]); const prevPathname = useRef(''); const navigate = useNavigate(); const location = useLocation(); const user = useMemo(() => JSON.parse(localStorage.getItem('user') || '{}'), []); const { hasPermission } = usePermission(); const screens = Grid.useBreakpoint(); const isMobile = !screens.sm; // < 576px (仅 xs) const isTablet = (screens.sm || screens.md) && !screens.lg; // 576-991px const isDesktop = !!screens.lg; // >= 992px // 按 permission 过滤菜单 const filterByPermission = (items: MenuItemType[]): MenuItemType[] => { return items .map((item) => { if (item.children) { const kids = filterByPermission(item.children); if (kids.length === 0) return null; return { ...item, children: kids }; } if (!item.permission) return item; return hasPermission(item.permission) ? item : null; }) .filter(Boolean) as MenuItemType[]; }; const menuItems = useMemo(() => filterByPermission(allMenuItems), [hasPermission]); const handleLogout = useCallback(() => { localStorage.removeItem('token'); localStorage.removeItem('user'); localStorage.removeItem('permissions'); navigate('/login'); }, [navigate]); const handleMenuClick = useCallback((key: string) => { navigate(key); if (isMobile) setDrawerOpen(false); }, [navigate, isMobile]); const findSelectedKeys = (items: MenuItemType[], pathname: string): string[] => { for (const item of items) { if (item.key === pathname) return [item.key]; if (item.children) { const found = findSelectedKeys(item.children, pathname); if (found.length > 0) return found; } } return [pathname]; }; const findOpenKeys = (items: MenuItemType[], pathname: string): string[] => { for (const item of items) { if (item.children) { if (item.children.some((c) => c.key === pathname || (c.children && c.children.some((gc) => gc.key === pathname)))) { return [item.key]; } } } return []; }; const selectedKeys = useMemo(() => findSelectedKeys(menuItems, location.pathname), [menuItems, location.pathname]); // 路径变化时同步展开的菜单(不干扰用户手动展开/收起) useEffect(() => { if (location.pathname !== prevPathname.current) { prevPathname.current = location.pathname; setOpenKeys(findOpenKeys(menuItems, location.pathname)); } }, [location.pathname, menuItems]); const handleOpenChange = useCallback((keys: string[]) => { // 只保留最新打开的一个子菜单 const latestKey = keys[keys.length - 1]; setOpenKeys(latestKey ? [latestKey] : []); }, []); const transformToMenuItems = (items: MenuItemType[]): any[] => { return items.map((item) => ({ key: item.key, icon: item.icon, label: item.label, children: item.children ? transformToMenuItems(item.children) : undefined, })); }; const menuContent = useMemo(() => ( handleMenuClick(key)} style={{ border: 'none' }} /> ), [selectedKeys, openKeys, menuItems, handleMenuClick]); return ( {!isMobile && ( {(isTablet || collapsed) ? '恭' : '恭学教育基地'} {menuContent} )} {isMobile && ( setDrawerOpen(false)} size={240} styles={{ body: { padding: 0 } }} title="恭学教育基地" > {menuContent} )} ) : collapsed ? ( ) : ( ) } onClick={() => (isMobile || isTablet ? setDrawerOpen(true) : setCollapsed(!collapsed))} /> , label: '退出登录', onClick: handleLogout, }, ], }} > } /> {isDesktop && {user.name || user.username || '用户'}} ); }; export default MainLayout;