设计左侧菜单和路由的关系
注意了:左侧菜单往往跟当前 登陆者的权限有关系,这一类数据可能是后端返回的,可能是多级的关系,那么就要用到递归的方式,所以我们用 Antd 中‘单文件递归菜单’
- layouts->SiderMenu.vue
- 递归当前 Antd 版本-1.5.0-rc.5 版本
- 你会发现你复制过来的代码有点乱,还报错,还可能看不大懂对吗?
- 因为这个代码中包含了左侧菜单的样式,还有一个递归项:SubMenu
- 索性咱们将 SubMenu 抽离出来做一个递归组件,于是呢,你就再 layouts 下面新建 SubMenu.vue,代码应该是这样的
<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>
- 于是乎呢,拆解之后你的 SiderMenu 应该是这个样子的
<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 宽度跟左侧布局的宽度调整一样
- SliderMenu.vue
<template> <div style="width: 256px"></div> </template>
- BasicLayout.vue
<a-layout-sider :theme="navTheme" v-if="navLayout === 'left'" :trigger="null" collapsible v-model="collapsed" width="256px" // 改成一个宽度即可 > </a-layout-sider>
到这你的左侧菜单就出来了
但是你发现切换主题颜色时,左侧 menu 没有颜色变化,我们要修改成同步的
SliderMenu
<a-menu :defaultSelectedKeys="['1']" :defaultOpenKeys="['2']" mode="inline"
:theme="theme" // 改成动态的 :inlineCollapsed="collapsed" >
props: {
theme: {
type: String,
default: 'dark' // 默认黑色
}
},
- BasicLayout.vue
<!-- 将当前页面切换的皮肤传进去 -->
<SiderMenu :theme="navTheme" />
将我们需要的真实路由渲染到菜单上,实现菜单控制路由
router->index.js
注意:菜单应该是我登陆之后需要使用的一些功能页面的连接目录,我们希望通过点击菜单目录切换页面,所以有些路由我们不需要渲染到菜单列表中去
- 比如:登陆页面,渲染到菜单上没意义,我们不需要那么怎么办呢?
- 约定一:添加一个排除渲染的标签,比如叫:hideInMenu 属性
path: '/user', hideInMenu: true, // 添加一个不渲染的标识符 component: () => { return import(/* webpackChunkName: "user" */ '../layouts/UserLayout.vue') }
- 约定二:我们之渲染带 name 属性的路由
- 约定三:同级路由下的分步展示,比如分步表单,只是步骤的切换,而不是页面切换时,这种情况也不渲染 menu,比如:hideChildrenInMenu: true
{ path: '/form/step-form', name: 'stepform', hideChildrenInMenu: true, // 注意看这里,分布操作时我们需要处理,子代路由不渲染 meta: { title: '分布表单' }, component: () => import(/* webpackChunkName: "form" */ '../views/Forms/StepForm'), children: [ {
除此之外了,我们希望有菜单名称和 icon
path: '/dashboard',
name: 'dashboard',
meta: { icon: 'dashboard', title: '仪表盘' }, // 给你需要渲染的menu自定义一个对象用来渲染名称和icon
component: { render: h => h('router-view') },
接下来我们就要将规定好的路由信息渲染到 menu 上去了
SiderMenu.vue
- 将原有默认的 list 干掉
// 通过路由对象获取所有的路由信息 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"
>
<!-- 将list改成menuData改成我们要的数据 -->
<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>
- 修改 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.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>
点击菜单跳转路由
- SiderMenu.vue
- 外层路由链接
<template>
<div style="width: 256px">
<a-menu
:selectedKeys="selectedKeys"
:openKeys.sync="openKeys"
mode="inline"
:theme="theme"
>
<template v-for="item in menuData">
<!-- 给每一个menu绑定一个点击事件,切换路由:看click事件 -->
<!-- 注意了:这里只是最外层链接哦,那如果路由曾经很多怎么办呢?是不是想到递归 -->
<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>
- 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.$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>
路由权限管理
- 根据用户权限,来渲染路由列表,达到权限控制的目的
- 新建 anth 文件夹
- auth->index.js
// 获取权限
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'
}
- 接下来去判断用户是否具有路由权限
- 1、给路由添加 authority 范围
- 2、在路由守卫中做统一处理
- router->index.js
- 在每个路由的 meta 对象中新增 authority 属性,然后 authority 的值就是权限类型
- 如:
{
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
- 这是个什么玩意儿呢?你可以理解成代码兵器库,what?,类似于 jquery 一样,用原生的 js 写出了很多趁手的常用的方法,供我们使用,很神奇,很高效哦~
为了方便咱们开发我们可以引入这个库
- npm i --save lodash
- https://www.lodashjs.com/docs/lodash.concat
使用 lodash import findLast from ‘lodash/findLast’ 引入即可
引入权限控制的文件
- import { check, isLogin } from ‘@/auth/index’
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
// 403页面
{
path: '/403',
name: '403',
component: Forbiden,
hideInMenu: true
},
- views->403.vue 新增一个页面
- 此时你去修改 auth->index.js 中的权限,比如把 admin,改成 user,你会发现,你的表单直接跳到 403,why? 那是因为你去 router->index,看看你新增的 meta 的 authority,是不是限制了权限?这样就关联起来了,懂么?
- 但是此时依旧有一个问题,我们通常希望,没有权限的路由咱就直接不让你看见了对吧,所以要微调一下,既然你要控制菜单渲染,就要回到菜单中去 SiderMenu.vue
// 引入权限校验的类库
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 组件
- 然后在 403 时做一个提示
router->index
import { Notification } from 'ant-design-vue' // 引入组件
} else if (to.path !== '/403') {
// 如果权限不够直接去403,需要去新建一个403的页面和路由
Notification.error({ // 做一个403全局提示
message: '403',
description: '没有访问权限,请联系管理员'
})
精细化权限控制(权限组件)
- 权限组件
- 我们采用函数式组件方式,这样性能更好,但是函数式组件跟template模板不是很友好,所以我们直接采用render方式渲染
- components新建Authority组件
<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才能操作设置
- layouts->BasicLayout.vue
此时你会发现只有admin时抽屉参会展示 <Authority :authority="['admin']"> <SettingDrawer /> </Authority>
- 至此:权限组件就Ok了
精细化权限控制(权限指令)
- 通过指令的方式来控制权限
- 新建指令仓库 directives用来存放各种自定义指令
- directives->auth.js
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
- 然后去进行指令的全局注册
- main.js
// 引入指令
import auth from "./directives/auth";
// 注册全局指令
Vue.use(auth);
测试
- layouts->BasicLayout.vue
<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>
至此我们通过路由,组件,指令三种方式来控制权限
注意:权限指令旨在第一次加载的时候有效果,如果动态的控制就会有问题
注意: 灵活度比较高,但是写法上稍微复杂度高一些
源码地址:git@github.com:sunhailiang/vue-public-ui.git
欢迎加微信一起学习:13671593005
未完待续…