From aa74a2535ba3d69b9a71b44df6b623c781e9772d Mon Sep 17 00:00:00 2001 From: AxiosLeo <13862149+AxiosLeo@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:09:37 +0800 Subject: [PATCH] fix(tabbar): visitHistory field (#7543) High Severity The visitHistory field is a Stack class instance persisted to sessionStorage via pinia-plugin-persistedstate. There's no custom serializer or hydration hook. When the page reloads, JSON.parse(JSON.stringify(stack)) produces a plain object {dedup, items, maxSize} that lacks all Stack methods (push, pop, remove, retain, etc.) and the size getter. Pinia's $patch replaces the Stack instance with this plain object, so subsequent calls like this.visitHistory.push(...) will throw a TypeError. --- packages/stores/src/modules/tabbar.ts | 75 ++++++++++++--------------- 1 file changed, 32 insertions(+), 43 deletions(-) diff --git a/packages/stores/src/modules/tabbar.ts b/packages/stores/src/modules/tabbar.ts index 6b18c26e..8d18d442 100644 --- a/packages/stores/src/modules/tabbar.ts +++ b/packages/stores/src/modules/tabbar.ts @@ -1,9 +1,5 @@ import type { ComputedRef } from 'vue'; -import type { - RouteLocationNormalized, - Router, - RouteRecordNormalized, -} from 'vue-router'; +import type { RouteLocationNormalized, Router, RouteRecordNormalized } from 'vue-router'; import type { TabDefinition } from '@vben-core/typings'; @@ -70,9 +66,7 @@ export const useTabbarStore = defineStore('core-tabbar', { */ async _bulkCloseByKeys(keys: string[]) { const keySet = new Set(keys); - this.tabs = this.tabs.filter( - (item) => !keySet.has(getTabKeyFromTab(item)), - ); + this.tabs = this.tabs.filter((item) => !keySet.has(getTabKeyFromTab(item))); if (isVisitHistory()) { this.visitHistory.remove(...keys); } @@ -136,25 +130,20 @@ export const useTabbarStore = defineStore('core-tabbar', { if (tabIndex === -1) { const maxCount = preferences.tabbar.maxCount; // 获取动态路由打开数,超过 0 即代表需要控制打开数 - const maxNumOfOpenTab = (routeTab?.meta?.maxNumOfOpenTab ?? - -1) as number; + const maxNumOfOpenTab = (routeTab?.meta?.maxNumOfOpenTab ?? -1) as number; // 如果动态路由层级大于 0 了,那么就要限制该路由的打开数限制了 // 获取到已经打开的动态路由数, 判断是否大于某一个值 if ( maxNumOfOpenTab > 0 && - this.tabs.filter((tab) => tab.name === routeTab.name).length >= - maxNumOfOpenTab + this.tabs.filter((tab) => tab.name === routeTab.name).length >= maxNumOfOpenTab ) { // 关闭第一个 - const index = this.tabs.findIndex( - (item) => item.name === routeTab.name, - ); + const index = this.tabs.findIndex((item) => item.name === routeTab.name); index !== -1 && this.tabs.splice(index, 1); } else if (maxCount > 0 && this.tabs.length >= maxCount) { // 关闭第一个 const index = this.tabs.findIndex( - (item) => - !Reflect.has(item.meta, 'affixTab') || !item.meta.affixTab, + (item) => !Reflect.has(item.meta, 'affixTab') || !item.meta.affixTab ); index !== -1 && this.tabs.splice(index, 1); } @@ -194,9 +183,7 @@ export const useTabbarStore = defineStore('core-tabbar', { this.tabs = newTabs.length > 0 ? newTabs : [...this.tabs].splice(0, 1); // 设置访问历史记录 if (isVisitHistory()) { - this.visitHistory.retain( - this.tabs.map((item) => getTabKeyFromTab(item)), - ); + this.visitHistory.retain(this.tabs.map((item) => getTabKeyFromTab(item))); } await this._goToDefaultTab(router); this.updateCacheTabs(); @@ -233,9 +220,7 @@ export const useTabbarStore = defineStore('core-tabbar', { for (const key of closeKeys) { if (key !== getTabKeyFromTab(tab)) { - const closeTab = this.tabs.find( - (item) => getTabKeyFromTab(item) === key, - ); + const closeTab = this.tabs.find((item) => getTabKeyFromTab(item) === key); if (!closeTab) { continue; } @@ -305,14 +290,12 @@ export const useTabbarStore = defineStore('core-tabbar', { break; } } - await (previousTab - ? this._goToTab(previousTab, router) - : this._goToDefaultTab(router)); + await (previousTab ? this._goToTab(previousTab, router) : this._goToDefaultTab(router)); return; } // 未开启访问历史记录,直接跳转下一个或上一个tab const index = this.getTabs.findIndex( - (item) => getTabKeyFromTab(item) === getTabKey(currentRoute.value), + (item) => getTabKeyFromTab(item) === getTabKey(currentRoute.value) ); const before = this.getTabs[index - 1]; @@ -336,9 +319,7 @@ export const useTabbarStore = defineStore('core-tabbar', { */ async closeTabByKey(key: string, router: Router) { const originKey = decodeURIComponent(key); - const index = this.tabs.findIndex( - (item) => getTabKeyFromTab(item) === originKey, - ); + const index = this.tabs.findIndex((item) => getTabKeyFromTab(item) === originKey); if (index === -1) { return; } @@ -354,9 +335,7 @@ export const useTabbarStore = defineStore('core-tabbar', { * @param key */ getTabByKey(key: string) { - return this.getTabs.find( - (item) => getTabKeyFromTab(item) === key, - ) as TabDefinition; + return this.getTabs.find((item) => getTabKeyFromTab(item) === key) as TabDefinition; }, /** * @zh_CN 新窗口打开标签页 @@ -583,6 +562,23 @@ export const useTabbarStore = defineStore('core-tabbar', { { pick: ['tabs', 'visitHistory'], storage: sessionStorage, + serializer: { + serialize: JSON.stringify, + deserialize(value: string) { + const parsed = JSON.parse(value); + // Stack 类实例经 JSON 序列化后会变成普通对象 {dedup, items, maxSize}, + // 丢失所有方法和 getter,需要重新构建 Stack 实例 + if (parsed.visitHistory && !(parsed.visitHistory instanceof Stack)) { + const raw = parsed.visitHistory; + const stack = createStack(true, MAX_VISIT_HISTORY); + if (Array.isArray(raw.items)) { + stack.push(...raw.items); + } + parsed.visitHistory = stack; + } + return parsed; + }, + }, }, ], state: (): TabbarState => ({ @@ -660,21 +656,14 @@ function isTabShown(tab: TabDefinition) { * @param tab */ function getTabKey(tab: RouteLocationNormalized | RouteRecordNormalized) { - const { - fullPath, - path, - meta: { fullPathKey } = {}, - query = {}, - } = tab as RouteLocationNormalized; + const { fullPath, path, meta: { fullPathKey } = {}, query = {} } = tab as RouteLocationNormalized; // pageKey可能是数组(查询参数重复时可能出现) - const pageKey = Array.isArray(query.pageKey) - ? query.pageKey[0] - : query.pageKey; + const pageKey = Array.isArray(query.pageKey) ? query.pageKey[0] : query.pageKey; let rawKey; if (pageKey) { rawKey = pageKey; } else { - rawKey = fullPathKey === false ? path : (fullPath ?? path); + rawKey = fullPathKey === false ? path : fullPath ?? path; } try { return decodeURIComponent(rawKey);