开始搭建后台系统的核心就是权限,不同的权限对应不同的侧边导航。一般的逻辑是:登录后获取得到token,再通过token获取用户信息(用户名、账号、角色、权限等),从接口获取到菜单后,计算出路由,并动态添加路由和按钮。接下来详细介绍一下登录和权限。
一、登录
登录流程
验证用户名和密码后,调用登录接口登录成功后,将返回的token存到store中,如果参数中含有redirect,则跳转到该路由。
js
const handleSubmitForm = (formEle: FormInstance | undefined | null) => {
if (!formEle) {
return
}
formEle.validate(async (valid) => {
if (valid) {
try {
loading.value = true
let { data } = await login(loginForm)
userStore.setToken(data)
router.replace({ path: (route.query.redirect as string) || HOME_URL })
} catch (error) {
loading.value = false
}
}
})
}
定义store:
- 存储token;
- 定义获取用户信息和权限的action,获取并存储用户信息等数据;
- 登出。
js
import { defineStore } from 'pinia'
import type { UserInfo } from './type'
import { getUserInfo, logout } from '@/api'
import { useAuthStore } from '../auth'
import { RESEETSTORE } from '@/utils/reset'
export const useUserStore = defineStore('user', {
state: () => {
return {
userInfo: {},
token: '',
}
},
actions: {
setUserInfo(userInfo: UserInfo) {
this.userInfo = userInfo
},
setToken(token: string) {
this.token = token
},
async GetInfoAction() {
const authStore = useAuthStore()
const { data } = await getUserInfo()
const { avatar, name, buttons, roles, routes } = data
this.setUserInfo({ avatar, name })
authStore.setAuth({ buttons, roles, routes })
},
async logout() {
await logout()
RESEETSTORE()
},
},
//缓存
persist: true,
})
二、用户权限
菜单权限
注意在beforeEach中,不能跳转到相同的path,也不能调用多次。next('xxx')会再次触发进入beforeEach,如果to.path相同,会造成死循环。
js
/**
* 路由前置守卫
*/
router.beforeEach(async (to, from, next) => {
NProgress.start()
const userStore = useUserStore()
const authStore = useAuthStore()
//1.白名单直接放行
if (ROUTER_WHITE_LIST.includes(to.path)) {
next()
}
// 2.如果没有token,携带redirect参数跳转login页面
// 需要判断是否是login,不然跳转login,会造成死循环进入beforeEach
if (!userStore.token) {
if (to.path === LOGIN_URL) next()
next({ path: LOGIN_URL })
}
//3.如果有token,判断store中是否有权限数据
if (!authStore.authRouterList.length) {
await getAuthRoutes()
// !!如果 addRoute 并未完成,路由守卫会一层一层的执行执行,直到 addRoute 完成,找到对应的路由
next({ ...to, replace: true })
} else {
next()
}
})
/**
* 路由后置守卫
*/
router.afterEach(() => {
NProgress.done()
})
/**
* 路由报错
*/
router.onError((error) => {
NProgress.done()
console.warn('路由错误', error.message)
})
/**
* 处理动态路由
*/
async function getAuthRoutes() {
const userStore = useUserStore()
const authStore = useAuthStore()
try {
//1.获取用户信息、权限列表
await userStore.GetInfoAction()
// 2.判断当前用户有没有菜单权限
if (!authStore.authRouterList.length) {
ElNotification({
title: '无权限访问',
message: '当前账号无任何菜单权限,请联系系统管理员!',
type: 'warning',
duration: 3000,
})
RESEETSTORE()
router.replace(LOGIN_URL)
return Promise.reject('No permission')
}
//3.与本地路由表对比,获取新的路由表
const authRoutes = filterDynamicRoutes(
dynamicRoutes,
authStore.authRouterList,
)
//4.动态添加路由
authRoutes.forEach((route) => {
router.addRoute(route)
})
//5.获取菜单数据:处理subMenu数据,静态路由和动态路由拼接,过滤isHide=true的路由
const menuList getMenuList([ ...staticRoutes, ...routerList, ] as unknown as Menu.MenuOptions[])
authStore.setAuthMenuList(menuList)
} catch (error) {
RESEETSTORE()
console.log(error)
}
}
//对比本地路由表,获取有权限的路由
function filterDynamicRoutes(
dynamicRoutes: RouteRecordRaw[],
authRouterList: string[],
) {
return dynamicRoutes.filter((route) => {
if (!authRouterList.includes(route.name as string)) return false
if (route.children?.length) {
route.children = filterDynamicRoutes(route.children, authRouterList)
}
return true
})
}
//过滤隐藏的菜单
function getMenuList(routeList: Menu.MenuOptions[]) {
return routeList.filter((route: Menu.MenuOptions) => {
if (route?.children?.length) {
route.children = getMenuList(route.children)
}
return !route.meta?.isHide
})
}
具体实现:
- 分别配置静态路由和动态路由数据
- 导航前置守卫,判断是否为在白名单,如果是则直接跳转next()
- 判断是否有token,没有则跳转到login页面
- 有token,则判断store中是否有权限菜单列表,如果没有则发起请求获取,对比本地动态路由数据,过滤后得到路由表,再遍历addRoutes添加。
按钮权限
- 自定义指令实现
首先看看注册自定义指令的语法:
js
const app = createApp({}) // 使 v-focus 在所有组件中都可用
app.directive('focus', { /* ... */ })
指令钩子
js
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
接下来新建directives/modules,在该文件下新建auth.ts写控制按钮权限的逻辑,一个按钮可能有一种权限或者多权限,单权限直接根据includes
,多权限通过循环判断,如果有权限就行渲染,无权限就直接remove
这个元素。
js
import { useAuthStore } from '@/store/modules/auth'
import type { Directive, DirectiveBinding } from 'vue'
const auth: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding
const authStore = useAuthStore()
const currentPageRoles = authStore.authButtonList ?? []
if (value instanceof Array && value.length) {
const hasPermission = value.every((item) =>
currentPageRoles.includes(item),
)
if (!hasPermission) el.remove()
} else {
if (!currentPageRoles.includes(value)) el.remove()
}
},
}
export default auth
在src/directives中创建index.ts文件,导出所有指令
js
import { App } from 'vue'
import auth from './modules/auth'
const directivesList: any = {
// Custom directives
auth,
}
const directives = {
install: function (app: App<Element>) {
Object.keys(directivesList).forEach((key) => {
// 注册所有自定义指令
app.directive(key, directivesList[key])
})
},
}
export default directives
最后在main.ts导入使用
js
import directives from '@/directives/index'
app.use(directives)
使用指令
js
<el-button
type="primary"
icon="Plus"
v-auth="btn.User.add"
@click="openDrawer('新增')"
>
添加
</el-button>
<el-button
type="danger"
icon="Delete"
plain
v-auth="['btn.User.remove', 'btn.User.BatchRemove']"
@click="batchDelete(scope.selectedListIds)"
:disabled="!scope.isSelected"
>
批量删除
</el-button>
参考来源: juejin.cn/post/721503...