import { useEffect, useMemo, useState } from 'react';
|
import {
|
BellOutlined,
|
DownOutlined,
|
LogoutOutlined,
|
MenuFoldOutlined,
|
MenuUnfoldOutlined,
|
UserOutlined,
|
} from '@ant-design/icons';
|
import {
|
App as AntdApp,
|
Avatar,
|
Breadcrumb,
|
Button,
|
Dropdown,
|
Layout,
|
Menu,
|
Space,
|
Tag,
|
Tooltip,
|
} from 'antd';
|
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
import { buildMenuItems, getOpenKeys } from '@/router/menu';
|
import { appRoutes, findBreadcrumb } from '@/router/routeConfig';
|
import { useAuthStore } from '@/store/auth';
|
import './BasicLayout.scss';
|
|
const { Header, Sider, Content, Footer } = Layout;
|
|
export default function BasicLayout() {
|
const location = useLocation();
|
const navigate = useNavigate();
|
const [collapsed, setCollapsed] = useState(false);
|
const [openKeys, setOpenKeys] = useState<string[]>(() => getOpenKeys(location.pathname));
|
const user = useAuthStore((state) => state.user);
|
const logout = useAuthStore((state) => state.logout);
|
const hasPermission = useAuthStore((state) => state.hasPermission);
|
const { modal, message } = AntdApp.useApp();
|
|
const menuItems = useMemo(
|
() => buildMenuItems(appRoutes, hasPermission),
|
[hasPermission, user],
|
);
|
|
const breadcrumbItems = useMemo(() => {
|
const trail = findBreadcrumb(location.pathname) ?? [];
|
return [{ title: '首页' }, ...trail.map((item) => ({ title: item.title }))];
|
}, [location.pathname]);
|
|
useEffect(() => {
|
const keys = getOpenKeys(location.pathname);
|
setOpenKeys((prev) => Array.from(new Set([...prev, ...keys])));
|
}, [location.pathname]);
|
|
useEffect(() => {
|
const trail = findBreadcrumb(location.pathname);
|
const current = trail?.[trail.length - 1]?.title;
|
document.title = current ? `${current} - CRM 后台管理平台` : 'CRM 后台管理平台';
|
}, [location.pathname]);
|
|
const handleLogout = () => {
|
modal.confirm({
|
title: '确认退出登录?',
|
content: '退出后需要重新登录才能继续访问系统。',
|
okText: '退出登录',
|
cancelText: '取消',
|
okButtonProps: { danger: true },
|
onOk: () => {
|
logout();
|
message.success('已安全退出');
|
navigate('/login', { replace: true });
|
},
|
});
|
};
|
|
const userMenuItems = [
|
{
|
key: 'profile',
|
icon: <UserOutlined />,
|
label: '个人中心',
|
disabled: true,
|
},
|
{ type: 'divider' as const },
|
{
|
key: 'logout',
|
icon: <LogoutOutlined />,
|
label: '退出登录',
|
danger: true,
|
},
|
];
|
|
return (
|
<Layout className="app-layout">
|
<Sider
|
className="app-sider"
|
theme="light"
|
width={240}
|
collapsedWidth={72}
|
trigger={null}
|
collapsible
|
collapsed={collapsed}
|
>
|
<div className="app-logo">
|
<div className="app-logo__mark">C</div>
|
{!collapsed && (
|
<div className="app-logo__text">
|
<span>CRM Admin</span>
|
<small>企业管理平台</small>
|
</div>
|
)}
|
</div>
|
|
<Menu
|
className="app-menu"
|
mode="inline"
|
theme="light"
|
items={menuItems}
|
selectedKeys={[location.pathname]}
|
openKeys={collapsed ? undefined : openKeys}
|
onOpenChange={(keys) => setOpenKeys(keys)}
|
onClick={({ key }) => {
|
if (key !== location.pathname) navigate(key);
|
}}
|
/>
|
|
{!collapsed && (
|
<div className="app-sider__footer">
|
<span>CRM Admin</span>
|
<span>v0.1.0</span>
|
</div>
|
)}
|
</Sider>
|
|
<Layout className="app-main">
|
<Header className="app-header">
|
<div className="app-header__left">
|
<Button
|
className="app-header__trigger"
|
type="text"
|
aria-label={collapsed ? '展开菜单' : '收起菜单'}
|
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
onClick={() => setCollapsed((value) => !value)}
|
/>
|
<Breadcrumb className="app-header__breadcrumb" items={breadcrumbItems} />
|
</div>
|
|
<Space size={16} className="app-header__right">
|
<Tooltip title="消息通知">
|
<Button
|
className="app-header__icon-btn"
|
type="text"
|
shape="circle"
|
icon={<BellOutlined />}
|
onClick={() => message.info('暂无新消息')}
|
/>
|
</Tooltip>
|
|
<Dropdown
|
trigger={['click']}
|
menu={{
|
items: userMenuItems,
|
onClick: ({ key }) => {
|
if (key === 'logout') handleLogout();
|
},
|
}}
|
>
|
<div className="app-header__user">
|
<Avatar className="app-header__avatar" size={34}>
|
{user?.nickname?.charAt(0) ?? 'U'}
|
</Avatar>
|
<div className="app-header__user-info">
|
<span className="app-header__nickname">{user?.nickname}</span>
|
<Tag
|
className="app-header__role"
|
color={user?.role === 'admin' ? 'purple' : 'blue'}
|
bordered={false}
|
>
|
{user?.role === 'admin' ? '管理员' : '普通用户'}
|
</Tag>
|
</div>
|
<DownOutlined className="app-header__user-arrow" />
|
</div>
|
</Dropdown>
|
</Space>
|
</Header>
|
|
<Content className="app-content">
|
<Outlet />
|
</Content>
|
|
<Footer className="app-footer">
|
CRM Admin ©2026 · Vite + React + Ant Design
|
</Footer>
|
</Layout>
|
</Layout>
|
);
|
}
|