【项目实战篇】Vue结合路由配置递归实现菜单栏

您所在的位置:网站首页 一级菜单对应多个二级菜单 【项目实战篇】Vue结合路由配置递归实现菜单栏

【项目实战篇】Vue结合路由配置递归实现菜单栏

2023-10-24 20:12| 来源: 网络整理| 查看: 265

作者:小土豆biubiubiu 博客园:www.cnblogs.com/HouJiao/ 掘金:juejin.im/user/243617… 作者文章的内容均来源于自己的实践,如果觉得有帮助到你的话,可以点赞给个鼓励或留下宝贵意见

前言

在日常开发中,项目中的菜单栏都是已经实现好了的。如果需要添加新的菜单,只需要在路由配置中新增一条路由,就可以实现菜单的添加。

相信大家和我一样,有时候会跃跃欲试自己去实现一个菜单栏。那今天我就将自己实现的菜单栏的整个思路和代码分享给大家。

本篇文章重在总结和分享菜单栏的一个递归实现方式,代码的优化、菜单权限等不在本篇文章范围之内,在文中的相关部分也会做一些提示,有个别不推荐的写法希望大家不要参考哦。

同时可能会存在一些细节的功能没有处理或者没有提及到,忘知晓。

最终的效果

本次实现的这个菜单栏包含有一级菜单、二级菜单和三级菜单这三种类型,基本上已经可以覆盖项目中不同的菜单需求。

后面会一步一步从易到难去实现这个菜单。

简单实现

我们都知道到element提供了 NavMenu 导航菜单组件,因此我们直接按照文档将这个菜单栏做一个简单的实现。

基本的布局架构图如下:

菜单首页-menuIndex

首先要实现的是菜单首页这个组件,根据前面的布局架构图并且参考官方文档,实现起来非常简单。

import LeftMenu from './leftMenu'; import TopMenu from './topMenu'; export default { name: 'MenuIndex', components: {LeftMenu, TopMenu}, data() { return { logoPath: require("../../assets/images/logo1.png"), name: '员工管理系统' } } } #menu-index{ .el-header{ padding: 0px; } } 顶部菜单栏-topMenu

顶部菜单栏主要就是一个logo和产品名称。

逻辑代码也很简单,我直接将代码贴上。

{{name}} export default { name: 'topMenu', props: ['logoPath', 'name'] } $topMenuWidth: 80px; $logoWidth: 50px; $bg-color: #409EFF; $name-color: #fff; $name-size: 18px; #top-menu{ height: $topMenuWidth; text-align: left; background-color: $bg-color; padding: 20px 20px 0px 20px; .logo { width: $logoWidth; display: inline-block; } .name{ display: inline-block; vertical-align: bottom; color: $name-color; font-size: $name-size; } }

这段代码中包含了父组件传递给子组件的两个数据。

props: ['logoPath', 'name']

这个是父组件menuIndex传递给子组件topMenu的两个数据,分别是logo图标的路径和产品名称。

完成后的界面效果如下。

左侧菜单栏-leftMenu

首先按照官方文档实现一个简单的菜单栏。

首页 员工管理 员工统计 员工管理 考勤管理 考勤统计 考勤列表 异常管理 工时管理 工时统计 工时列表 选项一 选项二 export default { name: 'LeftMenu' } // 使左边的菜单外层的元素高度充满屏幕 #left-container{ position: absolute; top: 100px; bottom: 0px; // 使菜单高度充满屏幕 #left-menu, .el-menu-vertical-demo{ height: 100%; } }

注意菜单的样式代码,设置了绝对定位,并且设置top、bottom使菜单高度撑满屏幕。

此时在看下界面效果。

基本上算是实现了一个简单的菜单布局。

不过在实际项目在设计的时候,菜单栏的内容有可能来自后端给我们返回的数据,其中包含菜单名称、菜单图标以及菜单之间的层级关系。

总而言之,我们的菜单是动态生成的,而不是像前面那种固定的写法。因此下面我将实现一个动态生成的菜单,菜单的数据来源于我们的路由配置。

结合路由配置实现动态菜单 路由配置

首先,我将项目的路由配置代码贴出来。

import Vue from 'vue'; import Router from "vue-router"; // 菜单 import MenuIndex from '@/components/menu/menuIndex.vue'; // 首页 import Index from '@/components/homePage/index.vue'; // 人员统计 import EmployeeStatistics from '@/components/employeeManage/employeeStatistics.vue'; import EmployeeManage from '@/components/employeeManage/employeeManage.vue' // 考勤 // 考勤统计 import AttendStatistics from '@/components/attendManage/attendStatistics'; // 考勤列表 import AttendList from '@/components/attendManage/attendList.vue'; // 异常管理 import ExceptManage from '@/components/attendManage/exceptManage.vue'; // 工时 // 工时统计 import TimeStatistics from '@/components/timeManage/timeStatistics.vue'; // 工时列表 import TimeList from '@/components/timeManage/timeList.vue'; Vue.use(Router) let routes = [ // 首页(仪表盘、快速入口) { path: '/index', name: 'index', component: MenuIndex, redirect: '/index', meta: { title: '首页', // 菜单标题 icon: 'el-icon-s-home', // 图标 hasSubMenu: false, // 是否包含子菜单,false 没有子菜单;true 有子菜单 }, children:[ { path: '/index', component: Index } ] }, // 员工管理 { path: '/employee', name: 'employee', component: MenuIndex, redirect: '/employee/employeeStatistics', meta: { title: '员工管理', // 菜单标题 icon: 'el-icon-user-solid', // 图标 hasSubMenu: true, // 是否包含子菜单 }, children: [ // 员工统计 { path: 'employeeStatistics', name: 'employeeStatistics', meta: { title: '员工统计', // 菜单标题, hasSubMenu: false // 是否包含子菜单 }, component: EmployeeStatistics, }, // 员工管理(增删改查) { path: 'employeeManage', name: 'employeeManage', meta: { title: '员工管理', // 菜单标题 hasSubMenu: false // 是否包含子菜单 }, component: EmployeeManage } ] }, // 考勤管理 { path: '/attendManage', name: 'attendManage', component: MenuIndex, redirect: '/attendManage/attendStatistics', meta: { title: '考勤管理', // 菜单标题 icon: 'el-icon-s-claim', // 图标 hasSubMenu: true, // 是否包含子节点,false 没有子菜单;true 有子菜单 }, children:[ // 考勤统计 { path: 'attendStatistics', name: 'attendStatistics', meta: { title: '考勤统计', // 菜单标题 hasSubMenu: false // 是否包含子菜单 }, component: AttendStatistics, }, // 考勤列表 { path: 'attendList', name: 'attendList', meta: { title: '考勤列表', // 菜单标题 hasSubMenu: false // 是否包含子菜单 }, component: AttendList, }, // 异常管理 { path: 'exceptManage', name: 'exceptManage', meta: { title: '异常管理', // 菜单标题 hasSubMenu: false // 是否包含子菜单 }, component: ExceptManage, } ] }, // 工时管理 { path: '/timeManage', name: 'timeManage', component: MenuIndex, redirect: '/timeManage/timeStatistics', meta: { title: '工时管理', // 菜单标题 icon: 'el-icon-message-solid', // 图标 hasSubMenu: true, // 是否包含子菜单,false 没有子菜单;true 有子菜单 }, children: [ // 工时统计 { path: 'timeStatistics', name: 'timeStatistics', meta: { title: '工时统计', // 菜单标题 hasSubMenu: false // 是否包含子菜单 }, component: TimeStatistics }, // 工时列表 { path: 'timeList', name: 'timeList', component: TimeList, meta: { title: '工时列表', // 菜单标题 hasSubMenu: true // 是否包含子菜单 }, children: [ { path: 'options1', meta: { title: '选项一', // 菜单标题 hasSubMenu: false // 是否包含子菜单 }, }, { path: 'options2', meta: { title: '选项二', // 菜单标题 hasSubMenu: false // 是否包含子菜单 }, }, ] } ] }, ]; export default new Router({ routes })

在这段代码的最开始部分,我们引入了需要使用的组件,接着就对路由进行了配置。

此处使用了直接引入组件的方式,项目开发中不推荐这种写法,应该使用懒加载的方式

路由配置除了最基础的path、component以及children之外,还配置了一个meta数据项。

meta: { title: '工时管理', // 菜单标题 icon: 'el-icon-message-solid', // 图标 hasSubMenu: true, // 是否包含子节点,false 没有子菜单;true 有子菜单 }

meta数据包含的配置有菜单标题(title)、图标的类名(icon)和是否包含子节点(hasSubMenu)。

根据title、icon这两个配置项,可以展示当前菜单的标题和图标。

hasSubMenu表示当前的菜单项是否有子菜单,如果当前菜单包含有子菜单(hasSubMenu为true),那当前菜单对应的标签元素就是el-submenu;否则当前菜单对应的菜单标签元素就是el-menu-item。

是否包含子菜单是一个非常关键的逻辑,我在实现的时候是直接将其配置到了meta.hasSubMenu这个参数里面。

根据路由实现多级菜单

路由配置完成后,我们就需要根据路由实现菜单了。

获取路由配置

既然要根据路由配置实现多级菜单,那第一步就需要获取我们的路由数据。这里我使用简单粗暴的方式去获取路由配置数据:this.$router.options.routes。

这种方式也不太适用日常的项目开发,因为无法在获取的时候对路由做进一步的处理,比如权限控制。

我们在组件加载时打印一下这个数据。

// 代码位置:src/menu/leftMenu.vue mounted(){ console.log(this.$router.options.routes); }

打印结果如下。

可以看到这个数据就是我们在router.js中配置的路由数据。

为了方便使用,我将这个数据定义到计算属性中。

// 代码位置:src/menu/leftMenu.vue computed: { routesInfo: function(){ return this.$router.options.routes; } } 一级菜单

首先我们来实现一级菜单。

主要的逻辑就是循环路由数据routesInfo,在循环的时候判断当前路由route是否包含子菜单,如果包含则当前菜单使用el-submenu实现,否则当前菜单使用el-menu-item实现。

{{route.meta.title}} {{route.meta.title}}

结果:

可以看到,我们第一级菜单已经生成了,员工管理、考勤管理、工时管理这三个菜单是有子菜单的,所以会有一个下拉按钮。

不过目前点开是没有任何内容的,接下来我们就来实现这三个菜单下的二级菜单。

二级菜单

二级菜单的实现和一级菜单的逻辑是相同的:循环子路由route.children,在循环的时候判断子路由childRoute是否包含子菜单,如果包含则当前菜单使用el-submenu实现,否则当前菜单使用el-menu-item实现。

那话不多说,直接上代码。

{{route.meta.title}} {{childRoute.meta.title}} {{childRoute.meta.title}} {{route.meta.title}}

结果如下:

可以看到二级菜单成功实现。

三级菜单

三级菜单就不用多说了,和一级、二级逻辑相同,这里还是直接上代码。

{{route.meta.title}} {{childRoute.meta.title}} {{child.meta.title}} {{child.meta.title}} {{childRoute.meta.title}} {{route.meta.title}}

可以看到工时列表下的三级菜单已经显示了。

总结

此时我们已经结合路由配置实现了这个动态的菜单。

不过这样的代码在逻辑上相关于三层嵌套的for循环,对应的是我们有三层的菜单。

假如我们有四层、五层甚至更多层的菜单时,那我们还得在嵌套更多层for循环。很显然这样的方式暴露了前面多层for循环的缺陷,所以我们就需要对这样的写法进行一个改进。

递归实现动态菜单

前面我们一直在说一级、二级、三级菜单的实现逻辑都是相同的:循环子路由,在循环的时候判断子路由是否包含子菜单,如果包含则当前菜单使用el-submenu实现,否则当前菜单使用el-menu-item实现。那这样的逻辑最适合的就是使用递归去实现。

所以我们需要将这部分共同的逻辑抽离出来作为一个独立的组件,然后递归的调用这个组件。

逻辑拆分 {{child.meta.title}} {{child.meta.title}} export default { name: 'MenuItem', props: ['route'] }

需要注意的是,这次抽离出来的组件循环的时候直接循环的是route数据,那这个route数据是什么呢。

我们先看一下前面三层循环中循环的数据源分别是什么。

为了看得更清楚,我将前面代码中一些不相关的内容进行了删减。

从上面的代码可以看到:

一级菜单循环的是`routeInfo`,即最初我们获取的路由数据`this.$router.options.routes`,循环出来的每一项定义为`route` 二级菜单循环的是`route.children`,循环出来的每一项定义为`childRoute` 三级菜单循环的是`childRoute.children`,循环出来的每一项定义为`child`

按照这样的逻辑,可以发现二级菜单、三级菜单循环的数据源都是相同的,即前一个循环结果项的children,而一级菜单的数据来源于this.$router.options.routes。

前面我们抽离出来的menuItem组件,循环的是route数据,即不管是一层菜单还是二层、三层菜单,都是同一个数据源,因此我们需要统一数据源。那当然也非常好实现,我们在调用组件的时候,为组件传递不同的值即可。

代码实现

前面公共组件已经拆分出来了,后面的代码就非常好实现了。

首先是抽离出来的meunItem组件,实现的是逻辑判断以及递归调用自身。

{{child.meta.title}} {{child.meta.title}} export default { name: 'MenuItem', props: ['route'] }

接着是leftMenu组件,调用menuIndex组件,传递原始的路由数据routesInfo。

import MenuItem from './menuItem' export default { name: 'LeftMenu', components: { MenuItem } } // 使左边的菜单外层的元素高度充满屏幕 #left-container{ position: absolute; top: 100px; bottom: 0px; // 使菜单高度充满屏幕 #left-menu, .el-menu-vertical-demo{ height: 100%; } }

最终的结果这里就不展示了,和我们需要实现的结果是一致的。

功能完善

到此,我们结合路由配置实现了菜单栏这个功能基本上已经完成了,不过这是一个缺乏灵魂的菜单栏,因为没有设置菜单的跳转,我们点击菜单栏还无法路由跳转到对应的组件,所以接下来就来实现这个功能。

菜单跳转的实现方式有两种,第一种是NavMenu组件提供的跳转方式。

第二种是在菜单上添加router-link实现跳转。

那本次我选择的是第一种方式实现跳转,这种实现方式需要两个步骤才能完成,第一步是启用el-menu上的router;第二步是设置导航的index属性。

那下面就来实现这两个步骤。

启用el-menu上的router 设置导航的index属性

首先我将每一个菜单标题对应需要设置的index属性值列出来。

index值对应的是每个菜单在路由中配置的path值

首页 员工管理 员工统计 index="/employee/employeeStatistics" 员工管理 index="/employee/employeeManage" 考勤管理 考勤统计 index="/attendManage/attendStatistics" 考勤列表 index="/attendManage/attendList" 异常管理 index="/attendManage/exceptManage" 员工统计 员工统计 index="/timeManage/timeStatistics" 员工统计 index="/timeManage/timeList" 选项一 index="/timeManage/timeList/options1" 选项二 index="/timeManage/timeList/options2"

接着在回顾前面递归调用的组件,导航菜单的index设置的是child.path,为了看清楚child.path的值,我将其添加菜单标题的右侧,让其显示到界面上。

{{child.meta.title}} | {{child.path}} {{child.meta.title}} | {{child.path}}

同时将菜单栏的宽度由200px设置为400px。

然后我们看一下效果。

可以发现,child.path的值就是当前菜单在路由中配置path值(router.js中配置的path值)。

那么问题就来了,前面我们整理了每一个菜单标题对应需要设置的index属性值,就目前来看,现在设置的index值是不符合要求的。不过仔细观察现在菜单设置的index值和正常值是有一点接近的,只是缺少了上一级菜单的path值,如果能将上一级菜单的path值和当前菜单的path值进行一个拼接,就能得到正确的index值了。

那这个思路实现的方式依然是在递归时将当前菜单的path作为参数传递给menuItem组件。

将当前菜单的path作为参数传递给menuItem组件之后,在下一级菜单实现时,就能拿到上一级菜单的path值。然后组件中将basepath的值和当前菜单的path值做一个拼接,作为当前菜单的index值。

import path from 'path' export default { name: 'MenuItem', props: ['route','basepath'], data(){ return { } }, methods :{ // routepath 为当前菜单的path值 // getpath: 拼接 当前菜单的上一级菜单的path 和 当前菜单的path getPath: function(routePath){ return path.resolve(this.basepath, routePath); } } }

再看一下界面。

我们可以看到二级菜单的index值已经没问题了,但是仔细看,发现工时管理-工时列表下的两个三级菜单index值还是有问题,缺少了工时管理这个一级菜单的path。

那这个问题是因为我们在调用组件自身是传递的basepath有问题。

basepath传递的只是上一级菜单的path,在递归二级菜单时,index的值是一级菜单的path值+二级菜单的path值;那当我们递归三级菜单时,index的值就是二级菜单的path值+三级菜单的path值,这也就是为什么工时管理-工时列表下的两个三级菜单index值存在问题。

所以这里的basepath值在递归的时候应该是累积的,而不只是上一级菜单的path值。因此借助递归算法的优势,basepath的值也需要通过getPath方法进行处理。

最终完整的代码如下。

{{child.meta.title}} {{child.meta.title}} import path from 'path' export default { name: 'MenuItem', props: ['route','basepath'], data(){ return { } }, methods :{ // routepath 为当前菜单的path值 // getpath: 拼接 当前菜单的上一级菜单的path 和 当前菜单的path getPath: function(routePath){ return path.resolve(this.basepath, routePath); } } }

删除其余用来调试的代码

最终效果

文章的最后呢,将本次实现的最终效果在此展示一下。

选项一和选项二这两个三级菜单在路由配置中没有设置component,这两个菜单只是为了实现三级菜单,在最后的结果演示中,我已经删除了路由中配置的这两个三级菜单

此处在leftMenu组件中为el-menu开启了unique-opened

在menuIndex组件中,将左侧菜单栏的宽度改为200px



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3