cosmo/frontend/src/pages/admin/AdminLayout.tsx

196 lines
5.0 KiB
TypeScript

/**
* Admin Layout with Sidebar
*/
import { useState, useEffect } from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { Layout, Menu, Avatar, Dropdown } from 'antd';
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
DashboardOutlined,
DatabaseOutlined,
DownloadOutlined,
UserOutlined,
LogoutOutlined,
RocketOutlined,
AppstoreOutlined,
SettingOutlined,
TeamOutlined,
ControlOutlined,
LockOutlined,
IdcardOutlined,
StarOutlined,
} from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { authAPI } from '../../utils/request';
import { auth } from '../../utils/auth';
import { useToast } from '../../contexts/ToastContext';
const { Header, Sider, Content } = Layout;
// Icon mapping
const iconMap: Record<string, any> = {
dashboard: <DashboardOutlined />,
database: <DatabaseOutlined />,
planet: <RocketOutlined />,
data: <DatabaseOutlined />,
download: <DownloadOutlined />,
settings: <SettingOutlined />,
users: <TeamOutlined />,
sliders: <ControlOutlined />,
profile: <IdcardOutlined />,
star: <StarOutlined />,
};
export function AdminLayout() {
const [collapsed, setCollapsed] = useState(false);
const [menus, setMenus] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
const location = useLocation();
const user = auth.getUser();
const toast = useToast();
// Load menus from backend
useEffect(() => {
loadMenus();
}, []);
// Redirect to first menu if on root path
useEffect(() => {
if (menus.length > 0 && (location.pathname === '/admin' || location.pathname === '/user')) {
const firstMenu = menus[0];
if (firstMenu.path) {
navigate(firstMenu.path, { replace: true });
}
}
}, [menus, location.pathname, navigate]);
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 userMenuItems: MenuProps['items'] = [
{
key: 'change-password',
icon: <LockOutlined />,
label: '修改密码',
onClick: () => navigate('/admin/change-password'),
},
{
type: 'divider',
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
onClick: handleLogout,
},
];
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider trigger={null} collapsible collapsed={collapsed}>
<div
style={{
height: 64,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 20,
fontWeight: 'bold',
color: '#fff',
}}
>
{collapsed ? '🌌' : '🌌 COSMO'}
</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={[location.pathname]}
items={convertMenus(menus)}
onClick={handleMenuClick}
/>
</Sider>
<Layout>
<Header
style={{
padding: '0 16px',
background: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div
onClick={() => setCollapsed(!collapsed)}
style={{ fontSize: 18, cursor: 'pointer' }}
>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</div>
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
<div style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<Avatar icon={<UserOutlined />} style={{ marginRight: 8 }} />
<span>{user?.username || 'User'}</span>
</div>
</Dropdown>
</Header>
<Content
style={{
margin: '16px',
padding: 24,
background: '#fff',
minHeight: 280,
overflow: 'auto',
maxHeight: 'calc(100vh - 64px - 32px)',
}}
>
<Outlet />
</Content>
</Layout>
</Layout>
);
}