This commit is contained in:
2026-05-19 19:31:37 +08:00
commit aab7206307
56 changed files with 5183 additions and 0 deletions

View File

@@ -0,0 +1,192 @@
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