import { useCallback, useState, memo, useEffect, useMemo } from 'react'; import { Layout, Avatar, Space, Dropdown, Tooltip, Spin, theme } from 'antd'; import { HomeOutlined, UserOutlined, SafetyOutlined, ApartmentOutlined, SettingOutlined, MenuFoldOutlined, MenuUnfoldOutlined, PartitionOutlined, LogoutOutlined, MessageOutlined, SearchOutlined, BulbOutlined, BulbFilled, AppstoreOutlined, TeamOutlined, TableOutlined, TagsOutlined, RightOutlined, HeartOutlined, CalendarOutlined, PhoneOutlined, CommentOutlined, MedicineBoxOutlined, TrophyOutlined, ShopOutlined, FileTextOutlined, DashboardOutlined, RobotOutlined, HistoryOutlined, BarChartOutlined, } from '@ant-design/icons'; import { useNavigate, useLocation } from 'react-router-dom'; import { useAppStore } from '../stores/app'; import { useAuthStore } from '../stores/auth'; import { usePluginStore } from '../stores/plugin'; import type { PluginMenuGroup } from '../stores/plugin'; import { getMenusForUser, type MenuInfo } from '../api/menus'; import NotificationPanel from '../components/NotificationPanel'; const { Header, Sider, Content, Footer } = Layout; interface MenuItem { key: string; icon: React.ReactNode; label: string; } // 完整图标映射表 const iconMap: Record = { HomeOutlined: , UserOutlined: , SafetyOutlined: , ApartmentOutlined: , SettingOutlined: , PartitionOutlined: , MessageOutlined: , AppstoreOutlined: , TeamOutlined: , TableOutlined: , TagsOutlined: , HeartOutlined: , CalendarOutlined: , PhoneOutlined: , CommentOutlined: , MedicineBoxOutlined: , TrophyOutlined: , ShopOutlined: , FileTextOutlined: , DashboardOutlined: , RobotOutlined: , HistoryOutlined: , BarChartOutlined: , }; function getIcon(name?: string): React.ReactNode { if (!name) return ; return iconMap[name] || ; } // 路由标题 fallback(给动态参数路由用) const routeTitleFallback: Record = { '/health/patients/:id': '患者详情', '/health/follow-up-records': '随访记录', '/health/consultations/:id': '咨询详情', '/health/points-rules': '积分规则管理', '/health/offline-events': '线下活动管理', '/health/articles': '内容管理', '/health/articles/new': '新建文章', '/health/articles/:id/edit': '编辑文章', '/health/article-categories': '分类管理', '/health/article-tags': '标签管理', '/health/alerts': '告警列表', '/health/alert-rules': '告警规则', }; function getTitleFromMenus(path: string, menus: MenuInfo[]): string | undefined { for (const m of menus) { if (m.path === path) return m.title; if (m.children) { const found = getTitleFromMenus(path, m.children); if (found) return found; } } return undefined; } // 侧边栏菜单项 const SidebarMenuItem = memo(function SidebarMenuItem({ item, isActive, collapsed, onClick, indented, }: { item: MenuItem; isActive: boolean; collapsed: boolean; onClick: () => void; indented?: boolean; }) { return (
{item.icon} {!collapsed && {item.label}}
); }); // 可折叠子分组(3 级菜单) const CollapsibleSubGroup = memo(function CollapsibleSubGroup({ directory, collapsed, currentPath, onNavigate, }: { directory: MenuInfo; collapsed: boolean; currentPath: string; onNavigate: (key: string) => void; }) { const [expanded, setExpanded] = useState(false); const visibleChildren = directory.children?.filter((c) => c.visible !== false) || []; const hasActive = visibleChildren.some((c) => currentPath === (c.path || c.id)); useEffect(() => { if (hasActive) setExpanded(true); }, [hasActive]); if (collapsed) { return (
{ const first = visibleChildren[0]; if (first) onNavigate(first.path || first.id); }} className={`erp-sidebar-item ${hasActive ? 'erp-sidebar-item-active' : ''}`} > {getIcon(directory.icon)}
); } return (
setExpanded((e) => !e)} > {directory.title}
{expanded && visibleChildren.map((child) => ( onNavigate(child.path || child.id)} indented /> ))}
); }); // 插件子菜单组 const SidebarSubMenu = memo(function SidebarSubMenu({ group, collapsed, currentPath, onNavigate, }: { group: PluginMenuGroup; collapsed: boolean; currentPath: string; onNavigate: (key: string) => void; }) { const [expanded, setExpanded] = useState(true); const hasActive = group.items.some((item) => currentPath === item.key); if (collapsed) { const tooltipContent = group.items.map((item) => item.label).join(' / '); return (
{ const first = group.items[0]; if (first) onNavigate(first.key); }} className={`erp-sidebar-item ${hasActive ? 'erp-sidebar-item-active' : ''}`} >
); } return (
setExpanded((e) => !e)} > {group.pluginName}
{expanded && group.items.map((item) => ( onNavigate(item.key)} indented /> ))}
); }); // 判断是否为可点击的叶子菜单类型 function isLeafType(menuType: string): boolean { return menuType === 'menu' || menuType === 'page'; } // 动态菜单渲染(支持多级嵌套) const DynamicMenuSection = memo(function DynamicMenuSection({ menus, collapsed, currentPath, onNavigate, }: { menus: MenuInfo[]; collapsed: boolean; currentPath: string; onNavigate: (key: string) => void; }) { return ( <> {menus.map((menu) => { if (menu.menu_type === 'directory') { const visibleChildren = menu.children?.filter((c) => c.visible !== false) || []; return (
{!collapsed &&
{menu.title}
}
{visibleChildren.map((child) => { if (child.menu_type === 'directory') { return ( ); } if (isLeafType(child.menu_type)) { return ( onNavigate(child.path || child.id)} /> ); } return null; })}
); } if (isLeafType(menu.menu_type) && menu.visible !== false) { return ( onNavigate(menu.path || menu.id)} /> ); } return null; })} ); }); export default function MainLayout({ children }: { children: React.ReactNode }) { const { sidebarCollapsed, toggleSidebar, theme: themeMode, setTheme } = useAppStore(); const { user, logout } = useAuthStore(); const pluginMenuItems = usePluginStore((s) => s.pluginMenuItems); const pluginMenuGroups = usePluginStore((s) => s.pluginMenuGroups); const fetchPlugins = usePluginStore((s) => s.fetchPlugins); theme.useToken(); const navigate = useNavigate(); const location = useLocation(); const currentPath = location.pathname || '/'; // 动态菜单状态 const [dynamicMenus, setDynamicMenus] = useState([]); const [menuLoading, setMenuLoading] = useState(true); useEffect(() => { let cancelled = false; (async () => { try { const menus = await getMenusForUser(); if (!cancelled) setDynamicMenus(menus); } catch { // fallback: 使用空数组,保留插件菜单 } if (!cancelled) setMenuLoading(false); })(); return () => { cancelled = true; }; }, []); // 加载插件菜单 useEffect(() => { fetchPlugins(1, 'running'); }, [fetchPlugins]); const handleLogout = useCallback(async () => { await logout(); navigate('/login'); }, [logout, navigate]); // 标题查找:先从动态菜单查找,再 fallback(支持动态路径参数匹配) const headerTitle = useMemo(() => { const fromMenus = getTitleFromMenus(currentPath, dynamicMenus); if (fromMenus) return fromMenus; // 尝试模式匹配 routeTitleFallback 的 key(如 /health/patients/:id) for (const [pattern, title] of Object.entries(routeTitleFallback)) { const regex = new RegExp('^' + pattern.replace(/:[^/]+/g, '[^/]+') + '$'); if (regex.test(currentPath)) return title; } return pluginMenuItems.find((p) => p.key === currentPath)?.label || '页面'; }, [currentPath, dynamicMenus, pluginMenuItems]); const userMenuItems = [ { key: 'profile', icon: , label: user?.display_name || user?.username || '用户', disabled: true, }, { type: 'divider' as const }, { key: 'logout', icon: , label: '退出登录', danger: true, onClick: handleLogout, }, ]; const sidebarWidth = sidebarCollapsed ? 72 : 240; const isDark = themeMode === 'dark'; return ( {/* 现代深色侧边栏 */} {/* Logo 区域 */}
navigate('/')}>
E
{!sidebarCollapsed && ( ERP Platform )}
{/* 动态菜单 */} {menuLoading ? (
) : ( navigate(key)} /> )} {/* 插件菜单 */} {pluginMenuGroups.length > 0 && ( <> {!sidebarCollapsed &&
插件
}
{pluginMenuGroups.map((group) => ( navigate(key)} /> ))}
)}
{/* 右侧主区域 */} {/* 顶部导航栏 */}
{/* 左侧:折叠按钮 + 标题 */}
{sidebarCollapsed ? : }
{headerTitle}
{/* 右侧:搜索 + 通知 + 主题切换 + 用户 */}
setTheme(isDark ? 'light' : 'dark')}> {isDark ? : }
{(user?.display_name?.[0] || user?.username?.[0] || 'U').toUpperCase()} {!sidebarCollapsed && ( {user?.display_name || user?.username || 'User'} )}
{/* 内容区域 */} {children} {/* 底部 */}
); }