2026-01-09 18:14:35 +08:00
|
|
|
|
import { createRouter, createWebHistory } from "vue-router"
|
|
|
|
|
|
import type { RouteRecordRaw } from "vue-router"
|
|
|
|
|
|
import { nextTick } from "vue"
|
|
|
|
|
|
import { useAuthStore } from "@/stores/auth"
|
|
|
|
|
|
import { convertMenusToRoutes } from "@/utils/menu"
|
|
|
|
|
|
import "@/types/router"
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 基础路由(不需要动态加载的)
|
|
|
|
|
|
const baseRoutes: RouteRecordRaw[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "/:tenantCode/login",
|
|
|
|
|
|
name: "Login",
|
|
|
|
|
|
component: () => import("@/views/auth/Login.vue"),
|
|
|
|
|
|
meta: { requiresAuth: false },
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "/login",
|
|
|
|
|
|
name: "LoginFallback",
|
|
|
|
|
|
component: () => import("@/views/auth/Login.vue"),
|
|
|
|
|
|
meta: { requiresAuth: false },
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "/:tenantCode",
|
|
|
|
|
|
name: "Main",
|
|
|
|
|
|
component: () => import("@/layouts/BasicLayout.vue"),
|
2026-01-13 14:01:17 +08:00
|
|
|
|
// 不设置固定redirect,由路由守卫根据用户菜单动态跳转到第一个可见菜单
|
2025-11-23 14:04:20 +08:00
|
|
|
|
meta: {},
|
|
|
|
|
|
children: [
|
2026-01-08 09:17:46 +08:00
|
|
|
|
// 创建比赛路由(不需要在菜单中显示)
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "contests/create",
|
|
|
|
|
|
name: "ContestsCreate",
|
|
|
|
|
|
component: () => import("@/views/contests/Create.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "创建比赛",
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
permissions: ["contest:create"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
// 赛事详情路由(不需要在菜单中显示)
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "contests/:id",
|
|
|
|
|
|
name: "ContestsDetail",
|
|
|
|
|
|
component: () => import("@/views/contests/Detail.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "赛事详情",
|
|
|
|
|
|
requiresAuth: true,
|
2026-01-12 20:04:11 +08:00
|
|
|
|
permissions: ["contest:read", "activity:read"],
|
2026-01-08 09:17:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
// 编辑比赛路由(不需要在菜单中显示)
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "contests/:id/edit",
|
|
|
|
|
|
name: "ContestsEdit",
|
|
|
|
|
|
component: () => import("@/views/contests/Create.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "编辑比赛",
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
permissions: ["contest:update"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
// 赛事评委管理路由(不需要在菜单中显示)
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "contests/:id/judges",
|
|
|
|
|
|
name: "ContestsJudges",
|
|
|
|
|
|
component: () => import("@/views/contests/judges/Index.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "评委管理",
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
permissions: ["contest:read"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-09 18:14:35 +08:00
|
|
|
|
// 个人赛报名路由
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "contests/:id/register/individual",
|
|
|
|
|
|
name: "ContestsRegisterIndividual",
|
|
|
|
|
|
component: () => import("@/views/contests/RegisterIndividual.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "赛事报名(个人赛)",
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
// 团队赛报名路由
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "contests/:id/register/team",
|
|
|
|
|
|
name: "ContestsRegisterTeam",
|
|
|
|
|
|
component: () => import("@/views/contests/RegisterTeam.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "赛事报名(团队赛)",
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-13 16:41:12 +08:00
|
|
|
|
// 报名管理列表路由
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "contests/registrations",
|
|
|
|
|
|
name: "ContestsRegistrations",
|
|
|
|
|
|
component: () => import("@/views/contests/registrations/Index.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "报名管理",
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
permissions: ["registration:read", "contest:read"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-09 18:14:35 +08:00
|
|
|
|
// 报名记录路由
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "contests/registrations/:id/records",
|
|
|
|
|
|
name: "RegistrationRecords",
|
|
|
|
|
|
component: () => import("@/views/contests/registrations/Records.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "报名记录",
|
|
|
|
|
|
requiresAuth: true,
|
2026-01-13 16:41:12 +08:00
|
|
|
|
permissions: ["registration:read", "contest:read"],
|
2026-01-09 18:14:35 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-12 16:06:34 +08:00
|
|
|
|
// 评审进度详情路由
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "contests/reviews/:id/progress",
|
|
|
|
|
|
name: "ReviewProgressDetail",
|
|
|
|
|
|
component: () => import("@/views/contests/reviews/ProgressDetail.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "评审进度详情",
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
permissions: ["review:read"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
// 参赛作品详情列表路由
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "contests/works/:id/list",
|
|
|
|
|
|
name: "WorksDetail",
|
|
|
|
|
|
component: () => import("@/views/contests/works/WorksDetail.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "参赛作品详情",
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
permissions: ["work:read"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-09 18:14:35 +08:00
|
|
|
|
// 作业提交记录路由
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "homework/submissions",
|
|
|
|
|
|
name: "HomeworkSubmissions",
|
|
|
|
|
|
component: () => import("@/views/homework/Submissions.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "作业提交记录",
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
permissions: ["homework:read"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
// 学生作业详情路由
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "homework/detail/:id",
|
|
|
|
|
|
name: "HomeworkDetail",
|
|
|
|
|
|
component: () => import("@/views/homework/StudentDetail.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "作业详情",
|
|
|
|
|
|
requiresAuth: true,
|
2026-01-13 16:41:12 +08:00
|
|
|
|
permissions: ["homework:read"],
|
2026-01-09 18:14:35 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-12 20:04:11 +08:00
|
|
|
|
// 教师我的指导路由
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "student-activities/guidance",
|
|
|
|
|
|
name: "TeacherGuidance",
|
|
|
|
|
|
component: () => import("@/views/contests/Guidance.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "我的指导",
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
permissions: ["activity:read"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-13 14:01:17 +08:00
|
|
|
|
// 3D建模实验室路由(工作台模块下)
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "workbench/3d-lab",
|
|
|
|
|
|
name: "3DModelingLab",
|
|
|
|
|
|
component: () => import("@/views/workbench/ai-3d/Index.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "3D建模实验室",
|
|
|
|
|
|
requiresAuth: true,
|
2026-01-14 14:29:16 +08:00
|
|
|
|
hideSidebar: true,
|
2026-01-13 16:41:12 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
// 3D模型生成页面
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "workbench/3d-lab/generate/:taskId",
|
|
|
|
|
|
name: "AI3DGenerate",
|
|
|
|
|
|
component: () => import("@/views/workbench/ai-3d/Generate.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "3D模型生成",
|
|
|
|
|
|
requiresAuth: true,
|
2026-01-14 14:29:16 +08:00
|
|
|
|
hideSidebar: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
// 3D创作历史页面
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "workbench/3d-lab/history",
|
|
|
|
|
|
name: "AI3DHistory",
|
|
|
|
|
|
component: () => import("@/views/workbench/ai-3d/History.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "创作历史",
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
hideSidebar: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
// 3D模型预览页面
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "workbench/model-viewer",
|
|
|
|
|
|
name: "ModelViewer",
|
|
|
|
|
|
component: () => import("@/views/model/ModelViewer.vue"),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: "3D模型预览",
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
hideSidebar: true,
|
2026-01-13 14:01:17 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-11-23 14:04:20 +08:00
|
|
|
|
// 动态路由将在这里添加
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "/403",
|
|
|
|
|
|
name: "Forbidden",
|
|
|
|
|
|
component: () => import("@/views/error/403.vue"),
|
|
|
|
|
|
meta: { requiresAuth: false },
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: "/:pathMatch(.*)*",
|
|
|
|
|
|
name: "NotFound",
|
|
|
|
|
|
component: () => import("@/views/error/404.vue"),
|
|
|
|
|
|
},
|
2026-01-09 18:14:35 +08:00
|
|
|
|
]
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
const router = createRouter({
|
|
|
|
|
|
history: createWebHistory(),
|
|
|
|
|
|
routes: baseRoutes,
|
2026-01-09 18:14:35 +08:00
|
|
|
|
})
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 标记是否已经添加了动态路由
|
2026-01-09 18:14:35 +08:00
|
|
|
|
let dynamicRoutesAdded = false
|
|
|
|
|
|
// 保存已添加的路由名称,用于清理
|
|
|
|
|
|
let addedRouteNames: string[] = []
|
|
|
|
|
|
// 保存上次的菜单数据,用于检测变化
|
|
|
|
|
|
let lastMenusHash: string = ""
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重置动态路由标记(用于登出或切换用户时)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function resetDynamicRoutes(): void {
|
|
|
|
|
|
dynamicRoutesAdded = false
|
|
|
|
|
|
addedRouteNames = []
|
|
|
|
|
|
lastMenusHash = ""
|
|
|
|
|
|
}
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 添加动态路由
|
|
|
|
|
|
* @returns Promise,当路由添加完成并生效后 resolve
|
|
|
|
|
|
*/
|
2026-01-09 18:14:35 +08:00
|
|
|
|
export async function addDynamicRoutes(): Promise<void> {
|
|
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
|
|
if (!authStore.menus || authStore.menus.length === 0) {
|
|
|
|
|
|
// 如果没有菜单,重置标记
|
|
|
|
|
|
if (dynamicRoutesAdded) {
|
|
|
|
|
|
resetDynamicRoutes()
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算菜单数据的哈希值,用于检测变化
|
|
|
|
|
|
const menusHash = JSON.stringify(
|
|
|
|
|
|
authStore.menus.map((m) => ({ id: m.id, path: m.path }))
|
|
|
|
|
|
)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
2026-01-09 18:14:35 +08:00
|
|
|
|
// 如果菜单数据没有变化且已经添加过路由,直接返回
|
|
|
|
|
|
if (dynamicRoutesAdded && menusHash === lastMenusHash) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已经添加过路由,先移除旧路由
|
|
|
|
|
|
if (dynamicRoutesAdded && addedRouteNames.length > 0) {
|
|
|
|
|
|
addedRouteNames.forEach((routeName) => {
|
|
|
|
|
|
if (router.hasRoute(routeName)) {
|
|
|
|
|
|
router.removeRoute(routeName)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
addedRouteNames = []
|
|
|
|
|
|
}
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 将菜单转换为路由
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const dynamicRoutes = convertMenusToRoutes(authStore.menus)
|
|
|
|
|
|
|
|
|
|
|
|
if (dynamicRoutes.length === 0) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加动态路由到根路由下
|
|
|
|
|
|
dynamicRoutes.forEach((route) => {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
router.addRoute("Main", route)
|
|
|
|
|
|
if (route.name) {
|
|
|
|
|
|
addedRouteNames.push(route.name as string)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
dynamicRoutesAdded = true
|
|
|
|
|
|
lastMenusHash = menusHash
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
2026-01-09 18:14:35 +08:00
|
|
|
|
// 等待多个 tick,确保路由已完全注册
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
await nextTick()
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
2026-01-09 18:14:35 +08:00
|
|
|
|
// 额外等待一小段时间,确保路由系统完全更新
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从路径中提取租户编码
|
|
|
|
|
|
*/
|
|
|
|
|
|
function extractTenantCodeFromPath(path: string): string | null {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const match = path.match(/^\/([^/]+)/)
|
|
|
|
|
|
return match ? match[1] : null
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 构建带租户编码的路径
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buildPathWithTenantCode(tenantCode: string, path: string): string {
|
|
|
|
|
|
// 如果路径已经包含租户编码,直接返回
|
|
|
|
|
|
if (path.startsWith(`/${tenantCode}/`)) {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
return path
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 移除开头的斜杠(如果有)
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const cleanPath = path.startsWith("/") ? path.slice(1) : path
|
2026-01-13 14:01:17 +08:00
|
|
|
|
// 如果路径是根路径,返回租户编码根路径(路由守卫会处理跳转到第一个菜单)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (cleanPath === "" || cleanPath === tenantCode) {
|
2026-01-13 14:01:17 +08:00
|
|
|
|
return `/${tenantCode}`
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
return `/${tenantCode}/${cleanPath}`
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
router.beforeEach(async (to, _from, next) => {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const authStore = useAuthStore()
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 从URL中提取租户编码
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const tenantCodeFromUrl = extractTenantCodeFromPath(to.path)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果 token 存在但用户信息不存在,先获取用户信息
|
|
|
|
|
|
if (authStore.token && !authStore.user) {
|
|
|
|
|
|
try {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const userInfo = await authStore.fetchUserInfo()
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果获取用户信息失败或用户信息为空,跳转到登录页
|
|
|
|
|
|
if (!userInfo) {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
authStore.logout()
|
2025-11-23 14:04:20 +08:00
|
|
|
|
const tenantCode =
|
2026-01-09 18:14:35 +08:00
|
|
|
|
tenantCodeFromUrl || extractTenantCodeFromPath(to.path)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (tenantCode) {
|
|
|
|
|
|
next({
|
|
|
|
|
|
path: `/${tenantCode}/login`,
|
|
|
|
|
|
query: { redirect: to.fullPath },
|
2026-01-09 18:14:35 +08:00
|
|
|
|
})
|
2025-11-23 14:04:20 +08:00
|
|
|
|
} else {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
next({ name: "LoginFallback", query: { redirect: to.fullPath } })
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户信息后,检查租户编码一致性
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const userTenantCode = userInfo?.tenantCode
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (userTenantCode) {
|
|
|
|
|
|
// 如果URL中的租户编码与用户信息不一致,更正URL
|
|
|
|
|
|
if (tenantCodeFromUrl && tenantCodeFromUrl !== userTenantCode) {
|
|
|
|
|
|
const correctedPath = buildPathWithTenantCode(
|
|
|
|
|
|
userTenantCode,
|
|
|
|
|
|
to.path.replace(`/${tenantCodeFromUrl}`, "")
|
2026-01-09 18:14:35 +08:00
|
|
|
|
)
|
|
|
|
|
|
next({ path: correctedPath, replace: true })
|
|
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 如果URL中没有租户编码,添加租户编码
|
|
|
|
|
|
if (!tenantCodeFromUrl) {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const correctedPath = buildPathWithTenantCode(userTenantCode, to.path)
|
|
|
|
|
|
next({ path: correctedPath, replace: true })
|
|
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 获取用户信息后,添加动态路由并等待生效
|
2026-01-09 18:14:35 +08:00
|
|
|
|
await addDynamicRoutes()
|
2025-11-23 14:04:20 +08:00
|
|
|
|
// 保存原始目标路径
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const targetPath = to.fullPath
|
2025-11-23 14:04:20 +08:00
|
|
|
|
// 路由已生效,重新解析目标路由
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const resolved = router.resolve(targetPath)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
// 如果解析后的路由不是404,说明路由存在,重新导航
|
|
|
|
|
|
if (resolved.name !== "NotFound") {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
next({ path: targetPath, replace: true })
|
2025-11-23 14:04:20 +08:00
|
|
|
|
} else {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
// 如果路由不存在,尝试重定向到用户第一个菜单
|
|
|
|
|
|
if (authStore.menus && authStore.menus.length > 0) {
|
|
|
|
|
|
const findFirstMenuPath = (menus: any[]): string | null => {
|
|
|
|
|
|
for (const menu of menus) {
|
|
|
|
|
|
if (menu.path && menu.component) {
|
|
|
|
|
|
// 移除开头的斜杠
|
|
|
|
|
|
return menu.path.startsWith("/")
|
|
|
|
|
|
? menu.path.slice(1)
|
|
|
|
|
|
: menu.path
|
|
|
|
|
|
}
|
|
|
|
|
|
if (menu.children && menu.children.length > 0) {
|
|
|
|
|
|
const childPath = findFirstMenuPath(menu.children)
|
|
|
|
|
|
if (childPath) return childPath
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const firstMenuPath = findFirstMenuPath(authStore.menus)
|
|
|
|
|
|
if (firstMenuPath) {
|
|
|
|
|
|
const user = authStore.user as { tenantCode?: string } | null
|
|
|
|
|
|
const userTenantCode = user?.tenantCode
|
|
|
|
|
|
const tenantCode =
|
|
|
|
|
|
tenantCodeFromUrl ||
|
|
|
|
|
|
extractTenantCodeFromPath(to.path) ||
|
|
|
|
|
|
userTenantCode
|
|
|
|
|
|
if (tenantCode) {
|
|
|
|
|
|
next({ path: `/${tenantCode}/${firstMenuPath}`, replace: true })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-23 14:04:20 +08:00
|
|
|
|
// 如果路由不存在,但需要认证,跳转到登录页(而不是404)
|
|
|
|
|
|
if (to.meta.requiresAuth === false) {
|
|
|
|
|
|
// 路由确实不存在,允许继续(会显示404页面)
|
2026-01-09 18:14:35 +08:00
|
|
|
|
next()
|
2025-11-23 14:04:20 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
const tenantCode =
|
2026-01-09 18:14:35 +08:00
|
|
|
|
tenantCodeFromUrl || extractTenantCodeFromPath(to.path)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (tenantCode) {
|
|
|
|
|
|
next({
|
|
|
|
|
|
path: `/${tenantCode}/login`,
|
|
|
|
|
|
query: { redirect: to.fullPath },
|
2026-01-09 18:14:35 +08:00
|
|
|
|
})
|
2025-11-23 14:04:20 +08:00
|
|
|
|
} else {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
next({ name: "LoginFallback", query: { redirect: to.fullPath } })
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 获取失败,清除 token 并跳转到登录页
|
2026-01-09 18:14:35 +08:00
|
|
|
|
console.error("获取用户信息失败:", error)
|
|
|
|
|
|
authStore.logout()
|
|
|
|
|
|
const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (tenantCode) {
|
|
|
|
|
|
next({
|
|
|
|
|
|
path: `/${tenantCode}/login`,
|
|
|
|
|
|
query: { redirect: to.fullPath },
|
2026-01-09 18:14:35 +08:00
|
|
|
|
})
|
2025-11-23 14:04:20 +08:00
|
|
|
|
} else {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
next({ name: "LoginFallback", query: { redirect: to.fullPath } })
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果 token 不存在,但需要认证,跳转到登录页
|
|
|
|
|
|
if (!authStore.token && to.meta.requiresAuth !== false) {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (tenantCode) {
|
|
|
|
|
|
next({
|
|
|
|
|
|
path: `/${tenantCode}/login`,
|
|
|
|
|
|
query: { redirect: to.fullPath },
|
2026-01-09 18:14:35 +08:00
|
|
|
|
})
|
2025-11-23 14:04:20 +08:00
|
|
|
|
} else {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
next({ name: "LoginFallback", query: { redirect: to.fullPath } })
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已登录,检查租户编码一致性
|
|
|
|
|
|
if (authStore.isAuthenticated && authStore.user) {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const userTenantCode = authStore.user.tenantCode
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (userTenantCode) {
|
|
|
|
|
|
// 如果URL中的租户编码与用户信息不一致,更正URL
|
|
|
|
|
|
if (tenantCodeFromUrl && tenantCodeFromUrl !== userTenantCode) {
|
|
|
|
|
|
const correctedPath = buildPathWithTenantCode(
|
|
|
|
|
|
userTenantCode,
|
|
|
|
|
|
to.path.replace(`/${tenantCodeFromUrl}`, "")
|
2026-01-09 18:14:35 +08:00
|
|
|
|
)
|
|
|
|
|
|
next({ path: correctedPath, replace: true })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果URL中没有租户编码,添加租户编码(排除不需要认证的特殊路由)
|
2026-01-14 14:29:16 +08:00
|
|
|
|
const skipTenantCodePaths = ["/login", "/403"]
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const shouldSkipTenantCode = skipTenantCodePaths.some(p => to.path.startsWith(p))
|
|
|
|
|
|
if (!tenantCodeFromUrl && !shouldSkipTenantCode) {
|
|
|
|
|
|
const correctedPath = buildPathWithTenantCode(userTenantCode, to.path)
|
|
|
|
|
|
next({ path: correctedPath, replace: true })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已登录且有菜单数据,添加或更新动态路由
|
|
|
|
|
|
if (authStore.isAuthenticated && authStore.menus.length > 0) {
|
|
|
|
|
|
// 保存添加路由前的状态
|
|
|
|
|
|
const wasRoutesAdded = dynamicRoutesAdded
|
|
|
|
|
|
|
|
|
|
|
|
// 添加或更新动态路由
|
|
|
|
|
|
await addDynamicRoutes()
|
|
|
|
|
|
|
|
|
|
|
|
// 如果这是第一次添加路由,需要重新导航
|
|
|
|
|
|
if (!wasRoutesAdded && dynamicRoutesAdded) {
|
|
|
|
|
|
// 等待路由完全生效
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
|
|
|
|
|
|
// 保存原始目标路径
|
|
|
|
|
|
const targetPath = to.fullPath
|
|
|
|
|
|
// 路由已生效,重新解析目标路由
|
|
|
|
|
|
const resolved = router.resolve(targetPath)
|
|
|
|
|
|
|
2026-01-13 14:01:17 +08:00
|
|
|
|
// 如果访问的是主路由,重定向到第一个菜单
|
|
|
|
|
|
const isMainRoute = to.name === "Main"
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果解析后的路由不是404,说明路由存在,重新导航
|
|
|
|
|
|
if (resolved.name !== "NotFound" && !isMainRoute) {
|
|
|
|
|
|
next({ path: targetPath, replace: true })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果路由不存在或是主路由,尝试重定向到用户第一个菜单
|
|
|
|
|
|
if (authStore.menus && authStore.menus.length > 0) {
|
|
|
|
|
|
const findFirstMenuPath = (menus: any[]): string | null => {
|
|
|
|
|
|
for (const menu of menus) {
|
|
|
|
|
|
if (menu.path && menu.component) {
|
|
|
|
|
|
// 移除开头的斜杠
|
|
|
|
|
|
return menu.path.startsWith("/") ? menu.path.slice(1) : menu.path
|
|
|
|
|
|
}
|
|
|
|
|
|
if (menu.children && menu.children.length > 0) {
|
|
|
|
|
|
const childPath = findFirstMenuPath(menu.children)
|
|
|
|
|
|
if (childPath) return childPath
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const firstMenuPath = findFirstMenuPath(authStore.menus)
|
|
|
|
|
|
if (firstMenuPath) {
|
|
|
|
|
|
const userTenantCode = authStore.user
|
|
|
|
|
|
? (authStore.user.tenantCode as string | undefined)
|
|
|
|
|
|
: undefined
|
|
|
|
|
|
const tenantCode =
|
|
|
|
|
|
tenantCodeFromUrl ||
|
|
|
|
|
|
extractTenantCodeFromPath(to.path) ||
|
|
|
|
|
|
userTenantCode
|
|
|
|
|
|
if (tenantCode) {
|
|
|
|
|
|
// 再次等待,确保路由完全注册
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
next({ path: `/${tenantCode}/${firstMenuPath}`, replace: true })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果没有任何菜单,跳转到404页面
|
|
|
|
|
|
const tenantCodeFor404 =
|
|
|
|
|
|
tenantCodeFromUrl ||
|
|
|
|
|
|
extractTenantCodeFromPath(to.path) ||
|
|
|
|
|
|
(authStore.user
|
|
|
|
|
|
? (authStore.user.tenantCode as string | undefined)
|
|
|
|
|
|
: undefined)
|
|
|
|
|
|
if (tenantCodeFor404) {
|
|
|
|
|
|
next({ path: `/${tenantCodeFor404}/404`, replace: true })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
next({ name: "NotFound" })
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 18:14:35 +08:00
|
|
|
|
// 如果已登录且有菜单,但路由已添加,检查当前路由是否存在
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (
|
|
|
|
|
|
authStore.isAuthenticated &&
|
|
|
|
|
|
authStore.menus.length > 0 &&
|
2026-01-09 18:14:35 +08:00
|
|
|
|
dynamicRoutesAdded
|
2025-11-23 14:04:20 +08:00
|
|
|
|
) {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const resolved = router.resolve(to.fullPath)
|
2026-01-13 14:01:17 +08:00
|
|
|
|
// 如果访问的是 Main 路由(无具体子路径)或路由不存在,重定向到用户第一个菜单
|
|
|
|
|
|
const isMainRouteWithoutChild = to.name === "Main" || to.matched.length === 1 && to.matched[0].name === "Main"
|
2026-01-09 18:14:35 +08:00
|
|
|
|
if (
|
2026-01-13 14:01:17 +08:00
|
|
|
|
(resolved.name === "NotFound" || isMainRouteWithoutChild) &&
|
2026-01-09 18:14:35 +08:00
|
|
|
|
to.name !== "Login" &&
|
|
|
|
|
|
to.name !== "LoginFallback"
|
|
|
|
|
|
) {
|
|
|
|
|
|
const findFirstMenuPath = (menus: any[]): string | null => {
|
|
|
|
|
|
for (const menu of menus) {
|
|
|
|
|
|
if (menu.path && menu.component) {
|
|
|
|
|
|
return menu.path.startsWith("/") ? menu.path.slice(1) : menu.path
|
|
|
|
|
|
}
|
|
|
|
|
|
if (menu.children && menu.children.length > 0) {
|
|
|
|
|
|
const childPath = findFirstMenuPath(menu.children)
|
|
|
|
|
|
if (childPath) return childPath
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const firstMenuPath = findFirstMenuPath(authStore.menus)
|
|
|
|
|
|
if (firstMenuPath) {
|
|
|
|
|
|
const userTenantCode = authStore.user
|
|
|
|
|
|
? (authStore.user.tenantCode as string | undefined)
|
|
|
|
|
|
: undefined
|
2025-11-23 14:04:20 +08:00
|
|
|
|
const tenantCode =
|
2026-01-09 18:14:35 +08:00
|
|
|
|
tenantCodeFromUrl ||
|
|
|
|
|
|
extractTenantCodeFromPath(to.path) ||
|
|
|
|
|
|
userTenantCode
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (tenantCode) {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
next({ path: `/${tenantCode}/${firstMenuPath}`, replace: true })
|
|
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有任何菜单,跳转到404页面
|
|
|
|
|
|
const tenantCodeFor404 =
|
|
|
|
|
|
tenantCodeFromUrl ||
|
|
|
|
|
|
extractTenantCodeFromPath(to.path) ||
|
|
|
|
|
|
(authStore.user
|
|
|
|
|
|
? (authStore.user.tenantCode as string | undefined)
|
|
|
|
|
|
: undefined)
|
|
|
|
|
|
if (tenantCodeFor404) {
|
|
|
|
|
|
next({ path: `/${tenantCodeFor404}/404`, replace: true })
|
|
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否需要认证
|
|
|
|
|
|
if (to.meta.requiresAuth !== false) {
|
|
|
|
|
|
// 如果没有 token,跳转到登录页
|
|
|
|
|
|
if (!authStore.token) {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (tenantCode) {
|
|
|
|
|
|
next({
|
|
|
|
|
|
path: `/${tenantCode}/login`,
|
|
|
|
|
|
query: { redirect: to.fullPath },
|
2026-01-09 18:14:35 +08:00
|
|
|
|
})
|
2025-11-23 14:04:20 +08:00
|
|
|
|
} else {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
next({ name: "LoginFallback", query: { redirect: to.fullPath } })
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 如果有 token 但没有用户信息,跳转到登录页
|
|
|
|
|
|
if (!authStore.user) {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (tenantCode) {
|
|
|
|
|
|
next({
|
|
|
|
|
|
path: `/${tenantCode}/login`,
|
|
|
|
|
|
query: { redirect: to.fullPath },
|
2026-01-09 18:14:35 +08:00
|
|
|
|
})
|
2025-11-23 14:04:20 +08:00
|
|
|
|
} else {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
next({ name: "LoginFallback", query: { redirect: to.fullPath } })
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已登录,访问登录页则重定向到首页
|
|
|
|
|
|
if (
|
|
|
|
|
|
(to.name === "Login" || to.name === "LoginFallback") &&
|
|
|
|
|
|
authStore.isAuthenticated
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 确保动态路由已添加并等待生效
|
|
|
|
|
|
if (!dynamicRoutesAdded && authStore.menus.length > 0) {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
await addDynamicRoutes()
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-13 14:01:17 +08:00
|
|
|
|
// 重定向到带租户编码的根路径(路由守卫会处理跳转到第一个菜单)
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const userTenantCode = authStore.user?.tenantCode || "default"
|
2026-01-13 14:01:17 +08:00
|
|
|
|
next({ path: `/${userTenantCode}` })
|
2026-01-09 18:14:35 +08:00
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理登录页面的租户编码
|
|
|
|
|
|
if (to.name === "LoginFallback" && !tenantCodeFromUrl) {
|
|
|
|
|
|
// 如果访问的是 /login,但没有租户编码,检查是否有用户信息中的租户编码
|
|
|
|
|
|
if (authStore.isAuthenticated && authStore.user?.tenantCode) {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const userTenantCode = authStore.user.tenantCode
|
|
|
|
|
|
next({ path: `/${userTenantCode}/login`, replace: true })
|
|
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 如果没有租户编码,允许访问(会显示租户输入框)
|
2026-01-09 18:14:35 +08:00
|
|
|
|
next()
|
|
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查角色权限
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const requiredRoles = to.meta.roles
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (requiredRoles && requiredRoles.length > 0) {
|
|
|
|
|
|
if (!authStore.hasAnyRole(requiredRoles)) {
|
|
|
|
|
|
// 没有所需角色,跳转到 403 页面
|
2026-01-09 18:14:35 +08:00
|
|
|
|
next({ name: "Forbidden" })
|
|
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查权限
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const requiredPermissions = to.meta.permissions
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (requiredPermissions && requiredPermissions.length > 0) {
|
|
|
|
|
|
if (!authStore.hasAnyPermission(requiredPermissions)) {
|
|
|
|
|
|
// 没有所需权限,跳转到 403 页面
|
2026-01-09 18:14:35 +08:00
|
|
|
|
next({ name: "Forbidden" })
|
|
|
|
|
|
return
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 18:14:35 +08:00
|
|
|
|
next()
|
|
|
|
|
|
})
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
2026-01-09 18:14:35 +08:00
|
|
|
|
export default router
|