329 lines
9.1 KiB
TypeScript
329 lines
9.1 KiB
TypeScript
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<string>();
|
||
|
||
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<void> = 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 };
|