Files
smartserve-client/frontend/src/layouts/BasicLayout/index.tsx
2026-05-19 19:31:37 +08:00

193 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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