fix: 修复页签路由切换页面未更新
- 拆分页签分组与页面渲染身份,确保列表和详情正确切换 - 让禁用缓存的路由绕过 KeepAlive,并补充资源切换回归测试
This commit is contained in:
@@ -9,9 +9,10 @@ import { computed } from 'vue';
|
||||
import { RouterView } from 'vue-router';
|
||||
|
||||
import { preferences, usePreferences } from '@easyflow/preferences';
|
||||
import { getTabKey, storeToRefs, useTabbarStore } from '@easyflow/stores';
|
||||
import { storeToRefs, useTabbarStore } from '@easyflow/stores';
|
||||
|
||||
import { IFrameRouterView } from '../../iframe';
|
||||
import { getRouteViewKey, shouldUseKeepAlive } from './route-view';
|
||||
|
||||
defineOptions({ name: 'LayoutContent' });
|
||||
|
||||
@@ -106,7 +107,7 @@ function transformComponent(
|
||||
appear
|
||||
>
|
||||
<KeepAlive
|
||||
v-if="keepAlive"
|
||||
v-if="shouldUseKeepAlive(route, keepAlive)"
|
||||
:exclude="getExcludeCachedTabs"
|
||||
:include="getCachedTabs"
|
||||
>
|
||||
@@ -114,18 +115,18 @@ function transformComponent(
|
||||
:is="transformComponent(Component, route)"
|
||||
v-if="renderRouteView"
|
||||
v-show="!route.meta.iframeSrc"
|
||||
:key="getTabKey(route)"
|
||||
:key="getRouteViewKey(route)"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component
|
||||
:is="Component"
|
||||
v-else-if="renderRouteView"
|
||||
:key="getTabKey(route)"
|
||||
:key="getRouteViewKey(route)"
|
||||
/>
|
||||
</Transition>
|
||||
<template v-else>
|
||||
<KeepAlive
|
||||
v-if="keepAlive"
|
||||
v-if="shouldUseKeepAlive(route, keepAlive)"
|
||||
:exclude="getExcludeCachedTabs"
|
||||
:include="getCachedTabs"
|
||||
>
|
||||
@@ -133,13 +134,13 @@ function transformComponent(
|
||||
:is="transformComponent(Component, route)"
|
||||
v-if="renderRouteView"
|
||||
v-show="!route.meta.iframeSrc"
|
||||
:key="getTabKey(route)"
|
||||
:key="getRouteViewKey(route)"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component
|
||||
:is="Component"
|
||||
v-else-if="renderRouteView"
|
||||
:key="getTabKey(route)"
|
||||
:key="getRouteViewKey(route)"
|
||||
/>
|
||||
</template>
|
||||
</RouterView>
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
|
||||
|
||||
import { getTabKey } from '@easyflow/stores';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getRouteViewKey, shouldUseKeepAlive } from './route-view';
|
||||
|
||||
interface RouteOptions {
|
||||
fullPath: string;
|
||||
id?: string;
|
||||
keepAlive?: boolean;
|
||||
name: string;
|
||||
navTitle?: string;
|
||||
pageKey?: string;
|
||||
paramId?: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
function createRoute(
|
||||
options: RouteOptions,
|
||||
): RouteLocationNormalizedLoadedGeneric {
|
||||
const query: Record<string, string> = {};
|
||||
if (options.id) {
|
||||
query.id = options.id;
|
||||
}
|
||||
if (options.navTitle) {
|
||||
query.navTitle = options.navTitle;
|
||||
}
|
||||
if (options.pageKey) {
|
||||
query.pageKey = options.pageKey;
|
||||
}
|
||||
return {
|
||||
fullPath: options.fullPath,
|
||||
hash: '',
|
||||
matched: [],
|
||||
meta: {
|
||||
keepAlive: options.keepAlive,
|
||||
title: options.name,
|
||||
},
|
||||
name: options.name,
|
||||
params: options.paramId ? { id: options.paramId } : {},
|
||||
path: options.path,
|
||||
query,
|
||||
redirectedFrom: undefined,
|
||||
} as RouteLocationNormalizedLoadedGeneric;
|
||||
}
|
||||
|
||||
describe('route view identity', () => {
|
||||
it('separates list and detail rendering while preserving one tab key', () => {
|
||||
const listRoute = createRoute({
|
||||
fullPath: '/ai/workflow',
|
||||
keepAlive: false,
|
||||
name: 'WorkflowList',
|
||||
path: '/ai/workflow',
|
||||
});
|
||||
const designRoute = createRoute({
|
||||
fullPath:
|
||||
'/ai/workflow/design?id=1&pageKey=/ai/workflow&navTitle=Workflow',
|
||||
id: '1',
|
||||
name: 'WorkflowDesign',
|
||||
navTitle: 'Workflow',
|
||||
pageKey: '/ai/workflow',
|
||||
path: '/ai/workflow/design',
|
||||
});
|
||||
|
||||
expect(getTabKey(listRoute)).toBe('/ai/workflow');
|
||||
expect(getTabKey(designRoute)).toBe('/ai/workflow');
|
||||
expect(getRouteViewKey(listRoute)).not.toBe(getRouteViewKey(designRoute));
|
||||
});
|
||||
|
||||
it('keeps title-only query changes on the same component instance', () => {
|
||||
const firstRoute = createRoute({
|
||||
fullPath: '/ai/workflow/design?id=1&pageKey=/ai/workflow&navTitle=First',
|
||||
id: '1',
|
||||
name: 'WorkflowDesign',
|
||||
navTitle: 'First',
|
||||
pageKey: '/ai/workflow',
|
||||
path: '/ai/workflow/design',
|
||||
});
|
||||
const renamedRoute = createRoute({
|
||||
fullPath:
|
||||
'/ai/workflow/design?id=1&pageKey=/ai/workflow&navTitle=Renamed',
|
||||
id: '1',
|
||||
name: 'WorkflowDesign',
|
||||
navTitle: 'Renamed',
|
||||
pageKey: '/ai/workflow',
|
||||
path: '/ai/workflow/design',
|
||||
});
|
||||
|
||||
expect(getRouteViewKey(firstRoute)).toBe(getRouteViewKey(renamedRoute));
|
||||
});
|
||||
|
||||
it('replaces the component instance when the resource id changes', () => {
|
||||
const firstRoute = createRoute({
|
||||
fullPath: '/ai/workflow/design?id=1&pageKey=/ai/workflow',
|
||||
id: '1',
|
||||
name: 'WorkflowDesign',
|
||||
pageKey: '/ai/workflow',
|
||||
path: '/ai/workflow/design',
|
||||
});
|
||||
const secondRoute = createRoute({
|
||||
fullPath: '/ai/workflow/design?id=2&pageKey=/ai/workflow',
|
||||
id: '2',
|
||||
name: 'WorkflowDesign',
|
||||
pageKey: '/ai/workflow',
|
||||
path: '/ai/workflow/design',
|
||||
});
|
||||
|
||||
expect(getRouteViewKey(firstRoute)).not.toBe(getRouteViewKey(secondRoute));
|
||||
});
|
||||
|
||||
it('separates path-param resources that share a tab key', () => {
|
||||
const firstRoute = createRoute({
|
||||
fullPath: '/ai/agents/designer/1?pageKey=/ai/agents',
|
||||
name: 'AgentDesigner',
|
||||
pageKey: '/ai/agents',
|
||||
paramId: '1',
|
||||
path: '/ai/agents/designer/1',
|
||||
});
|
||||
const secondRoute = createRoute({
|
||||
fullPath: '/ai/agents/designer/2?pageKey=/ai/agents',
|
||||
name: 'AgentDesigner',
|
||||
pageKey: '/ai/agents',
|
||||
paramId: '2',
|
||||
path: '/ai/agents/designer/2',
|
||||
});
|
||||
|
||||
expect(getRouteViewKey(firstRoute)).not.toBe(getRouteViewKey(secondRoute));
|
||||
});
|
||||
|
||||
it('bypasses KeepAlive only when caching is explicitly disabled', () => {
|
||||
const uncachedRoute = createRoute({
|
||||
fullPath: '/ai/workflow',
|
||||
keepAlive: false,
|
||||
name: 'WorkflowList',
|
||||
path: '/ai/workflow',
|
||||
});
|
||||
const defaultRoute = createRoute({
|
||||
fullPath: '/ai/workflow/design?id=1',
|
||||
id: '1',
|
||||
name: 'WorkflowDesign',
|
||||
path: '/ai/workflow/design',
|
||||
});
|
||||
|
||||
expect(shouldUseKeepAlive(uncachedRoute, true)).toBe(false);
|
||||
expect(shouldUseKeepAlive(defaultRoute, true)).toBe(true);
|
||||
expect(shouldUseKeepAlive(defaultRoute, false)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
|
||||
|
||||
import { getTabKey } from '@easyflow/stores';
|
||||
|
||||
function resolveRouteResourceId(
|
||||
route: RouteLocationNormalizedLoadedGeneric,
|
||||
): string {
|
||||
const rawId = route.params?.id ?? route.query?.id;
|
||||
const resourceId = Array.isArray(rawId) ? rawId[0] : rawId;
|
||||
return resourceId === null || resourceId === undefined
|
||||
? ''
|
||||
: String(resourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成独立于页签分组语义的页面渲染 key。
|
||||
*
|
||||
* 同一个 pageKey 可以继续合并到同一页签,但不同路由组件或业务资源
|
||||
* 必须拥有不同的虚拟节点身份,避免 KeepAlive 复用错误页面。
|
||||
*/
|
||||
function getRouteViewKey(route: RouteLocationNormalizedLoadedGeneric): string {
|
||||
return JSON.stringify([
|
||||
getTabKey(route),
|
||||
route.name === null || route.name === undefined
|
||||
? route.path
|
||||
: String(route.name),
|
||||
resolveRouteResourceId(route),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前路由是否应进入 KeepAlive 容器。
|
||||
*
|
||||
* 仅显式关闭缓存的路由绕过 KeepAlive,保留其余路由的既有行为。
|
||||
*/
|
||||
function shouldUseKeepAlive(
|
||||
route: RouteLocationNormalizedLoadedGeneric,
|
||||
keepAliveEnabled: boolean | undefined,
|
||||
): boolean {
|
||||
return keepAliveEnabled === true && route.meta.keepAlive !== false;
|
||||
}
|
||||
|
||||
export { getRouteViewKey, shouldUseKeepAlive };
|
||||
Reference in New Issue
Block a user