业务需求

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

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

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持阿兔在线工具。

点赞(0)

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部