开始搭建后台系统的核心就是权限,不同的权限对应不同的侧边导航。一般的逻辑是:登录后获取得到token,再通过token获取用户信息(用户名、账号、角色、权限等),从接口获取到菜单后,计算出路由,并动态添加路由和按钮。接下来详细介绍一下登录和权限。
一、登录登录流程验证用户名和密码后,调用登录接口登录成功后,将返回的token存到store中,如果参数中含有redirect,则跳转到该路由。
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,获取并存储用户信息等数据;登出。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相同,会造成死循环。
/** * 路由前置守卫 */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添加。按钮权限自定义指令实现首先看看注册自定义指令的语法:
const app = createApp({}) // 使 v-focus 在所有组件中都可用 app.directive('focus', { /* ... */ })
指令钩子
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这个元素。
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文件,导出所有指令
import { App } from 'vue'import auth from './modules/auth'const directivesList: any = { // Custom directives auth,}const directives = { install: function (app: App) { Object.keys(directivesList).forEach((key) => { // 注册所有自定义指令 app.directive(key, directivesList[key]) }) },}export default directives
最后在main.ts导入使用
import directives from '@/directives/index'app.use(directives)
使用指令
添加 批量删除
作者:不凸不凹链接:https://juejin.cn/post/7298635806475010084