前端布局
XinAdmin 提供了灵活的前端布局系统,支持四种布局模式、完整的主题定制、移动端适配和菜单管理功能。
布局架构
布局系统由以下核心模块组成:
layout/
├── index.tsx # 主布局组件
├── LayoutContext.tsx # 布局上下文(菜单状态管理)
├── HeaderRender.tsx # 顶栏
├── HeaderRightRender.tsx # 顶栏右侧操作区
├── MenuRender.tsx # 菜单渲染
├── ColumnsMenu.tsx # 分栏布局菜单
├── BreadcrumbRender.tsx # 面包屑
├── FooterRender.tsx # 页脚
├── MobileDrawerMenu.tsx # 移动端抽屉菜单
├── SettingDrawer.tsx # 主题设置抽屉
├── theme.ts # 主题配置常量
├── algorithm.ts # 主题算法
├── typing.ts # 类型定义
└── utils.ts # 工具函数
布局组件 LayoutRender 通过 LayoutProvider 包裹,负责初始化菜单数据并在 URL 变化时自动恢复菜单展开状态。
// 入口:LayoutProvider 包裹整个布局
const LayoutRender = () => (
<LayoutProvider>
<LayoutContent />
</LayoutProvider>
);
四种布局模式
布局对比
side(侧边导航)
- 顶栏:Logo + 名称 + 折叠按钮 + 面包屑 + 右侧操作区
- 侧边:完整树形菜单
- 适合:常规后台管理
top(顶部导航)
- 顶栏:Logo + 名称 + 水平菜单 + 右侧操作区
- 侧边:无
- 适合:菜单项少于 7 个的简洁应用
mix(混合导航)
- 顶栏:Logo + 名称 + 折叠按钮 + 一级水平菜单 + 右侧操作区
- 侧边:当前一级菜单下的二级菜单
- 适合:菜单层级较深的大型应用
columns(分栏导航)
- 顶栏:同 side
- 侧边:左侧图标栏(一级菜单) + 右侧展开面板(二级菜单)
- 适合:一级菜单分组多,需要快速切换
布局切换通过全局状态 layout 控制,用户可在主题设置抽屉中可视化切换。
// 通过 store 切换布局
const setLayout = useGlobalStore(state => state.setLayout);
setLayout('columns');
布局上下文(LayoutContext)
LayoutContext 是布局系统的核心状态管理中心,负责菜单数据和菜单交互状态。
状态字段
初始化流程
- 组件挂载时调用
menu() API 获取菜单数据
- 根据当前 URL 路径匹配菜单,自动恢复展开状态和选中项
- 通过
getMenuParentKeys() 工具函数构建从根到当前项的 key 链
import { useLayoutContext } from '@/layout/LayoutContext';
function MyComponent() {
const { menus, selectKey, setCollapsed, collapsed } = useLayoutContext();
// ...
}
菜单系统
菜单数据结构
菜单数据通过 API 接口 /api/system/sys_rule/menu 获取,数据结构如下:
interface IMenus {
id?: number;
pid?: number; // 上级菜单 ID,顶级为 0
type?: 'menu' | 'route' | 'rule'; // 菜单 / 路由 / 权限规则
key?: string; // 唯一标识
name?: string; // 名称(未配置国际化时使用)
path?: string; // 路由路径
icon?: string; // 图标(iconfont 名称)
local?: string; // 国际化 key
hidden?: number; // 是否隐藏(0: 隐藏, 1: 显示)
status?: number; // 是否启用
link?: number; // 是否外链(1: 新窗口打开)
children?: IMenus[]; // 子菜单
}
类型说明
MenuRender 负责将菜单数据转换为 Ant Design Menu 组件可用的格式,核心逻辑:
- 过滤
hidden 为 0 或非 route/menu 类型的菜单项
- 递归构建菜单树结构
- 根据布局模式自动切换 horizontal/inline 模式
- 在 mix/columns 模式下只渲染当前一级菜单的子菜单
- 支持
local 字段的国际化翻译
// 菜单渲染核心逻辑
<Menu
mode={layout === 'top' ? 'horizontal' : 'inline'}
items={menuItems}
onSelect={(info) => onMenuChange(info.keyPath)}
selectedKeys={selectKey}
/>
顶栏分为桌面端和移动端两套渲染,统一包含以下元素:
桌面端
移动端
移动端顶栏简化,仅显示 Logo + 标题 + 右侧操作区。菜单通过底部浮动按钮展开为抽屉。
<Space>
<Button icon={<HomeOutlined/>} /> {/* 官网首页 */}
<Button icon={<GithubOutlined/>} /> {/* GitHub */}
<Button icon={<SearchOutlined/>} /> {/* 菜单搜索 */}
<Button>全屏切换</Button> {/* 浏览器全屏 */}
<LanguageSwitcher /> {/* 中/英切换 */}
<Button icon={<SettingOutlined/>} /> {/* 主题设置 */}
<Dropdown> {/* 用户信息 */}
<Avatar /> + 昵称
{/* 下拉:个人资料 / 退出登录 */}
</Dropdown>
</Space>
搜索弹窗
点击搜索按钮弹出 Modal,支持键盘快捷键提示:
Enter — 确认选择
↑ ↓ — 切换选项
Esc — 关闭搜索
面包屑(BreadcrumbRender)
面包屑通过 buildBreadcrumbMap() 工具函数预构建所有菜单的路径映射,然后根据当前 URL 匹配展示。
// 构建面包屑映射
buildBreadcrumbMap(menus);
// 生成格式:{ [menuKey]: [{ title, icon, href, local }, ...] }
// 渲染效果
// 首页 > 系统管理 > 用户列表
若菜单变更,面包屑会自动更新。未匹配到对应菜单时显示默认首页面包屑。
支持固定和相对两种模式,通过 themeConfig.fixedFooter 控制:
// 固定模式:页脚吸底,内容区自动留白
// 相对模式:页脚随内容流
<Footer style={{ borderTop: '1px solid ' + themeConfig.colorBorder }}>
Xin Admin ©{year} Created by xiaoliu
</Footer>
移动端适配
移动端通过 useMobile() hook 检测,使用独立的菜单展示方式。
- 底部浮动圆形按钮触发左侧抽屉
- 抽屉内渲染完整菜单(
MenuRender)
- 抽屉底部包含首页、GitHub、语言切换、主题设置快捷操作
- 侧边栏在移动端完全隐藏,顶栏简化
// 移动端检测
const isMobile = useMobile();
// 条件渲染
{isMobile && <MobileDrawerMenu />}
{layout !== "top" && !isMobile && <Sider>...</Sider>}
主题系统
主题配置(ThemeProps)
主题配置通过 Zustand store 管理,持久化到 localStorage:
interface ThemeProps {
// 颜色系统
themeScheme: 'light' | 'dark' | 'pink' | 'green';
colorPrimary: string; // 品牌色(默认 #1677ff)
colorError: string; // 错误色
colorSuccess: string; // 成功色
colorWarning: string; // 警告色
colorText: string; // 基础文字颜色
colorBg: string; // 基础背景颜色
background: string; // 全局背景
// 区域颜色
bodyBg: string; // 内容区背景
footerBg: string; // 页脚背景
headerBg: string; // 头部背景
headerColor: string; // 头部文字颜色
siderBg: string; // 侧边栏背景
siderColor: string; // 侧边栏文字颜色
colorBorder: string; // 分割线颜色
// 尺寸配置
borderRadius: number; // 圆角大小(默认 10)
controlHeight: number; // 控件高度(默认 32)
headerPadding: number; // 头部内边距(默认 20)
headerHeight: number; // 头部高度(默认 56)
siderWeight: number; // 侧边栏宽度(默认 226)
bodyPadding: number; // 内容区内边距(默认 20)
fixedFooter: boolean; // 固定页脚(默认 false)
// 算法
algorithm: algorithmType; // 主题算法
}
预设主题
主题算法
自定义主题
const setThemeConfig = useGlobalStore(state => state.setThemeConfig);
// 修改单个配置
setThemeConfig({
...currentConfig,
colorPrimary: '#52c41a',
borderRadius: 4,
});
// 在组件中使用主题色(基于 Ant Design useToken)
const { token } = theme.useToken();
<div style={{ background: token.colorPrimary }} />
主题设置抽屉(SettingDrawer)
可视化主题配置界面,通过右上角齿轮图标打开,分为三个区域:
1. 布局样式
以缩略图形式展示四种布局模式,点击实时切换,当前选中模式显示品牌色边框。
2. 预设主题
3. 主题颜色
通过 ColorPicker 独立配置 13 种主题颜色:
- 品牌色、文字色、背景色
- 成功色、警告色、错误色
- 内容区背景、页脚背景
- 头部背景/文字色
- 侧边栏背景/文字色
- 分割线颜色
4. 风格配置
调整布局尺寸参数:固定页脚、圆角、控件高度、头部高度、侧边栏宽度、内容区内边距等。
所有配置修改通过 400ms 防抖延迟实时生效,并自动持久化到 localStorage。
全局状态管理
布局相关状态通过 Zustand store 管理:
interface GlobalStore {
// 网站信息
logo: string; // Logo 图片 URL
title: string; // 网站标题
subtitle: string; // 副标题
describe: string; // 描述
// 布局
layout: LayoutType; // 当前布局模式
themeConfig: ThemeProps; // 当前主题配置
themeDrawer: boolean; // 主题设置抽屉开关
// 操作
setLayout: (layout: LayoutType) => void;
setThemeConfig: (themeConfig: ThemeProps) => void;
setThemeDrawer: (open: boolean) => void;
initWebInfo: () => Promise<void>; // 初始化网站信息
}
状态通过 persist 中间件持久化到 localStorage,devtools 中间件支持 Redux DevTools 调试。
工具函数
布局系统提供以下工具函数,位于 layout/utils.ts:
完整示例
初始化网站信息
import { useEffect } from 'react';
import useGlobalStore from '@/stores/global';
function AppInit() {
const initWebInfo = useGlobalStore(state => state.initWebInfo);
useEffect(() => {
initWebInfo(); // 从 API 获取 logo/title 等配置
}, []);
return null;
}
动态修改主题
import useGlobalStore from '@/stores/global';
function ThemeToggle() {
const themeConfig = useGlobalStore(state => state.themeConfig);
const setThemeConfig = useGlobalStore(state => state.setThemeConfig);
const toggleDark = () => {
setThemeConfig({
...themeConfig,
themeScheme: 'dark',
bodyBg: '#000',
headerBg: '#141414',
siderBg: '#141414',
colorText: '#fff',
colorBg: '#000',
colorBorder: '#282828',
algorithm: 'darkAlgorithm',
});
};
return <Button onClick={toggleDark}>切换深色</Button>;
}
编程式控制菜单
import { useLayoutContext } from '@/layout/LayoutContext';
function CustomMenuControl() {
const { collapsed, setCollapsed, menus, selectKey, setSelectKey } = useLayoutContext();
return (
<div>
<Button onClick={() => setCollapsed(!collapsed)}>折叠菜单</Button>
<span>当前选中:{selectKey.join(' > ')}</span>
<span>菜单数量:{menus.length}</span>
</div>
);
}