- 路由守卫按可访问菜单 path 校验,无权跳转 /403 - 新增 403 页 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
99 lines
2.6 KiB
TypeScript
99 lines
2.6 KiB
TypeScript
import {
|
|
createRouter,
|
|
createWebHistory,
|
|
type RouteLocationNormalized,
|
|
type RouteRecordRaw,
|
|
} from 'vue-router'
|
|
import type { App } from 'vue'
|
|
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),
|
|
// routes
|
|
routes: setupLayouts(routes as RouteRecordRaw[]),
|
|
})
|
|
|
|
async function loadRouteLocales(to: RouteLocationNormalized) {
|
|
const currentLang = localStorage.getItem('locale') || 'zh-CN'
|
|
|
|
// 确保通用语言已加载
|
|
await loadLanguage(currentLang)
|
|
|
|
// 加载路由所需的语言模块
|
|
if (to.meta.locales) {
|
|
for (const locale of to.meta.locales as string[]) {
|
|
try {
|
|
await import(`@/i18n/locales/modules/${currentLang}/${locale}.ts`).then((messages) => {
|
|
// 合并到当前语言的消息中
|
|
i18n.global.mergeLocaleMessage(currentLang, {
|
|
[locale]: messages.default,
|
|
})
|
|
})
|
|
} catch (e) {
|
|
console.warn(`Failed to load locale module: ${locale}`, e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 无需菜单权限校验的路径(首页 + 错误页)
|
|
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) {
|
|
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')
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 安装路由
|
|
* @param app Vue 应用实例
|
|
* 采用模块化方法注册路由,提高代码可维护性
|
|
*/
|
|
export const installRouter = (app: App) => {
|
|
app.use(router)
|
|
}
|
|
|
|
export default router
|