2026-07-23 11:27:33 +08:00
|
|
|
import router from '@/router';
|
2026-07-23 15:00:19 +08:00
|
|
|
import { auth } from '@/hooks/useAuth';
|
2026-07-29 09:32:14 +08:00
|
|
|
import { useMenuStore } from '@/stores/menuStore';
|
2026-07-30 17:16:29 +08:00
|
|
|
import { usePermissionStore } from '@/stores/permissionStore';
|
|
|
|
|
import { useTabsStore } from '@/stores/tabsStore';
|
2026-07-23 11:27:33 +08:00
|
|
|
|
2026-07-27 10:06:45 +08:00
|
|
|
const appTitle = import.meta.env.VITE_APP_TITLE || 'CPMS 运营平台';
|
|
|
|
|
|
|
|
|
|
/** 导航完成后设置页面标题,避免动态路由加载前误匹配 404 导致 title 闪烁 */
|
|
|
|
|
function updateDocumentTitle(title?: string) {
|
|
|
|
|
document.title = title ? `${title} - ${appTitle}` : appTitle;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-29 09:32:14 +08:00
|
|
|
function getRedirectPath(queryValue: unknown): string | undefined {
|
|
|
|
|
if (Array.isArray(queryValue)) return queryValue.find((item) => typeof item === 'string');
|
|
|
|
|
return typeof queryValue === 'string' ? queryValue : undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 11:27:33 +08:00
|
|
|
/**
|
|
|
|
|
* 全局路由守卫
|
|
|
|
|
* 核心流程:
|
2026-07-23 15:00:19 +08:00
|
|
|
* 1. /login → 已登录则跳首页,未登录则放行
|
|
|
|
|
* 2. 未登录 → 跳转 /login
|
|
|
|
|
* 3. 已登录但菜单未加载 → 加载菜单+权限 → 重新导航(确保动态路由生效)
|
|
|
|
|
* 4. 已登录且菜单已加载 → 放行
|
|
|
|
|
*
|
|
|
|
|
* 关键:当 loadMenu 动态注册路由后,必须用 next({ path, replace: true })
|
|
|
|
|
* 重新触发导航,否则当前导航仍使用旧路由表,会匹配到 404。
|
|
|
|
|
* 注意:不能用 next({ ...to, replace: true }),因为展开 to 会把旧的
|
|
|
|
|
* matched 信息带过去,导致仍然匹配到 /:pathMatch(.*)*。
|
2026-07-23 11:27:33 +08:00
|
|
|
*/
|
|
|
|
|
router.beforeEach(async (to, _from, next) => {
|
2026-07-23 15:00:19 +08:00
|
|
|
// ── /login 特殊处理 ──
|
|
|
|
|
if (to.path === '/login') {
|
|
|
|
|
if (auth.isLoggedIn()) {
|
2026-07-30 17:16:29 +08:00
|
|
|
const { loadMenu, loaded, homePath, isRoutePathAvailable } = useMenuStore();
|
2026-07-29 09:32:14 +08:00
|
|
|
if (!loaded.value) {
|
2026-07-30 17:16:29 +08:00
|
|
|
const { loadPermissions } = usePermissionStore();
|
2026-07-29 09:32:14 +08:00
|
|
|
await Promise.all([loadMenu(), loadPermissions()]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 15:30:09 +08:00
|
|
|
if (!auth.isLoggedIn()) {
|
|
|
|
|
next();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-29 09:32:14 +08:00
|
|
|
const redirectPath = getRedirectPath(to.query.redirect);
|
|
|
|
|
const targetPath =
|
|
|
|
|
redirectPath && isRoutePathAvailable(redirectPath)
|
|
|
|
|
? redirectPath
|
|
|
|
|
: homePath.value || '/404';
|
|
|
|
|
next(targetPath);
|
2026-07-23 15:00:19 +08:00
|
|
|
} else {
|
|
|
|
|
next();
|
2026-07-23 11:27:33 +08:00
|
|
|
}
|
2026-07-23 15:00:19 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── 未登录 → 跳转登录页 ──
|
|
|
|
|
if (!auth.isLoggedIn()) {
|
2026-07-29 09:32:14 +08:00
|
|
|
next({ path: '/login', query: { redirect: to.fullPath } });
|
2026-07-23 15:00:19 +08:00
|
|
|
return;
|
2026-07-23 11:27:33 +08:00
|
|
|
}
|
2026-07-23 15:00:19 +08:00
|
|
|
|
|
|
|
|
// ── 已登录:确保菜单和动态路由已加载 ──
|
2026-07-30 17:16:29 +08:00
|
|
|
const { loadMenu, loaded } = useMenuStore();
|
2026-07-23 15:00:19 +08:00
|
|
|
if (!loaded.value) {
|
2026-07-30 17:16:29 +08:00
|
|
|
const { loadPermissions } = usePermissionStore();
|
2026-07-23 15:00:19 +08:00
|
|
|
await Promise.all([loadMenu(), loadPermissions()]);
|
|
|
|
|
|
2026-08-05 15:30:09 +08:00
|
|
|
// 权限获取失败时会清空会话,此时回登录页而不是继续进入业务页
|
|
|
|
|
if (!auth.isLoggedIn()) {
|
|
|
|
|
next({ path: '/login', replace: true });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 15:00:19 +08:00
|
|
|
// 动态路由刚注册,需要用新路由表重新匹配当前路径
|
|
|
|
|
next({ path: to.fullPath, replace: true });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next();
|
2026-07-23 11:27:33 +08:00
|
|
|
});
|
2026-07-27 10:06:45 +08:00
|
|
|
|
|
|
|
|
router.afterEach((to) => {
|
|
|
|
|
updateDocumentTitle(to.meta.title as string | undefined);
|
2026-07-27 16:29:12 +08:00
|
|
|
|
|
|
|
|
// ── 路由跳转 → tab 来源 ──
|
|
|
|
|
// 隐藏的路由(如详情页、编辑页)不进 tab,避免标签栏被噪声污染
|
|
|
|
|
// /login 也不进 tab
|
|
|
|
|
if (to.meta?.hideInMenu || to.path === '/login') return;
|
2026-07-27 16:30:12 +08:00
|
|
|
if (typeof to.meta?.title !== 'string') return;
|
|
|
|
|
|
|
|
|
|
const componentName = (to.meta.componentName as string) || (to.name as string);
|
|
|
|
|
if (!componentName) return;
|
2026-07-27 16:29:12 +08:00
|
|
|
|
2026-07-29 09:32:14 +08:00
|
|
|
const { isRoutePathAvailable } = useMenuStore();
|
|
|
|
|
if (!isRoutePathAvailable(to.path)) return;
|
|
|
|
|
|
2026-07-27 16:29:12 +08:00
|
|
|
const { addTab } = useTabsStore();
|
|
|
|
|
addTab({
|
|
|
|
|
path: to.path,
|
2026-07-27 16:30:12 +08:00
|
|
|
name: componentName,
|
2026-07-27 16:29:12 +08:00
|
|
|
title: to.meta.title,
|
2026-07-28 17:40:35 +08:00
|
|
|
icon: to.meta.icon as string | undefined,
|
2026-07-27 16:29:12 +08:00
|
|
|
});
|
2026-07-27 10:06:45 +08:00
|
|
|
});
|