第十六节:递归组件实现无限层级折叠侧边栏菜单
第十六节递归组件实现无限层级折叠侧边栏菜单本节目标基于已经写好的动态路由、v‑perm 权限指令实现无限层级侧边栏菜单支持二级、三级甚至更多层级菜单展开折叠点击菜单跳转完整路由地址杜绝 404 报错完全贴合你现有项目目录结构。文件对应关系你的项目真实目录递归子组件本体src/layout/components/SidebarMenuItem.vue使用递归组件的父组件src/layout/components/SidebarMenu.vue重要SidebarMenuItem.vue属于局部组件不需要在 main.js 全局注册只在 SidebarMenu.vue 内部导入使用。步骤 1编写递归菜单组件 SidebarMenuItem.vue路径src/layout/components/SidebarMenuItem.vuetemplate !-- 判断当前菜单是否存在子菜单 -- template v-ifhasChildren !-- 有子菜单渲染可折叠父菜单 el‑sub‑menu -- !-- index必须绑定完整pathel-menu的router模式下index就是跳转路径 -- el-sub-menu :indexmenuItem.path template #title !-- 渲染菜单图标 -- el-icon v-ifmenuItem.meta?.icon component :ismenuItem.meta.icon/ /el-icon !-- 菜单标题 -- span{{ menuItem.meta.title }}/span /template !-- ✅递归核心组件自己调用自己把子菜单继续向下传递实现无限层级 -- SidebarMenuItem v-forsubItem in menuItem.children :keysubItem.path :menu-itemsubItem / /el-sub-menu /template !-- 没有子菜单普通点击菜单项 -- el-menu-item v-else :indexmenuItem.path el-icon v-ifmenuItem.meta?.icon component :ismenuItem.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核心要点组件内部调用自身标签SidebarMenuItem /就完成递归不管多少层子菜单都可以自动渲染。:indexmenuItem.path必须是完整绝对路由路径例/system/user/list如果只写片段list点击会直接 404。步骤 2在侧边栏主组件 SidebarMenu.vue 引入并使用递归组件打开文件src/layout/components/SidebarMenu.vue1script 部分导入组件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) /script2template 模板循环渲染菜单template div classsidebar-container :class{ collapse: isCollapse } div classlogo span v-if!isCollapse数字智能管理平台/span /div !-- ⚠️router属性必须写开启index作为路由跳转功能不写点击菜单不会跳转 -- el-menu :default-activeactiveMenu :collapseisCollapse router classsidebar-el-menu !-- 遍历顶层菜单每一项交给递归组件处理 -- SidebarMenuItem v-foritem in menuList :keyitem.path :menu-itemitem / /el-menu /div /template3侧边栏主组件 SidebarMenu.vue 完成代码templateel-menu:collapseappStore.sidebarCollapsemodeverticalrouter background-color#304156text-color#bfcbd9:default-active$route.pathactive-text-color#409eff!--循环顶层菜单每一项交给递归组件处理--SidebarMenuItem v-forroute in menuList:keyroute.path:menu-itemroute//el-menu/templatescript setupimport{computed}fromvueimport{useUserStore}from/stores/userimport{useAppStore}from/stores/app// 同目录导入递归组件import SidebarMenuItem from./SidebarMenuItem.vue// 手动导入用到的图标import{House,Setting,Menu}fromelement-plus/icons-vueconstuserStoreuseUserStore()constappStoreuseAppStore()// 图标映射consticonMap{House,Setting,Menu}// 从 store 读取动态路由把图标字符串转成组件对象constmenuListcomputed((){returnuserStore.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 }) }步骤 4mock 配置多级菜单测试数据src/mock/index.js中getUserInfo接口内的 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: 用户列表 } } ] } ] } ]✅测试流程全部文件保存关闭 vite 服务重新执行pnpm dev浏览器清除 localStorage清空 token刷新页面重新登录侧边栏查看效果系统管理可以展开用户管理可以展开看到用户列表子菜单点击【用户列表】浏览器地址栏出现完整路径/system/user/list页面正常加载无 404 报错❗高频踩坑汇总导入路径错误import SidebarMenuItem from /components/SidebarMenuItem.vue你的组件在 layout/components必须使用./SidebarMenuItem.vue相对导入。el-menu忘记写router属性点击菜单没有任何跳转效果。filterAsyncRoutes 没有拼接完整 pathindex 绑定片段路径点击跳转 404。修改import.meta.glob相关代码之后没有重启 pnpm dev修改不会生效。测试正常后回复继续下一节我们改造面包屑导航组件适配多级嵌套路由。现在遗留问题二级三级菜单图标空白修改 mock getUserInfo 里面 routesroutes: [ { path: /dashboard, component: Layout, meta: { title: 首页, icon: House }, children: [ { path: index, component: dashboard/index, meta: { title: 工作台, icon: House } } ] }, { path: /article, component: Layout, meta: { title: 文章管理, icon: Document }, children: [ { path: edit, component: article/edit, meta: { title: 编辑公告, icon: Document } } ] }, { path: /system, component: Layout, // ✅一级Layout meta: { title: 系统管理, icon: Setting }, children: [ { path: user, // ❗删掉 component:Layout中间父菜单不要Layout meta: { title: 用户管理, icon: User }, children: [ { path: list, component: system/user/index, // ✅叶子写页面组件 meta: { title: 用户列表, icon: User } } ] } ] } ]两种修复方案二选一推荐方案 1方案①在SidebarMenuItem.vue直接导入全部用到图标script setup import { computed } from vue // ✅在递归组件内部导入所有菜单用到的图标 import { House, Document, Setting, User } from element-plus/icons-vue const props defineProps({ menuItem: { type: Object, required: true } }) const hasChildren computed(() { return props.menuItem.children props.menuItem.children.length 0 }) /script原理component :isUser当前组件 script 必须存在User这个变量否则无法解析组件。之前只在 layout 导入子组件拿不到。方案②全局注册图标main.jsimport * as ElementPlusIconsVue from element-plus/icons-vue const app createApp(App) // 全局注册全部图标 for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) }全局注册后任意模板直接写名字字符串不需要每个组件单独 import。你这里写了本地 iconMap 图标映射只手动注册了少量图标House, Setting, Menu。meta.icon字符串Document、User、List、HomeFilled、DataBoard不在你的iconMap对象里面命中兜底Menu全部强制变成方块网格图标就是你截图看到的现象。注意main.js 做的全局组件注册只对模板里el-icon :componentxxx/生效你这里是 JS 脚本内iconMap[route.meta.icon]对象取值全局注册不生效必须对象内有对应导入。两种方案推荐方案 2不用一个个手动导入图标。方案 1你现在的写法不推荐要逐个导入script setup import { computed } from vue import { useUserStore } from /stores/user.js import { useAppStore } from /stores/app.js import SidebarMenuItem from ./SidebarMenuItem.vue // 所有菜单用到的图标全部导入进来 import { House, Setting, Menu, Document, User, List, HomeFilled, DataBoard } from element-plus/icons-vue const userStore useUserStore() const appStore useAppStore() // 全部图标放进映射表 const iconMap { House, Setting, Menu, Document, User, List, HomeFilled, DataBoard } const menuList computed(() { return userStore.addRoutes.map((route) ({ ...route, meta: { ...route.meta, icon: iconMap[route.meta?.icon] || Menu } })) }) /script✅方案 2推荐一次性全部导入图标不用每次新增菜单都导入完整替换你的 sidebar 代码template el-menu :collapseappStore.sidebarCollapse modevertical router background-color#f6f6f6 text-color#333333 :default-active$route.path active-text-color#007AFF SidebarMenuItem v-forroute in menuList :keyroute.path :menu-itemroute / /el-menu /template script setup import { computed } from vue import { useUserStore } from /stores/user.js import { useAppStore } from /stores/app.js import SidebarMenuItem from ./SidebarMenuItem.vue // 一次性导入全部图标 import * as ElementPlusIconsVue from element-plus/icons-vue const userStore useUserStore() const appStore useAppStore() const menuList computed(() { return userStore.addRoutes.map((route) ({ ...route, meta: { ...route.meta, // 直接从全部图标对象拿找不到就兜底Menu icon: ElementPlusIconsVue[route.meta?.icon] || ElementPlusIconsVue.Menu } })) }) /script再检查递归组件 SidebarMenuItem.vue内部渲染图标必须使用:componentmenuItem.meta.icon!-- SidebarMenuItem.vue 图标部分示例 -- el-icon component :ismenuItem.meta.icon / /el-icon完整测试步骤把 sidebar 组件替换方案 2 全部代码清除 localStorage重启 vite 服务重新登录各个菜单图标首页、资讯、系统管理、用户管理全部显示正确不再统一变成网格方块。关键点总结main.js全局注册只作用于模板标签写法JS 脚本中需要直接拿到图标组件对象需要导入* as ElementPlusIconsVue通过对象属性取图标。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →