Vue3.0实现无限级菜单

Querida ·
更新时间:2024-09-20
· 1027 次阅读

业务需求

菜单项是业务系统的重要组成部分,一般业务系统都要支持显示多级业务菜单,但是根据每个业务人员的权责不同,看到的的菜单项也是不同的。

这就要求页面可以支持无限极菜单显示,根据每个用户的权限不同,后台服务返回对应的菜单项。

本文基于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,   }, ];



VUE 菜单

需要 登录 后方可回复, 如果你还没有账号请 注册新账号