Vue3.0无限级菜单
懒人Ethan 人气:0业务需求
菜单项是业务系统的重要组成部分,一般业务系统都要支持显示多级业务菜单,但是根据每个业务人员的权责不同,看到的的菜单项也是不同的。
这就要求页面可以支持无限极菜单显示,根据每个用户的权限不同,后台服务返回对应的菜单项。
本文基于Vue 3.0实现了一个可配置的无限等级菜单,关键代码如下:
后端返回的菜单项数据结构
后端服务一般不会直接返回一个树型结构菜单集合给前端,这样做也不合理。前端应该根据自己的具体需求,构建自己的菜型单树。后端返回的数据结构一般包含以下一个字段:
- Id 菜单ID, 数字类型
- pId当前菜单的父级菜单ID, 数字类型
- title 菜单的标题
- link 菜单对应的链接
- order 同级菜单的排列顺序,数字类型
其他业务字段需要具体问题具体分析,在这里不再赘述。本文不再讨论后端如何进行菜单项的权限控制,所使用的菜单内容,包括在一个JSON文件中,具体见附录。
菜单内容是一个足球数据管理系统,包括多级菜单:
- 第一级菜单只有一项,是所有节点的祖先节点。
- 第二级菜单包括联赛管理,俱乐部管理和球员管理
- 第三级菜单包括二级菜单内容的CRUD。
关键代码
为了支持无限级菜单,本文所有关键算法全部基于递归实现。主要包括:
1.后端数据转换为树形结构
2.后端数据排序
3.基于菜单树形结构生成Vue的路由数据
4.菜单组件的递归调用
后端数据转为树形结构
dataToTree函数调用的实参是附录的JSON数据,该代码参考Vue 3.0的AST树转换的代码,具体思想是:
1.将集合的数据分为父节点和子节集合,最外层的父节点为pId为0的节点。
2.在子节点中找到当前父节点的直接子节点,将其从当前子节点集合剔除。
3.递归回到1,寻找子节点的子节点。
4.如果当前子节点不是任何节点的父节点,将该子节点放入父节点的children集合中。
在生成当前树型结构菜单数据后,可以将该数据保存在vuex中,作为公共数据便于其他模块使用。
function dataToTree(data) { const parents = data.filter((item) => item.pId === 0); const children = data.filter((item) => item.pId !== 0); toTree(parents, children); return parents; function toTree(parents, children) { for (var i = 0; i < parents.length; ++i) { for (var j = 0; j < children.length; ++j) { if (children[j].pId === parents[i].Id) { let _children = deepClone(children, []); toTree([children[j]], _children); if (parents[i].children) { parents[i].children.push(children[j]); } else { parents[i].children = [children[j]]; } } } } } } function deepClone(source, target) { var _tar = target || {}; let keys = Reflect.ownKeys(source); keys.map((key) => { if (typeof source[key] === "object") { _tar[key] = Object.prototype.toString.call(source[key]) === "[object Array]" ? [] : {}; deepClone(source[key], _tar[key]); } else { _tar[key] = source[key]; } }); return _tar; }
菜单项排序
根据同级节点的order值进行排序,本文没有将该排序和上节的树型结构转换放在一起,主要是考虑有些系统可能不需要排序。如果需要,每次添加元素都要进行一次排序,效率低下,所以在获取树型结构后,再进行一次排序,具体排序函数如下:
function SortTree(tree) { tree = tree.sort((a, b) => a.order - b.order); tree.map((t) => { if (t.children) { t.children = SortTree(t.children); } }); return tree;
采用最简单的递归方式,遍历当前树型集合,按照order字段的升序方式进行排序,如果当前节点有children项,递归排序。
基于菜单树形结构生成Vue的路由数据
在获取树型菜单后后,我们可以基于当前数据,生成该用户在App中要使用到的路由项,具体代码如下:
function TreeToRoutes(treeData, routes) { routes = routes || []; for (var i = 0; i < treeData.length; ++i) { routes[i] = { path: treeData[i].link, name: treeData[i].name, component: () => import(`@/views/${treeData[i].name}`), }; if (treeData[i].children) { routes[i].children = TreeToRoutes( treeData[i].children, routes[i].children ); } } return routes; }
1.遍历树型菜单,将当前菜单项的link和tname复制到Vue路由数据的path和name上,component采用动态加载方式。
2.如果当前菜单项包含子节点children,递归调用,复制其子节点内容。
在main.js方法中,将菜单数据通过vuex进行读取,然后调用上述算法生成路由数据。将该数据直接加载到Vue的路由中,保证了如果当前用户没有某一个菜单的权限,即使通过URL进行访问,也是访问不到的,因为App只会为有权限的菜单项生成路由数据。如果用户没有某一个菜单的权限,也就不会从后端获取到该菜单的数据,也就不会为该菜单项生成路由。
菜单组件的递归调用
菜单组件代码如下:
<template> <div> <ul v-if="data.children && data.children.length > 0"> <li><router-link :to="data.link">{{data.title}}</router-link></li> <menu-item :data="item" :key="index" v-for="(item,index) in data.children"> </ul> <ul v-else> <li><router-link :to="data.link">{{data.title}}</router-link></li> </ul> </div> </template> <script> export default { name: "MenuItem", props:{ data: Object } } </script>
如果当前菜单项包含子节点,则递归调用MenuItem组件自己
菜单组件调用的代码如下:
<template> <div> <menu-item :data="item" :key="index" v-for="(item,index) in data" /> </div> </template> <script> import MenuItem from './MenuItem' export default { name: "Page", components:{ MenuItem } } </script>
由于生成的菜单数据结构最外层是数据,所以MenuItem组件需要进行循环调用。
附录-菜单项数据
export default [ { Id: 15, pId: 0, name: "all", title: "all", link: "/all", order: 2, }, { Id: 1, pId: 15, name: "clubs", title: "Club Management", link: "/clubs", order: 2, }, { Id: 2, pId: 15, name: "leagues", title: "League Management", link: "/leagues", order: 1, }, { Id: 3, pId: 15, name: "players", title: "Player Management", link: "/players", order: 3, }, { Id: 5, pId: 2, name: "LeagueDelete", title: "Delete League", link: "/leagues/delete", order: 3, }, { Id: 6, pId: 2, name: "LeagueUpdate", title: "Update League", link: "/leagues/update", order: 2, }, { Id: 7, pId: 2, name: "LeagueAdd", title: "Add League", link: "/leagues/add", order: 1, }, { Id: 8, pId: 3, name: "PlayerAdd", title: "Add Player", link: "/players", order: 1, }, { Id: 9, pId: 3, name: "PlayerUpdate", title: "Update Player", link: "/players", order: 3, }, { Id: 10, pId: 3, name: "PlayerDelete", title: "Delete Player", link: "/players", order: 2, }, { Id: 11, pId: 1, name: "ClubAdd", title: "Add Club", link: "/clubs/add", order: 3, }, { Id: 12, pId: 1, name: "ClubUpdate", title: "Update Club", link: "/clubs/update", order: 1, }, { Id: 13, pId: 1, name: "ClubDelete", title: "Delete Club", link: "/clubs/delete", order: 2, }, ];
加载全部内容