193 lines
5.6 KiB
TypeScript
193 lines
5.6 KiB
TypeScript
import { Layout, Menu, Button, Popconfirm, Avatar, theme } from 'antd'
|
||
import { MenuFoldOutlined, MenuUnfoldOutlined, LogoutOutlined, UserOutlined } from '@ant-design/icons'
|
||
import { useNavigate, useLocation, Outlet } from 'react-router-dom'
|
||
import { routes } from '@/router/routes'
|
||
import { useAppStore } from '@/store/app'
|
||
import { useUserStore } from '@/store/user'
|
||
import type { MenuProps } from 'antd'
|
||
|
||
const { Sider, Header, Content } = Layout
|
||
|
||
const SIDER_WIDTH = 220
|
||
const SIDER_COLLAPSED_WIDTH = 64
|
||
const HEADER_HEIGHT = 64
|
||
|
||
// 将 RouteConfig 转换为 antd Menu 的 items 格式
|
||
const menuItems: MenuProps['items'] = routes
|
||
.filter(r => !r.hideInMenu)
|
||
.map(r => ({
|
||
key: r.path,
|
||
icon: r.icon,
|
||
label: r.label,
|
||
children: r.children
|
||
?.filter(c => !c.hideInMenu)
|
||
.map(c => ({
|
||
key: c.path,
|
||
icon: c.icon,
|
||
label: c.label,
|
||
})),
|
||
}))
|
||
|
||
function BasicLayout() {
|
||
const navigate = useNavigate()
|
||
const location = useLocation()
|
||
const { token } = theme.useToken()
|
||
|
||
// app store:侧边栏折叠状态
|
||
const collapsed = useAppStore(s => s.siderCollapsed)
|
||
const setSiderCollapsed = useAppStore(s => s.setSiderCollapsed)
|
||
const toggleSider = useAppStore(s => s.toggleSider)
|
||
|
||
// user store:登出、用户信息
|
||
const logout = useUserStore(s => s.logout)
|
||
const userInfo = useUserStore(s => s.userInfo)
|
||
|
||
const siderWidth = collapsed ? SIDER_COLLAPSED_WIDTH : SIDER_WIDTH
|
||
|
||
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
|
||
navigate(key)
|
||
}
|
||
|
||
const handleLogout = () => {
|
||
logout()
|
||
navigate('/login', { replace: true })
|
||
}
|
||
|
||
return (
|
||
<Layout style={{ minHeight: '100vh' }}>
|
||
{/* 固定侧边栏 */}
|
||
<Sider
|
||
collapsed={collapsed}
|
||
width={SIDER_WIDTH}
|
||
collapsedWidth={SIDER_COLLAPSED_WIDTH}
|
||
breakpoint="md"
|
||
onBreakpoint={broken => setSiderCollapsed(broken)}
|
||
theme="light"
|
||
style={{
|
||
position: 'fixed',
|
||
insetInlineStart: 0,
|
||
top: 0,
|
||
bottom: 0,
|
||
overflowY: 'auto',
|
||
overflowX: 'hidden',
|
||
borderInlineEnd: `1px solid ${token.colorBorderSecondary}`,
|
||
zIndex: 100,
|
||
}}
|
||
>
|
||
{/* Logo 区域 */}
|
||
<div
|
||
style={{
|
||
height: HEADER_HEIGHT,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||
padding: collapsed ? 0 : '0 16px',
|
||
overflow: 'hidden',
|
||
borderBlockEnd: `1px solid ${token.colorBorderSecondary}`,
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
fontSize: 18,
|
||
fontWeight: 700,
|
||
color: token.colorPrimary,
|
||
whiteSpace: 'nowrap',
|
||
}}
|
||
>
|
||
{collapsed ? 'S' : 'SmartServe'}
|
||
</span>
|
||
</div>
|
||
|
||
<Menu
|
||
mode="inline"
|
||
selectedKeys={[location.pathname]}
|
||
items={menuItems}
|
||
onClick={handleMenuClick}
|
||
style={{ borderInlineEnd: 'none', paddingTop: 8 }}
|
||
/>
|
||
</Sider>
|
||
|
||
{/* 右侧主区域,留出侧边栏宽度 */}
|
||
<Layout
|
||
style={{
|
||
marginInlineStart: siderWidth,
|
||
transition: 'margin-inline-start 0.2s',
|
||
}}
|
||
>
|
||
{/* 固定顶部标题栏 */}
|
||
<Header
|
||
style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
insetInlineStart: siderWidth,
|
||
insetInlineEnd: 0,
|
||
height: HEADER_HEIGHT,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
padding: '0 24px 0 16px',
|
||
background: token.colorBgContainer,
|
||
borderBlockEnd: `1px solid ${token.colorBorderSecondary}`,
|
||
zIndex: 99,
|
||
transition: 'inset-inline-start 0.2s',
|
||
}}
|
||
>
|
||
{/* 收起/展开按钮 */}
|
||
<Button
|
||
type="text"
|
||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||
onClick={toggleSider}
|
||
style={{ fontSize: 16, width: 36, height: 36 }}
|
||
/>
|
||
|
||
{/* 当前页面标题 */}
|
||
<span style={{ fontSize: 16, fontWeight: 600, color: token.colorText }}>
|
||
{routes.find(r => r.path === location.pathname)?.label ?? ''}
|
||
</span>
|
||
|
||
{/* 右侧操作区 */}
|
||
<div style={{ marginInlineStart: 'auto', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
{/* 用户昵称 */}
|
||
<Avatar
|
||
size="small"
|
||
src={userInfo?.avatar}
|
||
icon={!userInfo?.avatar && <UserOutlined />}
|
||
style={{ background: token.colorPrimary }}
|
||
/>
|
||
<span style={{ color: token.colorText, fontSize: 14 }}>
|
||
{userInfo?.nickname ?? ''}
|
||
</span>
|
||
|
||
<Popconfirm
|
||
title="确认退出登录?"
|
||
onConfirm={handleLogout}
|
||
okText="退出"
|
||
cancelText="取消"
|
||
placement="bottomRight"
|
||
>
|
||
<Button type="text" icon={<LogoutOutlined />} style={{ color: token.colorTextSecondary }}>
|
||
退出登录
|
||
</Button>
|
||
</Popconfirm>
|
||
</div>
|
||
</Header>
|
||
|
||
{/* 内容区,撑开顶部空间 */}
|
||
<Content
|
||
style={{
|
||
marginBlockStart: HEADER_HEIGHT,
|
||
padding: 24,
|
||
minHeight: `calc(100vh - ${HEADER_HEIGHT}px)`,
|
||
background: token.colorBgLayout,
|
||
}}
|
||
>
|
||
<Outlet />
|
||
</Content>
|
||
</Layout>
|
||
</Layout>
|
||
)
|
||
}
|
||
|
||
export default BasicLayout
|