190 lines
6.9 KiB
TypeScript
190 lines
6.9 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|
import { Avatar, Breadcrumb, Drawer, Dropdown, Layout } from 'antd';
|
|
import type { MenuProps } from 'antd';
|
|
import {
|
|
LockOutlined,
|
|
LogoutOutlined,
|
|
MenuFoldOutlined,
|
|
MenuUnfoldOutlined,
|
|
RightOutlined,
|
|
UserOutlined,
|
|
} from '@ant-design/icons';
|
|
import SidebarMenu from './SidebarMenu';
|
|
import PlatformSelector from './PlatformSelector';
|
|
import { getBreadcrumbItems } from './menuConfig';
|
|
import { useSession } from '@/services/session';
|
|
import './MainLayout.css';
|
|
|
|
const { Header, Sider, Content } = Layout;
|
|
|
|
const MainLayout = () => {
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
const [isMobile, setIsMobile] = useState(false);
|
|
const [loggingOut, setLoggingOut] = useState(false);
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const { session, logout } = useSession();
|
|
|
|
const breadcrumbItems = useMemo(
|
|
() =>
|
|
getBreadcrumbItems(location.pathname).map((crumb) => ({
|
|
title: (
|
|
<span className="breadcrumb-crumb">
|
|
{crumb.icon}
|
|
<span>{crumb.label}</span>
|
|
</span>
|
|
),
|
|
})),
|
|
[location.pathname]
|
|
);
|
|
|
|
const userDisplayName = session ? (session.user.display_name ?? session.user.email) : '';
|
|
const userInitial = userDisplayName.charAt(0).toUpperCase();
|
|
|
|
const handleLogout = async () => {
|
|
setLoggingOut(true);
|
|
try {
|
|
await logout();
|
|
} finally {
|
|
setLoggingOut(false);
|
|
navigate('/login', { replace: true });
|
|
}
|
|
};
|
|
|
|
const handleUserMenuClick: MenuProps['onClick'] = ({ key }) => {
|
|
if (key === 'profile') {
|
|
navigate('/settings/profile');
|
|
} else if (key === 'password') {
|
|
navigate('/settings/password');
|
|
} else if (key === 'logout') {
|
|
void handleLogout();
|
|
}
|
|
};
|
|
|
|
const userMenuItems: MenuProps['items'] = [
|
|
{ key: 'profile', icon: <UserOutlined />, label: '修改资料' },
|
|
{ key: 'password', icon: <LockOutlined />, label: '修改密码' },
|
|
{ type: 'divider' },
|
|
{
|
|
key: 'logout',
|
|
icon: <LogoutOutlined />,
|
|
label: loggingOut ? '退出中…' : '退出登录',
|
|
disabled: loggingOut,
|
|
},
|
|
];
|
|
|
|
useEffect(() => {
|
|
const checkMobile = () => {
|
|
setIsMobile(window.innerWidth < 768);
|
|
if (window.innerWidth >= 768) setMobileMenuOpen(false);
|
|
};
|
|
checkMobile();
|
|
window.addEventListener('resize', checkMobile);
|
|
return () => window.removeEventListener('resize', checkMobile);
|
|
}, []);
|
|
|
|
return (
|
|
<Layout className="main-layout">
|
|
{!isMobile && (
|
|
<Sider
|
|
className="main-sider"
|
|
width={200}
|
|
collapsedWidth={60}
|
|
collapsed={collapsed}
|
|
theme="dark"
|
|
style={{
|
|
overflow: 'auto',
|
|
height: '100vh',
|
|
position: 'fixed',
|
|
left: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
}}
|
|
>
|
|
<div className="sidebar-header">
|
|
<div className="logo">
|
|
<span className="logo-full">Atlas Kit</span>
|
|
<span className="logo-short">A</span>
|
|
</div>
|
|
<div className="logo-subtitle">商品工作台</div>
|
|
</div>
|
|
<SidebarMenu collapsed={collapsed} />
|
|
</Sider>
|
|
)}
|
|
|
|
{isMobile && (
|
|
<Drawer
|
|
title="Atlas Kit"
|
|
placement="left"
|
|
onClose={() => setMobileMenuOpen(false)}
|
|
open={mobileMenuOpen}
|
|
styles={{ body: { padding: 0, background: 'var(--sider-bg)' } }}
|
|
width={200}
|
|
>
|
|
<SidebarMenu collapsed={false} onMenuClick={() => setMobileMenuOpen(false)} />
|
|
</Drawer>
|
|
)}
|
|
|
|
<Layout
|
|
className="main-content-layout"
|
|
style={{
|
|
marginLeft: isMobile ? 0 : collapsed ? 60 : 200,
|
|
transition: 'margin-left 0.2s',
|
|
}}
|
|
>
|
|
<Header className="main-header">
|
|
<div className="header-left">
|
|
{isMobile && (
|
|
<MenuFoldOutlined
|
|
className="mobile-menu-trigger"
|
|
onClick={() => setMobileMenuOpen(true)}
|
|
/>
|
|
)}
|
|
{!isMobile && (
|
|
<div
|
|
className="collapse-trigger"
|
|
onClick={() => setCollapsed(!collapsed)}
|
|
>
|
|
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
</div>
|
|
)}
|
|
<Breadcrumb
|
|
className="page-breadcrumb"
|
|
separator={<RightOutlined style={{ fontSize: 12 }} />}
|
|
items={breadcrumbItems}
|
|
/>
|
|
</div>
|
|
{session && (
|
|
<div className="header-right">
|
|
<PlatformSelector />
|
|
<Dropdown
|
|
menu={{ items: userMenuItems, onClick: handleUserMenuClick }}
|
|
trigger={['hover']}
|
|
placement="bottomRight"
|
|
>
|
|
<div className="header-user-trigger" title={userDisplayName}>
|
|
<Avatar
|
|
size={28}
|
|
style={{ backgroundColor: 'var(--sider-bg)' }}
|
|
>
|
|
{userInitial}
|
|
</Avatar>
|
|
<span className="header-user-name">{userDisplayName}</span>
|
|
</div>
|
|
</Dropdown>
|
|
</div>
|
|
)}
|
|
</Header>
|
|
|
|
<Content className="main-content">
|
|
<Outlet />
|
|
</Content>
|
|
</Layout>
|
|
</Layout>
|
|
);
|
|
};
|
|
|
|
export default MainLayout;
|