fix: 修复 Hash 路由跳转丢失部署基路径

- 修复懒加载分包失败后跳转到站点根路径的问题

- 统一管理端和用户中心新窗口路由解析

- 增加部署基路径与 Hash 路由回归测试
This commit is contained in:
2026-08-31 18:47:54 +08:00
parent 263f5f4b8b
commit 3e34b65bcb
11 changed files with 83 additions and 23 deletions

View File

@@ -0,0 +1,26 @@
import { createRouter, createWebHashHistory } from 'vue-router';
import { describe, expect, it } from 'vitest';
import { resolveChunkReloadHref } from '../guard';
describe('chunk error reload', () => {
it('preserves the deployment base and hash route', () => {
const router = createRouter({
history: createWebHashHistory('/flow/'),
routes: [
{
component: { template: '<div>workflow</div>' },
path: '/ai/workflow',
},
],
});
const href = resolveChunkReloadHref(router, '/ai/workflow?source=sidebar');
expect(href).toBe('#/ai/workflow?source=sidebar');
expect(new URL(href, 'https://easyflowtech.cn/flow/').href).toBe(
'https://easyflowtech.cn/flow/#/ai/workflow?source=sidebar',
);
});
});

View File

@@ -71,6 +71,10 @@ function isDynamicImportChunkError(error: unknown) {
); );
} }
function resolveChunkReloadHref(router: Router, fullPath: string) {
return router.resolve(fullPath).href;
}
function setupChunkErrorGuard(router: Router) { function setupChunkErrorGuard(router: Router) {
router.onError((error, to) => { router.onError((error, to) => {
if (!isDynamicImportChunkError(error)) { if (!isDynamicImportChunkError(error)) {
@@ -94,7 +98,7 @@ function setupChunkErrorGuard(router: Router) {
return; return;
} }
sessionStorage.setItem(CHUNK_ERROR_RELOAD_KEY, fullPath); sessionStorage.setItem(CHUNK_ERROR_RELOAD_KEY, fullPath);
window.location.assign(fullPath); window.location.assign(resolveChunkReloadHref(router, fullPath));
}); });
router.isReady().finally(() => { router.isReady().finally(() => {
@@ -325,4 +329,4 @@ function createRouterGuard(router: Router) {
setupChunkErrorGuard(router); setupChunkErrorGuard(router);
} }
export { createRouterGuard }; export { createRouterGuard, resolveChunkReloadHref };

View File

@@ -30,7 +30,7 @@
"preview": "turbo-run preview", "preview": "turbo-run preview",
"publint": "vsh publint", "publint": "vsh publint",
"reinstall": "pnpm clean --del-lock && pnpm install", "reinstall": "pnpm clean --del-lock && pnpm install",
"test:deployment-contract": "vitest run --dom app/vite-base-path-redirect.test.ts app/src/startup-error.test.ts app/src/router/__tests__/environment-contract.test.ts app/src/router/navigation-user-info.test.ts app/src/utils/share-route-context.test.ts app/src/utils/__tests__/login-redirect.test.ts app/src/views/ai/workflow/workflow-share-context.test.ts", "test:deployment-contract": "vitest run --dom app/vite-base-path-redirect.test.ts app/src/startup-error.test.ts app/src/router/__tests__/environment-contract.test.ts app/src/router/__tests__/chunk-error-reload.test.ts app/src/router/navigation-user-info.test.ts app/src/utils/share-route-context.test.ts app/src/utils/__tests__/login-redirect.test.ts app/src/views/ai/workflow/workflow-share-context.test.ts",
"test:unit": "vitest run --dom", "test:unit": "vitest run --dom",
"test:e2e": "turbo run test:e2e", "test:e2e": "turbo run test:e2e",
"update:deps": "npx taze -r -w", "update:deps": "npx taze -r -w",

View File

@@ -1,17 +1,20 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { openWindow } from '../window'; import { openRouteInNewWindow, openWindow } from '../window';
describe('openWindow', () => { describe('openWindow', () => {
// 保存原始的 window.open 函数 // 保存原始的 window.open 函数
let originalOpen: typeof window.open; let originalOpen: typeof window.open;
let originalUrl: string;
beforeEach(() => { beforeEach(() => {
originalOpen = window.open; originalOpen = window.open;
originalUrl = window.location.href;
}); });
afterEach(() => { afterEach(() => {
window.open = originalOpen; window.open = originalOpen;
window.history.replaceState({}, '', originalUrl);
}); });
it('should call window.open with correct arguments', () => { it('should call window.open with correct arguments', () => {
@@ -30,4 +33,17 @@ describe('openWindow', () => {
'noopener=yes,noreferrer=yes', 'noopener=yes,noreferrer=yes',
); );
}); });
it('should preserve a router-resolved base and hash route', () => {
window.history.replaceState({}, '', '/flow/#/dashboard');
window.open = vi.fn();
openRouteInNewWindow('#/ai/workflow');
expect(window.open).toHaveBeenCalledWith(
`${window.location.origin}/flow/#/ai/workflow`,
'_blank',
'noopener=yes,noreferrer=yes',
);
});
}); });

View File

@@ -25,13 +25,10 @@ function openWindow(url: string, options: OpenWindowOptions = {}): void {
/** /**
* 在新窗口中打开路由。 * 在新窗口中打开路由。
* @param path * @param href Vue Router 解析后的地址
*/ */
function openRouteInNewWindow(path: string) { function openRouteInNewWindow(href: string) {
const { hash, origin } = location; openWindow(new URL(href, location.href).href, { target: '_blank' });
const fullPath = path.startsWith('/') ? path : `/${path}`;
const url = `${origin}${hash && !fullPath.startsWith('/#') ? '/#' : ''}${fullPath}`;
openWindow(url, { target: '_blank' });
} }
export { openRouteInNewWindow, openWindow }; export { openRouteInNewWindow, openWindow };

View File

@@ -47,7 +47,7 @@ export function useTabs() {
} }
async function openTabInNewWindow(tab?: RouteLocationNormalized) { async function openTabInNewWindow(tab?: RouteLocationNormalized) {
await tabbarStore.openTabInNewWindow(tab || route); await tabbarStore.openTabInNewWindow(tab || route, router);
} }
async function closeTabByKey(key: string) { async function closeTabByKey(key: string) {

View File

@@ -307,9 +307,11 @@ export const useTabbarStore = defineStore('core-tabbar', {
/** /**
* @zh_CN 新窗口打开标签页 * @zh_CN 新窗口打开标签页
* @param tab * @param tab
* @param router
*/ */
async openTabInNewWindow(tab: TabDefinition) { async openTabInNewWindow(tab: TabDefinition, router: Router) {
openRouteInNewWindow(tab.fullPath || tab.path); const href = router.resolve(tab.fullPath || tab.path).href;
openRouteInNewWindow(href);
}, },
/** /**

View File

@@ -1,17 +1,20 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { openWindow } from '../window'; import { openRouteInNewWindow, openWindow } from '../window';
describe('openWindow', () => { describe('openWindow', () => {
// 保存原始的 window.open 函数 // 保存原始的 window.open 函数
let originalOpen: typeof window.open; let originalOpen: typeof window.open;
let originalUrl: string;
beforeEach(() => { beforeEach(() => {
originalOpen = window.open; originalOpen = window.open;
originalUrl = window.location.href;
}); });
afterEach(() => { afterEach(() => {
window.open = originalOpen; window.open = originalOpen;
window.history.replaceState({}, '', originalUrl);
}); });
it('should call window.open with correct arguments', () => { it('should call window.open with correct arguments', () => {
@@ -30,4 +33,17 @@ describe('openWindow', () => {
'noopener=yes,noreferrer=yes', 'noopener=yes,noreferrer=yes',
); );
}); });
it('should preserve a router-resolved base and hash route', () => {
window.history.replaceState({}, '', '/flow/#/dashboard');
window.open = vi.fn();
openRouteInNewWindow('#/ai/workflow');
expect(window.open).toHaveBeenCalledWith(
`${window.location.origin}/flow/#/ai/workflow`,
'_blank',
'noopener=yes,noreferrer=yes',
);
});
}); });

View File

@@ -25,13 +25,10 @@ function openWindow(url: string, options: OpenWindowOptions = {}): void {
/** /**
* 在新窗口中打开路由。 * 在新窗口中打开路由。
* @param path * @param href Vue Router 解析后的地址
*/ */
function openRouteInNewWindow(path: string) { function openRouteInNewWindow(href: string) {
const { hash, origin } = location; openWindow(new URL(href, location.href).href, { target: '_blank' });
const fullPath = path.startsWith('/') ? path : `/${path}`;
const url = `${origin}${hash && !fullPath.startsWith('/#') ? '/#' : ''}${fullPath}`;
openWindow(url, { target: '_blank' });
} }
export { openRouteInNewWindow, openWindow }; export { openRouteInNewWindow, openWindow };

View File

@@ -47,7 +47,7 @@ export function useTabs() {
} }
async function openTabInNewWindow(tab?: RouteLocationNormalized) { async function openTabInNewWindow(tab?: RouteLocationNormalized) {
await tabbarStore.openTabInNewWindow(tab || route); await tabbarStore.openTabInNewWindow(tab || route, router);
} }
async function closeTabByKey(key: string) { async function closeTabByKey(key: string) {

View File

@@ -307,9 +307,11 @@ export const useTabbarStore = defineStore('core-tabbar', {
/** /**
* @zh_CN 新窗口打开标签页 * @zh_CN 新窗口打开标签页
* @param tab * @param tab
* @param router
*/ */
async openTabInNewWindow(tab: TabDefinition) { async openTabInNewWindow(tab: TabDefinition, router: Router) {
openRouteInNewWindow(tab.fullPath || tab.path); const href = router.resolve(tab.fullPath || tab.path).href;
openRouteInNewWindow(href);
}, },
/** /**