Vue权限控制

423 阅读2分钟

需求:

中台租户的路由、菜单栏和按钮显示与否,由后台权限分配控制。

思路

菜单与路由完全由后端返回,前端先定义数据,交给后端,后端返回数据后,格式化数据用于动态路由和渲染菜单栏的数据。

  • 路由权限:addRoute合并动态路由和静态路由。
  • 菜单权限:asyncRouters数据遍历渲染菜单栏,利用el-menu的select事件判断跳转。
  • 按钮权限:定义指令v-permissions,判断meta.permissions是否包含当前按钮来决定是否隐藏按钮。
{ 
    "path": "/system", // 面包屑跳转
    "name": "system", // 唯一标识,index of currently active menu
    "component": "layouts/index.vue", //嵌套路由
    "hidden": false, //是否隐藏
    "redirect": '/system/userList', // 重定向
    "meta": {
      "title": "系统管理", // 一级菜单名
    },
    "children": [ // 子路由
      {
        "path": "userList",
        "name": "userList",
        "component": "views/system/user/list.vue",
        "hidden": false,
        "meta": {
          "title": "用户列表", // 二级菜单名
          "permissions": ["新建用户", "分配权限", "查看", "编辑", "删除",] // 按钮权限
        },
      },
    ],
  }

实现

路由权限

//router/index.ts
import { defineStore } from 'pinia'
import { asyncRouterHandle } from '@/utils/asyncRouter'
import { ref } from 'vue'


// 格式化路由
const formatRouter = (routes, routeMap) => {
  routes && routes.forEach(item => {
    routeMap[item.name] = item
    if (item.children && item.children.length > 0) {
      formatRouter(item.children, routeMap)
    }
  })
}

export const useRouterStore = defineStore('router', () => {
  const asyncRouters = ref([])
  const routeMap = ({})
  const SetAsyncRouter = async() => {
  // 模拟后端返回的权限数据
  const asyncRouter = [{ 
    "path": "/system", // 面包屑跳转
    "name": "system", // 唯一标识,index of currently active menu
    "component": "layouts/index.vue", //嵌套路由
    "hidden": false, //是否隐藏
    "redirect": '/system/userList', // 重定向
    "meta": {
      "title": "系统管理", // 一级菜单名
    },
    "children": [ // 子路由
      {
        "path": "userList",
        "name": "userList",
        "component": "views/system/user/list.vue",
        "hidden": false,
        "meta": {
          "title": "用户列表", // 二级菜单名
          "permissions": ["新建用户", "分配权限", "查看", "编辑", "删除",] // 按钮权限
        },
      },
    ],
  }]
    
    asyncRouter && asyncRouter.push(
      {
        path: '/404',
        name: '404',
        hidden: true,
        meta: {
          title: '迷路了*。*',
        },
        component: 'views/error/index.vue'
      },
      {
        path: '/reload',
        name: 'Reload',
        hidden: true,
        meta: {
          title: '',
        },
        component: 'views/error/reload.vue'
      },
      {
        path: '/:catchAll(.*)',
        name: 'NotFound',
        hidden: true,
        meta: {
          title: '',
        },
        redirect: '/404'
      }
    )

      formatRouter(asyncRouter, routeMap)
      asyncRouterHandle(asyncRouter)
      asyncRouters.value = asyncRouter
      return true
    }
    
    return {
      asyncRouters, // 用于渲染el-menu
      SetAsyncRouter,
      routeMap // 用于渲染el-menu
    }
})

// asyncRouter.ts
const modules = import.meta.glob('../views/**/*.vue')
const modules2 = import.meta.glob('../layouts/*.vue')

// views/system/user/list.vue 处理成 import('@/views/system/user/list.vue'),webpack能进行编译打包
export const asyncRouterHandle = (asyncRouter) => {
  asyncRouter.forEach(item => {
    if (item.component) {
      if (item.redirect) {
        item.component = dynamicImport(modules2, item.component)
      } else {
        item.component = dynamicImport(modules, item.component)
      }
    } else {
      delete item['component']
    }
    if (item.children) {
      asyncRouterHandle(item.children)
    }
  })
}

function dynamicImport(
  dynamicViewsModules,
  component
) {
  const keys = Object.keys(dynamicViewsModules)
  const matchKeys = keys.filter((key) => {
  const k = key.replace('../', '')
    return k === component
  })
  const matchKey = matchKeys[0]
  return dynamicViewsModules[matchKey]
}

// permission.ts
import { useUserStore } from '@/pinia/modules/user'
import { useRouterStore } from '@/pinia/modules/router'
import router from '@/router'

let asyncRouterFlag = 0

const whiteList = ['login'] // 白名单

const getRouter = async (userStore) => {
  const routerStore = useRouterStore()
  await routerStore.SetAsyncRouter()
  await userStore.GetUserInfo()
  const asyncRouters = routerStore.asyncRouters

  asyncRouters.forEach(asyncRouter => { // 路由合并
    router.addRoute(asyncRouter)
  })
}

router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  to.meta.matched = [...to.matched]
  const token = userStore.token
  // 在白名单中的判断情况
  if (whiteList.indexOf(to.name) > -1) {
    if (token) {
    // 在白名单中并且已经登陆的时候
      if (!asyncRouterFlag && whiteList.indexOf(from.name) < 0) {
        asyncRouterFlag++
        await getRouter(userStore) // 获取完整路由
      }
      next({ name: 'customerList' })
    } else {
      next()
    }
  } else {
    // 不在白名单中并且已经登陆的时候
    if (token) {
      // 添加flag防止多次获取动态路由和栈溢出
      if (!asyncRouterFlag && whiteList.indexOf(from.name) < 0) {
        asyncRouterFlag++
        await getRouter(userStore)
        if (userStore.token) {
          next({ ...to, replace: true })
        } else {
          next({
            name: 'login',
            query: { redirect: to.href }
          })
        }
      } else {
         // 路由匹配数组
        if (to.matched.length) {
          next()
        } else { 没有该路由,跳转404
          next({ path: '/layout/404' })
        }
      }
    }
    // 不在白名单中并且未登陆的时候
    if (!token) {
      next({
        name: 'login',
        query: {
          redirect: document.location.hash //记录历史访问路径
        }
      })
    }
  }
})

菜单权限

<template>
      <el-aside width="260px" class="bg-white mt-12">
        <!--
          default-active	当前激活菜单的 index
        -->
        <el-menu
          text-color="#191919"
          active-text-color="#4368EC"
          :default-active="activeMenu"
          @select="selectMenuItem"
        >
          <template v-for="routerInfo in routerStore.asyncRouters" :key="routerInfo.name">
            <el-sub-menu ref="subMenu" :index="routerInfo.name" v-if="!routerInfo.hidden">
              <template #title>
                <span>{{ routerInfo.meta.title }}</span>
              </template>
              <template v-for="item in routerInfo.children" :key="item.name">
                <el-menu-item :index="item.name" v-if="!item.hidden">
                  <div class="gva-menu-item">
                    <span class="gva-menu-item-title">{{ item.meta.title }}</span>
                  </div>
                </el-menu-item>
              </template>
            </el-sub-menu>
          </template>
        </el-menu>
      </el-aside>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useRouterStore } from '@/pinia/modules/router'

const route = useRoute()
const router = useRouter()
const routerStore = useRouterStore()
const activeMenu = ref<string>('')

watch(
  () => route,
  () => {
    activeMenu.value = route.name as string
  },
  { deep: true }
)

// 页面初始化
const initPage = () => {
  activeMenu.value = route.name as string
}

initPage()

const selectMenuItem = (index, _, _ele, _aaa) => {
  const query = {}
  const params = {}
  routerStore.routeMap[index]?.parameters &&
    routerStore.routeMap[index]?.parameters.forEach((item) => {
      if (item.type === 'query') {
        query[item.key] = item.value
      } else {
        params[item.key] = item.value
      }
    })
  if (index === route.name) return
  if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) {
    window.open(index)
  } else {
    router.push({ name: index, query, params })
  }
}
</script>

按钮权限

定义指令

// permission.ts
import router from '@/router'

export default {
    install: (app) => {
    app.directive('permission', {
      // 当被绑定的元素插入到 DOM 中时……
      mounted: function (el, binding) {
        const { value } = binding
        const permissions = router.currentRoute.value.meta.permissions
        // 按钮权限数组是否包含当前按钮,不包含代表隐藏当前按钮,DOM移除元素
        if (!permissions.includes(value)) {
          el.parentNode.removeChild(el)
        }
      }
    })
  }
}
// main.ts
import permission from '@/directive/permission'

const app = createApp(App);

app.use(permission).mount('#app')
// user/list.vue
  <el-row class="mb-26">
    <el-button v-permission="'新建用户'" @click="newUser()"> 新建用户 </el-button>
  </el-row>