import type { Router } from 'vue-router'; import { LOGIN_PATH } from '@easyflow/constants'; import { preferences } from '@easyflow/preferences'; import { useAccessStore, useUserStore } from '@easyflow/stores'; import { startProgress, stopProgress } from '@easyflow/utils'; import { accessRoutes, coreRouteNames } from '#/router/routes'; import { useAuthStore } from '#/store'; import { buildForcePasswordRoute, isForcePasswordRoute, notifyForcePasswordChange, shouldForcePasswordChange, } from '#/utils/password-reset'; import { generateAccess } from './access'; import { getDevLoginAccount, removeDevLoginQuery, shouldAttemptDevLogin, } from './dev-login'; import { resolveNavigationUserInfo } from './navigation-user-info'; interface NetworkConnectionLike { effectiveType?: string; saveData?: boolean; } const CHUNK_ERROR_RELOAD_KEY = '__easyflow_chunk_error_reload_path__'; function isExternalShareRoute(path: string) { return ( path === '/share/knowledge' || path === '/share/knowledge/expired' || path === '/share/workflow' || path === '/share/workflow/expired' ); } function isSlowNetworkConnection() { if (typeof navigator === 'undefined') { return false; } const nav = navigator as Navigator & { connection?: NetworkConnectionLike; mozConnection?: NetworkConnectionLike; webkitConnection?: NetworkConnectionLike; }; const connection = nav.connection ?? nav.mozConnection ?? nav.webkitConnection; if (!connection) { return false; } if (connection.saveData) { return true; } return ['2g', '3g', 'slow-2g'].includes(connection.effectiveType ?? ''); } function isDynamicImportChunkError(error: unknown) { const message = String((error as Error | undefined)?.message ?? error ?? ''); if (!message) { return false; } return ( /Loading chunk/i.test(message) || /Importing a module script failed/i.test(message) || /Failed to fetch dynamically imported module/i.test(message) || /dynamically imported module/i.test(message) ); } function setupChunkErrorGuard(router: Router) { router.onError((error, to) => { if (!isDynamicImportChunkError(error)) { return; } // 开发环境保留当前页面和调试上下文,避免模块热更新期间反复整页刷新。 if (import.meta.env.DEV) { console.warn('Dynamic route module loading failed:', error); return; } const fullPath = to?.fullPath; if (!fullPath) { return; } const lastReloadPath = sessionStorage.getItem(CHUNK_ERROR_RELOAD_KEY); if (lastReloadPath === fullPath) { sessionStorage.removeItem(CHUNK_ERROR_RELOAD_KEY); return; } sessionStorage.setItem(CHUNK_ERROR_RELOAD_KEY, fullPath); window.location.assign(fullPath); }); router.isReady().finally(() => { const reloadedPath = sessionStorage.getItem(CHUNK_ERROR_RELOAD_KEY); if (reloadedPath && router.currentRoute.value.fullPath === reloadedPath) { sessionStorage.removeItem(CHUNK_ERROR_RELOAD_KEY); } }); } function shouldUseRouteProgress() { if (preferences.transition.progress) { return true; } // 普通网络下,loading遮罩不显示时自动回退到顶部进度条 return preferences.transition.loading && !isSlowNetworkConnection(); } /** * 通用守卫配置 * @param router */ function setupCommonGuard(router: Router) { // 记录已经加载的页面 const loadedPaths = new Set(); router.beforeEach((to) => { to.meta.loaded = loadedPaths.has(to.path); // 页面加载进度条 if (!to.meta.loaded && shouldUseRouteProgress()) { startProgress(); } return true; }); router.afterEach((to, _from, failure) => { // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行 if (!failure) { loadedPaths.add(to.path); } // 关闭页面加载进度条 if (shouldUseRouteProgress()) { stopProgress(); } }); } /** * 权限访问守卫配置 * @param router */ function setupAccessGuard(router: Router) { let devLoginPromise: null | Promise = null; router.beforeEach(async (to, from) => { // 公开路由必须在读取或刷新登录态之前短路,避免浏览器残留的过期 // token 把匿名分享页重定向到登录页。 if (to.meta.ignoreAccess) { return true; } const accessStore = useAccessStore(); const userStore = useUserStore(); const authStore = useAuthStore(); const devLoginAccount = getDevLoginAccount(to.query); const resolveCleanFullPath = () => router.resolve({ hash: to.hash, path: to.path, query: removeDevLoginQuery(to.query), }).fullPath; const resolveUserInfo = () => resolveNavigationUserInfo({ cachedUserInfo: userStore.userInfo, fetchUserInfo: authStore.fetchUserInfo, getAccessToken: () => accessStore.accessToken, }); if (devLoginAccount && import.meta.env.DEV && accessStore.accessToken) { return resolveCleanFullPath(); } if ( shouldAttemptDevLogin({ account: devLoginAccount, hasAccessToken: !!accessStore.accessToken, isDev: import.meta.env.DEV, }) ) { const account = devLoginAccount; try { devLoginPromise ??= authStore .authDevLogin(account ?? '') .then(() => undefined) .finally(() => { devLoginPromise = null; }); await devLoginPromise; const cleanFullPath = resolveCleanFullPath(); return cleanFullPath; } catch { const cleanFullPath = resolveCleanFullPath(); return { path: LOGIN_PATH, query: cleanFullPath === preferences.app.defaultHomePath ? {} : { redirect: encodeURIComponent(cleanFullPath) }, replace: true, }; } } // 基本路由,这些路由不需要进入权限拦截 if (coreRouteNames.includes(to.name as string)) { if (to.path === LOGIN_PATH && accessStore.accessToken) { const currentUser = await resolveUserInfo(); if (!currentUser) { return true; } if (shouldForcePasswordChange(currentUser)) { return buildForcePasswordRoute(); } return decodeURIComponent( (to.query?.redirect as string) || userStore.userInfo?.homePath || preferences.app.defaultHomePath, ); } return true; } // accessToken 检查 if (!accessStore.accessToken) { // 没有访问权限,跳转登录页面 if (to.fullPath !== LOGIN_PATH) { const cleanFullPath = devLoginAccount && import.meta.env.DEV ? resolveCleanFullPath() : to.fullPath; return { path: LOGIN_PATH, // 如不需要,直接删除 query query: cleanFullPath === preferences.app.defaultHomePath ? {} : { redirect: encodeURIComponent(cleanFullPath) }, // 携带当前跳转的页面,登录后重新跳转该页面 replace: true, }; } return to; } const userInfo = await resolveUserInfo(); if (!userInfo) { return { path: LOGIN_PATH, query: to.fullPath === preferences.app.defaultHomePath ? {} : { redirect: encodeURIComponent(to.fullPath) }, replace: true, }; } if (shouldForcePasswordChange(userInfo) && !isForcePasswordRoute(to)) { if (from.name) { notifyForcePasswordChange(); } return buildForcePasswordRoute(); } if (isExternalShareRoute(to.path)) { return true; } // 是否已经生成过动态路由 if (accessStore.isAccessChecked) { return true; } // 页面菜单与按钮权限码是两套数据源。每次重新构建动态菜单时, // 同步刷新一次 accessCodes,避免后端权限模型调整后页面仍持有旧按钮权限。 await authStore.fetchAccessCodes(); // 生成路由表 // 当前登录用户拥有的角色标识列表 const userRoles = userInfo.roles ?? []; // 生成菜单和路由 const { accessibleMenus, accessibleRoutes } = await generateAccess({ roles: userRoles, router, // 则会在菜单中显示,但是访问会被重定向到403 routes: accessRoutes, }); // 保存菜单信息和路由信息 accessStore.setAccessMenus(accessibleMenus); accessStore.setAccessRoutes(accessibleRoutes); accessStore.setIsAccessChecked(true); const redirectPath = (from.query.redirect ?? (to.path === preferences.app.defaultHomePath ? userInfo.homePath || preferences.app.defaultHomePath : to.fullPath)) as string; return { ...router.resolve(decodeURIComponent(redirectPath)), replace: true, }; }); } /** * 项目守卫配置 * @param router */ function createRouterGuard(router: Router) { /** 通用 */ setupCommonGuard(router); /** 权限访问 */ setupAccessGuard(router); /** chunk加载错误兜底 */ setupChunkErrorGuard(router); } export { createRouterGuard };