forked from wangziqi/gongxue-base
feat(task1): restructure directories for turborepo monorepo
- Move backend/ to apps/server/ via git mv - Move frontend/ to apps/admin/ via git mv - Create packages/typescript-config/ with base, nestjs, and react-vite presets
This commit is contained in:
166
apps/admin/src/layouts/MainLayout.tsx
Normal file
166
apps/admin/src/layouts/MainLayout.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Layout, Menu, Button, Avatar, Dropdown, Drawer } from 'antd';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
TeamOutlined,
|
||||
HomeOutlined,
|
||||
SwapOutlined,
|
||||
DollarOutlined,
|
||||
FileTextOutlined,
|
||||
LogoutOutlined,
|
||||
UserOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
AppstoreOutlined,
|
||||
AuditOutlined,
|
||||
SettingOutlined,
|
||||
WalletOutlined,
|
||||
ReadOutlined,
|
||||
TagsOutlined,
|
||||
FileProtectOutlined,
|
||||
CalendarOutlined,
|
||||
SafetyOutlined,
|
||||
KeyOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { usePermission } from '../hooks/usePermission';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
|
||||
interface MenuItemType {
|
||||
key: string;
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
permission?: string;
|
||||
children?: MenuItemType[];
|
||||
}
|
||||
|
||||
const allMenuItems: MenuItemType[] = [
|
||||
{ key: '/dashboard', icon: <DashboardOutlined />, label: '数据面板', permission: 'dashboard:view' },
|
||||
{ key: '/room-visual', icon: <AppstoreOutlined />, label: '宿舍总览', permission: 'room:view' },
|
||||
{ key: '/students', icon: <TeamOutlined />, label: '学生管理', permission: 'student:view' },
|
||||
{ key: '/rooms', icon: <HomeOutlined />, label: '宿舍管理', permission: 'room:view' },
|
||||
{ key: '/occupancies', icon: <SwapOutlined />, label: '入住管理', permission: 'occupancy:view' },
|
||||
{ key: '/expenses', icon: <DollarOutlined />, label: '费用录入', permission: 'expense:view' },
|
||||
{ key: '/deposits', icon: <WalletOutlined />, label: '押金管理', permission: 'deposit:view' },
|
||||
{ key: '/bills', icon: <FileTextOutlined />, label: '账单管理', permission: 'bill:view' },
|
||||
{
|
||||
key: 'classroom-group',
|
||||
icon: <ReadOutlined />,
|
||||
label: '教室管理',
|
||||
permission: 'classroom:view',
|
||||
children: [
|
||||
{ key: '/classroom-schedule', icon: <CalendarOutlined />, label: '排期总览', permission: 'classroom:view' },
|
||||
{ key: '/classrooms', icon: <ReadOutlined />, label: '教室列表', permission: 'classroom:view' },
|
||||
{ key: '/classroom-rentals', icon: <FileProtectOutlined />, label: '租赁订单', permission: 'rental:view' },
|
||||
{ key: '/tenants', icon: <TagsOutlined />, label: '租赁方', permission: 'tenant:view' },
|
||||
],
|
||||
},
|
||||
{ key: '/operation-logs', icon: <AuditOutlined />, label: '操作日志', permission: 'log:view' },
|
||||
{ key: '/roles', icon: <SafetyOutlined />, label: '角色管理', permission: 'role:view' },
|
||||
{ key: '/permissions', icon: <KeyOutlined />, label: '权限一览', permission: 'role:view' },
|
||||
{ key: '/users', icon: <SettingOutlined />, label: '账号管理', permission: 'user:view' },
|
||||
];
|
||||
|
||||
const MainLayout: React.FC = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const { hasPermission } = usePermission();
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => setIsMobile(window.innerWidth < 768);
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// 按 permission 过滤菜单
|
||||
const filterByPermission = (items: MenuItemType[]): MenuItemType[] => {
|
||||
return items
|
||||
.map(item => {
|
||||
if (item.children) {
|
||||
const kids = filterByPermission(item.children);
|
||||
if (kids.length === 0) return null;
|
||||
return { ...item, children: kids };
|
||||
}
|
||||
if (!item.permission) return item;
|
||||
return hasPermission(item.permission) ? item : null;
|
||||
})
|
||||
.filter(Boolean) as MenuItemType[];
|
||||
};
|
||||
|
||||
const menuItems = filterByPermission(allMenuItems);
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('permissions');
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const handleMenuClick = (key: string) => {
|
||||
navigate(key);
|
||||
if (isMobile) setDrawerOpen(false);
|
||||
};
|
||||
|
||||
const transformToMenuItems = (items: MenuItemType[]): any[] => {
|
||||
return items.map(item => ({
|
||||
key: item.key,
|
||||
icon: item.icon,
|
||||
label: item.label,
|
||||
children: item.children ? transformToMenuItems(item.children) : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
const menuContent = (
|
||||
<Menu
|
||||
theme="light"
|
||||
mode="inline"
|
||||
selectedKeys={[location.pathname]}
|
||||
items={transformToMenuItems(menuItems)}
|
||||
onClick={({ key }) => handleMenuClick(key)}
|
||||
style={{ border: 'none' }}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
{!isMobile && (
|
||||
<Sider trigger={null} collapsible collapsed={collapsed} theme="light" style={{ background: '#fff', borderRight: '1px solid #e5e5e7' }}>
|
||||
<div style={{ height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#1d1d1f', fontSize: collapsed ? 16 : 17, fontWeight: 600, borderBottom: '1px solid #e5e5e7' }}>
|
||||
{collapsed ? '恭' : '恭学教育基地'}
|
||||
</div>
|
||||
{menuContent}
|
||||
</Sider>
|
||||
)}
|
||||
{isMobile && (
|
||||
<Drawer placement="left" open={drawerOpen} onClose={() => setDrawerOpen(false)} width={240} styles={{ body: { padding: 0 } }} title="恭学教育基地">
|
||||
{menuContent}
|
||||
</Drawer>
|
||||
)}
|
||||
<Layout style={{ background: '#f5f5f7' }}>
|
||||
<Header style={{ padding: '0 16px', background: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderBottom: '1px solid #e5e5e7', boxShadow: 'none' }}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={isMobile ? <MenuUnfoldOutlined /> : (collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />)}
|
||||
onClick={() => isMobile ? setDrawerOpen(true) : setCollapsed(!collapsed)}
|
||||
/>
|
||||
<Dropdown menu={{ items: [{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', onClick: handleLogout }] }}>
|
||||
<div style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Avatar icon={<UserOutlined />} />
|
||||
<span>{user.name || user.username || '用户'}</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Header>
|
||||
<Content style={{ margin: isMobile ? 12 : 24, padding: isMobile ? 12 : 24, background: '#fff', borderRadius: 12, overflow: 'auto' }}>
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayout;
|
||||
Reference in New Issue
Block a user