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" // 基础路由(不需要动态加载的) 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"), // 不设置固定redirect,由路由守卫根据用户菜单动态跳转到第一个可见菜单 meta: {}, children: [ // 创建比赛路由(不需要在菜单中显示) { 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, permissions: ["contest:read", "activity:read"], }, }, // 编辑比赛路由(不需要在菜单中显示) { 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"], }, }, // 个人赛报名路由 { 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, }, }, // 报名管理列表路由 { path: "contests/registrations", name: "ContestsRegistrations", component: () => import("@/views/contests/registrations/Index.vue"), meta: { title: "报名管理", requiresAuth: true, permissions: ["registration:read", "contest:read"], }, }, // 报名记录路由 { path: "contests/registrations/:id/records", name: "RegistrationRecords", component: () => import("@/views/contests/registrations/Records.vue"), meta: { title: "报名记录", requiresAuth: true, permissions: ["registration:read", "contest:read"], }, }, // 评审进度详情路由 { path: "contests/reviews/:id/progress", name: "ReviewProgressDetail", component: () => import("@/views/contests/reviews/ProgressDetail.vue"), meta: { title: "评审进度详情", requiresAuth: true, }, }, // 赛果发布详情路由 { path: "contests/results/:id", name: "ContestsResultsDetail", component: () => import("@/views/contests/results/Detail.vue"), meta: { title: "赛果发布详情", requiresAuth: true, }, }, // 参赛作品详情列表路由 { path: "contests/works/:id/list", name: "WorksDetail", component: () => import("@/views/contests/works/WorksDetail.vue"), meta: { title: "参赛作品详情", requiresAuth: true, permissions: ["work:read"], }, }, // 作业提交记录路由 { 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, permissions: ["homework:read"], }, }, // 教师我的指导路由 { path: "student-activities/guidance", name: "TeacherGuidance", component: () => import("@/views/contests/Guidance.vue"), meta: { title: "我的指导", requiresAuth: true, permissions: ["activity:read"], }, }, // 评委评审详情页 { path: "activities/review/:id", name: "ReviewDetail", component: () => import("@/views/activities/ReviewDetail.vue"), meta: { title: "作品评审", requiresAuth: true, }, }, // 预设评语页面 { path: "activities/preset-comments", name: "PresetComments", component: () => import("@/views/activities/PresetComments.vue"), meta: { title: "预设评语", requiresAuth: true, }, }, // 3D建模实验室路由(工作台模块下) { path: "workbench/3d-lab", name: "3DModelingLab", component: () => import("@/views/workbench/ai-3d/Index.vue"), meta: { title: "3D建模实验室", requiresAuth: true, hideSidebar: true, }, }, // 3D模型生成页面 { path: "workbench/3d-lab/generate/:taskId", name: "AI3DGenerate", component: () => import("@/views/workbench/ai-3d/Generate.vue"), meta: { title: "3D模型生成", requiresAuth: true, 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, }, }, // 动态路由将在这里添加 ], }, { path: "/403", name: "Forbidden", component: () => import("@/views/error/403.vue"), meta: { requiresAuth: false }, }, { path: "/:pathMatch(.*)*", name: "NotFound", component: () => import("@/views/error/404.vue"), }, ] const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: baseRoutes, }) // 标记是否已经添加了动态路由 let dynamicRoutesAdded = false // 保存已添加的路由名称,用于清理 let addedRouteNames: string[] = [] // 保存上次的菜单数据,用于检测变化 let lastMenusHash: string = "" /** * 重置动态路由标记(用于登出或切换用户时) */ export function resetDynamicRoutes(): void { dynamicRoutesAdded = false addedRouteNames = [] lastMenusHash = "" } /** * 添加动态路由 * @returns Promise,当路由添加完成并生效后 resolve */ export async function addDynamicRoutes(): Promise { 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 })) ) // 如果菜单数据没有变化且已经添加过路由,直接返回 if (dynamicRoutesAdded && menusHash === lastMenusHash) { return } // 如果已经添加过路由,先移除旧路由 if (dynamicRoutesAdded && addedRouteNames.length > 0) { addedRouteNames.forEach((routeName) => { if (router.hasRoute(routeName)) { router.removeRoute(routeName) } }) addedRouteNames = [] } // 将菜单转换为路由 const dynamicRoutes = convertMenusToRoutes(authStore.menus) if (dynamicRoutes.length === 0) { return } // 添加动态路由到根路由下 dynamicRoutes.forEach((route) => { router.addRoute("Main", route) if (route.name) { addedRouteNames.push(route.name as string) } }) dynamicRoutesAdded = true lastMenusHash = menusHash // 等待多个 tick,确保路由已完全注册 await nextTick() await nextTick() await nextTick() // 额外等待一小段时间,确保路由系统完全更新 await new Promise((resolve) => setTimeout(resolve, 50)) } /** * 从路径中提取租户编码 */ function extractTenantCodeFromPath(path: string): string | null { const match = path.match(/^\/([^/]+)/) return match ? match[1] : null } /** * 构建带租户编码的路径 */ function buildPathWithTenantCode(tenantCode: string, path: string): string { // 如果路径已经包含租户编码,直接返回 if (path.startsWith(`/${tenantCode}/`)) { return path } // 移除开头的斜杠(如果有) const cleanPath = path.startsWith("/") ? path.slice(1) : path // 如果路径是根路径,返回租户编码根路径(路由守卫会处理跳转到第一个菜单) if (cleanPath === "" || cleanPath === tenantCode) { return `/${tenantCode}` } return `/${tenantCode}/${cleanPath}` } router.beforeEach(async (to, _from, next) => { const authStore = useAuthStore() // 从URL中提取租户编码 const tenantCodeFromUrl = extractTenantCodeFromPath(to.path) // 如果 token 存在但用户信息不存在,先获取用户信息 if (authStore.token && !authStore.user) { try { const userInfo = await authStore.fetchUserInfo() // 如果获取用户信息失败或用户信息为空,跳转到登录页 if (!userInfo) { authStore.logout() const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path) if (tenantCode) { next({ path: `/${tenantCode}/login`, query: { redirect: to.fullPath }, }) } else { next({ name: "LoginFallback", query: { redirect: to.fullPath } }) } return } // 获取用户信息后,检查租户编码一致性 const userTenantCode = userInfo?.tenantCode if (userTenantCode) { // 如果URL中的租户编码与用户信息不一致,更正URL if (tenantCodeFromUrl && tenantCodeFromUrl !== userTenantCode) { const correctedPath = buildPathWithTenantCode( userTenantCode, to.path.replace(`/${tenantCodeFromUrl}`, "") ) next({ path: correctedPath, query: to.query, replace: true }) return } // 如果URL中没有租户编码,添加租户编码 if (!tenantCodeFromUrl) { const correctedPath = buildPathWithTenantCode(userTenantCode, to.path) next({ path: correctedPath, query: to.query, replace: true }) return } } // 获取用户信息后,添加动态路由并等待生效 await addDynamicRoutes() // 保存原始目标路径 const targetPath = to.fullPath // 路由已生效,重新解析目标路由 const resolved = router.resolve(targetPath) // 如果解析后的路由不是404,说明路由存在,重新导航 if (resolved.name !== "NotFound") { next({ path: targetPath, replace: true }) } else { // 如果路由不存在,尝试重定向到用户第一个菜单 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 } } } // 如果路由不存在,但需要认证,跳转到登录页(而不是404) if (to.meta.requiresAuth === false) { // 路由确实不存在,允许继续(会显示404页面) next() } else { const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path) if (tenantCode) { next({ path: `/${tenantCode}/login`, query: { redirect: to.fullPath }, }) } else { next({ name: "LoginFallback", query: { redirect: to.fullPath } }) } } } return } catch (error) { // 获取失败,清除 token 并跳转到登录页 console.error("获取用户信息失败:", error) authStore.logout() const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path) if (tenantCode) { next({ path: `/${tenantCode}/login`, query: { redirect: to.fullPath }, }) } else { next({ name: "LoginFallback", query: { redirect: to.fullPath } }) } return } } // 如果 token 不存在,但需要认证,跳转到登录页 if (!authStore.token && to.meta.requiresAuth !== false) { const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path) if (tenantCode) { next({ path: `/${tenantCode}/login`, query: { redirect: to.fullPath }, }) } else { next({ name: "LoginFallback", query: { redirect: to.fullPath } }) } return } // 如果已登录,检查租户编码一致性 if (authStore.isAuthenticated && authStore.user) { const userTenantCode = authStore.user.tenantCode if (userTenantCode) { // 如果URL中的租户编码与用户信息不一致,更正URL if (tenantCodeFromUrl && tenantCodeFromUrl !== userTenantCode) { const correctedPath = buildPathWithTenantCode( userTenantCode, to.path.replace(`/${tenantCodeFromUrl}`, "") ) next({ path: correctedPath, query: to.query, replace: true }) return } // 如果URL中没有租户编码,添加租户编码(排除不需要认证的特殊路由) const skipTenantCodePaths = ["/login", "/403"] const shouldSkipTenantCode = skipTenantCodePaths.some(p => to.path.startsWith(p)) if (!tenantCodeFromUrl && !shouldSkipTenantCode) { const correctedPath = buildPathWithTenantCode(userTenantCode, to.path) next({ path: correctedPath, query: to.query, 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) // 如果访问的是主路由,重定向到第一个菜单 const isMainRoute = to.name === "Main" console.log('Route guard debug:', { targetPath, resolvedName: resolved.name, resolvedPath: resolved.path, isMainRoute, toName: to.name, toPath: to.path, }) // 如果解析后的路由不是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 } } } // 如果没有任何菜单,跳转到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" }) } return } } // 如果已登录且有菜单,但路由已添加,检查当前路由是否存在 if ( authStore.isAuthenticated && authStore.menus.length > 0 && dynamicRoutesAdded ) { const resolved = router.resolve(to.fullPath) // 如果访问的是 Main 路由(无具体子路径)或路由不存在,重定向到用户第一个菜单 const isMainRouteWithoutChild = to.name === "Main" || to.matched.length === 1 && to.matched[0].name === "Main" if ( (resolved.name === "NotFound" || isMainRouteWithoutChild) && 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 const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path) || userTenantCode if (tenantCode) { next({ path: `/${tenantCode}/${firstMenuPath}`, replace: true }) return } } // 如果没有任何菜单,跳转到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 } } } // 检查是否需要认证 if (to.meta.requiresAuth !== false) { // 如果没有 token,跳转到登录页 if (!authStore.token) { const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path) if (tenantCode) { next({ path: `/${tenantCode}/login`, query: { redirect: to.fullPath }, }) } else { next({ name: "LoginFallback", query: { redirect: to.fullPath } }) } return } // 如果有 token 但没有用户信息,跳转到登录页 if (!authStore.user) { const tenantCode = tenantCodeFromUrl || extractTenantCodeFromPath(to.path) if (tenantCode) { next({ path: `/${tenantCode}/login`, query: { redirect: to.fullPath }, }) } else { next({ name: "LoginFallback", query: { redirect: to.fullPath } }) } return } } // 如果已登录,访问登录页则重定向到首页 if ( (to.name === "Login" || to.name === "LoginFallback") && authStore.isAuthenticated ) { // 确保动态路由已添加并等待生效 if (!dynamicRoutesAdded && authStore.menus.length > 0) { await addDynamicRoutes() } // 重定向到带租户编码的根路径(路由守卫会处理跳转到第一个菜单) const userTenantCode = authStore.user?.tenantCode || "default" next({ path: `/${userTenantCode}` }) return } // 处理登录页面的租户编码 if (to.name === "LoginFallback" && !tenantCodeFromUrl) { // 如果访问的是 /login,但没有租户编码,检查是否有用户信息中的租户编码 if (authStore.isAuthenticated && authStore.user?.tenantCode) { const userTenantCode = authStore.user.tenantCode next({ path: `/${userTenantCode}/login`, replace: true }) return } // 如果没有租户编码,允许访问(会显示租户输入框) next() return } // 检查角色权限 const requiredRoles = to.meta.roles if (requiredRoles && requiredRoles.length > 0) { if (!authStore.hasAnyRole(requiredRoles)) { // 没有所需角色,跳转到 403 页面 next({ name: "Forbidden" }) return } } // 检查权限 const requiredPermissions = to.meta.permissions if (requiredPermissions && requiredPermissions.length > 0) { if (!authStore.hasAnyPermission(requiredPermissions)) { // 没有所需权限,跳转到 403 页面 next({ name: "Forbidden" }) return } } next() }) export default router