library-picturebook-activity/frontend/src/router/index.ts
zhangxiaohua 62cdebc388 修改bug
2026-01-14 10:06:08 +08:00

703 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: "/model-viewer",
name: "ModelViewer",
component: () => import("@/views/model/ModelViewer.vue"),
meta: { requiresAuth: false },
},
{
path: "/:tenantCode/3d-lab-fullscreen",
name: "3DLabFullscreen",
component: () => import("@/views/workbench/ai-3d/Index.vue"),
meta: { requiresAuth: true },
},
{
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,
permissions: ["review:read"],
},
},
// 参赛作品详情列表路由
{
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"],
},
},
// 3D建模实验室路由工作台模块下
{
path: "workbench/3d-lab",
name: "3DModelingLab",
component: () => import("@/views/workbench/ai-3d/Index.vue"),
meta: {
title: "3D建模实验室",
requiresAuth: true,
},
},
// 3D模型生成页面
{
path: "workbench/3d-lab/generate/:taskId",
name: "AI3DGenerate",
component: () => import("@/views/workbench/ai-3d/Generate.vue"),
meta: {
title: "3D模型生成",
requiresAuth: 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(),
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<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 }))
)
// 如果菜单数据没有变化且已经添加过路由,直接返回
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, replace: true })
return
}
// 如果URL中没有租户编码添加租户编码
if (!tenantCodeFromUrl) {
const correctedPath = buildPathWithTenantCode(userTenantCode, to.path)
next({ path: correctedPath, 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, replace: true })
return
}
// 如果URL中没有租户编码添加租户编码排除不需要认证的特殊路由
const skipTenantCodePaths = ["/login", "/model-viewer", "/403"]
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)
// 如果访问的是主路由,重定向到第一个菜单
const isMainRoute = to.name === "Main"
// 如果解析后的路由不是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