/** * Admin Layout with Sidebar */ import { useState, useEffect } from 'react'; import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Layout, Menu, Avatar, Dropdown, Modal, Form, Input, Button, message } from 'antd'; import { MenuFoldOutlined, MenuUnfoldOutlined, DashboardOutlined, DatabaseOutlined, DownloadOutlined, UserOutlined, LogoutOutlined, RocketOutlined, AppstoreOutlined, SettingOutlined, TeamOutlined, ControlOutlined, } from '@ant-design/icons'; import type { MenuProps } from 'antd'; import { authAPI, request } from '../../utils/request'; import { auth } from '../../utils/auth'; import { useToast } from '../../contexts/ToastContext'; const { Header, Sider, Content } = Layout; // Icon mapping const iconMap: Record = { dashboard: , database: , planet: , data: , download: , settings: , users: , sliders: , }; export function AdminLayout() { const [collapsed, setCollapsed] = useState(false); const [menus, setMenus] = useState([]); const [loading, setLoading] = useState(true); const [profileModalOpen, setProfileModalOpen] = useState(false); const [passwordModalOpen, setPasswordModalOpen] = useState(false); const [profileForm] = Form.useForm(); const [passwordForm] = Form.useForm(); const [userProfile, setUserProfile] = useState(null); const navigate = useNavigate(); const location = useLocation(); const user = auth.getUser(); const toast = useToast(); // Load menus from backend useEffect(() => { loadMenus(); }, []); const loadMenus = async () => { try { const { data } = await authAPI.getMenus(); setMenus(data); } catch (error) { toast.error('加载菜单失败'); } finally { setLoading(false); } }; // Convert backend menu to Ant Design menu format const convertMenus = (menus: any[], isChild = false): MenuProps['items'] => { return menus.map((menu) => { const item: any = { key: menu.path || menu.name, icon: isChild ? null : (iconMap[menu.icon || ''] || null), label: menu.title, }; if (menu.children && menu.children.length > 0) { item.children = convertMenus(menu.children, true); } return item; }); }; const handleMenuClick: MenuProps['onClick'] = ({ key }) => { navigate(key); }; const handleLogout = async () => { try { await authAPI.logout(); auth.logout(); toast.success('登出成功'); navigate('/login'); } catch (error) { // Even if API fails, clear local auth auth.logout(); navigate('/login'); } }; const handleProfileClick = async () => { try { const { data } = await request.get('/users/me'); setUserProfile(data); profileForm.setFieldsValue({ username: data.username, email: data.email || '', full_name: data.full_name || '', }); setProfileModalOpen(true); } catch (error) { toast.error('获取用户信息失败'); } }; const handleProfileUpdate = async (values: any) => { try { await request.put('/users/me/profile', { full_name: values.full_name, email: values.email || null, }); toast.success('个人信息更新成功'); setProfileModalOpen(false); // Update local user info const updatedUser = { ...user, full_name: values.full_name, email: values.email }; auth.setUser(updatedUser); } catch (error: any) { toast.error(error.response?.data?.detail || '更新失败'); } }; const handlePasswordChange = async (values: any) => { try { await request.put('/users/me/password', { old_password: values.old_password, new_password: values.new_password, }); toast.success('密码修改成功'); setPasswordModalOpen(false); passwordForm.resetFields(); } catch (error: any) { toast.error(error.response?.data?.detail || '密码修改失败'); } }; const userMenuItems: MenuProps['items'] = [ { key: 'profile', icon: , label: '个人信息', onClick: handleProfileClick, }, { type: 'divider', }, { key: 'logout', icon: , label: '退出登录', onClick: handleLogout, }, ]; return (
{collapsed ? '🌌' : '🌌 COSMO'}
setCollapsed(!collapsed)} style={{ fontSize: 18, cursor: 'pointer' }} > {collapsed ? : }
} style={{ marginRight: 8 }} /> {user?.username || 'User'}
{/* Profile Modal */} setProfileModalOpen(false)} footer={null} width={500} >
{/* Password Change Modal */} { setPasswordModalOpen(false); passwordForm.resetFields(); }} footer={null} width={450} >
({ validator(_, value) { if (!value || getFieldValue('new_password') === value) { return Promise.resolve(); } return Promise.reject(new Error('两次输入的密码不一致')); }, }), ]} >
); }