vue3实现权限管理

页面权限

思路

  • 用户可以访问的页面权限都放在了用户信息userInfo/permission/menu
  • 左侧菜单是根据routes自动生成的
  • 为每个权限路由创建一个name属性,每个name对应一个页面权限
  • router/index.js定义私有路由表privateRoutes和公开路由表publicRoutes
  • store/modules目录下新建一个permission文件,用来创建权限相关的操作
  • 通过filterRoutes这个动作来根据name与页面权限相匹配的方式筛选出权限路由,并返回出去
  • src/permission.js中我们通过router.beforeach拦截路由,获取用户信息,获取筛选后的私有路由表,利用router.addRoute来动态添加路由表
  • 退出用户登录的时候记得删除路由表,也就是用removeRoute这个方法

实现

获取用户信息

页面权限数据在用户个人信息中,有一个permission字段,里面有一个menu字段。这是一个数组,数组的每一项是当前用户可以访问的页面name属性。

1
const menu = ['userManage', 'roleList', 'permissionList', 'articleRanking', 'articleCreate']

私有路由表不再被直接添加到routes

1
2
3
4
5
6
7
8
9
export const publicRoutes = []
export const privateRoutes = []

const router = createRouter({
history: createWebHashHistory(),
routes: publicRoutes
})

export default router

通过route.addRoutes()动态添加路由到路由表中

通过筛选生成权限路由表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/store/modules/permission.js
import { publicRoutes, privateRoutes } from '@/router/index'

export default {
namespaced: true,
state: {
routes: publicRoutes
},
mutations: {
setRoutes(state, newRoutes) {
state.routes = [...publicRoutes, ...newRoutes]
}
},
actions: {
filterRoutes(context, menus) {
const routes = []
menus.forEach(key => {
routes.push(...privateRoutes.filter(item => item.name === key))
})
routes.push({
path: '/:catchAll(.*)',
redirect: '/404'
})
context.commit('setRoutes', routes)
return routes
}
}
}

引入permission模块

1
2
3
4
5
6
7
8
9
import { createStore } from 'vuex'
import permission from './modules/permission'

export default createStore({
...
modules: {
permission
},
})

路由拦截获取用户数据调用filterRoutes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import router from './router'
import store from './store'

const whiteList = ['/login']

router.beforeEach(async (to, from, next) => {
if (store.getters.token) {
if (to.path === '/login') {
next('/')
} else {
if (!store.getters.hasUserInfo) {
// 获取用户信息
const { permission } = await store.dispatch('user/getUserInfo')
// 根据当前用户的权限筛选生成对应的路由表
const filterRoutes = await store.dispatch('permission/filterRoutes', permission.menus)
// 循环便利添加路由
filterRoutes.forEach(item => {
router.addRoute(item)
})
// 释放next钩子进行一次跳转
return next(to.path)
}
next()
}
} else {
if (whiteList.indexOf(to.path) > -1) {
next()
} else {
next('/login')
}
}
})

重置路由表数据

1
2
3
4
5
6
7
8
9
// src/router/index.js
export function resetRouter() {
if (store.getters.userInfo && store.getters.userInfo.permission && store.getters.userInfo.permission.menus) {
const menus = store.getters.userInfo.permission.menus
menus.forEach(menu => {
router.removeRoute(menu)
})
}
}
1
2
3
4
5
6
7
8
9
// src/store/modules/user.js
import { resetRouter } from '@/router/index'
export default {
actions: {
logout(context) {
resetRouter()
}
}
}

左侧菜单的生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<!-- 一级 menu 菜单 -->
<el-menu
:default-active="activeMenu"
:collapse="!$store.getters.sidebarOpened"
:background-color="$store.getters.cssVar.menuBg"
:text-color="$store.getters.cssVar.menuText"
:active-text-color="$store.getters.cssVar.menuActiveText"
:unique-opened="true"
router
>
<sidebar-item
v-for="item in routes"
:key="item.path"
:route="item"
></sidebar-item>
</el-menu>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { computed } from 'vue'
import SidebarItem from './SidebarItem'
import { useRouter, useRoute } from 'vue-router'
import { filterRouters, generateMenus } from '@/utils/route'
// 计算路由表结构
const router = useRouter()
const routes = computed(() => {
const filterRoutes = filterRouters(router.getRoutes())
return generateMenus(filterRoutes)
})
// 计算高亮 menu 的方法
const route = useRoute()
const activeMenu = computed(() => {
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
})

SidebarItem组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<el-sub-menu v-if="route.children.length > 0" :index="route.path">
<template #title>
<menu-item :title="route.meta.title" :icon="route.meta.icon"></menu-item>
</template>
<sidebar-item
v-for="item in route.children"
:key="item.path"
:route="item"
></sidebar-item>
</el-sub-menu>
<el-menu-item v-else :index="route.path">
<menu-item :title="route.meta.title" :icon="route.meta.icon"></menu-item>
</el-menu-item>
</template>

<script setup>
import MenuItem from './MenuItem.vue'
import { defineProps } from 'vue'
defineProps({
route: {
type: Object,
required: true
}
})
</script>

功能权限