vue-stack-tabs 1.0.0 → 1.1.0

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.
@@ -12,7 +12,15 @@
12
12
  * 使用范围:内部 hook,由 StackTabs、useTabActions、useTabRouter、ContextMenu 调用
13
13
  */
14
14
  import type { ITabBase, ITabData, ITabItem, ITabPage } from '../model/TabModel'
15
- import { defineComponent, onActivated, onDeactivated, onMounted, onUnmounted } from 'vue'
15
+ import {
16
+ defineComponent,
17
+ onActivated,
18
+ onDeactivated,
19
+ onMounted,
20
+ onUnmounted,
21
+ ref,
22
+ cloneVNode
23
+ } from 'vue'
16
24
  import type { DefineComponent, VNode } from 'vue'
17
25
  import { useRouter } from 'vue-router'
18
26
  import type { RouteLocationNormalizedLoaded } from 'vue-router'
@@ -32,7 +40,6 @@ import {
32
40
  components,
33
41
  tabIdsToEvict,
34
42
  refreshKey,
35
- excludedCacheIdsForRefresh,
36
43
  iframeRefreshKeys,
37
44
  isInitialized,
38
45
  maxTabCount,
@@ -48,7 +55,8 @@ import {
48
55
  markTabPagesForEvictionOnly,
49
56
  addCache,
50
57
  removeCache,
51
- evictMarkedCaches
58
+ evictMarkedCaches,
59
+ evictPageCache
52
60
  } from './tabPanel/evict'
53
61
  import { restoreScroller, saveScroller, removeScroller, addPageScroller } from './tabPanel/scroll'
54
62
  import {
@@ -97,7 +105,7 @@ export default () => {
97
105
  refreshable: true,
98
106
  iframe: false
99
107
  })
100
- const cacheName = createPageId(tabId, uri.path, uri.query)
108
+ const cacheName = createPageId()
101
109
  const page: ITabPage = {
102
110
  id: cacheName,
103
111
  tabId,
@@ -112,7 +120,10 @@ export default () => {
112
120
  closable: config.closable,
113
121
  refreshable: config.refreshable,
114
122
  iframe: config.iframe,
115
- iframeRefreshMode: (config as { iframeRefreshMode?: string }).iframeRefreshMode === 'reload' ? 'reload' : 'postMessage',
123
+ iframeRefreshMode:
124
+ (config as { iframeRefreshMode?: string }).iframeRefreshMode === 'reload'
125
+ ? 'reload'
126
+ : 'postMessage',
116
127
  active: false,
117
128
  pages
118
129
  }
@@ -126,18 +137,25 @@ export default () => {
126
137
  const restoredTab = restoreTabFromSession(storedTabJson)
127
138
  if (restoredTab && !tabs.value.some((t) => t.id === restoredTab!.id)) {
128
139
  if (restoredTab.iframe && restoredTab.url && restoredTab.iframeRefreshMode === undefined) {
129
- restoredTab.iframeRefreshMode = isCrossOriginUrl(restoredTab.url)
130
- ? 'reload'
131
- : 'postMessage'
140
+ restoredTab.iframeRefreshMode = isCrossOriginUrl(restoredTab.url) ? 'reload' : 'postMessage'
141
+ }
142
+
143
+ // 核心修复:刷新恢复后,不仅要把 Tab 塞入列表,
144
+ // 更要把里面所有的 Page 的 ULID 重新注册进 keep-alive 的 includes,防止彻底沦为白板无缓存组件
145
+ if (restoredTab.pages && typeof restoredTab.pages.list === 'function') {
146
+ const pages = restoredTab.pages.list()
147
+ pages.forEach((page) => {
148
+ if (page && page.id) addCache(page.id)
149
+ })
132
150
  }
151
+
133
152
  tabs.value.push(restoredTab)
134
153
  }
135
154
  isInitialized.value = true
136
155
  }
137
156
 
138
157
  const hasTab = (id: string) => tabs.value.some((t) => t.id === id)
139
- const canAddTab = () =>
140
- maxTabCount <= 0 || (maxTabCount > 0 && maxTabCount > tabs.value.length)
158
+ const canAddTab = () => maxTabCount <= 0 || (maxTabCount > 0 && maxTabCount > tabs.value.length)
141
159
 
142
160
  const addTab = (tab: ITabItem) => {
143
161
  tabs.value.push(tab)
@@ -160,7 +178,18 @@ export default () => {
160
178
  return tabInfo
161
179
  }
162
180
 
163
- /** 查找或创建目标 tab,同步 active 并添加 page */
181
+ /**
182
+ * 查找或创建目标标签,并同步更新 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
+ */
164
193
  const findOrCreateTargetTab = (
165
194
  tabInfo: ITabBase,
166
195
  route: RouteLocationNormalizedLoaded,
@@ -174,6 +203,7 @@ export default () => {
174
203
  }
175
204
 
176
205
  if (targetTab === null) {
206
+ // 全新标签:初始化 pages 栈、从 query.__src 获取 iframe url
177
207
  const pages = new Stack<ITabPage>()
178
208
  pages.push(page)
179
209
  const src = route.query.__src
@@ -187,12 +217,14 @@ export default () => {
187
217
  url: decodeURIComponent(src as string),
188
218
  active: true,
189
219
  pages
190
- }
220
+ } as unknown as ITabItem
191
221
  addTab(targetTab).then(() => {
192
222
  emitter.emit(TabEventType.TAB_ACTIVE, { id: tabInfo.id!, isRoute: false })
193
223
  })
194
224
  } else {
195
225
  targetTab.active = true
226
+ // 幂等保护:page.id 与当前栈顶相同时不重复入栈
227
+ // 响应式重渲染(如 caches 变更)会重复调用 tabWrapper,需过滤此类重入
196
228
  if (targetTab.pages.isEmpty() || targetTab.pages.peek()!.id !== page.id) {
197
229
  targetTab.pages.push(page)
198
230
  }
@@ -201,8 +233,25 @@ export default () => {
201
233
  }
202
234
 
203
235
  /**
204
- * 根据当前路由更新/创建标签与页面状态
205
- * @returns 缓存名、标签信息、目标标签,或 empty/recovered
236
+ * 暴露给外部使用,用于获取指定路由对应的 cacheName
237
+ */
238
+ const getCacheName = () => {
239
+ return createPageId()
240
+ }
241
+
242
+ /**
243
+ * 根据当前路由更新/创建标签与页面状态(核心路由适配器)。
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 的键。
206
255
  */
207
256
  const updatePageState = (
208
257
  route: RouteLocationNormalizedLoaded
@@ -214,62 +263,210 @@ export default () => {
214
263
  if (tabIdsToEvict.has(tabInfo.id!)) return { empty: true }
215
264
 
216
265
  const path = normalizePathForCache(route)
217
- const cacheName = createPageId(tabInfo.id!, path, route.query.valueOf())
266
+
267
+ // 核心重构:一切以数据栈为准,不再计算 URL 哈希
268
+ let targetTab: ITabItem | null = tabInfo.id ? (getTab(tabInfo.id) as unknown as ITabItem) : null
269
+ let cacheName: string
270
+ let page: ITabPage | null = null
271
+
272
+ if (targetTab && !targetTab.pages.isEmpty()) {
273
+ const top = targetTab.pages.peek()
274
+ if (!top) throw new Error('[vue-stack-tabs] Stack is not empty but peek() returned undefined')
275
+
276
+ // 数据栈是唯一真相源,只比对 path 即可判断当前路由是否与栈顶一致
277
+ if (top.path === path) {
278
+ cacheName = top.id
279
+ } else {
280
+ // [边界/修复] "空降兵"或"浏览器原生后退"拦截
281
+ // URL 和栈顶对不上!我们被外部改变了。
282
+ const pagesList = targetTab.pages.list()
283
+ let foundIndex = -1
284
+ // 从栈顶往下找,有没有曾经来过的同路由(以此判定是否为 PopState 后退)
285
+ // 只比对 path,因为数据栈才是唯一真相源
286
+ for (let i = pagesList.length - 2; i >= 0; i--) {
287
+ const pastPage = pagesList[i]
288
+ if (pastPage && pastPage.path === path) {
289
+ foundIndex = i
290
+ break
291
+ }
292
+ }
293
+
294
+ if (foundIndex !== -1) {
295
+ // 找到了!这是一次浏览器的原生后退行为,因为路由事件触发了所以栈还没来得及退。
296
+ // 手动执行类似 `backward` 的栈回退逻辑以亡羊补牢。
297
+ console.warn(
298
+ '[vue-stack-tabs] Detected native browser back navigation, repairing page stack...'
299
+ )
300
+ const stepsToPop = pagesList.length - 1 - foundIndex
301
+ for (let i = 0; i < stepsToPop; i++) {
302
+ const popped = targetTab.pages.pop()
303
+ // 立即驱逐被错误保留在内存的那些未来组件(在 router 的钩子里也可以,但这里既然已经渲染就直接驱)
304
+ if (popped) evictPageCache(popped.id)
305
+ }
306
+ // 因为我们把不要的都出栈了,现在的栈顶就是以前的这一个记录
307
+ const newTop = targetTab.pages.peek()
308
+ cacheName = newTop ? newTop.id : createPageId()
309
+
310
+ if (!newTop) {
311
+ page = {
312
+ id: cacheName,
313
+ tabId: tabInfo.id!,
314
+ path: route.path, // 保持原 query 等参数
315
+ query: route.query as Record<string, string>
316
+ }
317
+ }
318
+ } else {
319
+ // 在历史栈完全没找到,真的是被强行推入的“空降兵”新路由,直接补发一个新的栈记录。
320
+ cacheName = createPageId()
321
+ page = {
322
+ id: cacheName,
323
+ tabId: tabInfo.id!,
324
+ path: route.path, // 保持原 query 等参数
325
+ query: route.query as Record<string, string>
326
+ }
327
+ }
328
+ }
329
+ } else {
330
+ // 这是一个全新创建的 Tab(被外部用空链接无中生有)
331
+ cacheName = createPageId()
332
+ page = {
333
+ id: cacheName,
334
+ tabId: tabInfo.id!,
335
+ path: route.path,
336
+ query: route.query as Record<string, string>
337
+ }
338
+ }
339
+
340
+ // 刷新恢复场景:依旧支持软驱逐和组件复用
218
341
  if (cacheIdsToEvict.has(cacheName)) {
219
342
  cacheIdsToEvict.delete(cacheName)
220
343
  const cached = components.get(cacheName)
221
344
  if (cached) return { recovered: cached, cacheName }
222
345
  }
223
346
 
224
- const page: ITabPage = {
225
- id: cacheName,
226
- tabId: tabInfo.id!,
227
- path: route.path,
228
- query: route.query as Record<string, string>
347
+ // 将 page (如果上面判断它为全新的时候)塞入栈中或建新 Tab
348
+ if (page) {
349
+ targetTab = findOrCreateTargetTab(tabInfo, route, cacheName, page)
350
+ } else {
351
+ // 否则说明这只是同页渲染复用(或者是刷新回来的无变动栈),把 targetTab 直接提拔为 active
352
+ for (const t of tabs.value) t.active = false
353
+ targetTab!.active = true
354
+ if (!cacheName && targetTab && !targetTab.pages.isEmpty()) {
355
+ cacheName = targetTab.pages.peek()!.id
356
+ }
229
357
  }
230
- const targetTab = findOrCreateTargetTab(tabInfo, route, cacheName, page)
231
- saveActiveTabToSession(targetTab)
232
- return { cacheName, tabInfo, targetTab }
358
+
359
+ saveActiveTabToSession(targetTab!)
360
+ return { cacheName, tabInfo, targetTab: targetTab! }
233
361
  }
234
362
 
235
- const resolvePageComponent = (
236
- ctx: { cacheName: string; tabInfo: ITabBase; targetTab: ITabItem },
237
- component: VNode
238
- ): DefineComponent => {
363
+ /**
364
+ * 获取或创建与 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
+ */
387
+ const resolvePageComponent = (ctx: {
388
+ cacheName: string
389
+ tabInfo: ITabBase
390
+ targetTab: ITabItem
391
+ }): DefineComponent => {
239
392
  const { cacheName, tabInfo } = ctx
393
+
394
+ // 已存在的组件定义直接复用(keep-alive 会在内部找到缓存实例),无需重建
240
395
  if (components.has(cacheName)) {
241
- addCache(cacheName)
396
+ addCache(cacheName) // 确保再次激活时仍在 include 列表中
242
397
  return components.get(cacheName)!
243
398
  }
399
+
400
+ // 首次进入该缓存页:创建专属 cacheComponent 包装组件
244
401
  const cacheComponent = defineComponent({
402
+ // name 与 cacheName 保持一致,作为 keep-alive include 的匹配键
245
403
  name: cacheName,
246
- components: { DynamicComponent: component },
404
+ // vnode 由 StackTabs.vue 的 :vnode="Component" 动态传入(解耦闭包)
405
+ props: {
406
+ vnode: {
407
+ type: Object as () => VNode,
408
+ required: false,
409
+ default: undefined
410
+ }
411
+ },
247
412
  emits: ['onLoaded'],
248
- setup(_props, context) {
413
+ setup(props, context) {
414
+ // 用于在当前组件活跃期内存下回退参数,离开或重返时将更新或清理,真正实现生命周期同步!
415
+ const localBackParams = ref<Record<string, unknown> | null>(null)
416
+
417
+ const checkAndConsumeBackParams = () => {
418
+ try {
419
+ const tab = getTab(tabInfo.id!)
420
+ const top = tab?.pages.peek()
421
+ if (top && top.id === cacheName && top._backParams) {
422
+ localBackParams.value = { ...top._backParams }
423
+ // 取出后阅后即焚,防止从别处循环回退时拿到过期老参
424
+ delete top._backParams
425
+ } else {
426
+ localBackParams.value = null
427
+ }
428
+ } catch {
429
+ localBackParams.value = null
430
+ }
431
+ }
432
+
249
433
  onMounted(() => {
250
434
  context.emit('onLoaded')
251
- addPageScroller(cacheName, '.cache-page-wrapper')
435
+ addPageScroller(cacheName, `#W-${cacheName}`)
436
+ checkAndConsumeBackParams()
252
437
  })
438
+
253
439
  onDeactivated(() => saveScroller(cacheName))
440
+
254
441
  onActivated(() => {
255
442
  context.emit('onLoaded')
256
443
  restoreScroller(cacheName)
257
444
  evictMarkedCaches()
258
445
  tabIdsToEvict.clear()
259
- clearExcludedCacheIdForRefresh(cacheName)
260
- component.props = null
446
+ checkAndConsumeBackParams()
261
447
  })
448
+
262
449
  onUnmounted(() => removeScroller(cacheName))
263
- return () => (
264
- <div
265
- class="cache-page-wrapper"
266
- id={'W-' + tabInfo.id}
267
- style={[useGlobalScroll ? 'overflow:auto' : 'overflow:hidden']}
268
- >
269
- <dynamic-component tId={tabInfo.id} pId={cacheName} />
270
- <PageLoading tabId={tabInfo.id!} />
271
- </div>
272
- )
450
+
451
+ return () => {
452
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
453
+ const Node = (props as any).vnode as VNode
454
+ const dynamicBackProps = localBackParams.value ? { _back: localBackParams.value } : {}
455
+
456
+ return (
457
+ <div
458
+ class="cache-page-wrapper"
459
+ id={`W-${cacheName}`}
460
+ style={[useGlobalScroll ? 'overflow:auto' : 'overflow:hidden', 'height: 100%']}
461
+ >
462
+ {/* cloneVNode 附加 tId / pId 以及每次动态截留的 _back 回退参数 */}
463
+ {Node
464
+ ? cloneVNode(Node, { tId: tabInfo.id, pId: cacheName, ...(dynamicBackProps || {}) })
465
+ : null}
466
+ <PageLoading tabId={tabInfo.id!} />
467
+ </div>
468
+ )
469
+ }
273
470
  }
274
471
  }) as DefineComponent
275
472
  components.set(cacheName, cacheComponent)
@@ -277,24 +474,37 @@ export default () => {
277
474
  return cacheComponent
278
475
  }
279
476
 
280
- const clearExcludedCacheIdForRefresh = (cacheId: string) => {
281
- excludedCacheIdsForRefresh.value = excludedCacheIdsForRefresh.value.filter((c) => c !== cacheId)
282
- }
477
+ // 刷新隔离名单功能已由全新 ID 替换法替代
283
478
 
284
479
  /**
285
- * 将路由对应的组件注册为缓存页面,由 tabWrapper 调用
286
- * @returns 包装后的缓存组件
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 包装定义
287
491
  */
492
+ /** 当前正在渲染的页面的缓存 Key(ULID),供 StackTabs.vue 的 :key 绑定使用 */
493
+ const activeCacheKey = ref<string>('')
494
+
288
495
  const addPage = (route: RouteLocationNormalizedLoaded, component: VNode): DefineComponent => {
289
496
  if (!component) return EmptyPlaceholderComponent as DefineComponent
290
497
 
291
498
  const state = updatePageState(route)
292
499
  if ('empty' in state) return EmptyPlaceholderComponent as DefineComponent
293
500
  if ('recovered' in state) {
501
+ // 刷新场景:组件定义仍存活,只需重新加入 include 列表即可
294
502
  addCache(state.cacheName)
503
+ activeCacheKey.value = state.cacheName
295
504
  return state.recovered
296
505
  }
297
- return resolvePageComponent(state, component)
506
+ activeCacheKey.value = state.cacheName
507
+ return resolvePageComponent(state)
298
508
  }
299
509
 
300
510
  const renewTab = (tab: ITabData) => {
@@ -430,22 +640,35 @@ export default () => {
430
640
  [id]: (iframeRefreshKeys.value[id] ?? 0) + 1
431
641
  }
432
642
  } else {
433
- ;(emitter as { emit: (t: string, p: string) => void }).emit('REFRESH_IFRAME_POSTMESSAGE', id)
643
+ ;(emitter as { emit: (t: string, p: string) => void }).emit(
644
+ 'REFRESH_IFRAME_POSTMESSAGE',
645
+ id
646
+ )
434
647
  }
435
648
  return
436
649
  }
437
650
 
438
- const currentPageId = tab.pages.peek()?.id
439
- if (!currentPageId) return
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)
440
661
 
441
- excludedCacheIdsForRefresh.value = [...excludedCacheIdsForRefresh.value, currentPageId]
442
662
  if (tab.active) {
663
+ // 激活状态下,增加 refreshKey 强制触发当前 Router 的重排,
664
+ // 由于 `<keep-alive>` 中之前记录的名字已经被驱逐出境,它会自动使用新 ID 创建一个全新页面。
443
665
  refreshKey.value++
444
666
  }
445
667
  }
446
668
 
447
669
  const refreshAllTabs = () => {
448
- const toRefresh: string[] = []
670
+ let needsActiveRefresh = false
671
+
449
672
  for (const tab of tabs.value) {
450
673
  if (!tab?.refreshable) continue
451
674
  if (tab.iframe) {
@@ -455,15 +678,27 @@ export default () => {
455
678
  [tab.id]: (iframeRefreshKeys.value[tab.id] ?? 0) + 1
456
679
  }
457
680
  } else {
458
- ;(emitter as { emit: (t: string, p: string) => void }).emit('REFRESH_IFRAME_POSTMESSAGE', tab.id)
681
+ ;(emitter as { emit: (t: string, p: string) => void }).emit(
682
+ 'REFRESH_IFRAME_POSTMESSAGE',
683
+ tab.id
684
+ )
459
685
  }
460
686
  } else {
461
- const pageId = tab.pages.peek()?.id
462
- if (pageId) toRefresh.push(pageId)
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
+ }
463
698
  }
464
699
  }
465
- if (toRefresh.length > 0) {
466
- excludedCacheIdsForRefresh.value = [...excludedCacheIdsForRefresh.value, ...toRefresh]
700
+
701
+ if (needsActiveRefresh) {
467
702
  refreshKey.value++
468
703
  }
469
704
  }
@@ -489,7 +724,19 @@ export default () => {
489
724
  tabs.value = tabs.value.slice()
490
725
  if (route) {
491
726
  const top = target.pages.peek()
492
- if (top) router.push({ path: top.path, query: top.query })
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 })
739
+ }
493
740
  }
494
741
  }
495
742
 
@@ -509,13 +756,14 @@ export default () => {
509
756
  tabs,
510
757
  caches,
511
758
  refreshKey,
512
- excludedCacheIdsForRefresh,
759
+ activeCacheKey,
513
760
  isInitialized,
514
761
  setMaxSize: setMaxTabCount,
515
762
  canAddTab,
516
763
  initialize,
517
764
  size,
518
765
  active,
766
+ getCacheName,
519
767
  destroy,
520
768
  reset,
521
769
  addCache,
@@ -535,6 +783,7 @@ export default () => {
535
783
  removeComponent: (id: string) => components.delete(id),
536
784
  markCacheForEviction,
537
785
  evictMarkedCaches,
786
+ evictPageCache,
538
787
  refreshTab,
539
788
  refreshAllTabs,
540
789
  iframeRefreshKeys,