Skip to content

perf: perf the control logic of Tab #6220

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/src/guide/essentials/route.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,10 @@ interface RouteMeta {
| 'success'
| 'warning'
| string;
/**
* 路由的完整路径作为key(默认true)
*/
fullPathKey?: boolean;
/**
* 当前路由的子级在菜单中不展现
* @default false
Expand Down Expand Up @@ -502,6 +506,13 @@ interface RouteMeta {

用于配置页面的徽标颜色。

### fullPathKey

- 类型:`boolean`
- 默认值:`true`

是否将路由的完整路径作为tab key(默认true)

### activePath

- 类型:`string`
Expand Down Expand Up @@ -602,3 +613,32 @@ const { refresh } = useRefresh();
refresh();
</script>
```

## 标签页与路由控制

在某些场景下,需要单个路由打开多个标签页,或者修改路由的query不打开新的标签页

每个标签页Tab使用唯一的key标识,设置Tab key有三种方式,优先级由高到低:

- 使用路由query参数pageKey

```vue
<script setup lang="ts">
import { useRouter } from 'vue-router';
// 跳转路由
const router = useRouter();
router.push({
path: 'path',
query: {
pageKey: 'key',
},
});
```

- 路由的完整路径作为key

`meta` 属性中的 `fullPathKey`不为false,则使用路由`fullPath`作为key

- 路由的path作为key

`meta` 属性中的 `fullPathKey`为false,则使用路由`path`作为key
7 changes: 6 additions & 1 deletion packages/@core/base/typings/src/tabs.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import type { RouteLocationNormalized } from 'vue-router';

export type TabDefinition = RouteLocationNormalized;
export interface TabDefinition extends RouteLocationNormalized {
/**
* 标签页的key
*/
key?: string;
}
4 changes: 4 additions & 0 deletions packages/@core/base/typings/src/vue-router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ interface RouteMeta {
| 'success'
| 'warning'
| string;
/**
* 路由的完整路径作为key(默认true)
*/
fullPathKey?: boolean;
/**
* 当前路由的子级在菜单中不展现
* @default false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ const style = computed(() => {

const tabsView = computed(() => {
return props.tabs.map((tab) => {
const { fullPath, meta, name, path } = tab || {};
const { fullPath, meta, name, path, key } = tab || {};
const { affixTab, icon, newTabTitle, tabClosable, title } = meta || {};
return {
affixTab: !!affixTab,
closable: Reflect.has(meta, 'tabClosable') ? !!tabClosable : true,
fullPath,
icon: icon as string,
key: fullPath || path,
key,
meta,
name,
path,
Expand Down
4 changes: 2 additions & 2 deletions packages/@core/ui-kit/tabs-ui/src/components/tabs/tabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ const typeWithClass = computed(() => {
const tabsView = computed(() => {
return props.tabs.map((tab) => {
const { fullPath, meta, name, path } = tab || {};
const { fullPath, meta, name, path, key } = tab || {};
const { affixTab, icon, newTabTitle, tabClosable, title } = meta || {};
return {
affixTab: !!affixTab,
closable: Reflect.has(meta, 'tabClosable') ? !!tabClosable : true,
fullPath,
icon: icon as string,
key: fullPath || path,
key,
meta,
name,
path,
Expand Down
10 changes: 5 additions & 5 deletions packages/effects/layouts/src/basic/content/content.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { computed } from 'vue';
import { RouterView } from 'vue-router';

import { preferences, usePreferences } from '@vben/preferences';
import { storeToRefs, useTabbarStore } from '@vben/stores';
import { getTabKey, storeToRefs, useTabbarStore } from '@vben/stores';

import { IFrameRouterView } from '../../iframe';

Expand Down Expand Up @@ -115,13 +115,13 @@ function transformComponent(
:is="transformComponent(Component, route)"
v-if="renderRouteView"
v-show="!route.meta.iframeSrc"
:key="route.fullPath"
:key="getTabKey(route)"
/>
</KeepAlive>
<component
:is="Component"
v-else-if="renderRouteView"
:key="route.fullPath"
:key="getTabKey(route)"
/>
</Transition>
<template v-else>
Expand All @@ -134,13 +134,13 @@ function transformComponent(
:is="transformComponent(Component, route)"
v-if="renderRouteView"
v-show="!route.meta.iframeSrc"
:key="route.fullPath"
:key="getTabKey(route)"
/>
</KeepAlive>
<component
:is="Component"
v-else-if="renderRouteView"
:key="route.fullPath"
:key="getTabKey(route)"
/>
</template>
</RouterView>
Expand Down
2 changes: 1 addition & 1 deletion packages/effects/layouts/src/basic/tabbar/tabbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const {
} = useTabbar();

const menus = computed(() => {
const tab = tabbarStore.getTabByPath(currentActive.value);
const tab = tabbarStore.getTabByKey(currentActive.value);
const menus = createContextMenus(tab);
return menus.map((item) => {
return {
Expand Down
12 changes: 8 additions & 4 deletions packages/effects/layouts/src/basic/tabbar/use-tabbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
X,
} from '@vben/icons';
import { $t, useI18n } from '@vben/locales';
import { useAccessStore, useTabbarStore } from '@vben/stores';
import { getTabKey, useAccessStore, useTabbarStore } from '@vben/stores';
import { filterTree } from '@vben/utils';

export function useTabbar() {
Expand All @@ -44,8 +44,11 @@ export function useTabbar() {
toggleTabPin,
} = useTabs();

/**
* 当前路径对应的tab的key
*/
const currentActive = computed(() => {
return route.fullPath;
return getTabKey(route);
});

const { locale } = useI18n();
Expand Down Expand Up @@ -73,7 +76,8 @@ export function useTabbar() {

// 点击tab,跳转路由
const handleClick = (key: string) => {
router.push(key);
const { fullPath, path } = tabbarStore.getTabByKey(key);
router.push(fullPath || path);
};

// 关闭tab
Expand All @@ -100,7 +104,7 @@ export function useTabbar() {
);

watch(
() => route.path,
() => route.fullPath,
() => {
const meta = route.matched?.[route.matched.length - 1]?.meta;
tabbarStore.addTab({
Expand Down
39 changes: 22 additions & 17 deletions packages/stores/src/modules/tabbar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ describe('useAccessStore', () => {
const tab: any = {
fullPath: '/home',
meta: {},
key: '/home',
name: 'Home',
path: '/home',
};
store.addTab(tab);
const addNewTab = store.addTab(tab);
expect(store.tabs.length).toBe(1);
expect(store.tabs[0]).toEqual(tab);
expect(store.tabs[0]).toEqual(addNewTab);
});

it('adds a new tab if it does not exist', () => {
Expand All @@ -38,20 +39,22 @@ describe('useAccessStore', () => {
name: 'New',
path: '/new',
};
store.addTab(newTab);
expect(store.tabs).toContainEqual(newTab);
const addNewTab = store.addTab(newTab);
expect(store.tabs).toContainEqual(addNewTab);
});

it('updates an existing tab instead of adding a new one', () => {
const store = useTabbarStore();
const initialTab: any = {
fullPath: '/existing',
meta: {},
meta: {
fullPathKey: false,
},
name: 'Existing',
path: '/existing',
query: {},
};
store.tabs.push(initialTab);
store.addTab(initialTab);
const updatedTab = { ...initialTab, query: { id: '1' } };
store.addTab(updatedTab);
expect(store.tabs.length).toBe(1);
Expand All @@ -60,9 +63,12 @@ describe('useAccessStore', () => {

it('closes all tabs', async () => {
const store = useTabbarStore();
store.tabs = [
{ fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
] as any;
store.addTab({
fullPath: '/home',
meta: {},
name: 'Home',
path: '/home',
} as any);
router.replace = vi.fn();

await store.closeAllTabs(router);
Expand Down Expand Up @@ -157,7 +163,7 @@ describe('useAccessStore', () => {
path: '/contact',
} as any);

await store._bulkCloseByPaths(['/home', '/contact']);
await store._bulkCloseByKeys(['/home', '/contact']);

expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.name).toBe('About');
Expand All @@ -183,9 +189,8 @@ describe('useAccessStore', () => {
name: 'Contact',
path: '/contact',
};
store.addTab(targetTab);

await store.closeLeftTabs(targetTab);
const addTargetTab = store.addTab(targetTab);
await store.closeLeftTabs(addTargetTab);

expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.name).toBe('Contact');
Expand All @@ -205,15 +210,15 @@ describe('useAccessStore', () => {
name: 'About',
path: '/about',
};
store.addTab(targetTab);
const addTargetTab = store.addTab(targetTab);
store.addTab({
fullPath: '/contact',
meta: {},
name: 'Contact',
path: '/contact',
} as any);

await store.closeOtherTabs(targetTab);
await store.closeOtherTabs(addTargetTab);

expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.name).toBe('About');
Expand All @@ -227,7 +232,7 @@ describe('useAccessStore', () => {
name: 'Home',
path: '/home',
};
store.addTab(targetTab);
const addTargetTab = store.addTab(targetTab);
store.addTab({
fullPath: '/about',
meta: {},
Expand All @@ -241,7 +246,7 @@ describe('useAccessStore', () => {
path: '/contact',
} as any);

await store.closeRightTabs(targetTab);
await store.closeRightTabs(addTargetTab);

expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.name).toBe('Home');
Expand Down
Loading