feat(rbac): 路由级权限过滤 + 403 页

- 路由守卫按可访问菜单 path 校验,无权跳转 /403
- 新增 403 页

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 16:22:19 +08:00
parent 2f7f83b8b1
commit 84edbc67c0
3 changed files with 76 additions and 3 deletions

25
src/pages/403.vue Normal file
View File

@ -0,0 +1,25 @@
<template>
<div class="error-page">
<el-result icon="warning" title="403" sub-title="抱歉,你没有权限访问该页面">
<template #extra>
<el-button type="primary" @click="goHome">回到首页</el-button>
</template>
</el-result>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
function goHome(): void {
router.push('/')
}
</script>
<style scoped lang="scss">
.error-page {
@apply flex h-full items-center justify-center;
}
</style>

View File

@ -9,6 +9,7 @@ import { routes } from 'vue-router/auto-routes'
import { setupLayouts } from 'virtual:generated-layouts'
import i18n, { loadLanguage } from '@/i18n'
import { getAccessToken } from '@/utils/token'
import { useMenuStore } from '@/stores/modules/menu'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -39,15 +40,49 @@ async function loadRouteLocales(to: RouteLocationNormalized) {
}
}
// 无需菜单权限校验的路径(首页 + 错误页)
const FREE_PATHS = new Set(['/', '/403'])
/** 收集菜单树中所有可访问 path。 */
function collectMenuPaths(nodes: { path?: string | null; children?: unknown[] }[]): string[] {
const paths: string[] = []
for (const n of nodes) {
if (n.path) paths.push(n.path)
if (n.children?.length) paths.push(...collectMenuPaths(n.children as { path?: string | null; children?: unknown[] }[]))
}
return paths
}
// 路由前置守卫
router.beforeEach(async (to, from, next) => {
await loadRouteLocales(to)
// 未标记 requiresAuth: false 的路由默认需要登录
const requiresAuth = to.meta.requiresAuth !== false
if (requiresAuth && !getAccessToken()) {
next('/login')
} else {
if (!requiresAuth) {
next()
return
}
if (!getAccessToken()) {
next('/login')
return
}
// 路由级权限:自由路径放行,其余须在可访问菜单中
if (FREE_PATHS.has(to.path)) {
next()
return
}
const menuStore = useMenuStore()
try {
await menuStore.fetch()
} catch {
next('/login')
return
}
const accessible = new Set(collectMenuPaths(menuStore.menus))
if (accessible.has(to.path)) {
next()
} else {
next('/403')
}
})

13
typed-router.d.ts vendored
View File

@ -37,6 +37,13 @@ declare module 'vue-router/auto-routes' {
{ all: ParamValue<false> },
| never
>,
'/403': RouteRecordInfo<
'/403',
'/403',
Record<never, never>,
Record<never, never>,
| never
>,
'/demo': RouteRecordInfo<
'/demo',
'/demo',
@ -174,6 +181,12 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'src/pages/403.vue': {
routes:
| '/403'
views:
| never
}
'src/pages/demo.vue': {
routes:
| '/demo'