一,前言
上一篇,为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 查看菜单生成:
八,测试多级菜单
由于菜单的层级是不确定的,可能是一级二级三级或更多层
当前根据路由配置动态生成菜单的功能能够支持多层级
修改路由,添加多级菜单配置:
新增"多级菜单"菜单项,并添加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")
}
]
}
测试多级菜单:
九,修改跳转
由于约定路由依靠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对象
十一,结尾
本篇实现了根据路由配置动态生成菜单功能
首先针对路由的各种情况进行了一个约定
并且根据这个约定为菜单组价添加了生成菜单数据的方法
使用单文件菜单组件对菜单数据进行递归,动态渲染菜单项
十二,代码下载
传送门:CSDN下载
传送门:GitHub下载-vue-framework-admin-v0.0.5
维护记录:
20190808:
编写文章,上传代码资源
20190809:
添加CSDN下载链接
完善多级路由部分的描述