vue-stack-tabs 1.1.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.en.md +526 -0
  2. package/README.md +141 -39
  3. package/README.ru.md +526 -0
  4. package/dist/iframe-bridge.d.ts +8 -0
  5. package/dist/iframe-bridge.html +77 -0
  6. package/dist/iframe-bridge.mjs +20 -0
  7. package/dist/iframe-child.html +19 -6
  8. package/dist/nuxt/favicon.svg +10 -0
  9. package/dist/nuxt/iframe-bridge.html +77 -0
  10. package/dist/nuxt/iframe-child.html +88 -0
  11. package/dist/nuxt/module.d.ts +6 -0
  12. package/dist/nuxt/module.mjs +28 -0
  13. package/dist/nuxt/plugin.mjs +9 -0
  14. package/dist/nuxt/runtime/plugin.d.ts +2 -0
  15. package/dist/nuxt/runtime/plugin.mjs +9 -0
  16. package/dist/src/lib/StackTabs.vue.d.ts +30 -16
  17. package/dist/src/lib/components/ContextMenu/ContextMenuItem.vue.d.ts +7 -1
  18. package/dist/src/lib/components/ContextMenu/index.vue.d.ts +5 -4
  19. package/dist/src/lib/components/PageLoading.vue.d.ts +2 -1
  20. package/dist/src/lib/components/StackKeepAlive/StackCacheRenderer.vue.d.ts +25 -0
  21. package/dist/src/lib/components/StackKeepAlive/StackKeepAlive.vue.d.ts +12 -0
  22. package/dist/src/lib/components/TabHeader/TabHeaderButton.vue.d.ts +11 -12
  23. package/dist/src/lib/components/TabHeader/TabHeaderItem.vue.d.ts +5 -4
  24. package/dist/src/lib/components/TabHeader/TabHeaderScroll.vue.d.ts +8 -22
  25. package/dist/src/lib/components/TabHeader/index.vue.d.ts +11 -164
  26. package/dist/src/lib/hooks/stackTabsContext.d.ts +49 -0
  27. package/dist/src/lib/hooks/tabPanel/evict.d.ts +21 -61
  28. package/dist/src/lib/hooks/tabPanel/navigationTransaction.d.ts +35 -0
  29. package/dist/src/lib/hooks/tabPanel/refresh.d.ts +17 -0
  30. package/dist/src/lib/hooks/tabPanel/scroll.d.ts +8 -8
  31. package/dist/src/lib/hooks/tabPanel/session.d.ts +9 -8
  32. package/dist/src/lib/hooks/tabPanel/state.d.ts +20 -72
  33. package/dist/src/lib/hooks/useIframeManager.d.ts +35 -0
  34. package/dist/src/lib/hooks/useTabActions.d.ts +5 -46
  35. package/dist/src/lib/hooks/useTabEventBus.d.ts +22 -9
  36. package/dist/src/lib/hooks/useTabPanel.d.ts +14 -73
  37. package/dist/src/lib/i18n/index.d.ts +6 -4
  38. package/dist/src/lib/i18n/lang/en.d.ts +5 -0
  39. package/dist/src/lib/i18n/lang/zh-CN.d.ts +5 -0
  40. package/dist/src/lib/i18n/stackTabsLocale.d.ts +2 -0
  41. package/dist/src/lib/iframe-bridge.d.ts +8 -0
  42. package/dist/src/lib/iframe.vue.d.ts +2 -1
  43. package/dist/src/lib/index.d.ts +4 -5
  44. package/dist/src/lib/model/TabModel.d.ts +11 -40
  45. package/dist/src/lib/nuxt/module.d.ts +6 -0
  46. package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +2 -0
  47. package/dist/src/lib/nuxt/runtime/plugin.d.ts +2 -0
  48. package/dist/src/lib/utils/iframeBridge.d.ts +11 -3
  49. package/dist/src/lib/utils/stackTabsA11y.d.ts +8 -0
  50. package/dist/src/lib/utils/stackTabsMessage.d.ts +7 -0
  51. package/dist/src/lib/utils/tabInfoEncoder.d.ts +0 -15
  52. package/dist/src/lib/utils/typeGuards.d.ts +2 -0
  53. package/dist/src/lib/utils/urlParser.d.ts +24 -3
  54. package/dist/src/lib/versionLogger.d.ts +1 -0
  55. package/dist/style.css +2 -1
  56. package/dist/utils/iframeBridge.d.ts +39 -0
  57. package/dist/vue-stack-tabs.css +2 -1
  58. package/dist/vue-stack-tabs.es.js +1 -2220
  59. package/package.json +71 -50
  60. package/src/lib/StackTabs.vue +223 -196
  61. package/src/lib/assets/style/stackTab.scss +110 -13
  62. package/src/lib/components/ContextMenu/ContextMenuItem.vue +23 -11
  63. package/src/lib/components/ContextMenu/index.vue +94 -10
  64. package/src/lib/components/PageLoading.vue +9 -7
  65. package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +70 -0
  66. package/src/lib/components/StackKeepAlive/StackKeepAlive.vue +57 -0
  67. package/src/lib/components/TabHeader/TabHeaderButton.vue +19 -8
  68. package/src/lib/components/TabHeader/TabHeaderItem.vue +74 -17
  69. package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
  70. package/src/lib/components/TabHeader/index.vue +181 -13
  71. package/src/lib/hooks/stackTabsContext.ts +135 -0
  72. package/src/lib/hooks/tabPanel/evict.ts +122 -87
  73. package/src/lib/hooks/tabPanel/navigationTransaction.ts +61 -0
  74. package/src/lib/hooks/tabPanel/refresh.ts +109 -0
  75. package/src/lib/hooks/tabPanel/scroll.ts +57 -40
  76. package/src/lib/hooks/tabPanel/session.ts +84 -22
  77. package/src/lib/hooks/tabPanel/state.ts +41 -44
  78. package/src/lib/hooks/useIframeManager.ts +253 -0
  79. package/src/lib/hooks/useTabActions.ts +64 -30
  80. package/src/lib/hooks/useTabEventBus.ts +29 -19
  81. package/src/lib/hooks/useTabLoading.ts +3 -2
  82. package/src/lib/hooks/useTabPanel.tsx +290 -313
  83. package/src/lib/hooks/useTabRouter.ts +111 -97
  84. package/src/lib/i18n/index.ts +53 -45
  85. package/src/lib/i18n/lang/en.ts +6 -1
  86. package/src/lib/i18n/lang/zh-CN.ts +6 -1
  87. package/src/lib/i18n/stackTabsLocale.ts +8 -0
  88. package/src/lib/iframe-bridge.ts +12 -0
  89. package/src/lib/index.ts +14 -19
  90. package/src/lib/model/TabModel.ts +27 -82
  91. package/src/lib/nuxt/module.d.ts +4 -0
  92. package/src/lib/nuxt/module.ts +8 -3
  93. package/src/lib/nuxt/runtime/plugin.client.ts +1 -0
  94. package/src/lib/nuxt/runtime/plugin.ts +11 -0
  95. package/src/lib/utils/iframeBridge.ts +29 -5
  96. package/src/lib/utils/scrollUtils.ts +9 -4
  97. package/src/lib/utils/stackTabsA11y.ts +12 -0
  98. package/src/lib/utils/stackTabsMessage.ts +99 -0
  99. package/src/lib/utils/tabInfoEncoder.ts +70 -21
  100. package/src/lib/utils/typeGuards.ts +3 -0
  101. package/src/lib/utils/urlParser.ts +144 -14
  102. package/src/lib/versionLogger.ts +7 -19
  103. package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +0 -1
  104. package/dist/src/lib/hooks/useTabPanel.spec.d.ts +0 -1
  105. package/dist/src/lib/model/TabModel.spec.d.ts +0 -1
  106. package/dist/src/lib/utils/iframeBridge.spec.d.ts +0 -1
  107. package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +0 -1
  108. package/dist/src/lib/utils/urlParser.spec.d.ts +0 -1
  109. package/dist/vue-stack-tabs.umd.js +0 -1
  110. package/src/lib/hooks/tabPanel/evict.spec.ts +0 -85
  111. package/src/lib/hooks/useTabPanel.spec.ts +0 -34
  112. package/src/lib/model/TabModel.spec.ts +0 -40
  113. package/src/lib/utils/iframeBridge.spec.ts +0 -37
  114. package/src/lib/utils/tabInfoEncoder.spec.ts +0 -32
  115. package/src/lib/utils/urlParser.spec.ts +0 -45
@@ -18,53 +18,36 @@ import {
18
18
  onDeactivated,
19
19
  onMounted,
20
20
  onUnmounted,
21
+ computed,
21
22
  ref,
23
+ shallowRef,
22
24
  cloneVNode
23
25
  } from 'vue'
24
26
  import type { DefineComponent, VNode } from 'vue'
25
- import { useRouter } from 'vue-router'
27
+ import { isNavigationFailure, useRouter } from 'vue-router'
26
28
  import type { RouteLocationNormalizedLoaded } from 'vue-router'
27
- import { ulid } from 'ulid'
28
29
  import { encodeTabInfo, createPageId, decodeTabInfo } from '../utils/tabInfoEncoder'
29
30
  import { defu } from 'defu'
30
31
  import { Stack } from '../model/TabModel'
31
- import { parseUrl, isCrossOriginUrl } from '../utils/urlParser'
32
+ import {
33
+ parseUrl,
34
+ isCrossOriginUrl,
35
+ omitStackTabsReservedQuery,
36
+ toSafeTabUrl,
37
+ decodeSafeTabUrl,
38
+ cloneLocationQuery,
39
+ clonePage
40
+ } from '../utils/urlParser'
32
41
  import PageLoading from '../components/PageLoading.vue'
33
42
  import { TabEventType, useTabEmitter } from './useTabEventBus'
43
+ import { runNavigationTransaction } from './tabPanel/navigationTransaction'
34
44
  import { useI18n } from 'vue-i18n-lite'
35
45
 
36
- import {
37
- tabs,
38
- defaultTabs,
39
- caches,
40
- components,
41
- tabIdsToEvict,
42
- refreshKey,
43
- iframeRefreshKeys,
44
- isInitialized,
45
- maxTabCount,
46
- useGlobalScroll,
47
- cacheIdsToEvict,
48
- setMaxTabCount,
49
- setUseGlobalScroll,
50
- setSessionPrefix
51
- } from './tabPanel/state'
52
- import {
53
- markCacheForEviction,
54
- markTabPagesForEviction,
55
- markTabPagesForEvictionOnly,
56
- addCache,
57
- removeCache,
58
- evictMarkedCaches,
59
- evictPageCache
60
- } from './tabPanel/evict'
61
- import { restoreScroller, saveScroller, removeScroller, addPageScroller } from './tabPanel/scroll'
62
- import {
63
- saveActiveTabToSession,
64
- clearSession,
65
- restoreTabFromSession,
66
- getSessionKey
67
- } from './tabPanel/session'
46
+ import { resolveStackTabsRuntimeContext } from './stackTabsContext'
47
+ import { createTabPanelEviction } from './tabPanel/evict'
48
+ import { createTabPanelScroll } from './tabPanel/scroll'
49
+ import { createTabPanelSession } from './tabPanel/session'
50
+ import { createTabPanelRefresh } from './tabPanel/refresh'
68
51
 
69
52
  /* eslint-disable vue/one-component-per-file */
70
53
  /** 占位组件:当路由组件为空或标签已被标记删除时返回 */
@@ -75,6 +58,11 @@ const EmptyPlaceholderComponent = defineComponent({
75
58
  }
76
59
  })
77
60
 
61
+ const cloneRouteQuery = (route: RouteLocationNormalizedLoaded, tabInfo: ITabBase) => ({
62
+ ...cloneLocationQuery(route.query),
63
+ __tab: route.query.__tab ?? encodeTabInfo(tabInfo)
64
+ })
65
+
78
66
  /** 规范化路径(nginx 等可能加尾部斜杠) */
79
67
  export const normalizePathForCache = (route: RouteLocationNormalizedLoaded): string => {
80
68
  const lastMatch = route.matched[route.matched.length - 1]
@@ -87,6 +75,37 @@ export default () => {
87
75
  const router = useRouter()
88
76
  const emitter = useTabEmitter()
89
77
  const { t } = useI18n()
78
+ const runtimeContext = resolveStackTabsRuntimeContext()
79
+ const {
80
+ tabs,
81
+ defaultTabs,
82
+ caches,
83
+ components,
84
+ tabIdsToEvict,
85
+ refreshKey,
86
+ iframeRefreshKeys,
87
+ isInitialized,
88
+ cacheIdsToEvict
89
+ } = runtimeContext
90
+ const {
91
+ markCacheForEviction,
92
+ markTabPagesForEviction,
93
+ markTabPagesForEvictionOnly,
94
+ addCache,
95
+ removeCache,
96
+ evictMarkedCaches,
97
+ evictPageCache,
98
+ replacePageCaches
99
+ } = createTabPanelEviction(runtimeContext)
100
+ const { restoreScroller, saveScroller, removeScroller, addPageScroller } =
101
+ createTabPanelScroll(runtimeContext)
102
+ const {
103
+ saveActiveTabToSession,
104
+ clearSession,
105
+ restoreTabFromSession,
106
+ restoreActiveTabSession,
107
+ getSessionKey
108
+ } = createTabPanelSession(runtimeContext)
90
109
 
91
110
  const size = (): number => tabs.value.length
92
111
 
@@ -95,33 +114,36 @@ export default () => {
95
114
  * @param staticTabs - 初始标签配置
96
115
  */
97
116
  const initialize = (staticTabs: ITabData[]) => {
117
+ const newDefaults: ITabItem[] = []
118
+ const newTabs: ITabItem[] = []
119
+
98
120
  for (const item of staticTabs) {
99
- const fullItem = defu(item, { id: ulid(), refreshable: true, closable: true, iframe: false })
100
- const tabId = fullItem.id ?? ulid()
101
- fullItem.id = tabId
102
- const uri = parseUrl(fullItem.path)
103
- const config = defu(fullItem, {
104
- closable: true,
121
+ const fullItem = defu(item, {
122
+ id: crypto.randomUUID(),
105
123
  refreshable: true,
124
+ closable: true,
106
125
  iframe: false
107
126
  })
127
+ const tabId = fullItem.id ?? crypto.randomUUID()
128
+ fullItem.id = tabId
129
+ const uri = parseUrl(fullItem.path)
108
130
  const cacheName = createPageId()
109
131
  const page: ITabPage = {
110
132
  id: cacheName,
111
133
  tabId,
112
134
  path: uri.path,
113
- query: defu(uri.query, { __tab: encodeTabInfo(fullItem) })
135
+ query: defu(omitStackTabsReservedQuery(uri.query), { __tab: encodeTabInfo(fullItem) })
114
136
  }
115
137
  const pages = new Stack<ITabPage>()
116
138
  pages.push(page)
117
139
  const tab: ITabItem = {
118
140
  id: tabId,
119
- title: config.title ?? '',
120
- closable: config.closable,
121
- refreshable: config.refreshable,
122
- iframe: config.iframe,
141
+ title: fullItem.title ?? '',
142
+ closable: fullItem.closable!,
143
+ refreshable: fullItem.refreshable!,
144
+ iframe: fullItem.iframe!,
123
145
  iframeRefreshMode:
124
- (config as { iframeRefreshMode?: string }).iframeRefreshMode === 'reload'
146
+ (fullItem as { iframeRefreshMode?: string }).iframeRefreshMode === 'reload'
125
147
  ? 'reload'
126
148
  : 'postMessage',
127
149
  active: false,
@@ -129,19 +151,21 @@ export default () => {
129
151
  }
130
152
 
131
153
  addCache(cacheName)
132
- defaultTabs.push(tab)
133
- tabs.value.push({ ...tab })
154
+ newDefaults.push(tab)
155
+ newTabs.push({ ...tab })
134
156
  }
135
157
 
158
+ defaultTabs.value = [...defaultTabs.value, ...newDefaults]
159
+ tabs.value = [...tabs.value, ...newTabs]
160
+
136
161
  const storedTabJson = sessionStorage.getItem(getSessionKey())
137
162
  const restoredTab = restoreTabFromSession(storedTabJson)
138
- if (restoredTab && !tabs.value.some((t) => t.id === restoredTab!.id)) {
163
+ if (restoredTab && !tabs.value.some((t) => t.id === restoredTab.id)) {
164
+ if (restoredTab.url) restoredTab.url = toSafeTabUrl(restoredTab.url)
139
165
  if (restoredTab.iframe && restoredTab.url && restoredTab.iframeRefreshMode === undefined) {
140
166
  restoredTab.iframeRefreshMode = isCrossOriginUrl(restoredTab.url) ? 'reload' : 'postMessage'
141
167
  }
142
168
 
143
- // 核心修复:刷新恢复后,不仅要把 Tab 塞入列表,
144
- // 更要把里面所有的 Page 的 ULID 重新注册进 keep-alive 的 includes,防止彻底沦为白板无缓存组件
145
169
  if (restoredTab.pages && typeof restoredTab.pages.list === 'function') {
146
170
  const pages = restoredTab.pages.list()
147
171
  pages.forEach((page) => {
@@ -149,46 +173,77 @@ export default () => {
149
173
  })
150
174
  }
151
175
 
152
- tabs.value.push(restoredTab)
176
+ tabs.value = [...tabs.value, restoredTab]
153
177
  }
154
178
  isInitialized.value = true
155
179
  }
156
180
 
157
181
  const hasTab = (id: string) => tabs.value.some((t) => t.id === id)
158
- const canAddTab = () => maxTabCount <= 0 || (maxTabCount > 0 && maxTabCount > tabs.value.length)
182
+ const canAddTab = () =>
183
+ runtimeContext.maxTabCount.value <= 0 ||
184
+ (runtimeContext.maxTabCount.value > 0 && runtimeContext.maxTabCount.value > tabs.value.length)
159
185
 
160
- const addTab = (tab: ITabItem) => {
161
- tabs.value.push(tab)
162
- return Promise.resolve(true)
186
+ const addTab = (tab: ITabItem): void => {
187
+ tabs.value = [...tabs.value, tab]
163
188
  }
164
189
 
165
190
  const getTab = (id: string) => tabs.value.find((t) => t.id === id) ?? null
166
191
 
167
- /** 从路由解析 tabInfo,若无则创建默认并写入 query */
192
+ /**
193
+ * 将指定标签移动到目标索引。标签对象本身不变,因此其页面栈和缓存关联保持不受影响。
194
+ * @returns 是否实际调整了顺序
195
+ */
196
+ const moveTab = (id: string, targetIndex: number): boolean => {
197
+ const sourceIndex = tabs.value.findIndex((tab) => tab.id === id)
198
+ if (
199
+ sourceIndex < 0 ||
200
+ targetIndex < 0 ||
201
+ targetIndex >= tabs.value.length ||
202
+ sourceIndex === targetIndex
203
+ ) {
204
+ return false
205
+ }
206
+
207
+ const reorderedTabs = tabs.value.slice()
208
+ const [tab] = reorderedTabs.splice(sourceIndex, 1)
209
+ if (!tab) return false
210
+ reorderedTabs.splice(targetIndex, 0, tab)
211
+ tabs.value = reorderedTabs
212
+ return true
213
+ }
214
+
215
+ /** 从路由解析 tabInfo,若无则创建默认信息,调用方负责把编码信息写入内部 page.query */
168
216
  const parseTabInfoFromRoute = (route: RouteLocationNormalizedLoaded): ITabBase => {
169
- if (route.query.__tab) return decodeTabInfo(route.query.__tab as string)
170
- const tabInfo: ITabBase = {
171
- id: ulid(),
217
+ if (typeof route.query.__tab === 'string') {
218
+ const tabInfo = decodeTabInfo(route.query.__tab)
219
+ if (tabInfo.id) return tabInfo
220
+ }
221
+
222
+ const path = normalizePathForCache(route)
223
+ const activeTab = tabs.value.find((tab) => tab.active)
224
+ const activePage = activeTab?.pages.peek()
225
+ if (activeTab && activePage?.path === path) {
226
+ return {
227
+ id: activeTab.id,
228
+ title: activeTab.title,
229
+ closable: activeTab.closable,
230
+ refreshable: activeTab.refreshable,
231
+ iframe: activeTab.iframe,
232
+ iframeRefreshMode: activeTab.iframeRefreshMode
233
+ }
234
+ }
235
+
236
+ return {
237
+ id: crypto.randomUUID(),
172
238
  title: t('VueStackTab.undefined'),
173
239
  closable: true,
174
240
  refreshable: true,
175
241
  iframe: false
176
242
  }
177
- route.query.__tab = encodeTabInfo(tabInfo)
178
- return tabInfo
179
243
  }
180
244
 
181
245
  /**
182
246
  * 查找或创建目标标签,并同步更新 active 状态与 pages 栈。
183
- *
184
- * 算法:
185
- * 1. 遍历所有 tab,将所有 tab.active 置为 false(全部失活)
186
- * 2. 同时查找 id 匹配的目标 tab(targetTab)
187
- * 3-a. 若目标 tab 不存在:说明是首次打开该标签,创建新的 ITabItem,初始化 pages 栈并推入当前 page
188
- * 3-b. 若目标 tab 已存在:
189
- * - 设为 active
190
- * - 判断当前 page 是否已在栈顶(避免重复入栈)
191
- * 当路由在同一标签内多次重渲染(响应式触发)时,page.id 与 peek().id 相同,跳过 push
192
247
  */
193
248
  const findOrCreateTargetTab = (
194
249
  tabInfo: ITabBase,
@@ -203,7 +258,6 @@ export default () => {
203
258
  }
204
259
 
205
260
  if (targetTab === null) {
206
- // 全新标签:初始化 pages 栈、从 query.__src 获取 iframe url
207
261
  const pages = new Stack<ITabPage>()
208
262
  pages.push(page)
209
263
  const src = route.query.__src
@@ -214,17 +268,14 @@ export default () => {
214
268
  refreshable: tabInfo.refreshable!,
215
269
  iframe: tabInfo.iframe!,
216
270
  iframeRefreshMode: tabInfo.iframeRefreshMode ?? 'postMessage',
217
- url: decodeURIComponent(src as string),
271
+ url: decodeSafeTabUrl(src),
218
272
  active: true,
219
273
  pages
220
274
  } as unknown as ITabItem
221
- addTab(targetTab).then(() => {
222
- emitter.emit(TabEventType.TAB_ACTIVE, { id: tabInfo.id!, isRoute: false })
223
- })
275
+ addTab(targetTab)
276
+ emitter.emit(TabEventType.TAB_ACTIVE, { id: tabInfo.id!, isRoute: false })
224
277
  } else {
225
278
  targetTab.active = true
226
- // 幂等保护:page.id 与当前栈顶相同时不重复入栈
227
- // 响应式重渲染(如 caches 变更)会重复调用 tabWrapper,需过滤此类重入
228
279
  if (targetTab.pages.isEmpty() || targetTab.pages.peek()!.id !== page.id) {
229
280
  targetTab.pages.push(page)
230
281
  }
@@ -232,26 +283,12 @@ export default () => {
232
283
  return targetTab
233
284
  }
234
285
 
235
- /**
236
- * 暴露给外部使用,用于获取指定路由对应的 cacheName
237
- */
238
- const getCacheName = () => {
286
+ const createCacheName = () => {
239
287
  return createPageId()
240
288
  }
241
289
 
242
290
  /**
243
291
  * 根据当前路由更新/创建标签与页面状态(核心路由适配器)。
244
- *
245
- * 该函数在每次 tabWrapper 被调用时执行(即每次 <router-view> 渲染或响应式重渲染时)。
246
- * 返回三种可能的结果:
247
- * - { empty: true } → 目标标签已被标记为驱逐(关闭态),返回空占位组件
248
- * - { recovered, cacheName } → 页面曾被软标记驱逐(刷新场景),原组件仍存活,直接恢复
249
- * - { cacheName, tabInfo, targetTab } → 正常流程,返回完整上下文供 resolvePageComponent 使用
250
- *
251
- * cacheName 生成算法:
252
- * createPageId(tabId, normalizedPath, queryObject)
253
- * 使用 tabId + 路径 + query(含 __tab)生成确定性哈希,
254
- * 同一标签同一路由的 cacheName 始终相同,用作 cacheComponent.name 及 keep-alive include 的键。
255
292
  */
256
293
  const updatePageState = (
257
294
  route: RouteLocationNormalizedLoaded
@@ -264,8 +301,7 @@ export default () => {
264
301
 
265
302
  const path = normalizePathForCache(route)
266
303
 
267
- // 核心重构:一切以数据栈为准,不再计算 URL 哈希
268
- let targetTab: ITabItem | null = tabInfo.id ? (getTab(tabInfo.id) as unknown as ITabItem) : null
304
+ let targetTab: ITabItem | null = tabInfo.id ? (getTab(tabInfo.id) as ITabItem | null) : null
269
305
  let cacheName: string
270
306
  let page: ITabPage | null = null
271
307
 
@@ -273,16 +309,11 @@ export default () => {
273
309
  const top = targetTab.pages.peek()
274
310
  if (!top) throw new Error('[vue-stack-tabs] Stack is not empty but peek() returned undefined')
275
311
 
276
- // 数据栈是唯一真相源,只比对 path 即可判断当前路由是否与栈顶一致
277
312
  if (top.path === path) {
278
313
  cacheName = top.id
279
314
  } else {
280
- // [边界/修复] "空降兵"或"浏览器原生后退"拦截
281
- // URL 和栈顶对不上!我们被外部改变了。
282
- const pagesList = targetTab.pages.list()
315
+ const pagesList = targetTab.pages.readonlyList()
283
316
  let foundIndex = -1
284
- // 从栈顶往下找,有没有曾经来过的同路由(以此判定是否为 PopState 后退)
285
- // 只比对 path,因为数据栈才是唯一真相源
286
317
  for (let i = pagesList.length - 2; i >= 0; i--) {
287
318
  const pastPage = pagesList[i]
288
319
  if (pastPage && pastPage.path === path) {
@@ -292,18 +323,16 @@ export default () => {
292
323
  }
293
324
 
294
325
  if (foundIndex !== -1) {
295
- // 找到了!这是一次浏览器的原生后退行为,因为路由事件触发了所以栈还没来得及退。
296
- // 手动执行类似 `backward` 的栈回退逻辑以亡羊补牢。
297
- console.warn(
298
- '[vue-stack-tabs] Detected native browser back navigation, repairing page stack...'
299
- )
326
+ if (!import.meta.env.PROD) {
327
+ console.warn(
328
+ '[vue-stack-tabs] Detected native browser back navigation, repairing page stack...'
329
+ )
330
+ }
300
331
  const stepsToPop = pagesList.length - 1 - foundIndex
301
332
  for (let i = 0; i < stepsToPop; i++) {
302
333
  const popped = targetTab.pages.pop()
303
- // 立即驱逐被错误保留在内存的那些未来组件(在 router 的钩子里也可以,但这里既然已经渲染就直接驱)
304
334
  if (popped) evictPageCache(popped.id)
305
335
  }
306
- // 因为我们把不要的都出栈了,现在的栈顶就是以前的这一个记录
307
336
  const newTop = targetTab.pages.peek()
308
337
  cacheName = newTop ? newTop.id : createPageId()
309
338
 
@@ -311,49 +340,41 @@ export default () => {
311
340
  page = {
312
341
  id: cacheName,
313
342
  tabId: tabInfo.id!,
314
- path: route.path, // 保持原 query 等参数
315
- query: route.query as Record<string, string>
343
+ path: route.path,
344
+ query: cloneRouteQuery(route, tabInfo)
316
345
  }
317
346
  }
318
347
  } else {
319
- // 在历史栈完全没找到,真的是被强行推入的“空降兵”新路由,直接补发一个新的栈记录。
320
348
  cacheName = createPageId()
321
349
  page = {
322
350
  id: cacheName,
323
351
  tabId: tabInfo.id!,
324
- path: route.path, // 保持原 query 等参数
325
- query: route.query as Record<string, string>
352
+ path: route.path,
353
+ query: cloneRouteQuery(route, tabInfo)
326
354
  }
327
355
  }
328
356
  }
329
357
  } else {
330
- // 这是一个全新创建的 Tab(被外部用空链接无中生有)
331
358
  cacheName = createPageId()
332
359
  page = {
333
360
  id: cacheName,
334
361
  tabId: tabInfo.id!,
335
362
  path: route.path,
336
- query: route.query as Record<string, string>
363
+ query: cloneRouteQuery(route, tabInfo)
337
364
  }
338
365
  }
339
366
 
340
- // 刷新恢复场景:依旧支持软驱逐和组件复用
341
367
  if (cacheIdsToEvict.has(cacheName)) {
342
368
  cacheIdsToEvict.delete(cacheName)
343
369
  const cached = components.get(cacheName)
344
370
  if (cached) return { recovered: cached, cacheName }
345
371
  }
346
372
 
347
- // 将 page (如果上面判断它为全新的时候)塞入栈中或建新 Tab
348
373
  if (page) {
349
374
  targetTab = findOrCreateTargetTab(tabInfo, route, cacheName, page)
350
375
  } else {
351
- // 否则说明这只是同页渲染复用(或者是刷新回来的无变动栈),把 targetTab 直接提拔为 active
352
376
  for (const t of tabs.value) t.active = false
353
377
  targetTab!.active = true
354
- if (!cacheName && targetTab && !targetTab.pages.isEmpty()) {
355
- cacheName = targetTab.pages.peek()!.id
356
- }
357
378
  }
358
379
 
359
380
  saveActiveTabToSession(targetTab!)
@@ -362,27 +383,6 @@ export default () => {
362
383
 
363
384
  /**
364
385
  * 获取或创建与 cacheName 对应的 cacheComponent(keep-alive 包装组件)。
365
- *
366
- * 设计要点:
367
- *
368
- * 1. 「组件定义与 VNode 解耦」
369
- * 旧版实现在 defineComponent 时通过 components: { DynamicComponent: component }
370
- * 将页面 VNode 锁定为闭包,导致:
371
- * - 组件定义本身持有旧 VNode 引用,即使路由参数变化也不更新
372
- * - evictPageCache 后重建新定义,但 keep-alive 以 name 为键仍激活旧实例
373
- * 新版改为通过 props: ['vnode'] + StackTabs.vue 的 :vnode="Component" 动态传入,
374
- * 每次渲染从父层获取最新 VNode,彻底消除闭包缓存残留。
375
- *
376
- * 2. 「cloneVNode 注入 tId / pId」
377
- * render 函数执行时,通过 cloneVNode(Node, { tId, pId }) 克隆页面 VNode 并附加标签标识属性。
378
- * 被渲染的页面组件(如 InternalRouteDetail)通过 attrs.tId / attrs.pId 读取这两个值,
379
- * 供 useTabRouter 内部定位所属标签与缓存 ID。
380
- * (未声明在 defineProps 里的属性自动进入 attrs)
381
- *
382
- * 3. 「onActivated 延迟驱逐」
383
- * 标签切换(非 backward)导致旧页面失活时,旧页面 ID 已由 markCacheForEviction 标记,
384
- * 等新页面的 cacheComponent onActivated 触发,再批量执行 evictMarkedCaches 是安全的——
385
- * 此时新页面已就绪,不存在重渲染重建的竞态问题。
386
386
  */
387
387
  const resolvePageComponent = (ctx: {
388
388
  cacheName: string
@@ -391,17 +391,13 @@ export default () => {
391
391
  }): DefineComponent => {
392
392
  const { cacheName, tabInfo } = ctx
393
393
 
394
- // 已存在的组件定义直接复用(keep-alive 会在内部找到缓存实例),无需重建
395
394
  if (components.has(cacheName)) {
396
- addCache(cacheName) // 确保再次激活时仍在 include 列表中
395
+ addCache(cacheName)
397
396
  return components.get(cacheName)!
398
397
  }
399
398
 
400
- // 首次进入该缓存页:创建专属 cacheComponent 包装组件
401
399
  const cacheComponent = defineComponent({
402
- // name 与 cacheName 保持一致,作为 keep-alive include 的匹配键
403
400
  name: cacheName,
404
- // vnode 由 StackTabs.vue 的 :vnode="Component" 动态传入(解耦闭包)
405
401
  props: {
406
402
  vnode: {
407
403
  type: Object as () => VNode,
@@ -411,8 +407,10 @@ export default () => {
411
407
  },
412
408
  emits: ['onLoaded'],
413
409
  setup(props, context) {
414
- // 用于在当前组件活跃期内存下回退参数,离开或重返时将更新或清理,真正实现生命周期同步!
415
410
  const localBackParams = ref<Record<string, unknown> | null>(null)
411
+ const lastVnode = shallowRef<VNode | null>(null)
412
+ const lastCloned = shallowRef<VNode | null>(null)
413
+ const lastBackParams = shallowRef<Record<string, unknown> | null>(null)
416
414
 
417
415
  const checkAndConsumeBackParams = () => {
418
416
  try {
@@ -420,7 +418,6 @@ export default () => {
420
418
  const top = tab?.pages.peek()
421
419
  if (top && top.id === cacheName && top._backParams) {
422
420
  localBackParams.value = { ...top._backParams }
423
- // 取出后阅后即焚,防止从别处循环回退时拿到过期老参
424
421
  delete top._backParams
425
422
  } else {
426
423
  localBackParams.value = null
@@ -451,18 +448,27 @@ export default () => {
451
448
  return () => {
452
449
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
453
450
  const Node = (props as any).vnode as VNode
454
- const dynamicBackProps = localBackParams.value ? { _back: localBackParams.value } : {}
451
+ const backParams = localBackParams.value
452
+
453
+ if (Node !== lastVnode.value || backParams !== lastBackParams.value) {
454
+ const dynamicBackProps = backParams ? { _back: backParams } : {}
455
+ lastCloned.value = Node
456
+ ? cloneVNode(Node, { tId: tabInfo.id, pId: cacheName, ...dynamicBackProps })
457
+ : null
458
+ lastVnode.value = Node
459
+ lastBackParams.value = backParams
460
+ }
455
461
 
456
462
  return (
457
463
  <div
458
464
  class="cache-page-wrapper"
459
465
  id={`W-${cacheName}`}
460
- style={[useGlobalScroll ? 'overflow:auto' : 'overflow:hidden', 'height: 100%']}
466
+ style={[
467
+ runtimeContext.useGlobalScroll.value ? 'overflow:auto' : 'overflow:hidden',
468
+ 'height: 100%'
469
+ ]}
461
470
  >
462
- {/* cloneVNode 附加 tId / pId 以及每次动态截留的 _back 回退参数 */}
463
- {Node
464
- ? cloneVNode(Node, { tId: tabInfo.id, pId: cacheName, ...(dynamicBackProps || {}) })
465
- : null}
471
+ {lastCloned.value}
466
472
  <PageLoading tabId={tabInfo.id!} />
467
473
  </div>
468
474
  )
@@ -474,45 +480,63 @@ export default () => {
474
480
  return cacheComponent
475
481
  }
476
482
 
477
- // 刷新隔离名单功能已由全新 ID 替换法替代
478
-
479
- /**
480
- * 将路由对应的组件注册为缓存页面(由 StackTabs.vue 的 tabWrapper 调用)。
481
- *
482
- * 流程:
483
- * 1. 若 component 为空(router-view 未匹配到路由),返回空占位组件
484
- * 2. updatePageState 解析路由状态:
485
- * - empty → 标签已关闭,返回空占位
486
- * - recovered → 刷新恢复,直接 addCache 并返回原组件
487
- * - 正常 → 调用 resolvePageComponent 获取/创建 cacheComponent
488
- *
489
- * @param component StackTabs.vue 传入的原始 VNode(来自 <router-view> 的 Component 插槽)
490
- * @returns 用于 <component :is> 的 cacheComponent 包装定义
491
- */
492
483
  /** 当前正在渲染的页面的缓存 Key(ULID),供 StackTabs.vue 的 :key 绑定使用 */
493
484
  const activeCacheKey = ref<string>('')
485
+ /** 只随当前活动页面刷新变化,避免一个 Tab 刷新导致其他 Tab 的缓存 key 失效。 */
486
+ const activePageRefreshVersion = computed(() => {
487
+ const activeTab = tabs.value.find((tab) => tab.active)
488
+ return activeTab?.pages.peek()?.refreshVersion ?? 0
489
+ })
490
+
491
+ let lastRouteKey = ''
492
+ let lastAddPageResult: DefineComponent | null = null
494
493
 
495
- const addPage = (route: RouteLocationNormalizedLoaded, component: VNode): DefineComponent => {
494
+ const addPage = (
495
+ route: RouteLocationNormalizedLoaded,
496
+ component?: VNode | null
497
+ ): DefineComponent => {
496
498
  if (!component) return EmptyPlaceholderComponent as DefineComponent
497
499
 
500
+ const routeKey = route.fullPath + (route.query.__tab || '')
501
+ if (routeKey === lastRouteKey && lastAddPageResult) {
502
+ return lastAddPageResult
503
+ }
504
+
498
505
  const state = updatePageState(route)
499
506
  if ('empty' in state) return EmptyPlaceholderComponent as DefineComponent
500
507
  if ('recovered' in state) {
501
- // 刷新场景:组件定义仍存活,只需重新加入 include 列表即可
502
508
  addCache(state.cacheName)
503
509
  activeCacheKey.value = state.cacheName
510
+ lastRouteKey = routeKey
511
+ lastAddPageResult = state.recovered
504
512
  return state.recovered
505
513
  }
506
514
  activeCacheKey.value = state.cacheName
507
- return resolvePageComponent(state)
515
+ const result = resolvePageComponent(state)
516
+ lastRouteKey = routeKey
517
+ lastAddPageResult = result
518
+ return result
508
519
  }
509
520
 
510
521
  const renewTab = (tab: ITabData) => {
511
522
  const currentTab = getTab(tab.id!)
512
- if (!currentTab) return
523
+ if (!currentTab) return undefined
524
+
525
+ const pageSnapshot = currentTab.pages.list().map((p) => clonePage(p))
526
+ const cacheSnapshot = [...caches.value]
527
+ const componentSnapshot = new Map(components)
528
+
513
529
  markTabPagesForEvictionOnly(currentTab)
514
530
  evictMarkedCaches()
515
531
  currentTab.pages.clear()
532
+
533
+ return () => {
534
+ currentTab.pages.clear()
535
+ for (const page of pageSnapshot) currentTab.pages.push(clonePage(page))
536
+ caches.value = [...cacheSnapshot]
537
+ components.clear()
538
+ for (const [id, component] of componentSnapshot) components.set(id, component)
539
+ }
516
540
  }
517
541
 
518
542
  const removeTab = (id: string): string => {
@@ -543,16 +567,21 @@ export default () => {
543
567
  return activeTabId
544
568
  }
545
569
 
546
- const removeAllTabs = () => {
570
+ /** 批量关闭满足条件的可关闭标签,并统一标记其页面缓存待驱逐。 */
571
+ const removeTabs = (shouldRemove: (tab: ITabItem, index: number) => boolean) => {
547
572
  const tabList = tabs.value
548
- for (let i = tabList.length - 1; i >= 0; i--) {
549
- const tab = tabList[i]
550
- if (tab?.closable) {
551
- markTabPagesForEviction(tab)
552
- tab.pages.clear()
553
- tabList.splice(i, 1)
554
- }
573
+ for (let index = tabList.length - 1; index >= 0; index--) {
574
+ const tab = tabList[index]
575
+ if (!tab || !tab.closable || !shouldRemove(tab, index)) continue
576
+ markTabPagesForEviction(tab)
577
+ tab.pages.clear()
578
+ tabList.splice(index, 1)
555
579
  }
580
+ }
581
+
582
+ const removeAllTabs = () => {
583
+ const tabList = tabs.value
584
+ removeTabs(() => true)
556
585
  const hasActive = tabList.some((t) => t.active)
557
586
  if (hasActive) {
558
587
  evictMarkedCaches()
@@ -565,18 +594,8 @@ export default () => {
565
594
 
566
595
  const removeOtherTabs = (id: string) => {
567
596
  const tabList = tabs.value
568
- let activeTab: ITabItem | undefined
569
- for (let i = tabList.length - 1; i >= 0; i--) {
570
- const tab = tabList[i]
571
- if (!tab) continue
572
- if (tab.id === id) {
573
- activeTab = tab as ITabItem
574
- } else if (tab.closable) {
575
- markTabPagesForEviction(tab)
576
- tab.pages.clear()
577
- tabList.splice(i, 1)
578
- }
579
- }
597
+ const activeTab = tabList.find((tab) => tab.id === id)
598
+ removeTabs((tab) => tab.id !== id)
580
599
  if (activeTab && !activeTab.active) {
581
600
  emitter.emit(TabEventType.TAB_ACTIVE, { id })
582
601
  } else {
@@ -590,14 +609,7 @@ export default () => {
590
609
  const pivot = tabList.findIndex((t) => t?.id === id)
591
610
  if (pivot <= 0) return
592
611
 
593
- for (let i = pivot - 1; i >= 0; i--) {
594
- const tab = tabList[i]
595
- if (tab?.closable) {
596
- markTabPagesForEviction(tab)
597
- tab.pages.clear()
598
- tabList.splice(i, 1)
599
- }
600
- }
612
+ removeTabs((_, index) => index < pivot)
601
613
  if (tabList.some((t) => t.active)) {
602
614
  evictMarkedCaches()
603
615
  tabIdsToEvict.clear()
@@ -612,14 +624,7 @@ export default () => {
612
624
  const pivot = tabList.findIndex((t) => t?.id === id)
613
625
  if (pivot < 0 || pivot >= tabList.length - 1) return
614
626
 
615
- for (let i = tabList.length - 1; i > pivot; i--) {
616
- const tab = tabList[i]
617
- if (tab?.closable) {
618
- markTabPagesForEviction(tab)
619
- tab.pages.clear()
620
- tabList.splice(i, 1)
621
- }
622
- }
627
+ removeTabs((_, index) => index > pivot)
623
628
  if (tabList.some((t) => t.active)) {
624
629
  evictMarkedCaches()
625
630
  tabIdsToEvict.clear()
@@ -629,79 +634,16 @@ export default () => {
629
634
  if (last) emitter.emit(TabEventType.TAB_ACTIVE, { id: last.id })
630
635
  }
631
636
 
632
- const refreshTab = (id: string) => {
633
- const tab = getTab(id)
634
- if (!tab?.refreshable) return
635
-
636
- if (tab.iframe) {
637
- if (tab.iframeRefreshMode === 'reload') {
638
- iframeRefreshKeys.value = {
639
- ...iframeRefreshKeys.value,
640
- [id]: (iframeRefreshKeys.value[id] ?? 0) + 1
641
- }
642
- } else {
643
- ;(emitter as { emit: (t: string, p: string) => void }).emit(
644
- 'REFRESH_IFRAME_POSTMESSAGE',
645
- id
646
- )
647
- }
648
- return
649
- }
650
-
651
- const currentPage = tab.pages.peek()
652
- if (!currentPage) return
653
-
654
- // 新架构:真正的刷新 = 把栈顶的旧组件数据连根拔起并注销,让它换个新 ID 重新走生命周期
655
- const oldId = currentPage.id
656
- evictPageCache(oldId) // 清除旧缓存
657
-
658
- // 生成全新 ID,相当于把原页面配方注入了一个崭新的组件身份
659
- currentPage.id = createPageId()
660
- addCache(currentPage.id)
661
-
662
- if (tab.active) {
663
- // 激活状态下,增加 refreshKey 强制触发当前 Router 的重排,
664
- // 由于 `<keep-alive>` 中之前记录的名字已经被驱逐出境,它会自动使用新 ID 创建一个全新页面。
665
- refreshKey.value++
666
- }
667
- }
668
-
669
- const refreshAllTabs = () => {
670
- let needsActiveRefresh = false
671
-
672
- for (const tab of tabs.value) {
673
- if (!tab?.refreshable) continue
674
- if (tab.iframe) {
675
- if (tab.iframeRefreshMode === 'reload') {
676
- iframeRefreshKeys.value = {
677
- ...iframeRefreshKeys.value,
678
- [tab.id]: (iframeRefreshKeys.value[tab.id] ?? 0) + 1
679
- }
680
- } else {
681
- ;(emitter as { emit: (t: string, p: string) => void }).emit(
682
- 'REFRESH_IFRAME_POSTMESSAGE',
683
- tab.id
684
- )
685
- }
686
- } else {
687
- const currentPage = tab.pages.peek()
688
- if (currentPage) {
689
- const oldId = currentPage.id
690
- evictPageCache(oldId)
691
- currentPage.id = createPageId()
692
- addCache(currentPage.id)
693
-
694
- if (tab.active) {
695
- needsActiveRefresh = true
696
- }
697
- }
698
- }
699
- }
700
-
701
- if (needsActiveRefresh) {
702
- refreshKey.value++
703
- }
704
- }
637
+ const { refreshTab, refreshAllTabs } = createTabPanelRefresh({
638
+ tabs,
639
+ iframeRefreshKeys,
640
+ getTab,
641
+ createPageId,
642
+ addCache,
643
+ evictPageCache,
644
+ replacePageCaches,
645
+ refreshIframeByPostMessage: (id) => emitter.emit(TabEventType.REFRESH_IFRAME_POSTMESSAGE, id)
646
+ })
705
647
 
706
648
  const getComponent = (id: string) => components.get(id)
707
649
 
@@ -711,44 +653,71 @@ export default () => {
711
653
  * @param route - 是否执行 router.push
712
654
  */
713
655
  const active = (id: string, route = true) => {
714
- emitter.emit('FORWARD')
656
+ emitter.emit(TabEventType.FORWARD)
715
657
  const target = getTab(id)
716
- if (!target) return
717
- if (target.active) return
658
+ if (!target) return Promise.resolve()
659
+ if (target.active) return Promise.resolve()
718
660
 
719
- for (const tab of tabs.value) {
720
- tab.active = tab.id === id
721
- }
722
- saveActiveTabToSession(target as ITabItem)
723
- // 强制触发 tabs 响应式更新,确保 TabHeader 高亮与 iframe 进入动画 timing 正确
724
- tabs.value = tabs.value.slice()
725
- if (route) {
726
- const top = target.pages.peek()
727
- if (top) {
728
- // 切换 Tab 走路由时,必须将 __tab 编码信息注入 query,
729
- // 否则 parseTabInfoFromRoute 找不到对应 Tab,会新建匿名标签!
730
- const __tab = encodeTabInfo({
731
- id: target.id,
732
- title: target.title,
733
- closable: target.closable,
734
- refreshable: target.refreshable,
735
- iframe: target.iframe
736
- })
737
- const query = defu({ __tab }, top.query || {})
738
- router.push({ path: top.path, query })
661
+ const top = target.pages.peek()
662
+ const activateTarget = () => {
663
+ for (const tab of tabs.value) {
664
+ tab.active = tab.id === id
739
665
  }
666
+ saveActiveTabToSession(target as ITabItem)
667
+ tabs.value = tabs.value.slice()
668
+ }
669
+
670
+ if (!route) {
671
+ activateTarget()
672
+ return Promise.resolve()
740
673
  }
674
+ if (!top) {
675
+ activateTarget()
676
+ return Promise.resolve()
677
+ }
678
+
679
+ const __tab = encodeTabInfo({
680
+ id: target.id,
681
+ title: target.title,
682
+ closable: target.closable,
683
+ refreshable: target.refreshable,
684
+ iframe: target.iframe
685
+ })
686
+ const query = defu({ __tab }, top.query || {})
687
+
688
+ return runNavigationTransaction({
689
+ apply: () => {
690
+ const prevActiveId = tabs.value.find((tab) => tab.active)?.id
691
+ const sessionSnapshot = window.sessionStorage.getItem(getSessionKey())
692
+ activateTarget()
693
+ return { prevActiveId, sessionSnapshot }
694
+ },
695
+ navigate: () => router.push({ path: top.path, query }),
696
+ rollback: (snapshot) => {
697
+ for (const tab of tabs.value) {
698
+ tab.active = tab.id === snapshot.prevActiveId
699
+ }
700
+ tabs.value = tabs.value.slice()
701
+ restoreActiveTabSession(snapshot.sessionSnapshot)
702
+ },
703
+ isFailureResult: isNavigationFailure,
704
+ rejectFailureResult: true
705
+ })
741
706
  }
742
707
 
743
708
  const reset = () => removeAllTabs()
744
709
 
745
710
  const destroy = () => {
746
- tabs.value.length = 0
711
+ tabs.value = []
712
+ defaultTabs.value = []
747
713
  cacheIdsToEvict.clear()
748
714
  tabIdsToEvict.clear()
749
715
  iframeRefreshKeys.value = {}
750
- caches.value.length = 0
716
+ caches.value = []
751
717
  components.clear()
718
+ runtimeContext.scrollPositionsByPageId.clear()
719
+ runtimeContext.isInitialized.value = false
720
+ runtimeContext.refreshKey.value = 0
752
721
  clearSession()
753
722
  }
754
723
 
@@ -757,18 +726,22 @@ export default () => {
757
726
  caches,
758
727
  refreshKey,
759
728
  activeCacheKey,
729
+ activePageRefreshVersion,
760
730
  isInitialized,
761
- setMaxSize: setMaxTabCount,
731
+ setMaxSize: (value: number) => {
732
+ runtimeContext.maxTabCount.value = value
733
+ },
762
734
  canAddTab,
763
735
  initialize,
764
736
  size,
765
737
  active,
766
- getCacheName,
738
+ getCacheName: createCacheName,
767
739
  destroy,
768
740
  reset,
769
741
  addCache,
770
742
  removeCache,
771
743
  getTab,
744
+ moveTab,
772
745
  addTab,
773
746
  removeTab,
774
747
  removeAllTabs,
@@ -788,8 +761,12 @@ export default () => {
788
761
  refreshAllTabs,
789
762
  iframeRefreshKeys,
790
763
  addPageScroller,
791
- setGlobalScroll: setUseGlobalScroll,
764
+ setGlobalScroll: (value: boolean) => {
765
+ runtimeContext.useGlobalScroll.value = value
766
+ },
792
767
  clearSession,
793
- setSessionPrefix
768
+ setSessionPrefix: (value: string) => {
769
+ runtimeContext.sessionPrefix.value = value
770
+ }
794
771
  }
795
772
  }