第十六节:递归组件实现无限层级折叠侧边栏菜单

第十六节:递归组件实现无限层级折叠侧边栏菜单

🎯本节目标

基于已经写好的动态路由、v‑perm 权限指令,实现无限层级侧边栏菜单,支持二级、三级甚至更多层级菜单展开折叠;点击菜单跳转完整路由地址,杜绝 404 报错;完全贴合你现有项目目录结构。

📂文件对应关系(你的项目真实目录)

  1. 递归子组件本体:src/layout/components/SidebarMenuItem.vue
  2. 使用递归组件的父组件:src/layout/components/SidebarMenu.vue

重要:SidebarMenuItem.vue属于局部组件,不需要在 main.js 全局注册,只在 SidebarMenu.vue 内部导入使用。


步骤 1:编写递归菜单组件 SidebarMenuItem.vue

路径:src/layout/components/SidebarMenuItem.vue

复制代码
<template>
  <!-- 判断当前菜单是否存在子菜单 -->
  <template v-if="hasChildren">
    <!-- 有子菜单:渲染可折叠父菜单 el‑sub‑menu -->
    <!-- index必须绑定完整path,el-menu的router模式下index就是跳转路径 -->
    <el-sub-menu :index="menuItem.path">
      <template #title>
        <!-- 渲染菜单图标 -->
        <el-icon v-if="menuItem.meta?.icon">
          <component :is="menuItem.meta.icon"/>
        </el-icon>
        <!-- 菜单标题 -->
        <span>{{ menuItem.meta.title }}</span>
      </template>
      <!-- ✅递归核心:组件自己调用自己,把子菜单继续向下传递,实现无限层级 -->
      <SidebarMenuItem
        v-for="subItem in menuItem.children"
        :key="subItem.path"
        :menu-item="subItem"
      />
    </el-sub-menu>
  </template>

  <!-- 没有子菜单:普通点击菜单项 -->
  <el-menu-item v-else :index="menuItem.path">
    <el-icon v-if="menuItem.meta?.icon">
      <component :is="menuItem.meta.icon"/>
    </el-icon>
    <span>{{ menuItem.meta.title }}</span>
  </el-menu-item>
</template>

<script setup>
import { computed } from 'vue'
// 接收父组件传递过来的单条菜单对象
const props = defineProps({
  menuItem: {
    type: Object,
    required: true
  }
})

// 计算属性:判断当前菜单是否拥有有效子菜单
const hasChildren = computed(() => {
  return props.menuItem.children && props.menuItem.children.length > 0
})
</script>

💡核心要点

  1. 组件内部调用自身标签<SidebarMenuItem />就完成递归,不管多少层子菜单都可以自动渲染。
  2. :index="menuItem.path"必须是完整绝对路由路径 ,例/system/user/list,如果只写片段list,点击会直接 404。

步骤 2:在侧边栏主组件 SidebarMenu.vue 引入并使用递归组件

打开文件:src/layout/components/SidebarMenu.vue

(1)script 部分导入组件

复制代码
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'

// ✅同目录相对路径导入递归组件,不要写@/components,目录不对会报找不到文件
import SidebarMenuItem from './SidebarMenuItem.vue'

const route = useRoute()
const userStore = useUserStore()

// 接收layout/index.vue传过来的侧边栏折叠状态
const props = defineProps({
  isCollapse: {
    type: Boolean,
    default: false
  }
})

// 菜单数据源:取自pinia中经过filterAsyncRoutes处理完成的动态路由
const menuList = computed(() => userStore.addRoutes || [])
// 当前页面路由,用来做菜单高亮
const activeMenu = computed(() => route.path)
</script>

(2)template 模板循环渲染菜单

复制代码
<template>
  <div class="sidebar-container" :class="{ collapse: isCollapse }">
    <div class="logo">
      <span v-if="!isCollapse">数字智能管理平台</span>
    </div>

    <!-- ⚠️router属性必须写!开启index作为路由跳转功能,不写点击菜单不会跳转 -->
    <el-menu
      :default-active="activeMenu"
      :collapse="isCollapse"
      router
      class="sidebar-el-menu"
    >
      <!-- 遍历顶层菜单,每一项交给递归组件处理 -->
      <SidebarMenuItem
        v-for="item in menuList"
        :key="item.path"
        :menu-item="item"
      />
    </el-menu>
  </div>
</template>

(3)侧边栏主组件 SidebarMenu.vue 完成代码

c 复制代码
<template>
  <el-menu
    :collapse="appStore.sidebarCollapse"
    mode="vertical"
    router
    background-color="#304156"
    text-color="#bfcbd9"
    :default-active="$route.path"
    active-text-color="#409eff">
    <!-- 循环顶层菜单,每一项交给递归组件处理 -->
    <SidebarMenuItem
      v-for="route in menuList"
      :key="route.path"
      :menu-item="route" />
  </el-menu>
</template>

<script setup>
  import { computed } from 'vue'
  import { useUserStore } from '@/stores/user'
  import { useAppStore } from '@/stores/app'
  // 同目录导入递归组件
  import SidebarMenuItem from './SidebarMenuItem.vue'
  // 手动导入用到的图标
  import { House, Setting, Menu } from '@element-plus/icons-vue'

  const userStore = useUserStore()
  const appStore = useAppStore()

  // 图标映射
  const iconMap = { House, Setting, Menu }

  // 从 store 读取动态路由,把图标字符串转成组件对象
  const menuList = computed(() => {
    return userStore.addRoutes.map((route) => ({
      ...route,
      meta: {
        ...route.meta,
        icon: iconMap[route.meta?.icon] || Menu
      }
    }))
  })
</script>

步骤 3:确认动态路由转换工具 filterAsyncRoutes.js

必须保证路由工具会拼接完整 path,给到侧边栏的每一条菜单都是完整路径,否则 index 绑定的值不对,点击 404。

复制代码
import { markRaw } from 'vue'
const viewsModules = import.meta.glob('@/views/**/*.vue')
import Layout from '@/layout/index.vue'

export function filterAsyncRoutes(asyncRoutes, parentPath = '') {
  return asyncRoutes.map((route) => {
    const tempRoute = { ...route }
    // 拼接完整路由path
    if (parentPath === '') {
      tempRoute.path = route.path
    } else {
      tempRoute.path = `${parentPath}/${route.path}`.replace(/\/+/g, '/')
    }

    if (tempRoute.component === 'Layout') {
      tempRoute.component = markRaw(Layout)
    } else if (route.component) {
      const filePath = `/src/views/${route.component}.vue`
      tempRoute.component = viewsModules[filePath]
    }

    // 递归处理子路由,把当前完整path传递给子节点
    if (tempRoute.children && tempRoute.children.length > 0) {
      tempRoute.children = filterAsyncRoutes(tempRoute.children, tempRoute.path)
    }
    return tempRoute
  })
}

步骤 4:mock 配置多级菜单测试数据

src/mock/index.jsgetUserInfo接口内的 routes,编写嵌套菜单用于测试:

复制代码
routes: [
  {
    path: '/dashboard',
    component: 'Layout',
    meta: { title: '首页', icon: 'House' },
    children: [
      {
        path: 'index',
        component: 'dashboard/index',
        meta: { title: '工作台' }
      }
    ]
  },
  {
    path: '/system',
    component: 'Layout',
    meta: { title: '系统管理', icon: 'Setting' },
    children: [
      {
        path: 'user',
        meta: { title: '用户管理', icon: 'User' },
        children: [
          {
            path: 'list',
            component: 'system/user/index',
            meta: { title: '用户列表' }
          }
        ]
      }
    ]
  }
]

✅测试流程

  1. 全部文件保存,关闭 vite 服务,重新执行pnpm dev
  2. 浏览器清除 localStorage,清空 token,刷新页面重新登录
  3. 侧边栏查看效果:系统管理可以展开,用户管理可以展开,看到用户列表子菜单
  4. 点击【用户列表】,浏览器地址栏出现完整路径/system/user/list,页面正常加载,无 404 报错

❗高频踩坑汇总

  1. 导入路径错误:import SidebarMenuItem from '@/components/SidebarMenuItem.vue',你的组件在 layout/components,必须使用./SidebarMenuItem.vue相对导入。
  2. <el-menu>忘记写router属性,点击菜单没有任何跳转效果。
  3. filterAsyncRoutes 没有拼接完整 path,index 绑定片段路径,点击跳转 404。
  4. 修改import.meta.glob相关代码之后,没有重启 pnpm dev,修改不会生效

测试正常后回复继续下一节,我们改造面包屑导航组件,适配多级嵌套路由。

相关推荐
神明不懂浪漫1 小时前
【第二章】库、表、增删改查操作
开发语言·数据库·经验分享·笔记
传奇开心果编程1 小时前
【Rust入门知识点学与练】第11课:HashMap 键值对集合
开发语言·学习·rust
yume_sibai2 小时前
03-Rust 函数式编程特性(闭包 + Iterator + Option/Result + 链式调用)
开发语言·后端·rust
奈斯先生Vector2 小时前
AIGC 视频生成实战:用 Kling Video 拆解文生视频、图生视频与完整工作流
开发语言·人工智能·windows·python·aigc·音视频
小玮看世界2 小时前
[Python]螺旋遍历 vs 最短路径:方向控制类算法的“同源异流”
开发语言·python·算法
BillKu2 小时前
vue3 字符串排序 localeCompare,确保排序为 “B01“, “B10“, “B2“
前端·javascript·vue.js
OPEN-F2 小时前
ROS2系列教程:tf2坐标变换详解(C++/Python)
开发语言·c++·python
然我2 小时前
从 Service 到生命周期:Agent Runtime 的插件内核
前端·javascript·agent
XR1234567882 小时前
医院基础网络改造升级选型深度解析
开发语言·网络·php