菜单组件需求分析
为什么要自己封装菜单组件
Element Plus 提供的 el-menu 组件是一个基础菜单结构,包含了折叠展开、子菜单嵌套等核心交互能力。但在实际项目中,官方组件的定制能力远远不够。最典型的问题是样式控制——旧版本中 el-menu 的 background-color 和 text-color 属性已被官方标记为废弃,推荐改用 CSS 变量(--bg-color)来统一管理。这种变化意味着如果你的后台管理框架需要支持主题切换、暗黑模式、自定义菜单宽度等功能,就必须在 el-menu 之上再做一层封装。
我们的目标很明确:保留 el-menu 的交互能力(折叠、展开、子菜单弹出),同时接管它的样式系统和数据驱动逻辑,最终实现一个"传入路由数据就能自动渲染出完整多级菜单"的通用组件。
功能需求梳理
多级菜单自动渲染
这是菜单组件最核心的功能。后台管理系统中的路由通常是一棵嵌套的树形结构,菜单需要根据这棵树自动渲染出来:
- 一级菜单:顶级分类,显示图标和文字
- 二级菜单:一级分类下的子页面
- 三级菜单:二级分类下的更细分页面
- 四级及以上:理论上需要支持,但实际项目中三级通常已经足够
渲染逻辑需要使用递归组件来实现——每个菜单项如果有 children,就继续渲染子菜单组件(el-sub-menu),如果没有子节点,就渲染叶子节点(el-menu-item)。这种递归结构可以天然支持任意深度的菜单嵌套。
<!-- SubMenu.vue 递归组件核心逻辑 -->
<template>
<!-- 有子菜单:渲染可展开的 sub-menu -->
<el-sub-menu v-if="item.children?.length" :index="item.path">
<template #title>
<el-icon v-if="item.meta?.icon"><component :is="item.meta.icon" /></el-icon>
<span>{{ item.meta?.title }}</span>
</template>
<!-- 递归调用自身 -->
<SubMenu v-for="child in item.children" :key="child.path" :item="child" />
</el-sub-menu>
<!-- 叶子节点:渲染可点击的 menu-item -->
<el-menu-item v-else :index="item.path">
<el-icon v-if="item.meta?.icon"><component :is="item.meta.icon" /></el-icon>
<span>{{ item.meta?.title }}</span>
</el-menu-item>
</template>
vue
多种导航模式支持
管理后台通常需要支持多种导航布局,不同模式下的菜单渲染方式有本质区别:
| 模式 | 描述 | 实现方式 |
|---|---|---|
| 侧边栏模式(vertical) | 所有菜单在左侧垂直排列 | el-menu 的 mode="vertical" |
| 顶部模式(horizontal) | 所有菜单在顶部水平排列 | el-menu 的 mode="horizontal" |
| 混合模式(mixed) | 顶部显示一级菜单,左侧显示对应二级菜单 | 拆分为两个 el-menu 实例联动 |
| 双栏模式(dual) | 左侧第一列显示一级图标,右侧显示二级菜单 | 两个独立区域,通过选中状态联动 |
横向菜单(horizontal)基本不需要改动,主要工作量在纵向菜单的定制上。
主题配置联动
菜单组件需要和整个主题系统联动,支持以下配置项:
- 菜单宽度:展开态宽度(默认 210px)、折叠态宽度(默认 64px),影响主内容区域的可用空间计算
- 背景色:通过 CSS 变量
--el-menu-bg-color设置,需要根据背景色明度自动切换文字颜色 - 暗黑模式:在
html根元素添加dark类名,所有 CSS 变量自动切换为暗色系值 - 头部固定标签页、Logo 显示 等布局配置也需要菜单组件配合
/* 主题色通过 CSS 变量统一管理 */
:root {
--el-menu-bg-color: #ffffff;
--el-menu-text-color: #303133;
--el-menu-active-color: #409eff;
}
html.dark {
--el-menu-bg-color: #1d1e1f;
--el-menu-text-color: #cfd3dc;
--el-menu-active-color: #3375b9;
}
css
数据来源与 Mock 数据策略
路由数据的来源
菜单数据来自项目的路由配置。使用 vite-plugin-auto-import 或类似插件后,可以通过以下方式获取自动生成的路由:
// 从自动路由插件导入生成的路由数据
import { routes } from 'vue-router/auto/routes'
typescript
但自动生成的路由有两个问题需要解决:
- 排序不确定:自动路由按文件系统顺序生成,无法保证菜单项的显示顺序
- 元信息不完整:缺少菜单所需的图标、标题、排序权重等字段
通过 meta 扩展路由信息
解决上述问题的标准做法是在路由的 meta 字段中补充菜单所需的元信息:
// pages/dashboard/index.vue
<route lang="yaml">
meta:
title: 仪表盘
icon: dashboard
order: 1
</route>
typescript
| meta 字段 | 作用 | 示例值 |
|---|---|---|
title | 菜单显示文字 | '用户管理' |
icon | 菜单图标名称 | 'user' |
order | 同级排序权重(越小越靠前) | 1 |
hidden | 是否在菜单中隐藏 | true(如详情页) |
alwaysShow | 只有一个子路由时是否仍显示父级 | true |
Mock 数据先行
在开发初期,页面数量少,自动生成的路由数据不够丰富,无法测试多级菜单的渲染效果。因此需要准备一份 Mock 数据来模拟真实场景下的菜单结构:
// mock/menuData.ts
export const mockMenuData = [
{
path: '/dashboard',
meta: { title: '仪表盘', icon: 'dashboard', order: 1 },
children: [
{ path: '/dashboard/workbench', meta: { title: '工作台', icon: 'workbench' } },
{ path: '/dashboard/analysis', meta: { title: '分析页', icon: 'analysis' } }
]
},
{
path: '/system',
meta: { title: '系统管理', icon: 'setting', order: 2 },
children: [
{
path: '/system/user',
meta: { title: '用户管理', icon: 'user' },
children: [
{ path: '/system/user/list', meta: { title: '用户列表' } },
{ path: '/system/user/role', meta: { title: '角色分配' } }
]
},
{ path: '/system/menu', meta: { title: '菜单管理', icon: 'menu' } },
{ path: '/system/permission', meta: { title: '权限管理', icon: 'lock' } }
]
},
{
path: '/nested',
meta: { title: '多级菜单', icon: 'nested', order: 3 },
children: [
{
path: '/nested/level1',
meta: { title: '一级菜单' },
children: [
{
path: '/nested/level1/level2',
meta: { title: '二级菜单' },
children: [
{ path: '/nested/level1/level2/level3', meta: { title: '三级菜单' } },
{
path: '/nested/level1/level2/level3-1',
meta: { title: '三级菜单-1' },
children: [
{ path: '/nested/level1/level2/level3-1/level4', meta: { title: '四级菜单' } }
]
}
]
}
]
}
]
}
]
typescript
这份 Mock 数据覆盖了以下场景:
- 一级直接跳转(仪表盘)
- 二级子菜单(系统管理下的用户管理、菜单管理等)
- 三级嵌套(用户管理下的用户列表、角色分配)
- 四级极限深度(多级菜单下的四级嵌套)
开发流程是:先用 Mock 数据完成菜单组件 → 组件开发完成后再对接自动路由数据。
组件拆分思路
根据需求分析,菜单组件的拆分结构如下:
components/Menu/
├── index.vue # 菜单入口组件,处理数据、模式切换
├── MenuItem.vue # 叶子菜单项组件(el-menu-item)
├── SubMenu.vue # 子菜单递归组件(el-sub-menu + 递归调用自身)
└── types.ts # 菜单数据类型定义
text
入口组件 index.vue 的职责:
- 接收路由数据(Mock 或真实路由)
- 根据
mode属性决定渲染方向(vertical / horizontal) - 处理菜单项点击事件,执行路由跳转
- 暴露主题相关的 CSS 变量接口
SubMenu.vue 是递归组件的核心,需要正确处理递归终止条件(没有 children 时渲染 MenuItem)。
实现路径总览
整个开发路径是一个从数据准备到组件实现再到数据对接的渐进过程。先确保组件本身能正确渲染任意深度的菜单树,再处理数据来源的切换和主题系统的集成。
主流后台框架的菜单实现参考
在动手之前,可以参考几个成熟的后台管理框架的菜单实现方式,了解它们的组件拆分和数据结构设计:
| 框架 | 菜单组件特点 | 值得学习的地方 |
|---|---|---|
| Vue Vben Admin | 支持四种导航模式切换,菜单数据完全由路由驱动 | 模式切换的实现方式、菜单与布局的联动逻辑 |
| Naive UI Admin | 基于 Naive UI 的 n-menu,递归组件写法清晰 | 递归组件的 TypeScript 类型定义 |
| Soybean Admin | 使用 @unplugin-vue-router 自动路由,菜单配置完全在 route 的 meta 中 | meta 字段设计的完整性和灵活性 |
| Pure Admin | 轻量级,菜单组件代码量少,易于阅读 | 适合初学者理解菜单组件的基本结构 |
这些框架的菜单实现各有侧重,建议以其中一个为参照,对照课程中的需求逐步实现。关键是理解"路由数据 → 菜单数据转换 → 递归渲染"这条核心链路,而不是生搬某个框架的实现。
↑