feat: 用户权限变更后,自动更新导航菜单和页面权限,无需刷新页面或者重新登录

重构了路由注册的逻辑,无权限的路由也会被注册,在访问页面时候进行权限控制
This commit is contained in:
Hooray 2023-03-05 18:05:44 +08:00
parent d12e9794f3
commit df9fd2ae65
6 changed files with 64 additions and 121 deletions

View File

@ -6,7 +6,10 @@ import hotkeys from 'hotkeys-js'
import eventBus from './utils/eventBus'
import useSettingsStore from '@/store/modules/settings'
const route = useRoute()
const settingsStore = useSettingsStore()
const { auth } = useAuth()
const buttonConfig = ref({
autoInsertSpace: true,
@ -62,11 +65,13 @@ import.meta.env.VITE_APP_DEBUG_TOOL === 'vconsole' && new VConsole()
<template>
<el-config-provider :locale="zhCn" :size="settingsStore.settings.app.elementSize" :button="buttonConfig">
<RouterView
v-if="auth(route.meta.auth ?? '')"
:style="{
'--g-main-sidebar-actual-width': mainSidebarActualWidth,
'--g-sub-sidebar-actual-width': subSidebarActualWidth,
}"
/>
<not-allowed v-else />
<system-info />
</el-config-provider>
</template>

View File

@ -1,8 +1,3 @@
<route lang="yaml">
meta:
enabled: false
</route>
<script lang="ts" setup>
const router = useRouter()
@ -11,15 +6,15 @@ const data = ref({
countdown: 5,
})
onBeforeRouteLeave(() => {
data.value.inter && clearInterval(data.value.inter)
onUnmounted(() => {
data.value.inter && window.clearInterval(data.value.inter)
})
onMounted(() => {
data.value.inter = setInterval(() => {
data.value.inter = window.setInterval(() => {
data.value.countdown--
if (data.value.countdown === 0) {
data.value.inter && clearInterval(data.value.inter)
data.value.inter && window.clearInterval(data.value.inter)
goBack()
}
}, 1000)

View File

@ -90,11 +90,6 @@ router.beforeEach(async (to, from, next) => {
removeRoutes.push(router.addRoute(route as RouteRecordRaw))
})
}
routeStore.flatRoutesUnallowed.forEach((route) => {
if (!/^(https?:|mailto:|tel:)/.test(route.path)) {
removeRoutes.push(router.addRoute(route as RouteRecordRaw))
}
})
routeStore.setCurrentRemoveRoutes(removeRoutes)
// 动态路由生成并注册后,重新进入当前路由
next({

View File

@ -1,5 +1,5 @@
import { cloneDeep } from 'lodash-es'
import type { RouteRecordRaw } from 'vue-router'
import type { RouteMeta, RouteRecordRaw } from 'vue-router'
import useSettingsStore from './settings'
import useUserStore from './user'
import api from '@/api'
@ -15,27 +15,27 @@ const useRouteStore = defineStore(
const userStore = useUserStore()
const isGenerate = ref(false)
const routes = ref<Route.recordMainRaw[]>([])
const routesUnallowed = ref<Route.recordMainRaw[]>([])
const fileSystemRoutes = ref<RouteRecordRaw[]>([])
const routesRaw = ref<Route.recordMainRaw[]>([])
const filesystemRoutesRaw = ref<RouteRecordRaw[]>([])
const currentRemoveRoutes = ref<Function[]>([])
// 将多层嵌套路由处理成两层,保留顶层和最子层路由,中间层级将被拍平
function flatAsyncRoutes<T extends RouteRecordRaw>(routes: T): T {
if (routes.children) {
routes.children = flatAsyncRoutesRecursive(routes.children, [{
path: routes.path,
title: routes.meta?.title,
hide: !routes.meta?.breadcrumb && routes.meta?.breadcrumb === false,
}], routes.path)
function flatAsyncRoutes<T extends RouteRecordRaw>(route: T): T {
if (route.children) {
route.children = flatAsyncRoutesRecursive(route.children, [{
path: route.path,
title: route.meta?.title,
hide: !route.meta?.breadcrumb && route.meta?.breadcrumb === false,
}], route.path, route.meta?.auth)
}
return routes
return route
}
function flatAsyncRoutesRecursive(routes: RouteRecordRaw[], breadcrumb: Route.breadcrumb[] = [], baseUrl = ''): RouteRecordRaw[] {
function flatAsyncRoutesRecursive(routes: RouteRecordRaw[], breadcrumb: Route.breadcrumb[] = [], baseUrl = '', baseAuth: RouteMeta['auth']): RouteRecordRaw[] {
const res: RouteRecordRaw[] = []
routes.forEach((route) => {
if (route.children) {
const childrenBaseUrl = resolveRoutePath(baseUrl, route.path)
const childrenBaseAuth = baseAuth ?? route.meta?.auth
const tmpBreadcrumb = cloneDeep(breadcrumb)
tmpBreadcrumb.push({
path: childrenBaseUrl,
@ -47,10 +47,11 @@ const useRouteStore = defineStore(
if (!tmpRoute.meta) {
tmpRoute.meta = {}
}
tmpRoute.meta.auth = childrenBaseAuth
tmpRoute.meta.breadcrumbNeste = tmpBreadcrumb
delete tmpRoute.children
res.push(tmpRoute)
const childrenRoutes = flatAsyncRoutesRecursive(route.children, tmpBreadcrumb, childrenBaseUrl)
const childrenRoutes = flatAsyncRoutesRecursive(route.children, tmpBreadcrumb, childrenBaseUrl, childrenBaseAuth)
childrenRoutes.forEach((item) => {
// 如果 path 一样则覆盖,因为子路由的 path 可能设置为空,导致和父路由一样,直接注册会提示路由重复
if (res.some(v => v.path === item.path)) {
@ -78,6 +79,7 @@ const useRouteStore = defineStore(
if (!tmpRoute.meta) {
tmpRoute.meta = {}
}
tmpRoute.meta.auth = baseAuth ?? tmpRoute.meta?.auth
tmpRoute.meta.breadcrumbNeste = tmpBreadcrumb
res.push(tmpRoute)
}
@ -88,45 +90,27 @@ const useRouteStore = defineStore(
const flatRoutes = computed(() => {
const settingsStore = useSettingsStore()
const returnRoutes: RouteRecordRaw[] = []
if (routes.value) {
if (routesRaw.value) {
if (settingsStore.settings.app.routeBaseOn !== 'filesystem') {
routes.value.forEach((item) => {
returnRoutes.push(...cloneDeep(item.children))
routesRaw.value.forEach((item) => {
const tmpRoutes = cloneDeep(item.children) as RouteRecordRaw[]
tmpRoutes.map((v) => {
if (!v.meta) {
v.meta = {}
}
v.meta.auth = item.meta?.auth ?? v.meta?.auth
return v
})
returnRoutes.push(...tmpRoutes)
})
returnRoutes.forEach(item => flatAsyncRoutes(item))
}
else {
returnRoutes.push(...cloneDeep(fileSystemRoutes.value))
returnRoutes.push(...cloneDeep(filesystemRoutesRaw.value))
}
}
return returnRoutes
})
const flatRoutesUnallowed = computed(() => {
const twoLevelRoutes: RouteRecordRaw[] = []
if (settingsStore.settings.app.routeBaseOn !== 'filesystem') {
if (routesUnallowed.value) {
routesUnallowed.value.forEach((item) => {
twoLevelRoutes.push(...cloneDeep(item.children) as RouteRecordRaw[])
})
twoLevelRoutes.forEach(item => flatAsyncRoutes(item))
}
}
else {
twoLevelRoutes.push(...cloneDeep(routesUnallowed.value) as RouteRecordRaw[])
}
const returnRoutes: RouteRecordRaw[] = []
twoLevelRoutes.forEach((item) => {
if (item.children) {
item.children.forEach((child) => {
const tmpChild = cloneDeep(child)
tmpChild.path = resolveRoutePath(item.path, tmpChild.path)
tmpChild.component = () => import('@/views/403.vue')
returnRoutes.push(tmpChild)
})
}
})
return returnRoutes
})
const flatSystemRoutes = computed(() => {
const routes = [...systemRoutes]
routes.forEach(item => flatAsyncRoutes(item))
@ -171,40 +155,26 @@ const useRouteStore = defineStore(
})
return res
}
// 根据权限过滤路由(过滤出没有权限的路由)
function filterAsyncRoutesNoAuth<T extends Route.recordMainRaw[] | RouteRecordRaw[]>(routes: T, permissions: string[]): T {
const res: any = []
routes.forEach((route) => {
const tmpRoute = cloneDeep(route)
if (hasPermission(permissions, route)) {
if (tmpRoute.children) {
tmpRoute.children = filterAsyncRoutesNoAuth(tmpRoute.children, permissions)
tmpRoute.children.length && res.push(tmpRoute)
}
}
else {
res.push(tmpRoute)
}
})
return res
}
// 根据权限动态生成路由(前端生成)
async function generateRoutesAtFront(asyncRoutes: Route.recordMainRaw[]) {
let allowedRoutes
let unallowedRoutes: Route.recordMainRaw[] = []
const routes = computed(() => {
let returnRoutes: Route.recordMainRaw[]
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.settings.app.enablePermission) {
const permissions = await userStore.getPermissions()
allowedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
unallowedRoutes = filterAsyncRoutesNoAuth(asyncRoutes, permissions)
returnRoutes = filterAsyncRoutes(routesRaw.value as any, userStore.permissions)
}
else {
allowedRoutes = cloneDeep(asyncRoutes)
returnRoutes = cloneDeep(routesRaw.value) as any
}
return returnRoutes
})
// 根据权限动态生成路由(前端生成)
async function generateRoutesAtFront(asyncRoutes: Route.recordMainRaw[]) {
// 设置 routes 数据
routesRaw.value = cloneDeep(asyncRoutes) as any
if (settingsStore.settings.app.enablePermission) {
await userStore.getPermissions()
}
isGenerate.value = true
routes.value = allowedRoutes.filter(item => item.children?.length !== 0) as any
routesUnallowed.value = unallowedRoutes as any
}
// 格式化后端路由数据
function formatBackRoutes(routes: any, views = import.meta.glob('../../views/**/*.vue')): Route.recordMainRaw[] {
@ -229,41 +199,23 @@ const useRouteStore = defineStore(
await api.get('route/list', {
baseURL: '/mock/',
}).then(async (res) => {
const asyncRoutes = formatBackRoutes(res.data)
let allowedRoutes
let unallowedRoutes: Route.recordMainRaw[] = []
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.settings.app.enablePermission) {
const permissions = await userStore.getPermissions()
allowedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
unallowedRoutes = filterAsyncRoutesNoAuth(asyncRoutes, permissions)
}
else {
allowedRoutes = cloneDeep(asyncRoutes)
}
// 设置 routes 数据
routesRaw.value = formatBackRoutes(res.data)
if (settingsStore.settings.app.enablePermission) {
userStore.getPermissions()
}
isGenerate.value = true
routes.value = allowedRoutes.filter(item => item.children.length !== 0) as any
routesUnallowed.value = unallowedRoutes as any
}).catch(() => {})
}
// 根据权限动态生成路由(文件系统生成)
async function generateRoutesAtFilesystem(asyncRoutes: RouteRecordRaw[]) {
let allowedRoutes
let unallowedRoutes: RouteRecordRaw[] = []
// 设置 routes 数据
filesystemRoutesRaw.value = cloneDeep(asyncRoutes) as any
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.settings.app.enablePermission) {
const permissions = await userStore.getPermissions()
allowedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
unallowedRoutes = filterAsyncRoutesNoAuth(asyncRoutes, permissions)
await userStore.getPermissions()
}
else {
allowedRoutes = cloneDeep(asyncRoutes)
}
// 设置 routes 数据
isGenerate.value = true
fileSystemRoutes.value = allowedRoutes.filter(item => item.children?.length !== 0) as any
routesUnallowed.value = unallowedRoutes as any
}
// 记录 accessRoutes 路由,用于登出时删除路由
function setCurrentRemoveRoutes(routes: Function[]) {
@ -272,7 +224,8 @@ const useRouteStore = defineStore(
// 清空动态路由
function removeRoutes() {
isGenerate.value = false
routes.value = []
routesRaw.value = []
filesystemRoutesRaw.value = []
currentRemoveRoutes.value.forEach((removeRoute) => {
removeRoute()
})
@ -282,9 +235,8 @@ const useRouteStore = defineStore(
return {
isGenerate,
routes,
fileSystemRoutes,
currentRemoveRoutes,
flatRoutes,
flatRoutesUnallowed,
flatSystemRoutes,
generateRoutesAtFront,
generateRoutesAtBack,

View File

@ -19,6 +19,7 @@ declare module '@vue/runtime-core' {
ImagePreview: typeof import('./../components/ImagePreview/index.vue')['default']
ImagesUpload: typeof import('./../components/ImagesUpload/index.vue')['default']
ImageUpload: typeof import('./../components/ImageUpload/index.vue')['default']
NotAllowed: typeof import('./../components/NotAllowed/index.vue')['default']
PageHeader: typeof import('./../components/PageHeader/index.vue')['default']
PageMain: typeof import('./../components/PageMain/index.vue')['default']
PcasCascader: typeof import('./../components/PcasCascader/index.vue')['default']

View File

@ -16,21 +16,16 @@ export default function useAuth() {
function auth(value: string | string[]) {
let auth
if (typeof value === 'string') {
auth = hasPermission(value)
auth = value !== '' ? hasPermission(value) : true
}
else {
auth = value.some((item) => {
return hasPermission(item)
})
auth = value.length > 0 ? value.some(item => hasPermission(item)) : true
}
return auth
}
function authAll(value: string[]) {
const auth = value.every((item) => {
return hasPermission(item)
})
return auth
return value.length > 0 ? value.every(item => hasPermission(item)) : true
}
return {