注意了:左侧菜单往往跟当前 登陆者的权限有关系,这一类数据可能是后端返回的,可能是多级的关系,那么就要用到递归的方式,所以我们用 Antd 中‘单文件递归菜单’
<template functional>
<a-sub-menu :key="props.menuInfo.key">
<span slot="title">
<a-icon type="mail" /><span>{{ props.menuInfo.title }}span>
span>
<template v-for="item in props.menuInfo.children">
<a-menu-item v-if="!item.children" :key="item.key">
<a-icon type="pie-chart" />
<span>{{ item.title }}span>
a-menu-item>
<sub-menu v-else :key="item.key" :menu-info="item" />
template>
a-sub-menu>
template>
<script>
export default {
props: ['menuInfo'],
name: 'SubMenu',
// must add isSubMenu: true
isSubMenu: true
}
</script>
<template>
<div style="width: 256px">
<a-button
type="primary"
@click="toggleCollapsed"
style="margin-bottom: 16px"
>
<a-icon :type="collapsed ? 'menu-unfold' : 'menu-fold'" />
a-button>
<a-menu
:defaultSelectedKeys="['1']"
:defaultOpenKeys="['2']"
mode="inline"
theme="dark"
:inlineCollapsed="collapsed"
>
<template v-for="item in list">
<a-menu-item v-if="!item.children" :key="item.key">
<a-icon type="pie-chart" />
<span>{{ item.title }}span>
a-menu-item>
<sub-menu v-else :menu-info="item" :key="item.key" />
template>
a-menu>
div>
template>
<script>
// 这个就是你新建的递归组件
import SubMenu from './SubMenu'
export default {
components: {
SubMenu
},
data() {
return {
collapsed: false,
list: [
{
key: '1',
title: 'Option 1'
},
{
key: '2',
title: 'Navigation 2',
children: [
{
key: '2.1',
title: 'Navigation 3',
children: [{ key: '2.1.1', title: 'Option 2.1.1' }]
}
]
}
]
}
},
methods: {
toggleCollapsed() {
this.collapsed = !this.collapsed
}
}
}
</script>
至此呢,你的左侧菜单就出来了,剩下的调整样式
是不是宽了? - 改啊,将 SliderMenu 宽度跟左侧布局的宽度调整一样
<template>
<div style="width: 256px">div>
template>
a-layout-sider>
到这你的左侧菜单就出来了
但是你发现切换主题颜色时,左侧 menu 没有颜色变化,我们要修改成同步的
SliderMenu
props: {
theme: {
type: String,
default: 'dark' // 默认黑色
}
},
<SiderMenu :theme="navTheme" />
router->index.js
注意:菜单应该是我登陆之后需要使用的一些功能页面的连接目录,我们希望通过点击菜单目录切换页面,所以有些路由我们不需要渲染到菜单列表中去
path: '/user',
hideInMenu: true, // 添加一个不渲染的标识符
component: () => {
return import(/* webpackChunkName: "user" */ '../layouts/UserLayout.vue')
}
{
path: '/form/step-form',
name: 'stepform',
hideChildrenInMenu: true, // 注意看这里,分布操作时我们需要处理,子代路由不渲染
meta: { title: '分布表单' },
component: () =>
import(/* webpackChunkName: "form" */ '../views/Forms/StepForm'),
children: [
{
path: '/dashboard',
name: 'dashboard',
meta: { icon: 'dashboard', title: '仪表盘' }, // 给你需要渲染的menu自定义一个对象用来渲染名称和icon
component: { render: h => h('router-view') },
SiderMenu.vue
// 通过路由对象获取所有的路由信息
let menuData = this.getMenuData(this.$router.options.routes)
getMenuData(routes) {
// 递归的方式获取路由列表,筛选出我们索要呈现的列表
const menuData = []
routes.forEach(item => {
if (item.name && !item.hideInMenu) {
const newItem = { ...item }
delete newItem.children
if (item.children && !item.hideChildrenInMenu) {
newItem.children = this.getMenuData(item.children)
}
menuData.push(newItem)
} else if (
!item.hideInMenu &&
!item.hideChildrenInMenu &&
item.children
) {
menuData.push(...this.getMenuData(item.children))
}
console.log('去你吗的', menuData)
})
return menuData
}
// 最后将list替换成menuData
修改模板
<template>
<div style="width: 256px">
<a-menu
:defaultSelectedKeys="['1']"
:defaultOpenKeys="['2']"
mode="inline"
:theme="theme"
:inlineCollapsed="collapsed"
>
<template v-for="item in menuData">
<a-menu-item v-if="!item.children" :key="item.path">
<a-icon v-if="item.meta.icon" :type="item.meta.icon" />
<span>{{ item.meta.title }}span>
a-menu-item>
<sub-menu v-else :menu-info="item" :key="item.path" />
template>
a-menu>
div>
template>
<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.key">
<a-icon v-if="item.meta.icon" :type="item.meta.icon" />
<span>{{ item.meta.title }}span>
a-menu-item>
<sub-menu v-else :key="item.path" :menu-info="item" />
template>
a-sub-menu>
template>
<script>
export default {
props: ['menuInfo'],
name: 'SubMenu',
// must add isSubMenu: true
isSubMenu: true
}
script>
<template>
<div style="width: 256px">
<a-menu
:selectedKeys="selectedKeys"
:openKeys.sync="openKeys"
mode="inline"
:theme="theme"
>
<template v-for="item in menuData">
<a-menu-item
v-if="!item.children"
:key="item.path"
@click="
() =>
this.$router.push({ path: item.path, query: this.$router.query })
"
>
<a-icon v-if="item.meta.icon" :type="item.meta.icon" />
<span>{{ item.meta.title }}span>
a-menu-item>
<sub-menu v-else :menu-info="item" :key="item.path" />
template>
a-menu>
div>
template>
<script>
import SubMenu from './SubMenu'
export default {
components: {
SubMenu
},
props: {
theme: {
type: String,
default: 'dark'
}
},
data() {
this.selectedKeysMap = {}
this.openKeysMap = {}
let menuData = this.getMenuData(this.$router.options.routes)
return {
menuData,
collapsed: false,
selectedKeys: this.selectedKeysMap[this.$route.path],
openKeys: this.collapsed ? [] : this.openKeysMap[this.$route.path]
}
},
watch: {
'$route.path': function(val) {
// 同步观察路由变换实时更新
this.selectedKeys = this.selectedKeysMap[val]
this.openKeys = this.collapsed ? [] : this.openKeysMap[val]
}
},
methods: {
toggleCollapsed() {
this.collapsed = !this.collapsed
},
getMenuData(routes = [], parentKeys = [], selectedKey) {
// 递归的方式获取路由列表,筛选出我们索要呈现的列表
const menuData = []
routes.forEach(item => {
if (item.name && !item.hideInMenu) {
// 过滤只有带name的属性的路由信息和非隐藏路由
this.openKeysMap[item.path] = parentKeys
this.selectedKeysMap[item.path] = [item.path || selectedKey]
const newItem = { ...item }
delete newItem.children
if (item.children && !item.hideChildrenInMenu) {
// 如果存在子项,就继续递归子项-解决多级路由的问题
newItem.children = this.getMenuData(item.children, [
...parentKeys,
item.path
])
} else {
this.getMenuData(
item.children,
selectedKey ? parentKeys : [...parentKeys, item.path], // 解释这一步,这个解决什么呢,比如分布表单,我们点击步骤,不能按步骤跳吧,是他的父级路由才会发生跳转,所以呢,我们找他的父级路由作为跳转对象
selectedKey || item.path
)
}
menuData.push(newItem)
} else if (
!item.hideInMenu &&
!item.hideChildrenInMenu &&
item.children
) {
menuData.push(
...this.getMenuData(item.children, [...parentKeys, item.path])
)
}
})
return menuData
}
}
}
</script>
<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.$router.query
})
"
>
<a-icon v-if="item.meta.icon" :type="item.meta.icon" />
<span>{{ item.meta.title }}span>
a-menu-item>
<sub-menu v-else :key="item.path" :menu-info="item" />
template>
a-sub-menu>
template>
// 获取权限
export function getCurrentAuthority() {
// 这里返回的权限应该是从后端读取回来的,此时用admin替代
return ['admin']
}
// 鉴权
export function check(authority) {
const current = getCurrentAuthority()
return current.some(item => authority.includes(item))
}
// 判断是否登陆
export function isLogin() {
const current = getCurrentAuthority()
return current && current[0] !== 'guest'
}
{
path: "/",
meta:{authority:['user','admin']}, // 约定只有user和admin才能访问
}
{
path: '/form',
name: 'form',
component: { render: h => h('router-view') },
meta: { icon: 'form', title: '表单', authority: ['admin'] }, // 约定表单只有admin才能访问
redirect: '/form/basic-form'
}
普及一个新的知识点:lodash.js
为了方便咱们开发我们可以引入这个库
使用 lodash import findLast from ‘lodash/findLast’ 引入即可
引入权限控制的文件
router->index
// 路由守卫
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
router.beforeEach((to, from, next) => {
// 只有在路由地址发生变化时触发进度条
if (to.path != from.path) {
NProgress.start()
}
const record = findLast(to.matched, record => record.meta.authority)
if (record && !check(record.meta.authority)) {
// 如果没有权限
// 再次判断是否登陆了
if (!isLogin() && to.path !== '/user/login') {
//登陆直接跳到登录页页面
next({
path: '/user/login'
})
} else if (to.path !== '/403') {
// 如果权限不够直接去403,需要去新建一个403的页面和路由
next({
path: '/403'
})
}
nProgress.done()
}
next()
})
router.afterEach(() => {
NProgress.done()
})
export default router
// 403页面
{
path: '/403',
name: '403',
component: Forbiden,
hideInMenu: true
},
// 引入权限校验的类库
import { check } from '@/auth/index'
getMenuData(routes = [], parentKeys = [], selectedKey) {
// 递归的方式获取路由列表,筛选出我们索要呈现的列表
const menuData = []
routes.forEach(item => {
// 注意这里:这里就是如果权限不够呢,直接阻止列表渲染
if (item.meta && item.meta.authority && !check(item.meta.authority)) {
return false
}
if (item.name && !item.hideInMenu) {
// 过滤只有带name的属性的路由信息和非隐藏路由
this.openKeysMap[item.path] = parentKeys
this.selectedKeysMap[item.path] = [item.path || selectedKey]
const newItem = { ...item }
delete newItem.children
if (item.children && !item.hideChildrenInMenu) {
// 如果存在子项,就继续递归子项
newItem.children = this.getMenuData(item.children, [
...parentKeys,
item.path
])
} else {
this.getMenuData(
item.children,
selectedKey ? parentKeys : [...parentKeys, item.path], // 解释这一步,这个解决什么呢,比如分布表单,我们点击步骤,不能按步骤跳吧,是他的父级路由才会发生跳转,所以呢,我们找他的父级路由作为跳转对象
selectedKey || item.path
)
}
menuData.push(newItem)
} else if (
!item.hideInMenu &&
!item.hideChildrenInMenu &&
item.children
) {
menuData.push(
...this.getMenuData(item.children, [...parentKeys, item.path])
)
}
})
return menuData
}
此时你去修改 auth->index 中的权限时,就会发现菜单列表不满足权限的都没有渲染
如果权限不够时,我希望有提示信息,于是乎呢,去 Antd 中找到 Notification 组件
router->index
import { Notification } from 'ant-design-vue' // 引入组件
} else if (to.path !== '/403') {
// 如果权限不够直接去403,需要去新建一个403的页面和路由
Notification.error({ // 做一个403全局提示
message: '403',
description: '没有访问权限,请联系管理员'
})
<script>
import { check } from “@/auth/index”;
import { constants } from “os”;
export default {
functional: true,
props: {
authority: {
type: Array,
required: true
}
},
// 解释一个函数式渲染,render函数有两个参数,一个式creatElement,包含了dom的信息,但是指向的是一个虚拟的dom
// context 则包含了该实例对象的各种属性
// 如果你用了权限校验的组件,那么将会做判断
render(creatElement, context) {
const { props, scopedSlots } = context; // 结构出参数和所有的插槽
// 如果校验通过则执行该组件内部的插槽组件,否则怎么也不做
return check(props.authority) ? scopedSlots.default() : null;
}
};
- 既然是权限校验,那么在整个项目钟肯定会出现多次,所以我们注册成全局组件
- main.js
```js
// 引入权限组件
import Authority from "./components/Authority.vue";
// 全局注册
Vue.component("Authority", Authority);
此时你会发现只有admin时抽屉参会展示
<Authority :authority="['admin']">
<SettingDrawer />
Authority>
import { check } from "@/auth/index";
// 是否加载?
function auth(Vue, options = []) {
Vue.directive(options.name || "auth", {
// 父级组件点调用时去判断
inserted(el, binding) {
// 如果传过来的值,没有通过校验就移除节点
if (!check(binding.value)) {
el.parentNode && el.parentNode.removeChild(el);
}
}
});
}
export default auth
// 引入指令
import auth from "./directives/auth";
// 注册全局指令
Vue.use(auth);
测试
<a-layout-header style="background: #fff; padding: 0">
<a-icon
v-auth="['admin']" // 使用组件,修改权限名称,此时会发现会权限不足就没法渲染
class="trigger"
:type="collapsed ? 'menu-unfold' : 'menu-fold'"
@click="() => (collapsed = !collapsed)"
/>
<Header />
</a-layout-header>
至此我们通过路由,组件,指令三种方式来控制权限
注意:权限指令旨在第一次加载的时候有效果,如果动态的控制就会有问题
注意: 灵活度比较高,但是写法上稍微复杂度高一些
欢迎加微信一起学习:13671593005
未完待续…