import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { Layout, Menu, Button, Avatar, Badge, Dropdown, Drawer, Grid, Tooltip } 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, TrophyOutlined, ApiOutlined, RobotOutlined, LoadingOutlined, } from '@ant-design/icons'; import { usePermission } from '../hooks/usePermission'; import api from '../api'; import { useAppStore } from '../store/app/appStore'; import { usePermissionStore } from '../store/permission/permissionStore'; import { useUserStore } from '../store/user/userStore'; import NotificationBell from '../components/NotificationBell'; import RouteDock from '../components/RouteDock'; import RouteKeeper from '../components/RouteKeeper'; import { buildMenu, type AppMenuItem } from '../auth/menu-policy'; const AiChatDrawer = React.lazy(() => import('../components/AiChat/AiChatDrawer')); const { Header, Sider, Content } = Layout; const iconMap: Record = { dashboard: , calendar: , workspace: , attendance: , academic: , students: , classes: , teachers: , exam: , home: , overview: , occupancy: , expense: , bill: , deposit: , wallet: , classroom: , rental: , organization: , settings: , users: , role: , permission: , log: , integration: , ai: , notification: , }; const MainLayout: React.FC = () => { const prevPathname = useRef(''); const navigate = useNavigate(); const location = useLocation(); const user = useUserStore((state) => state.user); const updateUser = useUserStore((state) => state.updateUser); const logoutUser = useUserStore((state) => state.logout); const collapsed = useAppStore((state) => state.sidebarCollapsed); const drawerOpen = useAppStore((state) => state.mobileDrawerOpen); const aiChatOpen = useAppStore((state) => state.aiChatOpen); const aiWorking = useAppStore((state) => state.aiWorking); const openKeys = useAppStore((state) => state.menuOpenKeys); const toggleSidebar = useAppStore((state) => state.toggleSidebar); const setDrawerOpen = useAppStore((state) => state.setMobileDrawerOpen); const setAiChatOpen = useAppStore((state) => state.setAiChatOpen); const setAiWorking = useAppStore((state) => state.setAiWorking); const setOpenKeys = useAppStore((state) => state.setMenuOpenKeys); const { permissions, hasPermission } = usePermission(); useEffect(() => { let cancelled = false; let retryTimer: number | undefined; let verificationInFlight = false; const verifyPermissions = () => { if (cancelled || verificationInFlight || !useUserStore.getState().token) return; if (retryTimer !== undefined) { window.clearTimeout(retryTimer); retryTimer = undefined; } verificationInFlight = true; usePermissionStore.getState().beginPermissionVerification(); api .get<{ id: number; username: string; permissions: string[]; roles?: string[] }>( '/auth/profile', ) .then((profile) => { if (cancelled) return; verificationInFlight = false; usePermissionStore.getState().writePermissions(profile.permissions || []); updateUser(profile); }) .catch(() => { verificationInFlight = false; if (cancelled || !useUserStore.getState().token) return; retryTimer = window.setTimeout(verifyPermissions, 5_000); }); }; const handleStorage = (event: StorageEvent) => { if (event.key !== 'token' && event.key !== 'permissions') return; usePermissionStore.getState().beginPermissionVerification(); window.location.reload(); }; const handleOnline = () => verifyPermissions(); const handleVisibilityChange = () => { if (document.visibilityState === 'visible') verifyPermissions(); }; verifyPermissions(); window.addEventListener('storage', handleStorage); window.addEventListener('online', handleOnline); document.addEventListener('visibilitychange', handleVisibilityChange); return () => { cancelled = true; if (retryTimer !== undefined) window.clearTimeout(retryTimer); window.removeEventListener('storage', handleStorage); window.removeEventListener('online', handleOnline); document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [updateUser]); 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 const usesDrawer = !isDesktop; const menuItems = useMemo( () => buildMenu(user?.roles ?? [], permissions), [user, permissions], ); const handleLogout = useCallback(() => { logoutUser(); usePermissionStore.getState().clearPermissions(); navigate('/login'); }, [logoutUser, navigate]); const handleMenuClick = useCallback( (key: string) => { navigate(key); if (usesDrawer) setDrawerOpen(false); }, [navigate, usesDrawer], ); const findSelectedKeys = (items: AppMenuItem[], 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: AppMenuItem[], 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; const routeOpenKeys = findOpenKeys(menuItems, location.pathname); setOpenKeys((currentKeys) => [...new Set([...currentKeys, ...routeOpenKeys])]); } }, [location.pathname, menuItems]); const handleOpenChange = useCallback((keys: string[]) => { setOpenKeys(keys); }, []); const transformToMenuItems = (items: AppMenuItem[]): any[] => { return items.map((item) => ({ key: item.key, icon: item.icon ? iconMap[item.icon] : undefined, label: item.label, children: item.children ? transformToMenuItems(item.children) : undefined, })); }; const menuContent = useMemo( () => ( handleMenuClick(key)} style={{ border: 'none' }} /> ), [selectedKeys, openKeys, menuItems, handleMenuClick], ); return ( {isDesktop && (
{collapsed ? '学' : '学生管理系统'}
{menuContent}
)} {usesDrawer && ( setDrawerOpen(false)} size={240} styles={{ body: { padding: 0 } }} className="app-navigation-drawer" title="学生管理系统" > {menuContent} )}
{hasPermission('ai:chat:use') && ( setAiChatOpen(false)} onRequestingChange={setAiWorking} /> )}
); }; export default MainLayout;