在企业级后台管理系统中,用户的权限通常是基于角色来进行区分的,不同角色的用户需要访问不同的路由、菜单和功能按钮。为了提升应用的灵活性和性能,需要通过接口动态获取路由和菜单数据,实现按需加载。通过这种方式,前端不需要预先加载所有的路由和菜单,减少了首屏加载时间,并且实现了更加灵活的权限管理,使得用户只能看到和操作自己有权限的内容。这种方案不仅优化了性能,还提高了系统的安全性和可维护性。
1.路由权限控制
后端接口返回的数据格式如下,其中parent_id代表上一级菜单的id,type属性区分菜单和按钮。
[{"id": "3001","parent_id": "0","path": "/system","name": "system","redirect": "/system/user","title": "系统管理","icon": "Tools","component": "/views/MxLayout","order": 0,"status": 1,"type": "menu","roles": null,"is_init": 1},
{"id": "3002","parent_id": "0","path": "/attachment","name": "attachment","redirect": "/attachment/photo","title": "附件管理","icon": "Files","component": "/views/MxLayout","order": 1,"status": 1,"type": "menu","roles": null,"is_init": 1},
{"id": "3003","parent_id": "3001","path": "/system/user","name": "MxUser","redirect": "","title": "用户管理","icon": "User","component": "/views/MxUser","order": 0,"status": 1,"type": "menu","roles": null,"is_init": 1},
{"id": "3004","parent_id": "3001","path": "/system/role","name": "MxRole","redirect": "","title": "角色管理","icon": "UserFilled","component": "/views/MxRole","order": 0,"status": 1,"type": "menu","roles": null,"is_init": 1},
{"id": "3005","parent_id": "3001","path": "/system/menu","name": "MxMenu","redirect": "","title": "菜单管理","icon": "Menu","component": "/views/MxMenu","order": 0,"status": 1,"type": "menu","roles": null,"is_init": 1},
{"id": "4001","parent_id": "3003","path": null,"name": "post_/user","redirect": null,"title": "添加用户","icon": null,"component": null,"order": null,"status": 1,"type": "button","roles": null,"is_init": 1},
{"id": "4002","parent_id": "3003","path": null,"name": "put_/user","redirect": null,"title": "编辑用户","icon": null,"component": null,"order": null,"status": 1,"type": "button","roles": null,"is_init": 1},
{"id": "4003","parent_id": "3003","path": null,"name": "delete_/user","redirect": null,"title": "删除用户","icon": null,"component": null,"order": null,"status": 1,"type": "button","roles": null,"is_init": 1}]
首先需要从接口返回中分离出菜单和按钮的数据,将菜单的扁平数组格式化为树形数组,并形成路由树。
// 将扁平数组格式化为树形数组
export const buildTree = (data: any) => {
const myMap = new Map()
const result: any[] = []
// 先按 id 存入 map,便于后续构建树形结构
for (const route of data) {
myMap.set(route.id, route)
}
for (const route of data) {
if (route.parent_id === '0') {
result.push(route)
} else {
const parent = myMap.get(route.parent_id)
if (parent) {
if (!parent.children) {
parent.children = []
}
parent.children.push(route)
}
}
}
// 对根节点 result 按 order 排序
result.sort((a: any, b: any) => a.order - b.order)
// 对每个父节点的 children 数组按 order 字段排序
const sortChildren = (node: any) => {
if (node.children) {
node.children.sort((a: any, b: any) => a.order - b.order)
node.children.forEach(sortChildren) // 递归对子节点进行排序
}
}
// 对根节点及其所有子节点进行排序
result.forEach(sortChildren)
return result
}
// 基于树形结构生成前端路由配置
export const treeToRoutes = (tree: any[]): any[] => {
const modules = import.meta.glob('../**/**.vue') // 动态导入vue文件
const result: any[] = []
// 遍历树形结构,生成符合前端路由配置的对象
const generateRoute = (node: any): any => {
const route: any = {
path: node.path,
name: node.name,
component: modules[`..${node.component}.vue`], // 重点
redirect: node.redirect || '',
meta: {
title: node.title || '',
icon: node.icon || ''
}
}
// 如果有子节点,递归生成子路由
if (node.children && node.children.length > 0) {
route.children = []
for (let i = 0; i < node.children.length; i++) {
const childRoute = generateRoute(node.children[i])
route.children.push(childRoute)
}
if (route.children.length === 0) {
delete route.children
}
}
return route
}
// 遍历树形结构的根节点,生成路由
for (const node of tree) {
result.push(generateRoute(node))
}
return result
}
const userInfo = ref<any>({})
const { data = {}} = await reqUserInfo() // 调用接口 返回可访问的资源信息
// 根据类型分离菜单和按钮
const filterMenus = (type: string) => allMenus.filter((menu: any) => menu.type === type)
userInfo.value.menus = filterMenus('menu')
userInfo.value.buttons = filterMenus('button')
// 调用上面的函数生成路由树
userInfo.value.routesTree = treeToRoutes(buildTree(cloneDeep(userInfo.value.menus)))
然后使用addRoute函数异步添加路由。
userInfo.value.routesTree.forEach((route: any) => {
router.addRoute(route)
})
// 在添加完异步路由后添加404路由
router.addRoute({
// 任意路由
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any',
meta: {
title: '任意路由'
}
})
最后需要调整前置路由守卫,目的是确保等待所有的异步路由加载完毕后再进入页面,否则会出现刷新白屏的情况。
const allowPaths = ['/login', '/login/auth']
router.beforeEach(async (to, _from, next) => {
nprogress.start()
const userStore = useUserStore(pinia)
if (userStore.token) { // 用户已登录
if (allowPaths.includes(to.path)) {
next({ path: '/' })
} else {
if (!userStore.userInfo) { // 刷新后仓库丢失用户信息 需要重新获取
await userStore.getUserInfo()
// next()
next({ ...to }) // 解决刷新白屏问题 等待组件渲染完毕再放行
} else {
next()
}
}
} else { // 用户未登录
if (allowPaths.includes(to.path)) {
next()
} else {
next({ path: '/login', query: { redirect: encodeURIComponent(to.fullPath) }})
}
}
})
2.菜单权限控制
基于elment-plus菜单组件封装递归菜单组件。
<template>
<template v-for="item in params" :key="item.name">
<el-sub-menu :index="item.path" v-if="item.children && item.children.length>0"> <!-- 有子级菜单 -->
<template #title>
<el-icon>
<component :is="item.meta?.icon"></component>
</el-icon>
<span>{{ item.meta?.title }}</span>
</template>
<MenuItem :params="item.children"></MenuItem> <!-- 递归调用组件自身,注意组件名 -->
</el-sub-menu>
<el-menu-item :index="item.path" v-else> <!-- 无子级菜单 -->
<template #title>
<el-icon>
<component :is="item.meta?.icon"></component>
</el-icon>
<span>{{ item.meta?.title }}</span>
</template>
</el-menu-item>
</template>
</template>
<script lang="ts" setup>
import type { RouteRecordRaw } from 'vue-router'
defineOptions({
name: 'MenuItem'
})
const { params = [] } = defineProps<{
params:RouteRecordRaw[]
}>()
</script>
父级组件调用
<template>
<el-container class="mx-aside">
<el-scrollbar>
<el-menu router :default-active="$route.path" active-text-color="#ffd04b" background-color="#324157"
class="el-menu-vertical-demo" text-color="#fff">
<el-menu-item index="/home">
<el-icon class="icon">
<HomeFilled />
</el-icon>
<template #title>
<span>首页</span>
</template>
</el-menu-item>
<MenuItem :params="userStore.userInfo.routesTree"></MenuItem>
</el-menu>
</el-scrollbar>
</el-container>
</template>
<script lang="ts" setup>
import { useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import MenuItem from './MxMenu.vue'
const $route = useRoute()
const userStore = useUserStore()
</script>
3.按钮权限控制
按钮权限主要通过自定义指令,将按钮标识名与按钮权限数组做对比实现。
import type { Directive } from 'vue'
import { useUserStore } from '@/stores/user'
import { useConfigStore } from '@/stores/config'
// 判断按钮权限 无权限删除元素
const permission: Directive = {
mounted (el: HTMLElement, { value = '' }) {
if (value === '') {
return
}
const userStore = useUserStore()
const configStore = useConfigStore()
const buttonStyle = configStore.config?.web.mxButtonStyle // 无权限时按钮的显示状态
if (buttonStyle === 1) return // 显示
const index = userStore.userInfo.buttons.findIndex((button: any) => button.name === value)
if (index === -1) {
if (buttonStyle === 2) { // 隐藏
el.remove()
} else if (buttonStyle === 3) { // 禁用
el.classList.add('is-disabled')
el.setAttribute('disabled', 'true') // 设置 disabled 属性
}
}
}
}
export default permission
自定义指令使用示例
<el-button type="danger" @click="deleteData" icon="Delete" v-permission="'delete_/user'">删除</el-button>






文章有(8)条网友点评
viagra medicine cost
viagra medicine cost
revatio vs sildenafil
revatio vs sildenafil
dutasteride for women
dutasteride for women
var finns semaglutid naturligt
var finns semaglutid naturligt
semaglutida comprimido preço pague menos
semaglutida comprimido preço pague menos
kan man köpa semaglutid receptfritt
kan man köpa semaglutid receptfritt
propecia coupon
propecia coupon
doxycycline hyclate cause fatigue
doxycycline hyclate cause fatigue