vue-stack-tabs 1.1.0 → 1.3.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.
Files changed (115) hide show
  1. package/README.en.md +526 -0
  2. package/README.md +139 -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 +68 -47
  60. package/src/lib/StackTabs.vue +223 -196
  61. package/src/lib/assets/style/stackTab.scss +84 -12
  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 +45 -16
  69. package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
  70. package/src/lib/components/TabHeader/index.vue +109 -12
  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 +62 -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 +266 -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
@@ -1,104 +1,139 @@
1
1
  /**
2
2
  * tabPanel/evict - 缓存驱逐逻辑
3
3
  *
4
- * 职责:标记待驱逐缓存、执行驱逐、维护 caches / components
5
- *
6
- * 驱逐机制分两种:
7
- *
8
- * 1. 「标记 + 延迟驱逐」—— 用于关闭标签、刷新标签等场景
9
- * 调用 markCacheForEviction 将 cacheName 放入 cacheIdsToEvict 集合,
10
- * 等到目标页面的 cacheComponent 触发 onActivated 时,
11
- * 再批量调用 evictMarkedCaches 真正执行驱逐。
12
- * 该策略确保只有切换到新的活动页后才清理旧缓存,避免渲染空白。
13
- *
14
- * 2. 「立即驱逐」—— 用于 backward 内部退栈场景
15
- * 调用 evictPageCache 直接同步清除单个缓存,
16
- * 但必须在 router.push().then() 回调内调用(即导航完成后),
17
- * 否则会触发以旧路由为基础的响应式重渲染,导致刚驱逐的组件被重建。
18
- * 详见 useTabRouter.ts backward 函数的时序说明。
4
+ * 职责:基于当前 StackTabsRuntimeContext 标记待驱逐缓存、执行驱逐、维护 caches / components。
19
5
  */
20
6
  import type { ITabPage } from '../../model/TabModel'
21
- import { caches, components, cacheIdsToEvict, tabIdsToEvict } from './state'
7
+ import { triggerRef } from 'vue'
8
+ import type { StackTabsRuntimeContext } from '../stackTabsContext'
22
9
 
23
- /**
24
- * 将指定 cacheName 放入待驱逐集合。
25
- * 不立即清除,等叫到 evictMarkedCaches 时统一处理。
26
- */
27
- export const markCacheForEviction = (cacheName: string) => {
28
- cacheIdsToEvict.add(cacheName)
10
+ export interface TabPanelEvictionApi {
11
+ markCacheForEviction: (cacheName: string) => void
12
+ markTabPagesForEviction: (tab: { id: string; pages: { list(): ITabPage[] } }) => void
13
+ markTabPagesForEvictionOnly: (tab: { pages: { list(): ITabPage[] } }) => void
14
+ addCache: (cacheName: string) => void
15
+ removeCache: (cacheName: string) => void
16
+ evictPageCache: (cacheName: string) => void
17
+ replacePageCaches: (
18
+ cacheNamesToEvict: Iterable<string>,
19
+ cacheNamesToAdd: Iterable<string>
20
+ ) => void
21
+ evictMarkedCaches: () => void
29
22
  }
30
23
 
31
- /**
32
- * 标记某个标签下所有页面为待驱逐,并标记该标签本身(用于「关闭标签」场景)。
33
- * tabIdsToEvict 中的标签 ID 会在 updatePageState 中被检测到,
34
- * 阻止被关闭的标签重新路由时意外复活。
35
- */
36
- export const markTabPagesForEviction = (tab: { id: string; pages: { list(): ITabPage[] } }) => {
37
- for (const page of tab.pages.list()) markCacheForEviction(page.id)
38
- tabIdsToEvict.add(tab.id)
39
- }
24
+ export const createTabPanelEviction = (context: StackTabsRuntimeContext): TabPanelEvictionApi => {
25
+ const { caches, components, cacheIdsToEvict, tabIdsToEvict } = context
40
26
 
41
- /**
42
- * 仅标记标签下所有页面为待驱逐,不标记标签本身(用于「续签/换页」场景)。
43
- * 适用于刷新标签:旧页面缓存清零,标签本身仍然存活,
44
- * 导航后由 updatePageState 重新创建页面。
45
- */
46
- export const markTabPagesForEvictionOnly = (tab: { pages: { list(): ITabPage[] } }) => {
47
- for (const page of tab.pages.list()) markCacheForEviction(page.id)
48
- }
27
+ const cacheSet = new Set<string>(caches.value)
28
+ let cacheList = caches.value
49
29
 
50
- /**
51
- * 将 cacheName 加入 <keep-alive> 的 include 列表(caches)。
52
- * 幂等:已存在时不重复添加,避免 include 数组中出现重复项
53
- * 影响 <keep-alive> 缓存范围,决定哪些 cacheComponent 实例会被保留。
54
- */
55
- export const addCache = (cacheName: string) => {
56
- if (!caches.value.includes(cacheName)) caches.value.push(cacheName)
57
- }
30
+ /** 外部重置 caches(例如 destroy)后,同步内部 Set,避免后续 addCache 被错误跳过。 */
31
+ const syncCacheSet = () => {
32
+ if (cacheList === caches.value) return
33
+ cacheList = caches.value
34
+ cacheSet.clear()
35
+ for (const cacheName of cacheList) cacheSet.add(cacheName)
36
+ }
58
37
 
59
- /**
60
- * 将 cacheName 从 include 列表(caches)中移除。
61
- * Vue 的 <keep-alive> 会以 flush:'post' 的 watcher 检测 include 变更,
62
- * 在下次渲染后自动销毁不再被 include 的缓存实例。
63
- */
64
- export const removeCache = (cacheName: string) => {
65
- const i = caches.value.indexOf(cacheName)
66
- if (i >= 0) caches.value.splice(i, 1)
67
- }
38
+ const markCacheForEviction = (cacheName: string): void => {
39
+ cacheIdsToEvict.add(cacheName)
40
+ }
68
41
 
69
- /**
70
- * 立即、同步、彻底驱逐单个页面的缓存。
71
- *
72
- * 操作步骤:
73
- * 1. 从 cacheIdsToEvict 移除(若存在,防止后续 evictMarkedCaches 二次处理)
74
- * 2. removeCache(cacheName) → 从 <keep-alive> include 列表删除,触发 Vue 销毁缓存组件实例
75
- * 3. components.delete(...) → 从 components Map 删除组件定义,
76
- * 使 resolvePageComponent 下次进入时必须重新创建全新的 cacheComponent 实例
77
- *
78
- * ⚠️ 调用时机要求:
79
- * 必须在 router.push() 完成(.then())之后调用,
80
- * 不能在导航前调用,否则驱逐触发的响应式更新会导致 StackTabs 以旧路由重渲染,
81
- * 进而重建刚被删除的组件(竞态)。详见 useTabRouter.ts backward 的注释。
82
- */
83
- export const evictPageCache = (cacheName: string) => {
84
- if (!cacheName) return
85
- cacheIdsToEvict.delete(cacheName)
86
- removeCache(cacheName)
87
- components.delete(cacheName)
88
- }
42
+ const markTabPagesForEviction = (tab: { id: string; pages: { list(): ITabPage[] } }): void => {
43
+ for (const page of tab.pages.list()) markCacheForEviction(page.id)
44
+ tabIdsToEvict.add(tab.id)
45
+ }
89
46
 
90
- /**
91
- * 执行所有已标记缓存的批量驱逐(由 cacheComponent 的 onActivated 调用)。
92
- *
93
- * 调用时机:
94
- * 当 cacheComponent 切换到激活(activated)状态时,说明用户切换到了一个新页面,
95
- * 相关的旧页面已经稳定进入「不活跃/待驱逐」状态,此时批量清理是安全的。
96
- */
97
- export const evictMarkedCaches = () => {
98
- if (cacheIdsToEvict.size > 0) {
99
- for (const cacheName of [...cacheIdsToEvict]) {
100
- evictPageCache(cacheName)
47
+ const markTabPagesForEvictionOnly = (tab: { pages: { list(): ITabPage[] } }): void => {
48
+ for (const page of tab.pages.list()) markCacheForEviction(page.id)
49
+ }
50
+
51
+ const addCache = (cacheName: string): void => {
52
+ syncCacheSet()
53
+ if (!cacheSet.has(cacheName)) {
54
+ cacheSet.add(cacheName)
55
+ cacheList.push(cacheName)
56
+ triggerRef(caches)
57
+ }
58
+ }
59
+
60
+ const removeCache = (cacheName: string): void => {
61
+ syncCacheSet()
62
+ if (cacheSet.has(cacheName)) {
63
+ cacheSet.delete(cacheName)
64
+ const index = cacheList.indexOf(cacheName)
65
+ if (index >= 0) cacheList.splice(index, 1)
66
+ triggerRef(caches)
67
+ }
68
+ }
69
+
70
+ const evictPageCache = (cacheName: string): void => {
71
+ if (!cacheName) return
72
+ cacheIdsToEvict.delete(cacheName)
73
+ removeCache(cacheName)
74
+ components.delete(cacheName)
75
+ }
76
+
77
+ /** 批量替换缓存 id,仅在所有变更完成后通知一次,供刷新全部等操作使用。 */
78
+ const replacePageCaches = (
79
+ cacheNamesToEvict: Iterable<string>,
80
+ cacheNamesToAdd: Iterable<string>
81
+ ): void => {
82
+ syncCacheSet()
83
+ const toEvict = new Set(cacheNamesToEvict)
84
+ let changed = false
85
+
86
+ if (toEvict.size > 0) {
87
+ for (let index = cacheList.length - 1; index >= 0; index--) {
88
+ const cacheName = cacheList[index]
89
+ if (cacheName && toEvict.has(cacheName)) {
90
+ cacheList.splice(index, 1)
91
+ changed = true
92
+ }
93
+ }
94
+ for (const cacheName of toEvict) {
95
+ cacheSet.delete(cacheName)
96
+ cacheIdsToEvict.delete(cacheName)
97
+ components.delete(cacheName)
98
+ }
99
+ }
100
+
101
+ for (const cacheName of cacheNamesToAdd) {
102
+ if (!cacheSet.has(cacheName)) {
103
+ cacheSet.add(cacheName)
104
+ cacheList.push(cacheName)
105
+ changed = true
106
+ }
107
+ }
108
+
109
+ if (changed) triggerRef(caches)
110
+ }
111
+
112
+ const evictMarkedCaches = (): void => {
113
+ if (cacheIdsToEvict.size <= 0) return
114
+
115
+ syncCacheSet()
116
+ const toEvict = new Set(cacheIdsToEvict)
117
+ for (let index = cacheList.length - 1; index >= 0; index--) {
118
+ const cacheName = cacheList[index]
119
+ if (cacheName && toEvict.has(cacheName)) cacheList.splice(index, 1)
101
120
  }
121
+ for (const cacheName of toEvict) {
122
+ cacheSet.delete(cacheName)
123
+ components.delete(cacheName)
124
+ }
125
+ triggerRef(caches)
102
126
  cacheIdsToEvict.clear()
103
127
  }
128
+
129
+ return {
130
+ markCacheForEviction,
131
+ markTabPagesForEviction,
132
+ markTabPagesForEvictionOnly,
133
+ addCache,
134
+ removeCache,
135
+ evictPageCache,
136
+ replacePageCaches,
137
+ evictMarkedCaches
138
+ }
104
139
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 导航事务管理器。
3
+ *
4
+ * 将「乐观 UI 更新 → 路由导航 → 确认/回滚」包装为原子事务:
5
+ * 1. apply() — 先对 UI 状态做乐观修改,返回快照
6
+ * 2. navigate() — 执行 router.push 等异步导航
7
+ * 3. 成功 → commit();失败/异常 → rollback(snapshot) 恢复快照
8
+ *
9
+ * 当连续快速操作导致事务过期时(isCurrent 返回 false),
10
+ * 不回滚而是调用 cleanupStale() 清理残留状态,避免覆盖新事务的结果。
11
+ */
12
+ interface NavigationTransactionOptions<TSnapshot, TResult> {
13
+ /** 乐观应用 UI 变更,返回可用于 rollback 的快照 */
14
+ apply: () => TSnapshot
15
+ /** 执行异步路由导航 */
16
+ navigate: () => Promise<TResult>
17
+ /** 导航失败时回滚到快照状态 */
18
+ rollback: (snapshot: TSnapshot) => void
19
+ /** 导航成功后的确认回调(如驱逐旧缓存) */
20
+ commit?: () => void
21
+ /** 判断导航结果是否为失败(如 vue-router 返回的 NavigationFailure) */
22
+ isFailureResult?: (result: TResult) => boolean
23
+ /** 若为失败结果,是否将其作为异常抛出 */
24
+ rejectFailureResult?: boolean
25
+ /** 检查当前事务是否仍然有效(防止过期事务覆盖新状态) */
26
+ isCurrent?: () => boolean
27
+ /** 事务已过期时的清理回调(替代 rollback) */
28
+ cleanupStale?: () => void
29
+ }
30
+
31
+ /**
32
+ * 执行一次导航事务:apply → navigate → commit/rollback。
33
+ * 保证乐观更新在导航失败时能安全回滚。
34
+ */
35
+ export const runNavigationTransaction = async <TSnapshot, TResult>(
36
+ options: NavigationTransactionOptions<TSnapshot, TResult>
37
+ ): Promise<TResult> => {
38
+ const snapshot = options.apply()
39
+
40
+ try {
41
+ const result = await options.navigate()
42
+ if (options.isFailureResult?.(result)) {
43
+ if (options.isCurrent?.() ?? true) {
44
+ options.rollback(snapshot)
45
+ } else {
46
+ options.cleanupStale?.()
47
+ }
48
+ if (options.rejectFailureResult) throw result
49
+ return result
50
+ }
51
+ if (options.isCurrent?.() ?? true) options.commit?.()
52
+ return result
53
+ } catch (error) {
54
+ if (options.isCurrent?.() ?? true) {
55
+ options.rollback(snapshot)
56
+ } else {
57
+ options.cleanupStale?.()
58
+ }
59
+ throw error
60
+ }
61
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * tabPanel/refresh - 普通页面与 iframe 标签的刷新逻辑。
3
+ *
4
+ * 活动普通页面保持 cache id 稳定,只递增页面级刷新版本;非活动页面可直接替换缓存。
5
+ */
6
+ import type { Ref } from 'vue'
7
+ import type { ITabItem } from '../../model/TabModel'
8
+
9
+ export interface TabPanelRefreshApi {
10
+ refreshTab: (id: string) => void
11
+ refreshAllTabs: () => void
12
+ }
13
+
14
+ export interface CreateTabPanelRefreshOptions {
15
+ tabs: Ref<ITabItem[]>
16
+ iframeRefreshKeys: Ref<Record<string, number>>
17
+ getTab: (id: string) => ITabItem | null
18
+ createPageId: () => string
19
+ addCache: (cacheName: string) => void
20
+ evictPageCache: (cacheName: string) => void
21
+ replacePageCaches: (
22
+ cacheNamesToEvict: Iterable<string>,
23
+ cacheNamesToAdd: Iterable<string>
24
+ ) => void
25
+ refreshIframeByPostMessage: (tabId: string) => void
26
+ }
27
+
28
+ export const createTabPanelRefresh = (
29
+ options: CreateTabPanelRefreshOptions
30
+ ): TabPanelRefreshApi => {
31
+ const {
32
+ tabs,
33
+ iframeRefreshKeys,
34
+ getTab,
35
+ createPageId,
36
+ addCache,
37
+ evictPageCache,
38
+ replacePageCaches,
39
+ refreshIframeByPostMessage
40
+ } = options
41
+
42
+ const refreshIframe = (tab: ITabItem) => {
43
+ if (tab.iframeRefreshMode === 'reload') {
44
+ iframeRefreshKeys.value = {
45
+ ...iframeRefreshKeys.value,
46
+ [tab.id]: (iframeRefreshKeys.value[tab.id] ?? 0) + 1
47
+ }
48
+ return
49
+ }
50
+ refreshIframeByPostMessage(tab.id)
51
+ }
52
+
53
+ const refreshPage = (tab: ITabItem, cacheNamesToEvict?: string[], cacheNamesToAdd?: string[]) => {
54
+ const currentPage = tab.pages.peek()
55
+ if (!currentPage) return
56
+
57
+ if (tab.active) {
58
+ currentPage.refreshVersion = (currentPage.refreshVersion ?? 0) + 1
59
+ return
60
+ }
61
+
62
+ const oldCacheName = currentPage.id
63
+ currentPage.id = createPageId()
64
+ currentPage.refreshVersion = 0
65
+ if (cacheNamesToEvict && cacheNamesToAdd) {
66
+ cacheNamesToEvict.push(oldCacheName)
67
+ cacheNamesToAdd.push(currentPage.id)
68
+ return
69
+ }
70
+ evictPageCache(oldCacheName)
71
+ addCache(currentPage.id)
72
+ }
73
+
74
+ const refreshTab = (id: string) => {
75
+ const tab = getTab(id)
76
+ if (!tab?.refreshable) return
77
+ if (tab.iframe) {
78
+ refreshIframe(tab)
79
+ return
80
+ }
81
+ refreshPage(tab)
82
+ }
83
+
84
+ const refreshAllTabs = () => {
85
+ const cacheNamesToEvict: string[] = []
86
+ const cacheNamesToAdd: string[] = []
87
+ const iframeKeysUpdate = { ...iframeRefreshKeys.value }
88
+ let iframeKeysChanged = false
89
+
90
+ for (const tab of tabs.value) {
91
+ if (!tab.refreshable) continue
92
+ if (tab.iframe) {
93
+ if (tab.iframeRefreshMode === 'reload') {
94
+ iframeKeysUpdate[tab.id] = (iframeKeysUpdate[tab.id] ?? 0) + 1
95
+ iframeKeysChanged = true
96
+ } else {
97
+ refreshIframeByPostMessage(tab.id)
98
+ }
99
+ continue
100
+ }
101
+ refreshPage(tab, cacheNamesToEvict, cacheNamesToAdd)
102
+ }
103
+
104
+ if (iframeKeysChanged) iframeRefreshKeys.value = iframeKeysUpdate
105
+ replacePageCaches(cacheNamesToEvict, cacheNamesToAdd)
106
+ }
107
+
108
+ return { refreshTab, refreshAllTabs }
109
+ }
@@ -1,56 +1,73 @@
1
1
  /**
2
2
  * tabPanel/scroll - 滚动位置保存与恢复
3
3
  *
4
- * 职责:在切出/切入页面时保存和恢复滚动位置
4
+ * 职责:基于当前 StackTabsRuntimeContext 在切出/切入页面时保存和恢复滚动位置。
5
5
  */
6
- import { scrollPositionsByPageId } from './state'
7
-
8
- /** 恢复指定页面的滚动位置 */
9
- export const restoreScroller = (pageCacheId: string) => {
10
- const positions = scrollPositionsByPageId.get(pageCacheId)
11
- if (!positions) return
12
- for (const [sel, pos] of positions) {
13
- const el = sel.startsWith('#')
14
- ? document.getElementById(sel.slice(1))
15
- : (document.querySelector(sel) as HTMLElement | null)
16
- if (el) {
17
- el.scrollTop = pos.top
18
- el.scrollLeft = pos.left
6
+ import type { StackTabsRuntimeContext } from '../stackTabsContext'
7
+
8
+ export interface TabPanelScrollApi {
9
+ restoreScroller: (pageCacheId: string) => void
10
+ saveScroller: (pageCacheId: string) => void
11
+ removeScroller: (pageCacheId: string) => void
12
+ addPageScroller: (pageCacheId: string, ...selectorIds: string[]) => void
13
+ }
14
+
15
+ const resolveScrollElement = (selector: string): HTMLElement | null =>
16
+ selector.startsWith('#')
17
+ ? document.getElementById(selector.slice(1))
18
+ : (document.querySelector(selector) as HTMLElement | null)
19
+
20
+ export const createTabPanelScroll = (context: StackTabsRuntimeContext): TabPanelScrollApi => {
21
+ const { scrollPositionsByPageId } = context
22
+
23
+ const restoreScroller = (pageCacheId: string): void => {
24
+ const positions = scrollPositionsByPageId.get(pageCacheId)
25
+ if (!positions) return
26
+
27
+ for (const [selector, position] of positions) {
28
+ const element = resolveScrollElement(selector)
29
+ if (element) {
30
+ element.scrollTop = position.top
31
+ element.scrollLeft = position.left
32
+ }
19
33
  }
20
34
  }
21
- }
22
35
 
23
- /** 保存指定页面的滚动位置 */
24
- export const saveScroller = (pageCacheId: string) => {
25
- const positions = scrollPositionsByPageId.get(pageCacheId)
26
- if (!positions) return
27
- for (const sel of positions.keys()) {
28
- const el = sel.startsWith('#')
29
- ? document.getElementById(sel.slice(1))
30
- : (document.querySelector(sel) as HTMLElement | null)
31
- positions.set(sel, {
32
- top: el?.scrollTop ?? 0,
33
- left: el?.scrollLeft ?? 0
34
- })
36
+ const saveScroller = (pageCacheId: string): void => {
37
+ const positions = scrollPositionsByPageId.get(pageCacheId)
38
+ if (!positions) return
39
+
40
+ for (const selector of positions.keys()) {
41
+ const element = resolveScrollElement(selector)
42
+ positions.set(selector, {
43
+ top: element?.scrollTop ?? 0,
44
+ left: element?.scrollLeft ?? 0
45
+ })
46
+ }
35
47
  }
36
- }
37
48
 
38
- /** 移除指定页面的滚动记录 */
39
- export const removeScroller = (pageCacheId: string) => {
40
- const positions = scrollPositionsByPageId.get(pageCacheId)
41
- if (positions) {
49
+ const removeScroller = (pageCacheId: string): void => {
50
+ const positions = scrollPositionsByPageId.get(pageCacheId)
51
+ if (!positions) return
52
+
42
53
  positions.clear()
43
54
  scrollPositionsByPageId.delete(pageCacheId)
44
55
  }
45
- }
46
56
 
47
- /** 为页面注册需要记录滚动位置的选择器 */
48
- export const addPageScroller = (pageCacheId: string, ...selectorIds: string[]) => {
49
- if (!scrollPositionsByPageId.has(pageCacheId)) {
50
- scrollPositionsByPageId.set(pageCacheId, new Map<string, { top: number; left: number }>())
57
+ const addPageScroller = (pageCacheId: string, ...selectorIds: string[]): void => {
58
+ if (!scrollPositionsByPageId.has(pageCacheId)) {
59
+ scrollPositionsByPageId.set(pageCacheId, new Map())
60
+ }
61
+ const positions = scrollPositionsByPageId.get(pageCacheId)!
62
+ for (const selector of selectorIds) {
63
+ positions.set(selector, { top: 0, left: 0 })
64
+ }
51
65
  }
52
- const positions = scrollPositionsByPageId.get(pageCacheId)!
53
- for (const sel of selectorIds) {
54
- positions.set(sel, { top: 0, left: 0 })
66
+
67
+ return {
68
+ restoreScroller,
69
+ saveScroller,
70
+ removeScroller,
71
+ addPageScroller
55
72
  }
56
73
  }
@@ -1,33 +1,95 @@
1
1
  /**
2
2
  * tabPanel/session - Session 持久化
3
3
  *
4
- * 职责:将当前激活标签保存到 sessionStorage,刷新后恢复
4
+ * 职责:基于当前 StackTabsRuntimeContext 将当前激活标签保存到 sessionStorage,刷新后恢复。
5
5
  */
6
- import type { ITabItem } from '../../model/TabModel'
6
+ import type { ITabItem, ITabPage } from '../../model/TabModel'
7
7
  import { Stack } from '../../model/TabModel'
8
- import type { ITabPage } from '../../model/TabModel'
9
- import { SESSION_TAB_NAME, sessionPrefix } from './state'
8
+ import { isRecord } from '../../utils/typeGuards'
9
+ import type { StackTabsRuntimeContext } from '../stackTabsContext'
10
+ import { SESSION_TAB_NAME } from './state'
10
11
 
11
- /** 获取 sessionStorage 的 key */
12
- export const getSessionKey = () => sessionPrefix + SESSION_TAB_NAME
12
+ const isRestoredTab = (value: unknown): value is ITabItem =>
13
+ isRecord(value) &&
14
+ typeof value.id === 'string' &&
15
+ typeof value.title === 'string' &&
16
+ typeof value.closable === 'boolean' &&
17
+ typeof value.refreshable === 'boolean' &&
18
+ typeof value.iframe === 'boolean' &&
19
+ typeof value.active === 'boolean' &&
20
+ value.pages instanceof Stack
13
21
 
14
- /** 将当前激活的标签保存到 sessionStorage */
15
- export const saveActiveTabToSession = (tab: ITabItem) => {
16
- if (!tab.id) return
17
- window.sessionStorage.setItem(getSessionKey(), JSON.stringify(tab))
22
+ export interface TabPanelSessionApi {
23
+ getSessionKey: () => string
24
+ saveActiveTabToSession: (tab: ITabItem) => void
25
+ clearSession: () => void
26
+ restoreActiveTabSession: (storedJson: string | null) => void
27
+ restoreTabFromSession: (storedJson: string | null) => ITabItem | null
18
28
  }
19
29
 
20
- /** 清除 sessionStorage 中的激活标签 */
21
- export const clearSession = () => {
22
- window.sessionStorage.removeItem(getSessionKey())
23
- }
30
+ export const createTabPanelSession = (context: StackTabsRuntimeContext): TabPanelSessionApi => {
31
+ const getSessionKey = (): string => context.sessionPrefix.value + SESSION_TAB_NAME
32
+ let pendingWrite: { tab: ITabItem; version: number } | null = null
33
+ let writeVersion = 0
34
+ let writeScheduled = false
35
+
36
+ const flushPendingWrite = () => {
37
+ writeScheduled = false
38
+ const write = pendingWrite
39
+ pendingWrite = null
40
+ if (!write || write.version !== writeVersion) return
41
+ window.sessionStorage.setItem(getSessionKey(), JSON.stringify(write.tab))
42
+ }
43
+
44
+ const invalidatePendingWrite = () => {
45
+ writeVersion++
46
+ pendingWrite = null
47
+ }
48
+
49
+ const saveActiveTabToSession = (tab: ITabItem): void => {
50
+ if (!tab.id) return
51
+ pendingWrite = { tab, version: ++writeVersion }
52
+ if (writeScheduled) return
53
+ writeScheduled = true
54
+ queueMicrotask(flushPendingWrite)
55
+ }
56
+
57
+ const clearSession = (): void => {
58
+ invalidatePendingWrite()
59
+ window.sessionStorage.removeItem(getSessionKey())
60
+ }
61
+
62
+ const restoreActiveTabSession = (storedJson: string | null): void => {
63
+ if (storedJson === null) {
64
+ clearSession()
65
+ return
66
+ }
67
+ invalidatePendingWrite()
68
+ window.sessionStorage.setItem(getSessionKey(), storedJson)
69
+ }
70
+
71
+ const restoreTabFromSession = (storedJson: string | null): ITabItem | null => {
72
+ if (storedJson == null) return null
73
+ try {
74
+ const restored = JSON.parse(storedJson, (key, value) =>
75
+ key === 'pages' && Array.isArray(value) ? new Stack<ITabPage>(value) : value
76
+ )
77
+ if (!isRestoredTab(restored)) {
78
+ clearSession()
79
+ return null
80
+ }
81
+ return restored as ITabItem
82
+ } catch {
83
+ clearSession()
84
+ return null
85
+ }
86
+ }
24
87
 
25
- /** 从 sessionStorage 解析恢复的标签(含 Stack 反序列化) */
26
- export const restoreTabFromSession = (
27
- storedJson: string | null
28
- ): ITabItem | null => {
29
- if (storedJson == null) return null
30
- return JSON.parse(storedJson, (k, v) =>
31
- k === 'pages' ? new Stack<ITabPage>(v) : v
32
- ) as ITabItem
88
+ return {
89
+ getSessionKey,
90
+ saveActiveTabToSession,
91
+ clearSession,
92
+ restoreActiveTabSession,
93
+ restoreTabFromSession
94
+ }
33
95
  }