需求:
中台租户的路由、菜单栏和按钮显示与否,由后台权限分配控制。
思路
菜单与路由完全由后端返回,前端先定义数据,交给后端,后端返回数据后,格式化数据用于动态路由和渲染菜单栏的数据。
- 路由权限: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>