布局模式与菜单系统演进
概述
后台布局系统不是把页面分成左右两栏,而是让头部、侧边栏、内容区与菜单模式协同工作。这一阶段组件开发从「单个组件」升级为「系统性功能模块」,复杂度来自状态同步、多模式切换、响应式差异与主题联动。本文先建立整体设计地图:先把布局系统、菜单数据结构、递归渲染与多导航模式这几个核心问题想清楚,后续各篇再逐一落地实现。
学习目标
- 理解后台布局系统的「整体协同」视角,而非只盯单个区块
- 掌握多导航模式(sidebar / mix / top / dual-sidebar)及其状态同步难点
- 区分桌面端「折叠」与移动端「全屏覆盖」两种菜单交互,并分开管理状态
- 理解菜单系统的本质:把路由树翻译成菜单树的数据建模
- 用「外层列表入口组件 + 单节点递归组件 + useMenu 加工」组织菜单代码
一、从单组件到布局系统
布局系统的难点不在写一个盒子,而在同时处理布局结构、状态同步、交互反馈、多模式切换、视觉主题联动和移动端差异。它本质是一个综合性功能模块:
布局系统
头部(工具区、顶部交互)
侧边栏(菜单系统容器)
内容区(路由视图)
菜单模式(sidebar / mix / top / dual-sidebar)
主题设置(宽度、暗黑、背景色)基础骨架先把三段结构搭出来,再逐步叠加响应式和模式切换逻辑——布局是检验此前基础组件是否真正可复用的真实场景。
二、多导航模式与状态同步
菜单支持多种导航模式后,难点不在摆放位置,而在一级、二级和当前高亮的联动:
sidebar:左侧承载主菜单mix:顶部一级菜单 + 左侧二级菜单,顶部选中项必须和左侧菜单上下文同步top:主导航全部上移,侧边栏不再承担主导航;窄屏时顶部菜单同样会拥挤,需要单独做折叠收纳dual-sidebar:第一列一级、第二列二级,难点在一级激活、二级激活、路由激活三者统一
这些模式最好先定义为状态枚举,再根据模式派生不同区域的可见菜单,而不是硬写多套分支 UI:
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
}
}三、响应式:折叠与全屏覆盖分开管理
响应式交互的第一个重点是侧边栏随宽度自动折叠/展开,但移动端不能简单等同于「缩小版侧边栏」——它更适合点击后展示全屏覆盖式菜单,因为可点击区域要更大、菜单层级要更清晰。
因此桌面端与移动端的状态应分开管理,比硬塞进一个布尔值更稳:
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 路由翻译成菜单可消费的树形结构:
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,再递归下一层
<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 配置应分层传递(item 与 subMenuProps 分开),避免 v-bind="menu" 把两类字段混在一起。useMenu 这类组合式函数则承接菜单树加工:统一生成 index/key、判断节点渲染形态、排序过滤——把这些算法从模板剥离,能让 Menu、SubMenu 与后续顶部/双列菜单复用同一套判断逻辑。
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 菜单、头部、内容区接不起来 | 没有先从整体布局视角设计 | 先搭最小三段骨架,再把功能接回布局 |
| 切换顶部一级菜单后左侧没同步 | 顶部激活态与侧边栏上下文未联动 | 单独维护 activeTopMenu 并派生侧边菜单 |
| 移动端菜单和桌面折叠混在一起 | 把两种状态塞进一个布尔值 | 分开管理 isCollapse 与 isDrawerOpen |
| 菜单只能写到两三层 | 模板结构写死,无递归组件 | 拆成 Menu 与 SubMenu 两层组件 |
| 路由生成了但菜单顺序乱 | 文件系统顺序不等于业务展示顺序 | 在 meta 增加 order 并做排序转换 |
| 颜色能改但刺眼 | 把主题定制简化成任意颜色输入 | 改为受控主题色,内部统一前景与状态色 |