一步一步实现中后台管理平台模板-05-根据路由配置动态生成菜单

一,前言

上一篇,为vue-framework-admin添加了路由配置,实现了点击菜单切换内容显示
本篇将实现根据路由配置动态生成菜单功能

首先菜单(路由)的层级是不固定的,有可能是1级2级3级或更多
其次,并不是所有路由都会渲染都菜单上:如登录注册404,订单详情,分布表单等

这就需要对路由配置信息进行递归处理,并跳过不需要显示在菜单中的路由配置项,
另外,菜单的图标,名称也都需要在路由中进行配置

二,实现思路

通过递归路由渲染菜单组件,路由配置需做以下约定:

1,不需要渲染到路由组件中的路由(包含子路由),添加hideInMenu:true
2,有name属性(且hideInMenu不等于true)的路由才会被渲染到菜单组件上
3,希望子路由不被渲染到菜单组件上,添加hideChildrenInMenu:true
4,使用meta元信息,对路由设置菜单图标和title等属性

三,修改路由

确定了实现的思路,开始按照约定修改路由配置:

1)登录,注册,404等视图组件,添加hideInMenu:true,使之不在菜单组件进行渲染
2)仪表盘,订单管理,订单列表,订单审批需要在菜单组件进行渲染,设置name属性,并使用meta元数据添加图标和title属性
3)订单详情不需要添加到菜单组件,不需要设置name属性和meta元数据
4)订单审批下的分布表单不在菜单组件进行渲染,且多个表单页面对应同一个菜单项,订单审批路由添加hideChildrenInMenu: true
router.js
import Vue from "vue";
import Router from "vue-router";
import NotFound from "./views/404";
import NProgress from "nprogress";
import "nprogress/nprogress.css";

Vue.use(Router);

/**
 * 路由约定:
 *
 * 不需要渲染到路由组件中的路由(包含子路由),添加hideInMenu:true
 * 有name属性(且hideInMenu不等于true)的路由才会被渲染到菜单组件上
 * 希望子路由不被渲染到菜单组件上,添加hideChildrenInMenu:true
 * 使用meta元信息,对路由设置菜单图标和title等属性
 */
const router = new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/user",
      hideInMenu: true,
      component: () =>
        import(/* webpackChunkName: "layout" */ "./layouts/UserLayout"),
      children: [
        {
          path: "/user",
          redirect: "/user/login"
        },
        {
          path: "/user/login",
          name: "login",
          component: () =>
            import(/* webpackChunkName: "user" */ "./views/User/Login")
        },
        {
          path: "/user/register",
          name: "register",
          component: () =>
            import(/* webpackChunkName: "user" */ "./views/User/Register")
        }
      ]
    },
    {
      path: "/",
      component: () =>
        import(/* webpackChunkName: "layout" */ "./layouts/BasicLayout"),
      children: [
        // 默认
        {
          path: "/",
          redirect: "/dashboard"
        },
        // dashboard
        {
          path: "/dashboard",
          name: "dashboard",
          meta: { icon: "dashboard", title: "仪表盘" },
          component: () =>
            import(/* webpackChunkName: "dashboard" */ "./views/Dashboard/Dashboard")
        },
        // order
        {
          path: "/order",
          name: "order",
          meta: { icon: "ordered-list", title: "订单管理" },
          component: { render: h => h("router-view") },
          children: [
            {
              path: "/order/orderlist",
              name: "orderlist",
              meta: { title: "订单列表" },
              component: () =>
                import(/* webpackChunkName: "order" */ "./views/Order/OrderList")
            },
            {
              path: "/order/orderdetails",
              component: () =>
                import(/* webpackChunkName: "order" */ "./views/Order/OrderDetails")
            },
            {
              path: "/order/approval",
              name: "approval",
              meta: { title: "订单审批" },
              hideChildrenInMenu: true,
              component: () =>
                import(/* webpackChunkName: "order" */ "./views/Order/Approval"),
              children: [
                {
                  path: "/order/approval",
                  redirect: "/order/approval/info"
                },
                {
                  path: "/order/approval/info",
                  name: "info",
                  component: () =>
                    import(/* webpackChunkName: "order" */ "./views/Order/Approval/ApprovalStep1")
                },
                {
                  path: "/order/approval/confirm",
                  name: "confirm",
                  component: () =>
                    import(/* webpackChunkName: "order" */ "./views/Order/Approval/ApprovalStep2")
                },
                {
                  path: "/order/approval/result",
                  name: "result",
                  component: () =>
                    import(/* webpackChunkName: "order" */ "./views/Order/Approval/ApprovalStep3")
                }
              ]
            }
          ]
        },
        // test
        {
          path: "/multiple",
          name: "multiple",
          meta: { icon: "form", title: "多级菜单" },
          component: () =>
            import(/* webpackChunkName: "multiple" */ "./views/Order/Approval"),
          children: [
            {
              path: "/order/approval/info",
              name: "info",
              meta: { title: "多级菜单1" },
              component: () =>
                import(/* webpackChunkName: "multiple" */ "./views/Order/Approval/ApprovalStep1"),
              children: [
                {
                  path: "/order/approval/result",
                  name: "result",
                  meta: { title: "多级菜单1-1" },
                  component: () =>
                    import(/* webpackChunkName: "multiple" */ "./views/Order/Approval/ApprovalStep3")
                }
              ]
            },
            {
              path: "/order/approval/confirm",
              name: "confirm",
              meta: { title: "多级菜单2" },
              component: () =>
                import(/* webpackChunkName: "multiple" */ "./views/Order/Approval/ApprovalStep2")
            }
          ]
        }
      ]
    },
    {
      path: "*",
      name: "404",
      hideInMenu: true,
      component: NotFound
    }
  ]
});

router.beforeEach((to, from, next) => {
  if (to.path !== from.path) {
    NProgress.start();
  }
  next();
});

router.afterEach(() => {
  NProgress.done();
});

export default router;

按照设计的约定对路由配置修改完成,下一步为菜单组件添加对应的逻辑


四,为菜单组件添加根据路由配置构建菜单数据方法

修改菜单组件SiderMenu.vue:,添加根据路由配置构建菜单数据方法

/**
 * 根据路由配置构建菜单数据
 */
buildMenuDataByRouter(routes = [], parentKeys = [], selectedKey) {
  const menuData = [];
  routes.forEach(item => {
    // 有name 且不隐藏 : 需要渲染到菜单组件上
    if (item.name && !item.hideInMenu) {
      // 保存openKeysMap和selectedKeysMap(path作为selectedKey)
      this.openKeysMap[item.path] = parentKeys;
      this.selectedKeysMap[item.path] = [selectedKey || item.path];
      // 解构并删除children,只保留当前层级
      const newItem = { ...item };
      delete newItem.children;
      // 如果有children且不隐藏 : 需要渲染到菜单组件上
      if (item.children && !item.hideChildrenInMenu) {
        // 对当前children递归并加入到结构后的children上
        newItem.children = this.buildMenuDataByRouter(item.children, [
          ...parentKeys,
          item.path
        ]);
      } else {
        this.buildMenuDataByRouter(
          item.children,
          selectedKey ? parentKeys : [...parentKeys, item.path],
          selectedKey || item.path
        );
      }
      menuData.push(newItem);
      // 没有name,但不隐藏本层及Children,并且有children -- 本层不显示,但子路由可显示
    } else if (
      !item.hideInMenu &&
      !item.hideChildrenInMenu &&
      item.children
    ) {
      menuData.push(
        ...this.buildMenuDataByRouter(item.children, [
          ...parentKeys,
          item.path
        ])
      );
    }
  });
  return menuData;
}
方法通过递归router配置,按照约定逻辑对数据进行处理,最终得到菜单数据menuData

五,完善菜单组件视图模板

根据路由配置构建菜单组件需要的数据后,需要为菜单组件编写对应的菜单渲染模板

由于每层菜单都可能存在children子菜单,同时也可能是最后一层
所以SiderMenu.vue菜单组件只负责处理一级菜单
如果一级菜单包含children数组对象,则将对children数组进行递归处理

子菜单数据children,将交由Ant Design Vue组件库提供的单文件递归组件处理
(https://vue.ant.design/components/menu-cn/#components-menu-demo-single-file-recursive-menu)

修改Sidermenu.vue:

<template>
  <div style="width: 256px">
    <a-menu
      :selectedKeys="selectedKeys"
      :openKeys.sync="openKeys"
      theme="dark"
      mode="inline"
    >
      <template v-for="item in menuData">
        <!-- 没有children的菜单(一级菜单) -->
        <a-menu-item
          v-if="!item.children"
          :key="item.path"
          @click="() => $router.push({ path: item.path, query: $route.query })"
        >
          <a-icon v-if="item.meta.icon" :type="item.meta.icon" />
          <span>{{ item.meta.title }}</span>
        </a-menu-item>
        <!-- 有children的菜单  -->
        <sub-menu v-else :menu-info="item" :key="item.path" />
      </template>
    </a-menu>
  </div>
</template>

SiderMenu.vue组件:

SiderMenu.vue菜单组件对根据路由配置生成的菜单数据menuData进行递归处理
使用a-menu-item标签,只处理没有children数组对象的一级菜单
若存在children对象,直接交由SubMenu组件进行处理

六,添加单文件递归菜单组件

Ant Design Vue组件库提供了单文件递归菜单组件

https://vue.ant.design/components/menu-cn/#components-menu-demo-single-file-recursive-menu

添加单文件递归菜单组件:layouts/SubMenu.vue

<template functional>
  <a-sub-menu :key="props.menuInfo.path">
    <span slot="title">
      <a-icon
        v-if="props.menuInfo.meta.icon"
        :type="props.menuInfo.meta.icon"
      />
      <span>{{ props.menuInfo.meta.title }}</span>
    </span>
	<!-- 遍历子菜单数据 -->
    <template v-for="item in props.menuInfo.children">
      <!-- 最后一层 -->
      <a-menu-item
        v-if="!item.children"
        :key="item.path"
        @click="
          () =>
            parent.$router.push({ path: item.path, query: parent.$route.query })
        "
      >
        <a-icon v-if="item.meta.icon" :type="item.meta.icon" />
        <span>{{ item.meta.title }}</span>
      </a-menu-item>
      <!-- 还有children,继续遍历 -->
      <sub-menu v-else :key="item.path" :menu-info="item" />
    </template>
  </a-sub-menu>
</template>
<script>
export default {
  props: ["menuInfo"]
};
</script>

SubMenu.vue组件:

菜单数据首先将通过siderMenu菜单组件对一级菜单进行处理
如果一级菜单包含children对象,则转交给SubMenu组件进行处理
所以进入SubMenu组件处理的数据,当前层级都必定包含children对象

SubMenu组件需要先对当前层做处理,再遍历子菜单数据children对象,
如果子菜单数据Item仍有children对象,则继续由SubMenu组件递归处理
直到递归终止条件:菜单数据Item到了最后一层,不再包含children数组对象

一级菜单使用a-sub-menu标签,二级菜单使用a-menu-item标签

七,测试菜单生成

npm run serve 查看菜单生成:
menu
Menu1

Menu2

八,测试多级菜单

由于菜单的层级是不确定的,可能是一级二级三级或更多层
当前根据路由配置动态生成菜单的功能能够支持多层级

修改路由,添加多级菜单配置:

新增"多级菜单"菜单项,并添加children:多级菜单1,多级菜单1-1,多级菜单2
// test
{
  path: "/multiple",
  name: "multiple",
  meta: { icon: "form", title: "多级菜单" },
  component: () =>
  import(/* webpackChunkName: "multiple" */ "./views/Order/Approval"),
  children: [
	{
	  path: "/order/approval/info",
	  name: "info",
	  meta: { title: "多级菜单1" },
	  component: () =>
	    import(/* webpackChunkName: "multiple" */ "./views/Order/Approval/ApprovalStep1"),
	  children: [
	    {
	      path: "/order/approval/result",
		  name: "result",
		  meta: { title: "多级菜单1-1" },
		  component: () =>
		    import(/* webpackChunkName: "multiple" */ "./views/Order/Approval/ApprovalStep3")
	    }
      ]
    },
    {
      path: "/order/approval/confirm",
      name: "confirm",
      meta: { title: "多级菜单2" },
      component: () =>
        import(/* webpackChunkName: "multiple" */ "./views/Order/Approval/ApprovalStep2")
    }
   ]
 }

测试多级菜单:

MutilMenu

九,修改跳转

由于约定路由依靠name属性判断是否加入到菜单中,
订单详情页不需要加入菜单,所以不能使用name属性

因此订单列表页跳转详情页时,不能使用$router.push({ name: xxx })进行跳转
需要修改为$router.push({ path: xxx }),使用path跳转
<template>
  <div>
    订单列表
    <a-button
    type="primary"
    @click="
    () => $router.push({ path: '/order/orderdetails', query: { orderId: '1' } })
">
	查看详情</a-button>
  </div>
</template>

<script>
export default {};
</script>

<style></style>


十,Menu中的属性

Menu 导航菜单文档:
https://vue.ant.design/components/menu-cn/

菜单选中Key和打开Key都会和路由的值相关

SelectedKeys
defaultSelectedKeys
defaultOpenKeys

defaultXXX:

在组件实例化时只有第一次时生效,加载完成后修改无效,defaultXXX值不会被改变

这个API的设计使用了react中受控组件和非受控组件的概念
不需要再为初始化状态声明并绑定变量,使用起来更加方便

路由变化时,打印selectKeys和openKeys对象

keys


十一,结尾

本篇实现了根据路由配置动态生成菜单功能
首先针对路由的各种情况进行了一个约定
并且根据这个约定为菜单组价添加了生成菜单数据的方法
使用单文件菜单组件对菜单数据进行递归,动态渲染菜单项

十二,代码下载

传送门:CSDN下载
传送门:GitHub下载-vue-framework-admin-v0.0.5


维护记录:

20190808:
编写文章,上传代码资源
20190809:
添加CSDN下载链接
完善多级路由部分的描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BraveWangDev

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值