fix: 提升管理端启动与导航稳定性
- 等待初始路由就绪并提供可恢复的启动失败页面 - 收敛失效登录导航并按需加载 SVG 图标资源
This commit is contained in:
@@ -42,6 +42,15 @@ function collectMenuIcons(routes: unknown[]): string[] {
|
||||
return icons;
|
||||
}
|
||||
|
||||
async function loadMenuIcons(iconNames: string[]) {
|
||||
await Promise.all([
|
||||
loadMissingIconCollections(iconNames),
|
||||
iconNames.some((icon) => icon.startsWith('svg:'))
|
||||
? import('@easyflow/icons/svg')
|
||||
: Promise.resolve(),
|
||||
]);
|
||||
}
|
||||
|
||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||
|
||||
@@ -58,7 +67,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
message: `${$t('common.loadingMenu')}...`,
|
||||
});
|
||||
const menuRoutes = await getAllMenusApi();
|
||||
await loadMissingIconCollections(collectMenuIcons(menuRoutes));
|
||||
await loadMenuIcons(collectMenuIcons(menuRoutes));
|
||||
return disableUnsafeMenuRouteCaching(menuRoutes);
|
||||
},
|
||||
// 可以指定没有权限跳转403页面
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
removeDevLoginQuery,
|
||||
shouldAttemptDevLogin,
|
||||
} from './dev-login';
|
||||
import { resolveNavigationUserInfo } from './navigation-user-info';
|
||||
|
||||
interface NetworkConnectionLike {
|
||||
effectiveType?: string;
|
||||
@@ -75,6 +76,13 @@ function setupChunkErrorGuard(router: Router) {
|
||||
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;
|
||||
@@ -156,6 +164,13 @@ function setupAccessGuard(router: Router) {
|
||||
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();
|
||||
}
|
||||
@@ -177,10 +192,6 @@ function setupAccessGuard(router: Router) {
|
||||
});
|
||||
await devLoginPromise;
|
||||
const cleanFullPath = resolveCleanFullPath();
|
||||
if (window.location.search.includes('devLogin=')) {
|
||||
window.location.replace(cleanFullPath);
|
||||
return false;
|
||||
}
|
||||
return cleanFullPath;
|
||||
} catch {
|
||||
const cleanFullPath = resolveCleanFullPath();
|
||||
@@ -198,8 +209,10 @@ function setupAccessGuard(router: Router) {
|
||||
// 基本路由,这些路由不需要进入权限拦截
|
||||
if (coreRouteNames.includes(to.name as string)) {
|
||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||
const currentUser =
|
||||
userStore.userInfo || (await authStore.fetchUserInfo());
|
||||
const currentUser = await resolveUserInfo();
|
||||
if (!currentUser) {
|
||||
return true;
|
||||
}
|
||||
if (shouldForcePasswordChange(currentUser)) {
|
||||
return buildForcePasswordRoute();
|
||||
}
|
||||
@@ -239,7 +252,17 @@ function setupAccessGuard(router: Router) {
|
||||
return to;
|
||||
}
|
||||
|
||||
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { resolveNavigationUserInfo } from './navigation-user-info';
|
||||
|
||||
describe('navigation user info', () => {
|
||||
it('uses cached user info without issuing a request', async () => {
|
||||
const cachedUserInfo = { id: 1 };
|
||||
const fetchUserInfo = vi.fn();
|
||||
|
||||
await expect(
|
||||
resolveNavigationUserInfo({
|
||||
cachedUserInfo,
|
||||
fetchUserInfo,
|
||||
getAccessToken: () => 'token',
|
||||
}),
|
||||
).resolves.toBe(cachedUserInfo);
|
||||
expect(fetchUserInfo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns null after an expired token has been cleared', async () => {
|
||||
await expect(
|
||||
resolveNavigationUserInfo({
|
||||
cachedUserInfo: null,
|
||||
fetchUserInfo: () => Promise.reject(new Error('expired')),
|
||||
getAccessToken: () => null,
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('keeps non-authentication failures visible', async () => {
|
||||
const error = new Error('network unavailable');
|
||||
await expect(
|
||||
resolveNavigationUserInfo({
|
||||
cachedUserInfo: null,
|
||||
fetchUserInfo: () => Promise.reject(error),
|
||||
getAccessToken: () => 'token',
|
||||
}),
|
||||
).rejects.toBe(error);
|
||||
});
|
||||
});
|
||||
26
easyflow-ui-admin/app/src/router/navigation-user-info.ts
Normal file
26
easyflow-ui-admin/app/src/router/navigation-user-info.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
interface ResolveNavigationUserInfoOptions<T> {
|
||||
cachedUserInfo: null | T;
|
||||
fetchUserInfo: () => Promise<T>;
|
||||
getAccessToken: () => null | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析路由守卫所需的用户信息,并识别已被拦截器清理的失效登录态。
|
||||
*/
|
||||
async function resolveNavigationUserInfo<T>(
|
||||
options: ResolveNavigationUserInfoOptions<T>,
|
||||
): Promise<null | T> {
|
||||
if (options.cachedUserInfo) {
|
||||
return options.cachedUserInfo;
|
||||
}
|
||||
try {
|
||||
return await options.fetchUserInfo();
|
||||
} catch (error) {
|
||||
if (!options.getAccessToken()) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export { resolveNavigationUserInfo };
|
||||
Reference in New Issue
Block a user