2

Vue 里,多级菜单要如何设计才显得专业?

 2 years ago
source link: http://www.javaboy.org/2022/0628/vue-menu.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
22 天前 17 分钟 读完 (大约 2606 个字)

Vue 里,多级菜单要如何设计才显得专业?

老生常谈了!

虽然我们是 Java 猿,但是写起来前端代码也不含糊!今天我想来和大家聊聊这个前端的动态菜单,要如何设计才显得专业!还是以我们的 TienChin 项目为例,大家一起来看看。

先来一张截图看看效果:

20220426215908.png

那么这样的菜单是如何设计出来的呢?

今天我也不想和大家聊过多的技术细节,就聊聊这个路由是如何设计的,一旦大家明白了路由是如何设计的,剩下的问题都是细枝末节的问题了。

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
}
}]
}]

这里我举了两个菜单的例子,这两个例子比较具有代表性,这个菜单最终显示效果大概类似下面这样:

大概显示效果如上图。

接下来我就来说一下这里几个典型属性:

  1. redirect:noRedirect 表示该路由在面包屑导航中不可被点击。
  2. alwaysShow:如果这个属性设置为 false,那么当当前菜单只有一个子菜单的时候,默认情况下就只会显示子菜单,而忽略父菜单(如 1.1 小节所述),但是如果将该属性设置为 true,则无论当前菜单有几个子菜单,都会将当前菜单展示出来(这就类似于 vhr 中的效果了)。
  3. 每一个父菜单都有自己的 path,每一个 children 也有自己的 path,父菜单的 path 加上每一个 children 的 path,共同组成每一个 children 的路径。
  4. 再来看第二个角色管理这个菜单项,由于它的父菜单中只有一个子菜单项,并且父菜单中也没有 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)
}

这个函数的主要左右,就是处理菜单的路径问题。

我们来看下这个具体的判断逻辑:

  1. 如果这个菜单的路径是一个外链(判断逻辑是查看这个 path 是否有 http 或者 https 等前缀),即 isExternal 返回 true,就把这个路径原封不动返回。
  2. 如果这个菜单的父菜单的路径是一个外链,则将父菜单的 path 原封不懂返回。
  3. 如果有查询参数,就把参数加上。
  4. 最后通过 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 项目中,菜单还存在一个外链的问题。

这个外链有两种不同的显示思路:

  1. 点击外链,直接打开一个新的选项卡,在新的选项卡中展示新的页面。
  2. 点击外链,在当前项目中打开一个新的选项卡,选项卡中展示新的内容。

对于第一种情况我就不和大家演示了,对于第二种情况,我截个图给大家看下:

20220627235423.png

就是在当前项目的选项卡中,展示一个外部链接的内容。

我们先来看第一种情况。即点击菜单之后,就在一个新的选项卡中打开网页,这种菜单的 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 组件上,最终就是大家上图中所看到的效果了。

好啦,今天就先和小伙伴们分析一下前端菜单的各种情况,后端菜单如何按照需要返回数据,咱们下篇文章继续~

前端代码就是繁琐呀~松哥后面再录视频和大家仔细捋一捋,对视频感兴趣的小伙伴戳戳戳这里:TienChin 项目配套视频来啦

喜欢这篇文章吗?扫码关注公众号【江南一点雨】【江南一点雨】专注于 SPRING BOOT+微服务以及前后端分离技术,每天推送原创技术干货,关注后回复 JAVA,领取松哥为你精心准备的 JAVA 干货!

javaboy.jpg

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK