注意了:左侧菜单往往跟当前 登陆者的权限有关系,这一类数据可能是后端返回的,可能是多级的关系,那么就要用到递归的方式,所以我们用 Antd 中‘单文件递归菜单’
layouts->SiderMenu.vue 递归当前 Antd 版本-1.5.0-rc.5 版本 你会发现你复制过来的代码有点乱,还报错,还可能看不大懂对吗? 因为这个代码中包含了左侧菜单的样式,还有一个递归项:SubMenu 索性咱们将 SubMenu 抽离出来做一个递归组件,于是呢,你就再 layouts 下面新建 SubMenu.vue,代码应该是这样的
{{ props.menuInfo.title }}
{{ item.title }}
export default {
props: ['menuInfo'],
name: 'SubMenu',
// must add isSubMenu: true
isSubMenu: true
}
于是乎呢,拆解之后你的 SiderMenu 应该是这个样子的
{{ item.title }}
// 这个就是你新建的递归组件
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
}
}
}
至此呢,你的左侧菜单就出来了,剩下的调整样式
是不是宽了? - 改啊,将 SliderMenu 宽度跟左侧布局的宽度调整一样
SliderMenu.vue
BasicLayout.vue
到这你的左侧菜单就出来了
但是你发现切换主题颜色时,左侧 menu 没有颜色变化,我们要修改成同步的
SliderMenu
props: {
theme: {
type: String,
default: 'dark' // 默认黑色
}
},
BasicLayout.vue
将我们需要的真实路由渲染到菜单上,实现菜单控制路由
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
修改模板
{{ item.meta.title }}
修改 SubMenu.vue
{{ props.menuInfo.meta.title }}
{{ item.meta.title }}
export default {
props: ['menuInfo'],
name: 'SubMenu',
// must add isSubMenu: true
isSubMenu: true
}
点击菜单跳转路由
SiderMenu.vue
外层路由链接
this.$router.push({ path: item.path, query: this.$router.query })
"
>
{{ item.meta.title }}
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
}
}
}
SubMenu.vue
循环多层路由链接
{{ props.menuInfo.meta.title }}
parent.$router.push({
path: item.path,
query: parent.$router.query
})
"
>
{{ item.meta.title }}
路由权限管理
根据用户权限,来渲染路由列表,达到权限控制的目的
新建 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组件
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时抽屉参会展示
至此:权限组件就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
(collapsed = !collapsed)"
/>
至此我们通过路由,组件,指令三种方式来控制权限
注意:权限指令旨在第一次加载的时候有效果,如果动态的控制就会有问题
注意: 灵活度比较高,但是写法上稍微复杂度高一些
源码地址:git@github.com:sunhailiang/vue-public-ui.git欢迎加微信一起学习:13671593005
未完待续…