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, RobotOutlined, } from '@ant-design/icons'; import { usePermission } from '../hooks/usePermission'; import api from '../api'; import { writePermissions } from '../auth/permission-store'; import NotificationBell from '../components/NotificationBell'; import { buildMenu, type AppMenuItem } from '../auth/menu-policy'; const { Header, Sider, Content } = Layout; const iconMap: Record = { dashboard: , calendar: , workspace: , attendance: , academic: , students: , classes: , teachers: , home: , overview: , occupancy: , expense: , bill: , deposit: , wallet: , classroom: , rental: , organization: , settings: , users: , role: , permission: , log: , integration: , ai: , notification: , }; 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, setUser] = useState<{ name?: string; username?: string; roles?: string[] }>(() => JSON.parse(localStorage.getItem('user') || '{}'), ); const { permissions, hasPermission } = usePermission(); useEffect(() => { let cancelled = false; api.get<{ id: number; username: string; permissions: string[]; roles?: string[] }>('/auth/profile') .then((profile) => { if (cancelled) return; writePermissions(profile.permissions || []); const cachedUser = JSON.parse(localStorage.getItem('user') || '{}'); const nextUser = { ...cachedUser, ...profile }; localStorage.setItem('user', JSON.stringify(nextUser)); setUser(nextUser); }) .catch(() => { // The API interceptor handles expired/invalid sessions. }); return () => { cancelled = true; }; }, []); 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 menuItems = useMemo( () => buildMenu(user.roles ?? [], permissions), [user.roles, permissions], ); 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: 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; 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: 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 ( {!isMobile && (
{(isTablet || collapsed) ? '恭' : '恭学教育基地'}
{menuContent}
)} {isMobile && ( setDrawerOpen(false)} size={240} styles={{ body: { padding: 0 } }} title="恭学教育基地" > {menuContent} )}
); }; export default MainLayout;