2025-11-23 14:04:20 +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";
|
|
|
|
|
|
|
|
|
|
|
|
// 基础路由(不需要动态加载的)
|
|
|
|
|
|
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: (to) => {
|
|
|
|
|
|
return { path: `/${to.params.tenantCode}/workbench` };
|
|
|
|
|
|
},
|
|
|
|
|
|
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,
|
|
|
|
|
|
permissions: ["contest: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"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
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"),
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const router = createRouter({
|
|
|
|
|
|
history: createWebHistory(),
|
|
|
|
|
|
routes: baseRoutes,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 标记是否已经添加了动态路由
|
|
|
|
|
|
let dynamicRoutesAdded = false;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 添加动态路由
|
|
|
|
|
|
* @returns Promise,当路由添加完成并生效后 resolve
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function addDynamicRoutes(): Promise<void> {
|
|
|
|
|
|
if (dynamicRoutesAdded) return;
|
|
|
|
|
|
|
|
|
|
|
|
const authStore = useAuthStore();
|
|
|
|
|
|
if (!authStore.menus || authStore.menus.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 将菜单转换为路由
|
|
|
|
|
|
const dynamicRoutes = convertMenusToRoutes(authStore.menus);
|
|
|
|
|
|
|
|
|
|
|
|
// 添加动态路由到根路由下
|
|
|
|
|
|
dynamicRoutes.forEach((route) => {
|
|
|
|
|
|
router.addRoute("Main", route);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
dynamicRoutesAdded = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 等待下一个 tick,确保路由已完全注册
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
// 额外等待一个 tick,确保路由系统完全更新
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从路径中提取租户编码
|
|
|
|
|
|
*/
|
|
|
|
|
|
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}/workbench`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return `/${tenantCode}/${cleanPath}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
router.beforeEach(async (to, _from, next) => {
|
|
|
|
|
|
console.log("to -----", to);
|
|
|
|
|
|
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);
|
|
|
|
|
|
console.log("resolved -----", resolved);
|
|
|
|
|
|
// 如果解析后的路由不是404,说明路由存在,重新导航
|
|
|
|
|
|
if (resolved.name !== "NotFound") {
|
|
|
|
|
|
next({ path: targetPath, replace: true });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果路由不存在,但需要认证,跳转到登录页(而不是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中没有租户编码,添加租户编码
|
|
|
|
|
|
if (!tenantCodeFromUrl && to.path !== "/login") {
|
|
|
|
|
|
const correctedPath = buildPathWithTenantCode(userTenantCode, to.path);
|
|
|
|
|
|
next({ path: correctedPath, replace: true });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已登录且有菜单数据,但动态路由未添加,则添加
|
|
|
|
|
|
if (
|
|
|
|
|
|
authStore.isAuthenticated &&
|
|
|
|
|
|
authStore.menus.length > 0 &&
|
|
|
|
|
|
!dynamicRoutesAdded
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 添加动态路由并等待生效
|
|
|
|
|
|
await addDynamicRoutes();
|
|
|
|
|
|
// 保存原始目标路径
|
|
|
|
|
|
const targetPath = to.fullPath;
|
|
|
|
|
|
// 路由已生效,重新解析目标路由
|
|
|
|
|
|
const resolved = router.resolve(targetPath);
|
|
|
|
|
|
// 如果解析后的路由不是404,说明路由存在,重新导航
|
|
|
|
|
|
if (resolved.name !== "NotFound") {
|
|
|
|
|
|
next({ path: targetPath, replace: true });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果路由不存在,但需要认证,跳转到登录页(而不是404)
|
|
|
|
|
|
if (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 } });
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 路由确实不存在,允许继续(会显示404页面)
|
|
|
|
|
|
next();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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}/workbench` });
|
|
|
|
|
|
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;
|