Browse Source

perf: optimize the closing jump logic of tabs

 * 依据tab访问历史回退上一个tab,原逻辑是返回一下个 或 上一个
zhongming4762 4 months ago
parent
commit
331da3c8c7

+ 107 - 0
packages/@core/base/shared/src/utils/__tests__/stack.test.ts

@@ -0,0 +1,107 @@
+import { beforeEach, describe, expect, it } from 'vitest';
+
+import { createStack, Stack } from '../stack';
+
+describe('stack', () => {
+  let stack: Stack<number>;
+
+  beforeEach(() => {
+    stack = new Stack<number>();
+  });
+
+  it('push & size should work', () => {
+    stack.push(1, 2);
+
+    expect(stack.size).toBe(2);
+  });
+
+  it('peek should return top element without removing it', () => {
+    stack.push(1, 2);
+
+    expect(stack.peek()).toBe(2);
+    expect(stack.size).toBe(2);
+  });
+
+  it('pop should remove and return top element', () => {
+    stack.push(1, 2);
+
+    expect(stack.pop()).toBe(2);
+    expect(stack.size).toBe(1);
+    expect(stack.peek()).toBe(1);
+  });
+
+  it('pop on empty stack should return undefined', () => {
+    expect(stack.pop()).toBeUndefined();
+    expect(stack.peek()).toBeUndefined();
+  });
+
+  it('clear should remove all elements', () => {
+    stack.push(1, 2);
+
+    stack.clear();
+
+    expect(stack.size).toBe(0);
+    expect(stack.peek()).toBeUndefined();
+  });
+
+  it('toArray should return a shallow copy', () => {
+    stack.push(1, 2);
+
+    const arr = stack.toArray();
+    arr.push(3);
+
+    expect(stack.size).toBe(2);
+    expect(stack.toArray()).toEqual([1, 2]);
+  });
+
+  it('dedup should remove existing item before push', () => {
+    stack.push(1, 2, 1);
+
+    expect(stack.toArray()).toEqual([2, 1]);
+    expect(stack.size).toBe(2);
+  });
+
+  it('dedup = false should allow duplicate items', () => {
+    const s = new Stack<number>(false);
+
+    s.push(1, 1, 1);
+
+    expect(s.toArray()).toEqual([1, 1, 1]);
+    expect(s.size).toBe(3);
+  });
+
+  it('remove should delete all matching items', () => {
+    stack.push(1, 2, 1);
+
+    stack.remove(1);
+
+    expect(stack.toArray()).toEqual([2]);
+    expect(stack.size).toBe(1);
+  });
+
+  it('maxSize should limit stack capacity', () => {
+    const s = new Stack<number>(true, 3);
+
+    s.push(1, 2, 3, 4);
+
+    expect(s.toArray()).toEqual([2, 3, 4]);
+    expect(s.size).toBe(3);
+  });
+
+  it('dedup + maxSize should work together', () => {
+    const s = new Stack<number>(true, 3);
+
+    s.push(1, 2, 3, 2); // 去重并重新入栈
+
+    expect(s.toArray()).toEqual([1, 3, 2]);
+    expect(s.size).toBe(3);
+  });
+
+  it('createStack should create a stack instance', () => {
+    const s = createStack<number>(true, 2);
+
+    s.push(1, 2, 3);
+
+    expect(s.toArray()).toEqual([2, 3]);
+  });
+});

+ 1 - 0
packages/@core/base/shared/src/utils/index.ts

@@ -8,6 +8,7 @@ export * from './letter';
 export * from './merge';
 export * from './nprogress';
 export * from './resources';
+export * from './stack';
 export * from './state-handler';
 export * from './to';
 export * from './tree';

+ 103 - 0
packages/@core/base/shared/src/utils/stack.ts

@@ -0,0 +1,103 @@
+/**
+ * @zh_CN 栈数据结构
+ */
+export class Stack<T> {
+  /**
+   * @zh_CN 栈内元素数量
+   */
+  get size() {
+    return this.items.length;
+  }
+  /**
+   * @zh_CN 是否去重
+   */
+  private readonly dedup: boolean;
+  /**
+   * @zh_CN 栈内元素
+   */
+  private items: T[] = [];
+
+  /**
+   * @zh_CN 栈的最大容量
+   */
+  private readonly maxSize?: number;
+
+  constructor(dedup = true, maxSize?: number) {
+    this.maxSize = maxSize;
+    this.dedup = dedup;
+  }
+
+  /**
+   * @zh_CN 清空栈内元素
+   */
+  clear() {
+    this.items.length = 0;
+  }
+
+  /**
+   * @zh_CN 查看栈顶元素
+   * @returns 栈顶元素
+   */
+  peek(): T | undefined {
+    return this.items[this.items.length - 1];
+  }
+
+  /**
+   * @zh_CN 出栈
+   * @returns 栈顶元素
+   */
+  pop(): T | undefined {
+    return this.items.pop();
+  }
+
+  /**
+   * @zh_CN 入栈
+   * @param items 要入栈的元素
+   */
+  push(...items: T[]) {
+    items.forEach((item) => {
+      // 去重
+      if (this.dedup) {
+        const index = this.items.indexOf(item);
+        if (index !== -1) {
+          this.items.splice(index, 1);
+        }
+      }
+      this.items.push(item);
+      if (this.maxSize && this.items.length > this.maxSize) {
+        this.items.splice(0, this.items.length - this.maxSize);
+      }
+    });
+  }
+  /**
+   * @zh_CN 移除栈内元素
+   * @param itemList 要移除的元素列表
+   */
+  remove(...itemList: T[]) {
+    this.items = this.items.filter((i) => !itemList.includes(i));
+  }
+  /**
+   * @zh_CN 保留栈内元素
+   * @param itemList 要保留的元素列表
+   */
+  retain(itemList: T[]) {
+    this.items = this.items.filter((i) => itemList.includes(i));
+  }
+
+  /**
+   * @zh_CN 转换为数组
+   * @returns 栈内元素数组
+   */
+  toArray(): T[] {
+    return [...this.items];
+  }
+}
+
+/**
+ * @zh_CN 创建一个栈实例
+ * @param dedup 是否去重
+ * @param maxSize 栈的最大容量
+ * @returns 栈实例
+ */
+export const createStack = <T>(dedup = true, maxSize?: number) =>
+  new Stack<T>(dedup, maxSize);

+ 32 - 18
packages/stores/src/modules/tabbar.ts

@@ -11,7 +11,9 @@ import { toRaw } from 'vue';
 
 import { preferences } from '@vben-core/preferences';
 import {
+  createStack,
   openRouteInNewWindow,
+  Stack,
   startProgress,
   stopProgress,
 } from '@vben-core/shared/utils';
@@ -47,8 +49,17 @@ interface TabbarState {
    * @zh_CN 更新时间,用于一些更新场景,使用watch深度监听的话,会损耗性能
    */
   updateTime?: number;
+  /**
+   * @zh_CN 上一个标签页打开的标签
+   */
+  visitHistory: Stack<string>;
 }
 
+/**
+ * @zh_CN 访问历史记录最大数量
+ */
+const MAX_VISIT_HISTORY = 50;
+
 /**
  * @zh_CN 访问权限相关
  */
@@ -62,6 +73,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
       this.tabs = this.tabs.filter(
         (item) => !keySet.has(getTabKeyFromTab(item)),
       );
+      this.visitHistory.remove(...keys);
 
       await this.updateCacheTabs();
     },
@@ -166,6 +178,8 @@ export const useTabbarStore = defineStore('core-tabbar', {
         this.tabs.splice(tabIndex, 1, mergedTab);
       }
       this.updateCacheTabs();
+      // 添加访问历史记录
+      this.visitHistory.push(tab.key as string);
       return tab;
     },
     /**
@@ -174,6 +188,8 @@ export const useTabbarStore = defineStore('core-tabbar', {
     async closeAllTabs(router: Router) {
       const newTabs = this.tabs.filter((tab) => isAffixTab(tab));
       this.tabs = newTabs.length > 0 ? newTabs : [...this.tabs].splice(0, 1);
+      // 设置访问历史
+      this.visitHistory.retain(this.tabs.map((item) => getTabKeyFromTab(item)));
       await this._goToDefaultTab(router);
       this.updateCacheTabs();
     },
@@ -249,29 +265,26 @@ export const useTabbarStore = defineStore('core-tabbar', {
      */
     async closeTab(tab: TabDefinition, router: Router) {
       const { currentRoute } = router;
+      const currentTabKey = getTabKey(currentRoute.value);
       // 关闭不是激活选项卡
-      if (getTabKey(currentRoute.value) !== getTabKeyFromTab(tab)) {
+      if (currentTabKey !== getTabKeyFromTab(tab)) {
         this._close(tab);
         this.updateCacheTabs();
+        // 移除访问历史
+        this.visitHistory.remove(getTabKeyFromTab(tab));
         return;
       }
-      const index = this.getTabs.findIndex(
-        (item) => getTabKeyFromTab(item) === getTabKey(currentRoute.value),
-      );
-
-      const before = this.getTabs[index - 1];
-      const after = this.getTabs[index + 1];
-
-      // 下一个tab存在,跳转到下一个
-      if (after) {
-        this._close(tab);
-        await this._goToTab(after, router);
-        // 上一个tab存在,跳转到上一个
-      } else if (before) {
-        this._close(tab);
-        await this._goToTab(before, router);
-      } else {
+      if (this.getTabs.length <= 1) {
         console.error('Failed to close the tab; only one tab remains open.');
+        return;
+      }
+      // 从访问历史记录中移除当前关闭的tab
+      this.visitHistory.remove(currentTabKey);
+      this._close(tab);
+      const previousTabKey = this.visitHistory.pop();
+      if (previousTabKey) {
+        // 跳转到上一个tab
+        await this._goToTab(this.getTabByKey(previousTabKey), router);
       }
     },
 
@@ -527,11 +540,12 @@ export const useTabbarStore = defineStore('core-tabbar', {
   persist: [
     // tabs不需要保存在localStorage
     {
-      pick: ['tabs'],
+      pick: ['tabs', 'visitHistory'],
       storage: sessionStorage,
     },
   ],
   state: (): TabbarState => ({
+    visitHistory: createStack<string>(true, MAX_VISIT_HISTORY),
     cachedTabs: new Set(),
     dragEndIndex: 0,
     excludeCachedTabs: new Set(),