import React, { useEffect, useState } from 'react'; import { Layout, Menu, Avatar, Typography, Dropdown, Spin, Modal, Form, Input, Space, message } from 'antd'; import { MenuOutlined, PlusOutlined, EditOutlined, DeleteOutlined, HomeOutlined, PlayCircleOutlined, WalletOutlined, RobotOutlined, SettingOutlined, BellOutlined, UserOutlined, AppstoreOutlined, FileTextOutlined, StarOutlined, HeartOutlined, CameraOutlined, DashboardOutlined, CalculatorOutlined, DollarOutlined, GiftOutlined, ThunderboltOutlined, FireOutlined, CloudOutlined, SmileOutlined, TrophyOutlined, RocketOutlined, BulbOutlined, CodeOutlined, PictureOutlined, VideoCameraOutlined, AudioOutlined, MailOutlined, PhoneOutlined, GlobalOutlined, ShoppingCartOutlined, TeamOutlined, BarChartOutlined, PieChartOutlined, LineChartOutlined, SecurityScanOutlined, ApiOutlined, DatabaseOutlined, CloudServerOutlined, LockOutlined, LogoutOutlined } from '@ant-design/icons'; import { Outlet, useNavigate, useLocation, Navigate } from 'react-router-dom'; import { useAdminStore } from '../store'; import { getMenuConfigs, adminChangePassword, getSiteInfo } from '../api'; const { Sider, Content } = Layout; const iconMap: Record = { HomeOutlined: , PlayCircleOutlined: , WalletOutlined: , RobotOutlined: , SettingOutlined: , BellOutlined: , UserOutlined: , AppstoreOutlined: , FileTextOutlined: , StarOutlined: , HeartOutlined: , CameraOutlined: , DashboardOutlined: , CalculatorOutlined: , DollarOutlined: , GiftOutlined: , ThunderboltOutlined: , FireOutlined: , CloudOutlined: , SmileOutlined: , TrophyOutlined: , RocketOutlined: , BulbOutlined: , CodeOutlined: , PictureOutlined: , VideoCameraOutlined: , AudioOutlined: , MailOutlined: , PhoneOutlined: , GlobalOutlined: , ShoppingCartOutlined: , TeamOutlined: , BarChartOutlined: , PieChartOutlined: , LineChartOutlined: , SecurityScanOutlined: , ApiOutlined: , DatabaseOutlined: , CloudServerOutlined: , MenuOutlined: , PlusOutlined: , EditOutlined: , DeleteOutlined: , LockOutlined: , LogoutOutlined: }; const AdminLayout: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); const { user, loading, logout } = useAdminStore(); const [menuItems, setMenuItems] = useState([]); const [pwdModal, setPwdModal] = useState(false); const [pwdForm] = Form.useForm(); const [siteName, setSiteName] = useState(() => { const cached = localStorage.getItem('siteInfo'); return cached ? JSON.parse(cached).siteName || '管理后台' : '管理后台'; }); const [siteLogo, setSiteLogo] = useState(() => { const cached = localStorage.getItem('siteInfo'); return cached ? JSON.parse(cached).siteLogo || '' : ''; }); useEffect(() => { getSiteInfo().then(info => { const name = info.siteName || '管理后台'; const logo = info.siteLogo || ''; setSiteName(name); setSiteLogo(logo); document.title = name === '管理后台' ? name : name + ' - 管理后台'; localStorage.setItem('siteInfo', JSON.stringify({ siteName: name, siteLogo: logo })); if (logo) { let faviconLink = document.querySelector('link[rel="icon"]') as HTMLLinkElement; if (!faviconLink) { faviconLink = document.createElement('link'); faviconLink.rel = 'icon'; document.head.appendChild(faviconLink); } faviconLink.href = logo; faviconLink.type = 'image/png'; } }).catch(() => {}); }, []); useEffect(() => { getMenuConfigs().then(data => { let menus = data.filter((m: any) => { const target = m.menu_target ?? m.menuTarget ?? 'admin'; const active = m.is_active ?? m.isActive ?? true; return active && (target === 'admin' || target === 'both'); }); // Non-admin backend users: filter by allowedMenus const isAdminUser = !!(user?.isAdmin ?? (user as any)?.is_admin); if (user && !isAdminUser) { const allowed = user.allowedMenus ?? (user as any)?.allowed_menus; if (allowed && Array.isArray(allowed) && allowed.length > 0) { const allowedSet = new Set(allowed); menus = menus.filter((m: any) => { const menuType = m.menu_type ?? m.menuType; if (menuType === 'group') { return menus.some((c: any) => { const pid = c.parent_id ?? c.parentId; return pid === m.id && allowedSet.has(c.path); }); } return allowedSet.has(m.path); }); } else { // No allowed menus set and not admin = show nothing menus = []; } } setMenuItems(menus); }).catch(() => {}); }, [user]); if (loading) { return (
); } if (!user) { return ; } // Build menu items for Ant Design Menu — single sorted list interleaving groups and pages const childMap: Record = {}; menuItems.filter(m => (m.menu_type ?? m.menuType) !== 'group' && (m.parent_id ?? m.parentId)).forEach(m => { const pid = m.parent_id ?? m.parentId; if (!childMap[pid]) childMap[pid] = []; childMap[pid].push(m); }); // Sort all menu items by sortOrder, then build ant menu items const sorted = [...menuItems].sort((a, b) => (a.sort_order ?? a.sortOrder ?? 0) - (b.sort_order ?? b.sortOrder ?? 0)); const antMenuItems: any[] = []; sorted.forEach(m => { const menuType = m.menu_type ?? m.menuType; const parentId = m.parent_id ?? m.parentId; if (menuType === 'group') { const children = (childMap[m.id] || []) .sort((a, b) => (a.sort_order ?? a.sortOrder ?? 0) - (b.sort_order ?? b.sortOrder ?? 0)) .map(c => ({ key: c.path, icon: iconMap[c.icon] || undefined, label: c.label, })); if (children.length > 0) { antMenuItems.push({ key: `group-${m.id}`, icon: iconMap[m.icon] || undefined, label: m.label, children, }); } } else if (!parentId) { antMenuItems.push({ key: m.path, icon: iconMap[m.icon] || undefined, label: m.label, }); } }); // Fallback only for admin users — non-admin users with no permissions see nothing const isAdminUser = !!(user?.isAdmin ?? (user as any)?.is_admin); if (antMenuItems.length === 0 && isAdminUser) { antMenuItems.push( { key: '/', icon: , label: '数据概览' }, { key: '/users', icon: , label: '用户管理' }, ); } const selectedKey = location.pathname; // Find the leaf menu key that matches (for sub-menus, need to find the right key) let activeKey = selectedKey; const allLeafKeys: string[] = []; antMenuItems.forEach(item => { if (item.children) { item.children.forEach((c: any) => allLeafKeys.push(c.key)); } else { allLeafKeys.push(item.key); } }); // Exact match or prefix match if (!allLeafKeys.includes(activeKey)) { activeKey = allLeafKeys.find(k => activeKey.startsWith(k)) || '/'; } // Find open keys for sub-menus const openKeys: string[] = []; antMenuItems.forEach(item => { if (item.children) { if (item.children.some((c: any) => c.key === activeKey)) { openKeys.push(item.key); } } }); return ( {/* Logo */}
{siteLogo ? ( logo ) : ( )}
{siteName}
{/* Menu */}
{ if (key.startsWith('group-')) return; navigate(key); }} theme="light" />
{/* User Actions - moved to bottom of sidebar */}
, label: user?.username, disabled: true }, { type: 'divider' as const }, { key: 'changePwd', icon: , label: '修改密码' }, { key: 'logout', icon: , label: '退出登录', danger: true }, ], onClick: ({ key }) => { if (key === 'logout') { logout(); navigate('/login'); } if (key === 'changePwd') { setPwdModal(true); pwdForm.resetFields(); } }, }} placement="topRight" arrow>
{ e.currentTarget.style.background = '#ffffff'; e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.08)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.boxShadow = 'none'; }} > } style={{ background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)', }} />
{user?.username} {isAdminUser ? '管理员' : '普通用户'}
{/* Header */}
{antMenuItems.find(m => m.key === activeKey)?.label || antMenuItems.flatMap(m => m.children || []).find((c: any) => c.key === activeKey)?.label || '管理后台'}
{/* Content */} {antMenuItems.length === 0 && !isAdminUser ? (
暂无任何权限
请联系管理员配置菜单权限
) : ( )}
修改密码} open={pwdModal} onOk={async () => { try { const values = await pwdForm.validateFields(); if (values.newPassword !== values.confirmPassword) { message.error('两次输入的密码不一致'); return; } await adminChangePassword(values.oldPassword, values.newPassword); message.success('密码修改成功'); setPwdModal(false); pwdForm.resetFields(); } catch (e: any) { if (e?.errorFields) return; message.error(e?.message || '修改失败'); } }} onCancel={() => { setPwdModal(false); pwdForm.resetFields(); }} okText="确认修改" cancelText="取消" width={420} >
); }; export default AdminLayout;