vue-stack-tabs 1.4.1 → 1.4.2

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.
@@ -4,8 +4,8 @@
4
4
  * 活动普通页面保持 cache id 稳定,只递增页面级刷新版本;非活动页面可直接替换缓存。
5
5
  */
6
6
  import type { Ref } from 'vue'
7
- import { triggerRef } from 'vue'
8
- import type { ITabItem } from '../../model/TabModel'
7
+ import type { ITabItem, ITabPage } from '../../model/TabModel'
8
+ import type { PagePatch } from './tabStore'
9
9
 
10
10
  export interface TabPanelRefreshApi {
11
11
  refreshTab: (id: string) => void
@@ -13,9 +13,11 @@ export interface TabPanelRefreshApi {
13
13
  }
14
14
 
15
15
  export interface CreateTabPanelRefreshOptions {
16
- tabs: Ref<ITabItem[]>
16
+ getTabs: () => readonly ITabItem[]
17
+ getTopPage: (tabId: string) => ITabPage | null
18
+ /** 修改页面字段(刷新版本 / 页面 id)并触发 tabs 更新 */
19
+ updatePage: (tabId: string, pageId: string, patch: PagePatch) => boolean
17
20
  iframeRefreshKeys: Ref<Record<string, number>>
18
- getTab: (id: string) => ITabItem | null
19
21
  createPageId: () => string
20
22
  addCache: (cacheName: string) => void
21
23
  evictPageCache: (cacheName: string) => void
@@ -30,9 +32,10 @@ export const createTabPanelRefresh = (
30
32
  options: CreateTabPanelRefreshOptions
31
33
  ): TabPanelRefreshApi => {
32
34
  const {
33
- tabs,
35
+ getTabs,
36
+ getTopPage,
37
+ updatePage,
34
38
  iframeRefreshKeys,
35
- getTab,
36
39
  createPageId,
37
40
  addCache,
38
41
  evictPageCache,
@@ -52,30 +55,31 @@ export const createTabPanelRefresh = (
52
55
  }
53
56
 
54
57
  const refreshPage = (tab: ITabItem, cacheNamesToEvict?: string[], cacheNamesToAdd?: string[]) => {
55
- const currentPage = tab.pages.peek()
58
+ const currentPage = getTopPage(tab.id)
56
59
  if (!currentPage) return
57
60
 
58
61
  if (tab.active) {
59
- currentPage.refreshVersion = (currentPage.refreshVersion ?? 0) + 1
60
- // 通知 activePageRefreshVersion 这类依赖 tabs.value 的计算属性重新计算
61
- triggerRef(tabs)
62
+ // updatePage 内部会触发 tabs 更新,activePageRefreshVersion 随之重新求值。
63
+ updatePage(tab.id, currentPage.id, {
64
+ refreshVersion: (currentPage.refreshVersion ?? 0) + 1
65
+ })
62
66
  return
63
67
  }
64
68
 
65
69
  const oldCacheName = currentPage.id
66
- currentPage.id = createPageId()
67
- currentPage.refreshVersion = 0
70
+ const nextPageId = createPageId()
71
+ updatePage(tab.id, oldCacheName, { id: nextPageId, refreshVersion: 0 })
68
72
  if (cacheNamesToEvict && cacheNamesToAdd) {
69
73
  cacheNamesToEvict.push(oldCacheName)
70
- cacheNamesToAdd.push(currentPage.id)
74
+ cacheNamesToAdd.push(nextPageId)
71
75
  return
72
76
  }
73
77
  evictPageCache(oldCacheName)
74
- addCache(currentPage.id)
78
+ addCache(nextPageId)
75
79
  }
76
80
 
77
81
  const refreshTab = (id: string) => {
78
- const tab = getTab(id)
82
+ const tab = getTabs().find((item) => item.id === id)
79
83
  if (!tab?.refreshable) return
80
84
  if (tab.iframe) {
81
85
  refreshIframe(tab)
@@ -90,7 +94,7 @@ export const createTabPanelRefresh = (
90
94
  const iframeKeysUpdate = { ...iframeRefreshKeys.value }
91
95
  let iframeKeysChanged = false
92
96
 
93
- for (const tab of tabs.value) {
97
+ for (const tab of getTabs()) {
94
98
  if (!tab.refreshable) continue
95
99
  if (tab.iframe) {
96
100
  if (tab.iframeRefreshMode === 'reload') {
@@ -0,0 +1,595 @@
1
+ /**
2
+ * tabPanel/tabStore - 标签/页面栈的唯一状态入口
3
+ *
4
+ * 内部使用规范化的三张“表”保存状态:
5
+ * - tabOrder + tabsById:标签顺序与标签记录
6
+ * - pageIdsByTabId:标签到页面 id 的一对多关系
7
+ * - pagesById:页面记录
8
+ *
9
+ * 对外仍通过 tabs Ref 暴露兼容的 ITabItem[];每个 tab.pages 是 PageStackView,
10
+ * 读写会转发到 store,因此渲染层与公开类型不需要感知内部结构变化。
11
+ *
12
+ * 注意:本模块不依赖 vue-router,也不触发业务事件;路由事务与事件由 useTabPanel /
13
+ * useTabRouter 等上层编排。
14
+ */
15
+ import { reactive, ref } from 'vue'
16
+ import type { DefineComponent } from 'vue'
17
+ import type { ITabItem, ITabPage } from '../../model/TabModel'
18
+ import { Stack } from '../../model/TabModel'
19
+ import { clonePage } from '../../utils/urlParser'
20
+ import type { StackTabsRuntimeContext } from '../stackTabsContext'
21
+ import { createTabPanelEviction, type TabPanelEvictionApi } from './evict'
22
+
23
+ export interface RemoveTabResult {
24
+ status: 'not-found' | 'not-closable' | 'removed'
25
+ removed: ITabItem | null
26
+ /** 关闭后应激活的相邻标签;没有可激活标签时为空字符串 */
27
+ nextActiveId: string
28
+ }
29
+
30
+ /** updatePage 允许修改的字段;页面身份与刷新版本必须通过 store 写入 */
31
+ export interface PagePatch {
32
+ id?: string
33
+ refreshVersion?: number
34
+ _backParams?: Record<string, unknown>
35
+ }
36
+
37
+ export interface TabStoreApi extends TabPanelEvictionApi {
38
+ // ---- 查询 ----
39
+ /** 返回标签列表的只读视图(运行时为同一个响应式数组) */
40
+ getTabs: () => readonly ITabItem[]
41
+ getTab: (id: string) => ITabItem | null
42
+ getActiveTab: () => ITabItem | null
43
+ /** 查找栈顶页面 path 与给定 path 相同的标签 */
44
+ findTabByTopPagePath: (path: string) => ITabItem | null
45
+ hasTab: (id: string) => boolean
46
+ canAddTab: () => boolean
47
+ getPages: (tabId: string) => readonly ITabPage[]
48
+ getTopPage: (tabId: string) => ITabPage | null
49
+ /** 返回深拷贝的页面栈快照,可直接用于 rollback */
50
+ getPageSnapshot: (tabId: string) => ITabPage[]
51
+
52
+ // ---- 标签写入 ----
53
+ addTab: (tab: ITabItem) => void
54
+ addTabs: (tabs: readonly ITabItem[]) => void
55
+ /** 唯一 active 写入口 */
56
+ activateTab: (id: string) => ITabItem | null
57
+ moveTab: (id: string, targetIndex: number) => boolean
58
+ removeTab: (id: string) => RemoveTabResult
59
+ removeTabs: (shouldRemove: (tab: ITabItem, index: number) => boolean) => ITabItem[]
60
+
61
+ // ---- 页面栈写入 ----
62
+ appendPage: (tabId: string, page: ITabPage) => void
63
+ /** 从栈顶弹出若干页,返回被弹出的页面(由调用方决定驱逐/回滚) */
64
+ popPages: (tabId: string, steps: number) => ITabPage[]
65
+ /** 用给定页面替换整个栈(内部深拷贝页面) */
66
+ replacePages: (tabId: string, pages: readonly ITabPage[]) => void
67
+ clearPages: (tabId: string) => void
68
+ /** 修改页面字段(id / refreshVersion / _backParams),并触发 tabs 更新 */
69
+ updatePage: (tabId: string, pageId: string, patch: PagePatch) => boolean
70
+ /** 取出并清除栈顶页面的 _backParams;pageId 不是当前栈顶时返回 null */
71
+ consumeBackParams: (tabId: string, pageId: string) => Record<string, unknown> | null
72
+
73
+ // ---- 驱逐标记辅助 ----
74
+ isTabMarkedForEviction: (id: string) => boolean
75
+ isCacheMarkedForEviction: (id: string) => boolean
76
+ unmarkCacheForEviction: (id: string) => void
77
+ clearTabEvictionMarks: () => void
78
+
79
+ // ---- 组件注册表 ----
80
+ getComponent: (id: string) => DefineComponent | undefined
81
+ setComponent: (id: string, component: DefineComponent) => void
82
+ deleteComponent: (id: string) => void
83
+ snapshotComponents: () => Map<string, DefineComponent>
84
+ restoreComponents: (snapshot: Map<string, DefineComponent>) => void
85
+
86
+ // ---- 缓存快照 / 响应式 ----
87
+ snapshotCaches: () => string[]
88
+ restoreCaches: (cacheNames: readonly string[]) => void
89
+ /** 通知依赖 tabs 的计算属性重新求值(如 activePageRefreshVersion) */
90
+ touchTabs: () => void
91
+
92
+ // ---- 生命周期 ----
93
+ /** 清空 tabs / caches / components / 驱逐标记,供 destroy 使用 */
94
+ reset: () => void
95
+ }
96
+
97
+ /** 标签表中保存的纯净记录;active 由 activeTabId 派生 */
98
+ interface TabRecord {
99
+ id: string
100
+ title: string
101
+ closable: boolean
102
+ refreshable: boolean
103
+ iframe: boolean
104
+ iframeRefreshMode?: 'postMessage' | 'reload'
105
+ url?: string
106
+ }
107
+
108
+ interface PageStackAccess {
109
+ getPages: (tabId: string) => readonly ITabPage[]
110
+ getTopPage: (tabId: string) => ITabPage | null
111
+ appendPage: (tabId: string, page: ITabPage) => void
112
+ popPages: (tabId: string, steps: number) => ITabPage[]
113
+ clearPages: (tabId: string) => void
114
+ }
115
+
116
+ /**
117
+ * 兼容视图:继承 Stack 以保持 ITabItem.pages 的类型不变,
118
+ * 所有读写都转发给 store 的规范化状态。
119
+ */
120
+ class PageStackView extends Stack<ITabPage> {
121
+ private readonly tabId: string
122
+ private readonly access: PageStackAccess
123
+
124
+ constructor(tabId: string, access: PageStackAccess) {
125
+ super()
126
+ this.tabId = tabId
127
+ this.access = access
128
+ }
129
+
130
+ override push(page: ITabPage): void {
131
+ this.access.appendPage(this.tabId, page)
132
+ }
133
+
134
+ override pop(): ITabPage | undefined {
135
+ return this.access.popPages(this.tabId, 1)[0]
136
+ }
137
+
138
+ override peek(): ITabPage | undefined {
139
+ return this.access.getTopPage(this.tabId) ?? undefined
140
+ }
141
+
142
+ override at(index: number): ITabPage | undefined {
143
+ return this.access.getPages(this.tabId)[index]
144
+ }
145
+
146
+ override isEmpty(): boolean {
147
+ return this.access.getPages(this.tabId).length === 0
148
+ }
149
+
150
+ override size(): number {
151
+ return this.access.getPages(this.tabId).length
152
+ }
153
+
154
+ override clear(): void {
155
+ this.access.clearPages(this.tabId)
156
+ }
157
+
158
+ override list(): ITabPage[] {
159
+ return [...this.access.getPages(this.tabId)]
160
+ }
161
+
162
+ override readonlyList(): readonly ITabPage[] {
163
+ return this.access.getPages(this.tabId)
164
+ }
165
+
166
+ override find(predicate: (element: ITabPage, index: number) => boolean): ITabPage | undefined {
167
+ return this.list().find(predicate)
168
+ }
169
+
170
+ override findIndex(predicate: (element: ITabPage, index: number) => boolean): number {
171
+ return this.list().findIndex(predicate)
172
+ }
173
+
174
+ override filter(predicate: (element: ITabPage, index: number) => boolean): ITabPage[] {
175
+ return this.list().filter(predicate)
176
+ }
177
+
178
+ override some(predicate: (element: ITabPage, index: number) => boolean): boolean {
179
+ return this.list().some(predicate)
180
+ }
181
+
182
+ override forEachTopDown(visit: (element: ITabPage, index: number) => void): void {
183
+ const pages = this.list()
184
+ for (let index = pages.length - 1; index >= 0; index--) {
185
+ visit(pages[index] as ITabPage, index)
186
+ }
187
+ }
188
+
189
+ override toJSON(): ITabPage[] {
190
+ return this.list()
191
+ }
192
+
193
+ override toString(): string {
194
+ return this.list().toString()
195
+ }
196
+ }
197
+
198
+ const createTabStore = (context: StackTabsRuntimeContext): TabStoreApi => {
199
+ const { tabs, caches, components, cacheIdsToEvict, tabIdsToEvict } = context
200
+ const eviction = createTabPanelEviction(context)
201
+
202
+ // ---- 规范化状态 ----
203
+ const tabOrder = ref<string[]>([])
204
+ const tabsById = reactive(new Map<string, TabRecord>())
205
+ const pageIdsByTabId = reactive(new Map<string, string[]>())
206
+ const pagesById = reactive(new Map<string, ITabPage>())
207
+ const activeTabId = ref('')
208
+ /** 稳定的兼容视图,保证 TabHeader 等使用方的对象身份不变 */
209
+ const tabViews = new Map<string, ITabItem>()
210
+
211
+ // ---- 查询 ----
212
+ const getTabs = (): readonly ITabItem[] => tabs.value
213
+
214
+ const getTab = (id: string): ITabItem | null => tabs.value.find((tab) => tab.id === id) ?? null
215
+
216
+ const getActiveTab = (): ITabItem | null => tabs.value.find((tab) => tab.active) ?? null
217
+
218
+ const getPageIds = (tabId: string): readonly string[] => pageIdsByTabId.get(tabId) ?? []
219
+
220
+ const getPages = (tabId: string): readonly ITabPage[] => {
221
+ const ids = pageIdsByTabId.get(tabId)
222
+ if (!ids || ids.length === 0) return []
223
+ const pages: ITabPage[] = []
224
+ for (const id of ids) {
225
+ const page = pagesById.get(id)
226
+ if (page) pages.push(page)
227
+ }
228
+ return pages
229
+ }
230
+
231
+ const getTopPage = (tabId: string): ITabPage | null => {
232
+ const ids = pageIdsByTabId.get(tabId)
233
+ if (!ids || ids.length === 0) return null
234
+ return pagesById.get(ids[ids.length - 1] as string) ?? null
235
+ }
236
+
237
+ const findTabByTopPagePath = (path: string): ITabItem | null => {
238
+ for (const id of tabOrder.value) {
239
+ if (getTopPage(id)?.path === path) return getTab(id)
240
+ }
241
+ return null
242
+ }
243
+
244
+ const hasTab = (id: string): boolean => tabsById.has(id)
245
+
246
+ const canAddTab = (): boolean =>
247
+ context.maxTabCount.value <= 0 ||
248
+ (context.maxTabCount.value > 0 && context.maxTabCount.value > tabOrder.value.length)
249
+
250
+ const getPageSnapshot = (tabId: string): ITabPage[] =>
251
+ getPages(tabId).map((page) => clonePage(page))
252
+
253
+ // ---- 标签写入 ----
254
+ const toRecord = (tab: ITabItem): TabRecord => ({
255
+ id: tab.id,
256
+ title: tab.title,
257
+ closable: tab.closable,
258
+ refreshable: tab.refreshable,
259
+ iframe: tab.iframe,
260
+ iframeRefreshMode: tab.iframeRefreshMode,
261
+ url: tab.url
262
+ })
263
+
264
+ const registerTab = (tab: ITabItem): void => {
265
+ tabsById.set(tab.id, toRecord(tab))
266
+
267
+ const pages = tab.pages.list()
268
+ const pageIds = pages.map((page) => page.id)
269
+ for (const page of pages) pagesById.set(page.id, page)
270
+ pageIdsByTabId.set(tab.id, pageIds)
271
+
272
+ tabViews.delete(tab.id)
273
+ if (!tabOrder.value.includes(tab.id)) {
274
+ tabOrder.value = [...tabOrder.value, tab.id]
275
+ }
276
+ if (tab.active) activeTabId.value = tab.id
277
+ }
278
+
279
+ const addTab = (tab: ITabItem): void => {
280
+ registerTab(tab)
281
+ syncTabsView()
282
+ }
283
+
284
+ const addTabs = (newTabs: readonly ITabItem[]): void => {
285
+ if (newTabs.length === 0) return
286
+ for (const tab of newTabs) registerTab(tab)
287
+ syncTabsView()
288
+ }
289
+
290
+ const activateTab = (id: string): ITabItem | null => {
291
+ activeTabId.value = tabsById.has(id) ? id : ''
292
+ syncTabsView()
293
+ return getTab(id)
294
+ }
295
+
296
+ const moveTab = (id: string, targetIndex: number): boolean => {
297
+ const order = tabOrder.value
298
+ const sourceIndex = order.indexOf(id)
299
+ if (
300
+ sourceIndex < 0 ||
301
+ targetIndex < 0 ||
302
+ targetIndex >= order.length ||
303
+ sourceIndex === targetIndex
304
+ ) {
305
+ return false
306
+ }
307
+
308
+ const reordered = [...order]
309
+ const [tabId] = reordered.splice(sourceIndex, 1)
310
+ if (!tabId) return false
311
+ reordered.splice(targetIndex, 0, tabId)
312
+ tabOrder.value = reordered
313
+ syncTabsView()
314
+ return true
315
+ }
316
+
317
+ const removeTab = (id: string): RemoveTabResult => {
318
+ const order = tabOrder.value
319
+ const index = order.indexOf(id)
320
+ if (index < 0) return { status: 'not-found', removed: null, nextActiveId: '' }
321
+
322
+ const record = tabsById.get(id)
323
+ if (!record || !record.closable) {
324
+ return { status: 'not-closable', removed: null, nextActiveId: id }
325
+ }
326
+
327
+ const removed = materializeTab(id)
328
+ const wasActive = activeTabId.value === id
329
+ if (removed) eviction.markTabPagesForEviction(removed)
330
+
331
+ for (const pageId of getPageIds(id)) pagesById.delete(pageId)
332
+ pageIdsByTabId.delete(id)
333
+ tabsById.delete(id)
334
+ tabViews.delete(id)
335
+
336
+ let nextActiveId = ''
337
+ if (order.length > 1 && wasActive) {
338
+ nextActiveId = (index === 0 ? order[1] : order[index - 1]) ?? ''
339
+ }
340
+ if (wasActive) activeTabId.value = ''
341
+
342
+ tabOrder.value = order.filter((tabId) => tabId !== id)
343
+ syncTabsView()
344
+
345
+ // 非激活标签关闭后没有 onActivated 兜底,立即执行驱逐。
346
+ if (!wasActive) {
347
+ eviction.evictMarkedCaches()
348
+ tabIdsToEvict.clear()
349
+ }
350
+
351
+ return { status: 'removed', removed, nextActiveId }
352
+ }
353
+
354
+ const removeTabs = (shouldRemove: (tab: ITabItem, index: number) => boolean): ITabItem[] => {
355
+ const removed: ITabItem[] = []
356
+ const remaining: string[] = []
357
+
358
+ tabOrder.value.forEach((id, index) => {
359
+ const record = tabsById.get(id)
360
+ const tab = materializeTab(id)
361
+ if (!record || !tab) return
362
+ if (!record.closable || !shouldRemove(tab, index)) {
363
+ remaining.push(id)
364
+ return
365
+ }
366
+
367
+ eviction.markTabPagesForEviction(tab)
368
+ for (const pageId of getPageIds(id)) pagesById.delete(pageId)
369
+ pageIdsByTabId.delete(id)
370
+ tabsById.delete(id)
371
+ tabViews.delete(id)
372
+ if (activeTabId.value === id) activeTabId.value = ''
373
+ removed.push(tab)
374
+ })
375
+
376
+ tabOrder.value = remaining
377
+ syncTabsView()
378
+ return removed
379
+ }
380
+
381
+ // ---- 页面栈写入 ----
382
+ const appendPage = (tabId: string, page: ITabPage): void => {
383
+ if (!tabsById.has(tabId)) return
384
+ const ids = pageIdsByTabId.get(tabId) ?? []
385
+ pageIdsByTabId.set(tabId, [...ids, page.id])
386
+ pagesById.set(page.id, page)
387
+ syncTabsView()
388
+ }
389
+
390
+ const popPages = (tabId: string, steps: number): ITabPage[] => {
391
+ if (!tabsById.has(tabId) || steps <= 0) return []
392
+
393
+ const ids = [...(pageIdsByTabId.get(tabId) ?? [])]
394
+ const popped: ITabPage[] = []
395
+ for (let index = 0; index < steps; index++) {
396
+ const pageId = ids.pop()
397
+ if (!pageId) break
398
+ const page = pagesById.get(pageId)
399
+ if (page) popped.push(page)
400
+ pagesById.delete(pageId)
401
+ }
402
+ pageIdsByTabId.set(tabId, ids)
403
+ syncTabsView()
404
+ return popped
405
+ }
406
+
407
+ const replacePages = (tabId: string, pages: readonly ITabPage[]): void => {
408
+ if (!tabsById.has(tabId)) return
409
+
410
+ for (const pageId of getPageIds(tabId)) pagesById.delete(pageId)
411
+ const nextPageIds: string[] = []
412
+ for (const page of pages) {
413
+ const cloned = clonePage(page)
414
+ pagesById.set(cloned.id, cloned)
415
+ nextPageIds.push(cloned.id)
416
+ }
417
+ pageIdsByTabId.set(tabId, nextPageIds)
418
+ syncTabsView()
419
+ }
420
+
421
+ const clearPages = (tabId: string): void => {
422
+ if (!tabsById.has(tabId)) return
423
+ for (const pageId of getPageIds(tabId)) pagesById.delete(pageId)
424
+ pageIdsByTabId.set(tabId, [])
425
+ syncTabsView()
426
+ }
427
+
428
+ const updatePage = (tabId: string, pageId: string, patch: PagePatch): boolean => {
429
+ const ids = pageIdsByTabId.get(tabId)
430
+ if (!ids || !ids.includes(pageId)) return false
431
+
432
+ const page = pagesById.get(pageId)
433
+ if (!page) return false
434
+
435
+ Object.assign(page, patch)
436
+ if (patch.id && patch.id !== pageId) {
437
+ pagesById.delete(pageId)
438
+ pagesById.set(patch.id, page)
439
+ pageIdsByTabId.set(
440
+ tabId,
441
+ ids.map((id) => (id === pageId ? (patch.id as string) : id))
442
+ )
443
+ }
444
+ syncTabsView()
445
+ return true
446
+ }
447
+
448
+ const consumeBackParams = (tabId: string, pageId: string): Record<string, unknown> | null => {
449
+ const top = getTopPage(tabId)
450
+ if (!top || top.id !== pageId || !top._backParams) return null
451
+ const backParams = { ...top._backParams }
452
+ delete top._backParams
453
+ syncTabsView()
454
+ return backParams
455
+ }
456
+
457
+ // ---- 缓存与组件注册表 ----
458
+ const snapshotCaches = (): string[] => [...caches.value]
459
+
460
+ const restoreCaches = (cacheNames: readonly string[]): void => {
461
+ caches.value = [...cacheNames]
462
+ }
463
+
464
+ const snapshotComponents = (): Map<string, DefineComponent> => new Map(components)
465
+
466
+ const restoreComponents = (snapshot: Map<string, DefineComponent>): void => {
467
+ components.clear()
468
+ for (const [id, component] of snapshot) components.set(id, component)
469
+ }
470
+
471
+ const reset = (): void => {
472
+ tabOrder.value = []
473
+ tabsById.clear()
474
+ pageIdsByTabId.clear()
475
+ pagesById.clear()
476
+ tabViews.clear()
477
+ activeTabId.value = ''
478
+ tabs.value = []
479
+ caches.value = []
480
+ components.clear()
481
+ cacheIdsToEvict.clear()
482
+ tabIdsToEvict.clear()
483
+ }
484
+
485
+ // ---- 兼容视图 ----
486
+ const pageAccess: PageStackAccess = {
487
+ getPages,
488
+ getTopPage,
489
+ appendPage,
490
+ popPages,
491
+ clearPages
492
+ }
493
+
494
+ function materializeTab(id: string): ITabItem | null {
495
+ const record = tabsById.get(id)
496
+ if (!record) return null
497
+
498
+ let view = tabViews.get(id)
499
+ if (!view) {
500
+ view = {
501
+ id: record.id,
502
+ title: record.title,
503
+ closable: record.closable,
504
+ refreshable: record.refreshable,
505
+ iframe: record.iframe,
506
+ iframeRefreshMode: record.iframeRefreshMode,
507
+ url: record.url,
508
+ active: activeTabId.value === id,
509
+ pages: new PageStackView(id, pageAccess)
510
+ }
511
+ tabViews.set(id, view)
512
+ } else {
513
+ view.title = record.title
514
+ view.closable = record.closable
515
+ view.refreshable = record.refreshable
516
+ view.iframe = record.iframe
517
+ view.active = activeTabId.value === id
518
+ if (record.iframeRefreshMode === undefined) {
519
+ delete view.iframeRefreshMode
520
+ } else {
521
+ view.iframeRefreshMode = record.iframeRefreshMode
522
+ }
523
+ if (record.url === undefined) {
524
+ delete view.url
525
+ } else {
526
+ view.url = record.url
527
+ }
528
+ }
529
+ return view
530
+ }
531
+
532
+ function syncTabsView(): void {
533
+ const next: ITabItem[] = []
534
+ for (const id of tabOrder.value) {
535
+ const view = materializeTab(id)
536
+ if (view) next.push(view)
537
+ }
538
+ tabs.value = next
539
+ }
540
+
541
+ return {
542
+ ...eviction,
543
+ getTabs,
544
+ getTab,
545
+ getActiveTab,
546
+ findTabByTopPagePath,
547
+ hasTab,
548
+ canAddTab,
549
+ getPages,
550
+ getTopPage,
551
+ getPageSnapshot,
552
+ addTab,
553
+ addTabs,
554
+ activateTab,
555
+ moveTab,
556
+ removeTab,
557
+ removeTabs,
558
+ appendPage,
559
+ popPages,
560
+ replacePages,
561
+ clearPages,
562
+ updatePage,
563
+ consumeBackParams,
564
+ isTabMarkedForEviction: (id) => tabIdsToEvict.has(id),
565
+ isCacheMarkedForEviction: (id) => cacheIdsToEvict.has(id),
566
+ unmarkCacheForEviction: (id) => cacheIdsToEvict.delete(id),
567
+ clearTabEvictionMarks: () => tabIdsToEvict.clear(),
568
+ getComponent: (id) => components.get(id),
569
+ setComponent: (id, component) => components.set(id, component),
570
+ deleteComponent: (id) => components.delete(id),
571
+ snapshotComponents,
572
+ restoreComponents,
573
+ snapshotCaches,
574
+ restoreCaches,
575
+ touchTabs: syncTabsView,
576
+ reset
577
+ }
578
+ }
579
+
580
+ const storesByContext = new WeakMap<StackTabsRuntimeContext, TabStoreApi>()
581
+
582
+ /**
583
+ * 每个运行时上下文只创建一个 store。
584
+ *
585
+ * useTabPanel 会被 StackKeepAlive、StackCacheRenderer、页面内的 useTabRouter 多次调用;
586
+ * 复用同一实例可避免 eviction 的 Set/数组出现多份互相不同步的状态。
587
+ */
588
+ export const getTabStore = (context: StackTabsRuntimeContext): TabStoreApi => {
589
+ const existing = storesByContext.get(context)
590
+ if (existing) return existing
591
+
592
+ const store = createTabStore(context)
593
+ storesByContext.set(context, store)
594
+ return store
595
+ }
@@ -37,6 +37,7 @@ export default function useTabActions(providedRuntimeContext?: StackTabsRuntimeC
37
37
  canAddTab,
38
38
  renewTab,
39
39
  getTab,
40
+ getTopPage,
40
41
  tabs,
41
42
  moveTab,
42
43
  removeTab,
@@ -152,8 +153,7 @@ export default function useTabActions(providedRuntimeContext?: StackTabsRuntimeC
152
153
  target: { tabId?: string; pageId?: string } = {}
153
154
  ): HTMLElement | undefined => {
154
155
  if (typeof document === 'undefined') return undefined
155
- const pageId =
156
- target.pageId ?? (target.tabId ? getTab(target.tabId)?.pages.peek()?.id : undefined)
156
+ const pageId = target.pageId ?? (target.tabId ? getTopPage(target.tabId)?.id : undefined)
157
157
  if (pageId) return document.getElementById(`W-${pageId}`) ?? undefined
158
158
  return document.querySelector<HTMLElement>('.cache-page-wrapper') ?? undefined
159
159
  }