Skip to content

perf: optimize the closing jump logic of tabs - #7518

Merged
jinmao88 merged 4 commits into
vbenjs:mainfrom
ming4762:tab-2026020401
Feb 8, 2026
Merged

jinmao88 merged 4 commits into
vbenjs:mainfrom
ming4762:tab-2026020401

Conversation

@ming4762

@ming4762 ming4762 commented Feb 4, 2026 •

Copy link
Copy Markdown
Contributor
  • 依据tab访问历史回退上一个tab,原逻辑是返回下一个 或 上一个

Summary by CodeRabbit

  • New Features

    • Tab visit history: closing the active tab now navigates to the last visited tab; history can be toggled in preferences and is persisted for the session.
    • Added underlying stack-backed visit history with capacity and deduplication (transparent to users).
  • UI

    • Preference switch and tooltip added to enable/disable tab visit history.
  • Localization

    • English and Chinese strings for the new preference and tooltip.
  • Tests

    • Comprehensive tests added to validate stack behavior and history interactions.

 * 依据tab访问历史回退上一个tab,原逻辑是返回一下个 或 上一个
@changeset-bot

changeset-bot Bot commented Feb 4, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 633c5f3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Feb 4, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap is excluded by !**/*.snap

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review
📝 Walkthrough

Walkthrough

This PR adds a generic Stack implementation (with dedup and maxSize), test coverage for it, and integrates a visit-history Stack into the tabbar store plus preference/UI wiring to enable/disable visit history.

Changes

Cohort / File(s) Summary
Stack implementation & API
packages/@core/base/shared/src/utils/stack.ts, packages/@core/base/shared/src/utils/index.ts
Add new generic Stack<T> (dedup, maxSize, push/pop/peek/remove/retain/toArray), factory createStack, and re-export from utils index.
Stack tests
packages/@core/base/shared/src/utils/__tests__/stack.test.ts
Add comprehensive tests covering push/size/peek/pop/clear/toArray, dedup behavior, removal/retain, maxSize truncation, dedup+maxSize interactions, and factory creation.
Tabbar store integration
packages/stores/src/modules/tabbar.ts
Add visitHistory: Stack<string> to state, initialize with createStack(true, MAX_VISIT_HISTORY), persist it, and update add/close/closeAll/close bulk logic to use visit history for navigation and removal.
Preferences types & defaults
packages/@core/preferences/src/types.ts, packages/@core/preferences/src/config.ts
Add visitHistory: boolean to TabbarPreferences and enable it by default in defaultPreferences.
Preferences UI wiring
packages/effects/layouts/src/widgets/preferences/blocks/layout/tabbar.vue, packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue
Add tabbarVisitHistory model and SwitchItem in tabbar preferences; wire v-model into preferences drawer component.
Locale additions
packages/locales/src/langs/en-US/preferences.json, packages/locales/src/langs/zh-CN/preferences.json
Add tabbar.visitHistory and tabbar.visitHistoryTip localization keys.

Sequence Diagram

sequenceDiagram
    actor User
    participant TabComponent as Tab Component
    participant TabbarStore as Tabbar Store
    participant VisitHistory as Visit History (Stack)
    participant SessionStorage as Session Storage

    User->>TabComponent: Click tab / Select tab
    TabComponent->>TabbarStore: addTab / selectTab(key)
    TabbarStore->>VisitHistory: push(key)
    VisitHistory->>VisitHistory: dedup + enforce maxSize
    TabbarStore->>SessionStorage: persist visitHistory

    User->>TabComponent: Close tab (active)
    TabComponent->>TabbarStore: closeTab(key)
    TabbarStore->>VisitHistory: remove(key)
    TabbarStore->>VisitHistory: peek()
    alt previous tab exists
        TabbarStore->>TabComponent: navigate to previous tab
    else none
        TabbarStore->>TabComponent: fallback navigation (default/next)
    end
    TabbarStore->>SessionStorage: persist visitHistory
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • anncwb
  • vince292007
  • jinmao88
  • mynetfan

Poem

🐇 I hopped in code to build a stack so bright,
I store each visit, from morning to night,
Dedup and trim, the history I keep,
When tabs close, I lead the jump — not too steep,
Hop on, go back, your navigation's light.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is minimal and in Chinese, missing critical sections from the template including detailed change description, type of change checkbox selection, and checklist verification. The template is not followed. Provide a comprehensive English description following the repository template, including the change details, type selection, and completion of all checklist items.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'perf: optimize the closing jump logic of tabs' accurately describes the main change—improving tab navigation when closing by implementing visit history-based logic instead of next/previous fallback.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/stores/src/modules/tabbar.ts (1)

135-158: ⚠️ Potential issue | 🟠 Major

Keep visitHistory in sync when auto-evicting tabs.
When maxNumOfOpenTab/maxCount triggers a splice, the evicted tab key remains in history and can be popped later, leading to navigation to a closed tab. Remove the evicted key at the same time.

🛠️ Suggested fix
-          index !== -1 && this.tabs.splice(index, 1);
+          if (index !== -1) {
+            const [removed] = this.tabs.splice(index, 1);
+            if (removed) {
+              this.visitHistory.remove(getTabKeyFromTab(removed));
+            }
+          }
@@
-          index !== -1 && this.tabs.splice(index, 1);
+          if (index !== -1) {
+            const [removed] = this.tabs.splice(index, 1);
+            if (removed) {
+              this.visitHistory.remove(getTabKeyFromTab(removed));
+            }
+          }
🤖 Fix all issues with AI agents
In `@packages/stores/src/modules/tabbar.ts`:
- Around line 281-288: After closing a tab (you call
this.visitHistory.remove(currentTabKey) and this._close(tab)), guard against
stale keys returned by this.visitHistory.pop() by looping: repeatedly pop keys
and call this.getTabByKey(previousTabKey) until you find a non-undefined tab or
visitHistory is exhausted; if no valid tab is found, fall back to a safe default
tab (e.g., the first open tab or a home tab) and call await
this._goToTab(validTab, router). Ensure you don’t call this._goToTab with
undefined and keep using the existing methods _close, visitHistory.pop(),
getTabByKey, and _goToTab.
- Around line 540-548: Change the persist hook from afterRestore to afterHydrate
and, inside that afterHydrate handler for the tabbar store, detect when
visitHistory was deserialized and rehydrate it to a proper Stack instance: use
createStack(true, MAX_VISIT_HISTORY) to create a new stack, populate it with the
deserialized items (preserving order) and assign it back to visitHistory on the
store (TabbarState.visitHistory), so the restored object regains its methods
(push/remove/pop/retain); ensure this hook runs only for the persisted keys
(pick includes 'visitHistory') and replaces the plain object with the new Stack
instance.

Comment thread packages/stores/src/modules/tabbar.ts Resolved
Comment on lines 540 to +548
persist: [
// tabs不需要保存在localStorage
{
pick: ['tabs'],
pick: ['tabs', 'visitHistory'],
storage: sessionStorage,
},
],
state: (): TabbarState => ({
visitHistory: createStack<string>(true, MAX_VISIT_HISTORY),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the tabbar.ts file and examine the full persist configuration
fd -t f "tabbar.ts" | head -5

Repository: vbenjs/vue-vben-admin

Length of output: 158


🏁 Script executed:

#!/bin/bash
# Search for Stack implementation
rg -n "createStack|class Stack|export.*Stack" -A 3

Repository: vbenjs/vue-vben-admin

Length of output: 2233


🏁 Script executed:

#!/bin/bash
# Look for persistence plugin configuration and any existing hooks
rg -n "pinia-plugin-persistedstate|createPersistedState|afterRestore|beforeRestore" -A 3 -B 1

Repository: vbenjs/vue-vben-admin

Length of output: 2542


🏁 Script executed:

#!/bin/bash
# Search for how visitHistory is used in the codebase to understand method calls
rg -n "visitHistory\." | head -20

Repository: vbenjs/vue-vben-admin

Length of output: 644


🏁 Script executed:

#!/bin/bash
# Read the full persist configuration in tabbar.ts
sed -n '535,560p' packages/stores/src/modules/tabbar.ts

Repository: vbenjs/vue-vben-admin

Length of output: 721


🏁 Script executed:

#!/bin/bash
# Read the Stack class implementation to understand its structure
cat -n packages/@core/base/shared/src/utils/stack.ts

Repository: vbenjs/vue-vben-admin

Length of output: 2573


🏁 Script executed:

#!/bin/bash
# Check if there's already a restore hook or serializer in the persistence setup
rg -n "afterRestore|beforeRestore|serializer|merge" packages/stores/src/modules/tabbar.ts -B 2 -A 5

Repository: vbenjs/vue-vben-admin

Length of output: 905


🏁 Script executed:

#!/bin/bash
# Find MAX_VISIT_HISTORY constant
rg -n "MAX_VISIT_HISTORY" packages/stores/src/modules/tabbar.ts -B 2 -A 2

Repository: vbenjs/vue-vben-admin

Length of output: 310


🌐 Web query:

pinia-plugin-persistedstate v4.7.1 afterRestore onHydrate hook signature documentation

💡 Result:

In pinia-plugin-persistedstate v4.7.1, the old v2 hook names beforeRestore / afterRestore were renamed to beforeHydrate / afterHydrate (and the global beforeRestore/afterRestore options were dropped in v4). [3]

v4.7.1 hook signatures (docs)

  • beforeHydrate?: (context: PiniaPluginContext) => void [1]
  • afterHydrate?: (context: PiniaPluginContext) => void [1]

So if you’re looking for afterRestore, the v4 equivalent is afterHydrate(ctx). [1][3]

“onHydrate”?

There isn’t an onHydrate hook in the v4 docs; hydration is done by the plugin, and you can also trigger it manually via:

  • store.$hydrate({ runHooks: false }) (by default it runs beforeHydrate/afterHydrate). [2]

Where afterRestore is documented (older docs)

If you specifically need the afterRestore signature, that’s documented in the v2 docs as:

  • afterRestore?: (context: PiniaPluginContext) => void [4]

Sources: [1] v4 config docs (beforeHydrate/afterHydrate types), [2] v4 advanced docs ($hydrate and runHooks), [3] v4.0.0 release notes (rename before/afterRestore → before/afterHydrate), [4] v2 config docs (afterRestore type).


Fix hook name: afterRestore → afterHydrate for pinia-plugin-persistedstate v4.7.1.

Persisting Stack directly will break after restore. Pinia's persistence serializes to JSON, so visitHistory deserializes as a plain object and loses methods (push/remove/pop/retain), causing runtime crashes. Rehydrate to a Stack instance in an afterHydrate hook.

🛠️ Suggested fix
  persist: [
    // tabs不需要保存在localStorage
    {
      pick: ['tabs', 'visitHistory'],
      storage: sessionStorage,
+      afterHydrate: (ctx) => {
+        const raw = ctx.store.visitHistory as any;
+        const history = createStack<string>(true, MAX_VISIT_HISTORY);
+        const items = Array.isArray(raw) ? raw : raw?.items ?? raw?.toArray?.() ?? [];
+        history.push(...items);
+        ctx.store.visitHistory = history;
+      },
    },
  ],
🤖 Prompt for AI Agents
In `@packages/stores/src/modules/tabbar.ts` around lines 540 - 548, Change the
persist hook from afterRestore to afterHydrate and, inside that afterHydrate
handler for the tabbar store, detect when visitHistory was deserialized and
rehydrate it to a proper Stack instance: use createStack(true,
MAX_VISIT_HISTORY) to create a new stack, populate it with the deserialized
items (preserving order) and assign it back to visitHistory on the store
(TabbarState.visitHistory), so the restored object regains its methods
(push/remove/pop/retain); ensure this hook runs only for the persisted keys
(pick includes 'visitHistory') and replaces the plain object with the new Stack
instance.

@jinmao88

jinmao88 commented Feb 6, 2026

Copy link
Copy Markdown
Collaborator

关闭逻辑增加一个选项 可以定义用哪种模式

 * 依据tab访问历史回退上一个tab,原逻辑是返回一下个 或 上一个
 * 支持在配置中开启或关闭
 * 依据tab访问历史回退上一个tab,原逻辑是返回一下个 或 上一个
 * 支持在配置中开启或关闭

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/stores/src/modules/tabbar.ts`:
- Around line 183-186: The addTab call currently appends every tab to
visitHistory (via this.visitHistory.push) which causes affix tabs added by
setAffixTabs at startup to be recorded as user visits; change addTab (or the
call site in setAffixTabs) to skip pushing to visitHistory for affix tabs by
checking tab.affix (or add an optional parameter like skipHistory) and only call
this.visitHistory.push when isVisitHistory() is true AND the tab is not an affix
tab.
🧹 Nitpick comments (1)
packages/stores/src/modules/tabbar.ts (1)

686-691: isVisitHistory() reads a reactive preference outside of a reactive context.

This helper accesses preferences.tabbar.visitHistory directly (not via computed or inside a Vue reactive scope). Within Pinia actions this is fine since it's called imperatively, but be aware this won't track reactively if ever used in a getter or computed property.

Comment on lines +183 to +186
// 添加访问历史记录
if (isVisitHistory()) {
this.visitHistory.push(tab.key as string);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

addTab is called for affix tabs during setAffixTabs — history records affix tabs too.

When setAffixTabs calls addTab for each affix tab at startup, those tabs get pushed into visitHistory. This means the visit history will contain affix tab entries from initialization, which may not reflect actual user visits. Consider whether this is intentional — if not, you could skip the history push for affix tabs.

🤖 Prompt for AI Agents
In `@packages/stores/src/modules/tabbar.ts` around lines 183 - 186, The addTab
call currently appends every tab to visitHistory (via this.visitHistory.push)
which causes affix tabs added by setAffixTabs at startup to be recorded as user
visits; change addTab (or the call site in setAffixTabs) to skip pushing to
visitHistory for affix tabs by checking tab.affix (or add an optional parameter
like skipHistory) and only call this.visitHistory.push when isVisitHistory() is
true AND the tab is not an affix tab.

 * 依据tab访问历史回退上一个tab,原逻辑是返回一下个 或 上一个
 * 支持在配置中开启或关闭
@ming4762

ming4762 commented Feb 8, 2026

Copy link
Copy Markdown
Contributor Author

@jinmao88
#7521 导致lint检测失败

@jinmao88
jinmao88 merged commit 57911d9 into vbenjs:main Feb 8, 2026
11 of 13 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Feb 11, 2026
5 of 16 tasks
@ming4762
ming4762 deleted the tab-2026020401 branch February 13, 2026 09:53
@github-actions github-actions Bot locked and limited conversation to collaborators Mar 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants