likes
comments
collection
share

vue3后台管理系统教程3(登录、菜单权限)

作者站长头像
站长
· 阅读数 0

开始搭建后台系统的核心就是权限,不同的权限对应不同的侧边导航。一般的逻辑是:登录后获取得到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<Element>) {
    Object.keys(directivesList).forEach((key) => {
      // 注册所有自定义指令
      app.directive(key, directivesList[key])
    })
  },
}

export default directives

最后在main.ts导入使用

import directives from '@/directives/index'
app.use(directives)

使用指令

<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>