Vue 里,多级菜单要如何设计才显得专业?
Posted _江南一点雨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue 里,多级菜单要如何设计才显得专业?相关的知识,希望对你有一定的参考价值。
老生常谈了!
虽然我们是 Java 猿,但是写起来前端代码也不含糊!今天我想来和大家聊聊这个前端的动态菜单,要如何设计才显得专业!还是以我们的 TienChin 项目为例,大家一起来看看。
先来一张截图看看效果:
那么这样的菜单是如何设计出来的呢?
今天我也不想和大家聊过多的技术细节,就聊聊这个路由是如何设计的,一旦大家明白了路由是如何设计的,剩下的问题都是细枝末节的问题了。
1. 路由设计
有的小伙伴做过 vhr,知道 vhr 里的动态菜单实现方式,松哥和大家一样,也是在不断学习不断进步中,今天我想和大家探讨 TienChin 项目中动态菜单的实现方案,看看是否是一种更佳的解决方案。
1.1 菜单设计
先来和小伙伴们回顾下 vhr 中的方案:
在 vhr 中,权限的控制,只控制到二级菜单,也就是一级菜单和权限没关系。举个例子,现在有一级菜单 A 和 二级菜单 B,B 是 A 中的菜单,现在假设:
- 如果当前用户权限可以查看 B 菜单,那么 A 菜单会自动显示出来。
- 如果当前用户权限无法查看 B 菜单,且 A 菜单中也没有其他子菜单可以展示,那么 A 菜单就不会显示出来。
换言之,A 菜单显示与否,主要看它里边有没有子菜单需要展示,如果有,A 菜单就显示,如果没有,A 菜单就不显示。
vhr 中的思路是这样的。
在 TienChin 项目中,这一块有一些变化:
如果 A 中只有一个 B,那么似乎就没有必要再做一个两级菜单了,直接把 B 展示出来不就行了?用户操作也方便!
这是第一个不一样的地方。
1.2 路由数据
基于第一点,就涉及到一个问题,就是路由接口该如何设计?最主要是接口返回的数据格式应该是什么样子的?
首先有一点小伙伴们应该知道,这里的路由是一个嵌套路由,也就是一级菜单中嵌套着二级菜单。即使这个地方在展示的时候,不存在层级关系,例如上图中的促销活动,但是底层的数据结构也应该是嵌套路由。
好啦,不卖关子了,我们来看一段路由 JSON:
[
"name": "Monitor",
"path": "/monitor",
"hidden": false,
"redirect": "noRedirect",
"component": "Layout",
"alwaysShow": true,
"meta":
"title": "系统监控",
"icon": "monitor",
"noCache": false,
"link": null
,
"children": [
"name": "Online",
"path": "online",
"hidden": false,
"component": "monitor/online/index",
"meta":
"title": "在线用户",
"icon": "online",
"noCache": false,
"link": null
,
"name": "Job",
"path": "job",
"hidden": false,
"component": "monitor/job/index",
"meta":
"title": "定时任务",
"icon": "job",
"noCache": false,
"link": null
]
,
"path": "/",
"hidden": false,
"component": "Layout",
"children": [
"name": "Role",
"path": "role",
"hidden": false,
"component": "system/role/index",
"meta":
"title": "角色管理",
"icon": "peoples",
"noCache": false,
"link": null
]
]
这里我举了两个菜单的例子,这两个例子比较具有代表性,这个菜单最终显示效果大概类似下面这样:
- 系统监控
- 在线用户
- 定时任务
- 角色管理
大概显示效果如上图。
接下来我就来说一下这里几个典型属性:
- redirect:noRedirect 表示该路由在面包屑导航中不可被点击。
- alwaysShow:如果这个属性设置为 false,那么当当前菜单只有一个子菜单的时候,默认情况下就只会显示子菜单,而忽略父菜单(如 1.1 小节所述),但是如果将该属性设置为 true,则无论当前菜单有几个子菜单,都会将当前菜单展示出来(这就类似于 vhr 中的效果了)。
- 每一个父菜单都有自己的 path,每一个 children 也有自己的 path,父菜单的 path 加上每一个 children 的 path,共同组成每一个 children 的路径。
- 再来看第二个角色管理这个菜单项,由于它的父菜单中只有一个子菜单项,并且父菜单中也没有 alwaysShow 属性,所以这个菜单项在最终展示的时候,就只展示里边的角色管理,父菜单则不会展示出来(正好,生成的 JSON 中也没说父菜单的名字、图标等属性)。
当然,不是说你的 JSON 这么写就自动这么显示,JSON 中的东西只是一个标记,最终怎么显示,还要看渲染:
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="'submenu-title-noDropdown':!isNest">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
还有一个函数我就没有列出来了,反正我们看名字也大概知道每一个函数的含义。
大家看,这个 div 中实际上分为了两部分,上面 template 专门用来处理 children 中只有一项的情况(角色管理),具体处理方式就是把 children 拿出来显示,其他的则不考虑,具体执行的时候不一定是只有一个 children,也有可能压根就没有 children,此时直接显示 parent 即可(参考 1.3 小节)。
下面的 el-submenu 则处理 children 有多个的情况(系统监控)。
另外这里涉及到了一个 resolvePath,也是特别关键的一个方法,我们来大致看下:
resolvePath(routePath, routeQuery)
if (isExternal(routePath))
return routePath
if (isExternal(this.basePath))
return this.basePath
if (routeQuery)
let query = JSON.parse(routeQuery);
return path: path.resolve(this.basePath, routePath), query: query
return path.resolve(this.basePath, routePath)
这个函数的主要左右,就是处理菜单的路径问题。
我们来看下这个具体的判断逻辑:
- 如果这个菜单的路径是一个外链(判断逻辑是查看这个 path 是否有 http 或者 https 等前缀),即 isExternal 返回 true,就把这个路径原封不动返回。
- 如果这个菜单的父菜单的路径是一个外链,则将父菜单的 path 原封不懂返回。
- 如果有查询参数,就把参数加上。
- 最后通过 path.resolve 对路径进行一个简单运算。
有的小伙伴可能对 path.resolve 不熟悉,我简单说下:
path.resolve() 方法可以将多个路径解析为一个规范化的绝对路径,它的处理方式类似于对这些路径逐一进行 cd 操作,然而与 cd 操作不同的是,这些路径可以是文件,并且可不必实际存在(resolve() 方法不会利用底层的文件系统判断路径是否存在,而只是进行路径字符串操作)。例如:
path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')
相当于:
cd foo/bar
cd /tmp/file/
cd ..
cd a/../subfile
pwd
举个简单的例子:
path.resolve('/foo/bar', './baz')
// 输出结果为
'/foo/bar/baz'
path.resolve('/foo/bar', '/tmp/file/')
// 输出结果为
'/tmp/file'
path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif')
// 当前的工作路径是 /home/javaboy/node,则输出结果为
'/home/javaboy/node/wwwroot/static_files/gif/image.gif'
现在大家知道菜单跳转的路径是怎么来的了吧!
1.3 外链问题
在 TienChin 项目中,菜单还存在一个外链的问题。
这个外链有两种不同的显示思路:
- 点击外链,直接打开一个新的选项卡,在新的选项卡中展示新的页面。
- 点击外链,在当前项目中打开一个新的选项卡,选项卡中展示新的内容。
对于第一种情况我就不和大家演示了,对于第二种情况,我截个图给大家看下:
就是在当前项目的选项卡中,展示一个外部链接的内容。
我们先来看第一种情况。即点击菜单之后,就在一个新的选项卡中打开网页,这种菜单的 JSON 格式如下:
"name": "Http://www.javaboy.org",
"path": "http://www.javaboy.org",
"hidden": false,
"component": "Layout",
"meta":
"title": "TienChin健身官网",
"icon": "guide",
"noCache": false,
"link": "http://www.javaboy.org"
这个大家看,也没有 children,因为不需要,这个显示的时候,就当成了只有一个 children 来处理,然后菜单项的 path 是一个 http 路径,一点击,自然就跳到新的选项卡了。
对于第二种情况,即点击外链,在当前项目中打开一个新的选项卡,选项卡中展示链接的内容,它的 JSON 结构类似下面这样:
"name": "Http://www.javaboy.org",
"path": "/",
"hidden": false,
"component": "Layout",
"meta":
"title": "TienChin健身官网",
"icon": "guide",
"noCache": false,
"link": null
,
"children": [
"name": "Www.javaboy.org",
"path": "www.javaboy.org",
"hidden": false,
"component": "InnerLink",
"meta":
"title": "TienChin健身官网",
"icon": "guide",
"noCache": false,
"link": "http://www.javaboy.org"
]
这个其实也没啥好说的,类似于上面系统监控的那种情况,但是只有一个子菜单,在菜单渲染的时候,也是只渲染一个子菜单。由于父子菜单的 path 都不是以 http 或者 https 之类的地址开头,所以这个链接最终生成的 path 是 /www.javaboy.org
,然后这个路径的内容将展示在 InnerLink 组件上,最终就是大家上图中所看到的效果了。
好啦,这就是前端菜单的各种情况,后端菜单如何按照需要返回数据,咱们继续~
2. 菜单表
首先我们来看看菜单表的定义,也就是 sys_menu
。
CREATE TABLE `sys_menu` (
`menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`menu_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称',
`parent_id` bigint(20) DEFAULT '0' COMMENT '父菜单ID',
`order_num` int(4) DEFAULT '0' COMMENT '显示顺序',
`path` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '路由地址',
`component` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '组件路径',
`query` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '路由参数',
`is_frame` int(1) DEFAULT '1' COMMENT '是否为外链(0是 1否)',
`is_cache` int(1) DEFAULT '0' COMMENT '是否缓存(0缓存 1不缓存)',
`menu_type` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
`visible` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '#' COMMENT '菜单图标',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '备注',
PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3054 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单权限表';
其实这里很多字段都和我们 vhr 项目项目很相似,我也就不重复啰嗦了,我这里主要和小伙伴们说一个字段,那就是 menu_type
。
menu_type
表示一个菜单字段的类型,一个菜单有三种类型,分别是目录(M)、菜单(C)以及按钮(F)。这里所说的目录,相当于我们在 vhr 中所说的一级菜单,菜单相当于我们在 vhr 中所说的二级菜单。
当用户从前端登录成功后,要去动态加载的菜单的时候,就查询 M 和 C 类型的数据即可,F 类型的数据不是菜单项,查询的时候直接过滤掉即可,通过 menu_type
这个字段可以轻松的过滤掉 F 类型的数据。小伙伴们想想,F 类型的数据过滤掉之后,剩下的数据不就是一级菜单和二级菜单了,那不就和 vhr 又一样了么!
在 vhr 中,考虑到菜单就是只有两级:一级菜单和二级菜单,一级菜单是目录,二级菜单是则是具体的菜单项,没有三级菜单!所以在 vhr 中,查询菜单的时候我直接用了一个一对多的查询,将一级菜单做一的一方,二级菜单做多的一方,这样比较省事。当然灵活度差一点,所以在 TienChin 项目中,这块还是用上了递归。
3. 前端菜单展示
接下来,前端菜单展示分为了几种情况?这个松哥在之前的文章中已经和大家聊过了,具体可以参考Vue 里,多级菜单要如何设计才显得专业?一文,这里不再赘述。
4. 菜单接口
当用户登录成功之后,会自动请求 /getRouters
接口来获取菜单信息,我们一起来看下:
/**
* 获取路由信息
*
* @return 路由信息
*/
@GetMapping("getRouters")
public AjaxResult getRouters()
Long userId = SecurityUtils.getUserId();
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
这里的查询实际上分为两个步骤:
- 根据用户 id 查询到所有的菜单信息,这一步的查询实际上是比较容易的,就单纯的多张表联合在一起,然后过滤出和当前用户相关并且菜单类型为 M 或者 C 的菜单(类型为 F 的表示按钮,就不要了),查询到菜单信息之后,然后进行一个递归操作,将菜单数据的层级排列出来。
menuService.buildMenus
这一步则是将菜单数据专为前端所需要的路由数据。
一共就这两个步骤,我们来逐一进行分析。
先来看查询菜单数据。
/**
* 根据用户ID查询菜单
*
* @param userId 用户名称
* @return 菜单列表
*/
@Override
public List<SysMenu> selectMenuTreeByUserId(Long userId)
List<SysMenu> menus = null;
if (SecurityUtils.isAdmin(userId))
menus = menuMapper.selectMenuTreeAll();
else
menus = menuMapper.selectMenuTreeByUserId(userId);
return getChildPerms(menus, 0);
/**
* 根据父节点的ID获取所有子节点
*
* @param list 分类表
* @param parentId 传入的父节点ID
* @return String
*/
public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId)
List<SysMenu> returnList = new ArrayList<SysMenu>();
for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext(); )
SysMenu t = (SysMenu) iterator.next();
// 一、根据传入的某个父节点ID,遍历该父节点的所有子节点
if (t.getParentId() == parentId)
recursionFn(list, t);
returnList.add(t);
return returnList;
/**
* 递归列表
*
* @param list
* @param t
*/
private void recursionFn(List<SysMenu> list, SysMenu t)
// 得到子节点列表
List<SysMenu> childList = getChildList(list, t);
t.setChildren(childList);
for (SysMenu tChild : childList)
if (hasChild(list, tChild))
recursionFn(list, tChild);
/**
* 得到子节点列表
*/
private List<SysMenu> getChildList(List<SysMenu> list, SysMenu t)
List<SysMenu> tlist = new ArrayList<SysMenu>();
Iterator<SysMenu> it = list.iterator();
while (it.hasNext())
SysMenu n = (SysMenu) it.next();
if (n.getParentId().longValue() == t.getMenuId().longValue()以上是关于Vue 里,多级菜单要如何设计才显得专业?的主要内容,如果未能解决你的问题,请参考以下文章
若依 vue前端 动态设置路由path不同参数 在页面容器里打开新页面(新路由),面包屑和标签页标题根据参数动态改变,面包屑多级标题,侧边栏对应菜单亮起