{T}

布局模式与菜单系统演进

概述

后台布局系统不是把页面分成左右两栏,而是让头部、侧边栏、内容区与菜单模式协同工作。这一阶段组件开发从「单个组件」升级为「系统性功能模块」,复杂度来自状态同步、多模式切换、响应式差异与主题联动。本文先建立整体设计地图:先把布局系统、菜单数据结构、递归渲染与多导航模式这几个核心问题想清楚,后续各篇再逐一落地实现。

学习目标

  • 理解后台布局系统的「整体协同」视角,而非只盯单个区块
  • 掌握多导航模式(sidebar / mix / top / dual-sidebar)及其状态同步难点
  • 区分桌面端「折叠」与移动端「全屏覆盖」两种菜单交互,并分开管理状态
  • 理解菜单系统的本质:把路由树翻译成菜单树的数据建模
  • 用「外层列表入口组件 + 单节点递归组件 + useMenu 加工」组织菜单代码

一、从单组件到布局系统

布局系统的难点不在写一个盒子,而在同时处理布局结构、状态同步、交互反馈、多模式切换、视觉主题联动和移动端差异。它本质是一个综合性功能模块:

text
布局系统
  头部(工具区、顶部交互)
  侧边栏(菜单系统容器)
  内容区(路由视图)
  菜单模式(sidebar / mix / top / dual-sidebar)
  主题设置(宽度、暗黑、背景色)

基础骨架先把三段结构搭出来,再逐步叠加响应式和模式切换逻辑——布局是检验此前基础组件是否真正可复用的真实场景。

二、多导航模式与状态同步

菜单支持多种导航模式后,难点不在摆放位置,而在一级、二级和当前高亮的联动:

  • sidebar:左侧承载主菜单
  • mix:顶部一级菜单 + 左侧二级菜单,顶部选中项必须和左侧菜单上下文同步
  • top:主导航全部上移,侧边栏不再承担主导航;窄屏时顶部菜单同样会拥挤,需要单独做折叠收纳
  • dual-sidebar:第一列一级、第二列二级,难点在一级激活、二级激活、路由激活三者统一

这些模式最好先定义为状态枚举,再根据模式派生不同区域的可见菜单,而不是硬写多套分支 UI:

ts
function resolveVisibleMenus(state: LayoutState, allMenus: AppMenuItem[]) {
  switch (state.mode) {
    case "mix":
    case "dual-sidebar":
      return allMenus.find((m) => m.path === state.activeTopMenu)?.children || []
    case "top":
      return []
    default:
      return allMenus
  }
}

三、响应式:折叠与全屏覆盖分开管理

响应式交互的第一个重点是侧边栏随宽度自动折叠/展开,但移动端不能简单等同于「缩小版侧边栏」——它更适合点击后展示全屏覆盖式菜单,因为可点击区域要更大、菜单层级要更清晰。

因此桌面端与移动端的状态应分开管理,比硬塞进一个布尔值更稳:

ts
interface LayoutState {
  mode: "sidebar" | "mix" | "top" | "dual-sidebar"
  isMobile: boolean
  isCollapse: boolean      // 桌面端侧边栏折叠
  isDrawerOpen: boolean    // 移动端全屏菜单展开
  menuWidth: number
  menuTheme: "light" | "dark" | "custom"
  activeTopMenu?: string
  activeSideMenu?: string
}

断点建议按桌面 / 平板 / 移动三档划分,而不是只有「宽」「窄」两种粗糙判断。主题设置也不是孤立面板——改菜单宽度、暗黑模式、背景色都应即时联动到布局,且菜单配色应做成受控主题色方案而非任意颜色输入,避免高饱和背景刺眼。

四、菜单数据结构:路由树到菜单树

官方 el-menu 更像「菜单引擎」,提供容器、子菜单展开、激活态与多层级机制;真正的后台菜单系统还需要数据驱动渲染、递归、风格切换与路由整合——即基于官方内核做业务级封装。

多级菜单的关键不在层级数量,而在递归渲染能力。菜单组件真正的任务,是把自动路由或 Mock 路由翻译成菜单可消费的树形结构:

text
Mock / auto-routes
  -> 补充 meta 信息(title / icon / order / hidden)
  -> 转换成菜单树
  -> 交给基础菜单组件递归渲染

unplugin-vue-router 可通过 vue-router/auto-routes 导出基础路由,但菜单所需的排序、图标、隐藏规则仍需在 meta 中扩展。菜单节点的核心业务字段通常落在 meta 上(title 作展示文案、name 作路由标识、order 控制排序、hidden 控制可见性),并允许用索引签名开放扩展。

五、递归渲染与组件拆分

菜单组件不应把层级写死,最合理的拆分是「外层列表入口组件 + 单节点递归组件」:

  • Menu.vue:接收整份菜单数组、设默认值、外层 v-for、控制外层 el-menu 配置
  • SubMenu.vue:接收单个节点,判断是否有 children,决定渲染 el-sub-menu 还是 el-menu-item,再递归下一层
Vue SFC
<template>
  <el-sub-menu v-if="hasChildren(item)" :index="item.path">
    <template #title>{{ resolveTitle(item) }}</template>
    <SubMenu
      v-for="child in item.children"
      :key="child.path"
      :item="child"
    />
  </el-sub-menu>
  <el-menu-item v-else :index="item.path">
    {{ resolveTitle(item) }}
  </el-menu-item>
</template>

业务数据与 Element Plus 配置应分层传递(itemsubMenuProps 分开),避免 v-bind="menu" 把两类字段混在一起。useMenu 这类组合式函数则承接菜单树加工:统一生成 index/key、判断节点渲染形态、排序过滤——把这些算法从模板剥离,能让 MenuSubMenu 与后续顶部/双列菜单复用同一套判断逻辑。


常见问题

问题原因解决方案
菜单、头部、内容区接不起来没有先从整体布局视角设计先搭最小三段骨架,再把功能接回布局
切换顶部一级菜单后左侧没同步顶部激活态与侧边栏上下文未联动单独维护 activeTopMenu 并派生侧边菜单
移动端菜单和桌面折叠混在一起把两种状态塞进一个布尔值分开管理 isCollapseisDrawerOpen
菜单只能写到两三层模板结构写死,无递归组件拆成 MenuSubMenu 两层组件
路由生成了但菜单顺序乱文件系统顺序不等于业务展示顺序meta 增加 order 并做排序转换
颜色能改但刺眼把主题定制简化成任意颜色输入改为受控主题色,内部统一前景与状态色

延伸阅读