vue3项目路由权限和按钮权限的实现

608 阅读4分钟

前言

在开发后台管理系统的时候,难免会遇到不同角色不同权限的情况,近期在工作中就遇到了这个情况,也踩了比较多的坑,这里做一下总结。

路由权限

用户在登录的时候,服务端会返回给我们角色的信息,其中就包括了权限信息,如果该用户有对应的权限,我们在进入页面的时候,菜单也只显示这些权限对应的路由。

首先我们要配置一些通用的路由,比如404路由,首页的路由等。

export const routes: RouteRecordRaw[] = [
    {
        path: '/',
        name: 'layout',
        component: Welcome,
        children: [],
    },
    {
        path: '/login',
        name: 'login',
        component: LoginPage
    },
    {
        path: '/:pathMatch(.*)*',
        name: '404',
        component: NotFoundPage
    }
]

然后我们需要配置需要动态加载的路由

export const asyncRoutes: RouteRecordRaw[] = [
    {
        path:'/commodity',
        name: 'commodity',
        component: Commodity,
        children: [
            {
                path: '/commodity/rawMaterial',
                name: 'rawMaterial',
                component: RawMaterial,
            },
            {
                path: 'commodity/parts',
                name: 'parts',
                component: parsts,
            }
        ],
    },
    ...
]

当我们的路由都配置完成后,通过和后端返回的数据进行匹配,最后得出我们需要为该用户添加的路由,我们可以把路由信息存放在pinia中,通过vue-router提供的router.addRoute来实现路由的动态添加。

// login.ts
store.setPermissionRoutes(res.data)   // 服务端获取的数据

// mani.ts
setPermissionRoutes = (menu) => {
  const routeList = defaultRoutes[0].children
  const routeArr: any[] = []
  routeList?.forEach((row) => {
    const routeInfo: any = {
      menu_name: '',
      path: '',
      children: [],
      buttonPermissions: {}
    }
    const { path, children } = row
    const info = menu.find((item) => item.path === path)
    const array = children?.filter((route) => menu.find((item) => item.path === route.path))
    if (info) {
      routeInfo.menu_name = info.menu_name
      routeInfo.path = info.path
      routeInfo.buttonPermissions = { ...info.permissions }
    }
    if (array && array.length) {
      routeInfo.children = [...array]
    }
    routeArr.push(routeInfo)
  })
  const routeData = routeArr.filter((row: any) => row.path !== '') as any
  routeData.map((route) => {
    // 将处理好的路由使用addRoute添加,我这里对应的是放在layout下
    router.addRoute('layout', route)
  })
}

这样我们在用户登录后就可以实现根据路由权限来动态添加路由,但是当我们刷新页面的时候会出现页面空白的情况,这是因为刷新的时候pinia中的数据是无法保存下来的,所以我们应该在路由守卫里面也加上动态添加路由的操作。

let hasInitAuth = false
router.beforeEach(async (to, from, next) => {
  const token = localStorage.getItem('token')
  if (to.path === '/login') {
    next()
  } else {
    if (!token && to.path !== '/login') {
      router.push({ path: '/login' })
    } else {
      if (!hasInitAuth) {
        const routes = JSON.parse(localStorage.getItem('menu') as any)
        generateRoutes(routes)
        // 确保路由已经添加出来再进行跳转,需要添加replace:true
        next({ ...to, replace: true })
        hasInitAuth = true
      } else {
        next()
      }
    }
  }
})

这样我们就基本实现了路由权限

按钮权限

实现了路由权限后,还需要将权限控制在按钮级别,按钮级别的权限的话,可以采用自定义指令的方法去实现。

export const buttonDirective = {
  mounted(el, binding, vNode) {
    if (!binding.value) return
    let { typeName, path } = binding.value
    const menu = JSON.parse(localStorage.getItem('menu'))
    const childrenRoutes = menu.filter((item) => item.rank === 2)
    const buttonRouteMessage = childrenRoutes.find((route) => route.path === path)
    if (!buttonRouteMessage?.list) {
      // 不显示按钮
      el.parentNode && el.parentNode.removeChild(el)
    } else {
      const currentButton = buttonRouteMessage.list.find((item) => item.menu_name === typeName)
      if (!currentButton) {
        // 不显示按钮
        el.parentNode && el.parentNode.removeChild(el)
      }
    }
  }
}

接下来我们只需要在对应的button使用就可以了

<div v-has={directiveParams}>新增</div>

// 也可以将相应逻辑封装在函数中,通过v-if来实现

export const setIsShowOption = (name, path) => {
  const menu = JSON.parse(localStorage.getItem('menu') as any)
  const childrenRoutes = menu.filter((item) => item.rank === 2)
  const buttonRouteMessage = childrenRoutes.find((route) => route.path === path)
  if (!buttonRouteMessage.list) {
    return false
  } else {
    return buttonRouteMessage.list.find((item) => item.menu_name === name)
  }
}

<button v-if ="setIsShowOption('删除', $route.path)">删除</button>

这样基本就可以实现相关的需求了,因为是刚接触使用vue3,之前没有了解过自定义指令,这里也做一下简单的使用总结。

自定义指令

首先要在main.ts中注册指令,和组件一样,自定义指令在模板中使用前必须先注册

指令钩子

// main.ts

app.directive('button', {
    // 在绑定元素的attribute前,或事件监听器应用前调用
    created(el, binding,vNode) {
    },
    // 在元素被插入到dom前调用
    beforeMount() {
    },
    // 在绑定元素的父组件以及他的所有子节点挂载完成后调用
    mounted() {
    },
    // 绑定元素的父组件更新前调用
    beforeUpdate() {
    },
    // 在绑定元素的父组件以及他的所有子节点都更新后调用
    updated() {
    },
    // 绑定元素的父组件卸载前调用
    beforeUnmount() {
    },
    // 绑定元素的父组件卸载后调用
    unmounted() {
    }
})

钩子参数

  • el:指令绑定到的元素,这可以用来直接操作dom。

  • binding:一个对象

  • value:传递给指令的值

  • oldvalue:之前的值,仅在beforeUpdate和update中可用

  • arg:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo中,参数是"foo"

  • instace:使用该指令的组件实例

  • dir:指令的定义对象

  • vnode:代表绑定元素的底层 VNode

  • prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在beforeUpdate和updated钩子中可用

自定义指令的介绍在vue官方文档中介绍的已经很详细了。