Vue3+ElementPlus实现动态递归菜单全攻略

发布时间:2026/10/29 8:44:48
Vue3+ElementPlus实现动态递归菜单全攻略

1. 为什么需要无限递归菜单?

在管理后台类项目中,菜单结构往往不是固定层级的。比如一个电商系统可能需要展示:首页 > 商品管理 > 品类管理 > 手机品类 > 智能手机子类...这种动态深度的嵌套结构。传统方案需要预先定义好所有可能的层级,而递归组件可以优雅地解决这个痛点。

我最近在重构一个CMS系统时就遇到了这个问题。旧版采用硬编码的二级菜单,每次新增分类都需要前端修改代码。改用递归方案后,后端只需要返回树形结构数据,前端就能自动渲染任意深度的菜单。

2. ElementPlus菜单组件基础配置

2.1 安装与基础使用

首先确保项目已安装Vue3和ElementPlus:

npm install element-plus

在main.js中全局引入:

import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' const app = createApp(App) app.use(ElementPlus) app.mount('#app')

基础菜单结构如下:

<el-menu> <el-sub-menu index="1"> <template #title>一级菜单</template> <el-menu-item index="1-1">子菜单项</el-menu-item> </el-sub-menu> </el-menu>

2.2 关键API解析

  • el-menu: 容器组件,常用props:

    • mode: 菜单模式(horizontal/vertical)
    • default-active: 默认激活的菜单项
    • unique-opened: 是否只保持一个子菜单展开
  • el-sub-menu: 可折叠的子菜单

    • index: 唯一标识(必须)
    • popper-class: 下拉菜单的自定义类名
  • el-menu-item: 菜单项

    • index: 唯一标识(必须)
    • disabled: 是否禁用

3. 实现递归菜单的核心逻辑

3.1 数据结构设计

后端返回的菜单数据通常是这样格式:

const menuData = [ { id: 1, name: '系统管理', children: [ { id: 11, name: '用户管理', children: [ { id: 111, name: '添加用户' } ] } ] } ]

3.2 递归组件实现

创建RecursiveMenu.vue组件:

<template> <el-sub-menu v-if="item.children" :index="item.id.toString()"> <template #title>{{ item.name }}</template> <recursive-menu v-for="child in item.children" :key="child.id" :item="child" /> </el-sub-menu> <el-menu-item v-else :index="item.id.toString()"> {{ item.name }} </el-menu-item> </template> <script setup> defineProps({ item: { type: Object, required: true } }) </script>

使用时:

<el-menu> <recursive-menu v-for="item in menuData" :key="item.id" :item="item" /> </el-menu>

3.3 动态图标支持

实际项目中常需要为菜单项添加图标:

// 在数据中添加icon字段 { id: 1, name: '仪表盘', icon: 'el-icon-monitor' }

修改组件:

<el-sub-menu> <template #title> <el-icon><component :is="item.icon" /></el-icon> <span>{{ item.name }}</span> </template> ... </el-sub-menu>

4. 高级功能与性能优化

4.1 路由集成方案

通常菜单需要与vue-router联动:

// 菜单数据增加path字段 { id: 111, name: '用户列表', path: '/user/list' }

修改菜单项:

<el-menu-item :index="item.path" @click="$router.push(item.path)" > {{ item.name }} </el-menu-item>

4.2 权限过滤实现

结合权限系统过滤菜单:

const filteredMenu = computed(() => { return filterMenu(menuData, userPermissions) }) function filterMenu(menu, permissions) { return menu.filter(item => { if (item.children) { item.children = filterMenu(item.children, permissions) return item.children.length > 0 } return permissions.includes(item.permission) }) }

4.3 性能优化技巧

  1. 避免重复渲染:

    • 为每个菜单项设置稳定的key
    • 使用v-memo缓存已渲染的菜单项
  2. 大数据量优化:

    <el-menu :collapse-transition="false"> <!-- 禁用折叠动画提升性能 --> </el-menu>
  3. 动态加载:

    // 只加载当前展开的菜单子项 const loadChildren = async (item) => { if (!item.children) { item.children = await fetchMenuChildren(item.id) } }

5. 常见问题排查

5.1 菜单项不显示

  • 检查数据结构是否正确,必须有唯一的index
  • 确保v-for的key是唯一值
  • 验证CSS是否被意外覆盖(可尝试!important)

5.2 样式错乱问题

常见于自定义主题时:

/* 修复子菜单缩进问题 */ .el-menu--inline { .el-menu-item { padding-left: 50px !important; } }

5.3 路由匹配失败

  • 确保index值与路由path完全匹配
  • 使用:router="true"属性自动激活匹配项
  • 检查路由配置的path是否带斜杠

6. 实际项目中的经验之谈

在最近的项目中,我遇到了几个值得分享的坑:

  1. 动态菜单的持久化:

    • 将菜单状态保存到Vuex/Pinia
    • 记住用户最后展开的菜单项
    const store = useMenuStore() const defaultOpeneds = store.lastOpenedMenus
  2. 多级菜单的缩进控制:

    @for $i from 1 through 10 { .menu-level-#{$i} { .el-menu-item { padding-left: 20px * $i; } } }
  3. 右键菜单扩展:

    <el-menu-item @contextmenu.prevent="openContextMenu($event, item)"> ... </el-menu-item>
  4. 菜单搜索功能:

    const searchMenu = (keyword) => { return menuData.flatMap(item => flattenMenu(item).filter(m => m.name.includes(keyword) ) ) }

这个方案已经在我们的生产环境稳定运行半年,支持最多7级菜单深度,性能测试在1000+菜单项下仍能保持流畅交互。关键点在于合理控制重渲染范围和使用虚拟滚动技术。