前端布局

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侧边栏垂直菜单 + 顶栏默认布局,适合大多数后台
顶部导航top顶部水平菜单菜单项较少,内容区域更宽
混合导航mix顶部一级菜单 + 侧边二级菜单菜单层级多,需要分组展示
分栏导航columns左侧图标栏 + 展开的侧边菜单菜单分组多,侧边栏更紧凑

布局对比

side(侧边导航)

  • 顶栏:Logo + 名称 + 折叠按钮 + 面包屑 + 右侧操作区
  • 侧边:完整树形菜单
  • 适合:常规后台管理

top(顶部导航)

  • 顶栏:Logo + 名称 + 水平菜单 + 右侧操作区
  • 侧边:无
  • 适合:菜单项少于 7 个的简洁应用

mix(混合导航)

  • 顶栏:Logo + 名称 + 折叠按钮 + 一级水平菜单 + 右侧操作区
  • 侧边:当前一级菜单下的二级菜单
  • 适合:菜单层级较深的大型应用

columns(分栏导航)

  • 顶栏:同 side
  • 侧边:左侧图标栏(一级菜单) + 右侧展开面板(二级菜单)
  • 适合:一级菜单分组多,需要快速切换

布局切换通过全局状态 layout 控制,用户可在主题设置抽屉中可视化切换。

// 通过 store 切换布局
const setLayout = useGlobalStore(state => state.setLayout);
setLayout('columns');

布局上下文(LayoutContext)

LayoutContext 是布局系统的核心状态管理中心,负责菜单数据和菜单交互状态。

状态字段

字段类型说明
menusIMenus[]从 API 获取的完整菜单树
parentKeystring | undefined当前选中的一级菜单 key
selectKeystring[]当前选中的菜单 key 路径(面包屑链)
collapsedboolean侧边栏折叠状态
setCollapsed(collapsed: boolean) => void设置折叠状态
setParentKey(key: string) => void设置父级菜单
setSelectKey(keys: string[]) => void设置选中路径

初始化流程

  1. 组件挂载时调用 menu() API 获取菜单数据
  2. 根据当前 URL 路径匹配菜单,自动恢复展开状态和选中项
  3. 通过 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[];    // 子菜单
}

类型说明

type 值说明点击行为
menu菜单分组展开/折叠子菜单,或切换父菜单(mix 模式)
route路由链接跳转到对应路由,外链则新窗口打开
rule权限规则不渲染,仅用于权限控制

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}
/>

顶栏(HeaderRender)

顶栏分为桌面端和移动端两套渲染,统一包含以下元素:

桌面端

元素说明
Logo + 标题品牌标识,支持自定义图片和文字
折叠按钮侧边/混合布局下显示,切换侧边栏折叠
面包屑side/columns 布局下显示路径导航
顶部菜单top 布局下渲染水平菜单;mix 布局下渲染一级菜单
右侧操作区搜索、全屏、语言切换、主题设置、用户信息

移动端

移动端顶栏简化,仅显示 Logo + 标题 + 右侧操作区。菜单通过底部浮动按钮展开为抽屉。

顶栏右侧操作区(HeaderRightRender)

<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 }, ...] }

// 渲染效果
// 首页 > 系统管理 > 用户列表

若菜单变更,面包屑会自动更新。未匹配到对应菜单时显示默认首页面包屑。

页脚(FooterRender)

支持固定和相对两种模式,通过 themeConfig.fixedFooter 控制:

// 固定模式:页脚吸底,内容区自动留白
// 相对模式:页脚随内容流
<Footer style={{ borderTop: '1px solid ' + themeConfig.colorBorder }}>
  Xin Admin ©{year} Created by xiaoliu
</Footer>

移动端适配

移动端通过 useMobile() hook 检测,使用独立的菜单展示方式。

移动端菜单(MobileDrawerMenu)

  • 底部浮动圆形按钮触发左侧抽屉
  • 抽屉内渲染完整菜单(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; // 主题算法
}

预设主题

主题说明
light浅色主题(默认),白色背景
dark深色主题,深色背景

主题算法

算法说明
defaultAlgorithmAnt Design 默认算法
darkAlgorithmAnt Design 暗黑算法
defaultCompactAlgorithm默认 + 紧凑算法
darkCompactAlgorithm暗黑 + 紧凑算法

自定义主题

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

函数说明
buildBreadcrumbMap(menus)构建面包屑映射表,返回 { [key]: BreadcrumbItem[] }
findMenuByPath(menus, path)通过路由路径查找菜单项
findMenuByKey(menus, key)通过 key 查找菜单项(递归)
getMenuParentKeys(menus, path)获取从根到指定路径的所有父级菜单 key

完整示例

初始化网站信息

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>
  );
}