ec-admin/src/router/index.ts
liuyun 5de298a956 feat(auth): 首登强制改密页面 + 创建用户开关
- 独立改密页 + 路由守卫拦截待改密
- auth store 持久化 mustChangePassword
- 用户创建表单加「首登强制改密」开关(默认勾选)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 21:54:41 +08:00

106 lines
2.9 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'
import { useAuthStore } from '@/stores/modules/auth'
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
}
// 首登强制改密:待改密用户仅放行改密页,其余路由重定向
const auth = useAuthStore()
if (auth.mustChangePassword && to.path !== '/force-change-password') {
next('/force-change-password')
return
}
// 路由级权限:自由路径或 meta.freeAccess 放行,其余须在可访问菜单中
if (FREE_PATHS.has(to.path) || to.meta.freeAccess === true) {
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