vue-stack-tabs 1.0.1 → 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 (116) hide show
  1. package/README.en.md +526 -0
  2. package/README.md +337 -520
  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 +88 -0
  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 -23
  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 -73
  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 +17 -74
  37. package/dist/src/lib/hooks/useTabRouter.d.ts +5 -5
  38. package/dist/src/lib/i18n/index.d.ts +6 -4
  39. package/dist/src/lib/i18n/lang/en.d.ts +5 -0
  40. package/dist/src/lib/i18n/lang/zh-CN.d.ts +5 -0
  41. package/dist/src/lib/i18n/stackTabsLocale.d.ts +2 -0
  42. package/dist/src/lib/iframe-bridge.d.ts +8 -0
  43. package/dist/src/lib/iframe.vue.d.ts +2 -1
  44. package/dist/src/lib/index.d.ts +4 -5
  45. package/dist/src/lib/model/TabModel.d.ts +14 -33
  46. package/dist/src/lib/nuxt/module.d.ts +6 -0
  47. package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +2 -0
  48. package/dist/src/lib/nuxt/runtime/plugin.d.ts +2 -0
  49. package/dist/src/lib/utils/iframeBridge.d.ts +11 -3
  50. package/dist/src/lib/utils/stackTabsA11y.d.ts +8 -0
  51. package/dist/src/lib/utils/stackTabsMessage.d.ts +7 -0
  52. package/dist/src/lib/utils/tabInfoEncoder.d.ts +1 -16
  53. package/dist/src/lib/utils/typeGuards.d.ts +2 -0
  54. package/dist/src/lib/utils/urlParser.d.ts +24 -3
  55. package/dist/src/lib/versionLogger.d.ts +1 -0
  56. package/dist/style.css +2 -1
  57. package/dist/utils/iframeBridge.d.ts +39 -0
  58. package/dist/vue-stack-tabs.css +2 -1
  59. package/dist/vue-stack-tabs.es.js +1 -2411
  60. package/package.json +68 -47
  61. package/src/lib/StackTabs.vue +216 -192
  62. package/src/lib/assets/style/stackTab.scss +84 -12
  63. package/src/lib/components/ContextMenu/ContextMenuItem.vue +23 -11
  64. package/src/lib/components/ContextMenu/index.vue +94 -10
  65. package/src/lib/components/PageLoading.vue +9 -7
  66. package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +70 -0
  67. package/src/lib/components/StackKeepAlive/StackKeepAlive.vue +57 -0
  68. package/src/lib/components/TabHeader/TabHeaderButton.vue +19 -8
  69. package/src/lib/components/TabHeader/TabHeaderItem.vue +45 -16
  70. package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
  71. package/src/lib/components/TabHeader/index.vue +109 -12
  72. package/src/lib/hooks/stackTabsContext.ts +135 -0
  73. package/src/lib/hooks/tabPanel/evict.ts +123 -41
  74. package/src/lib/hooks/tabPanel/navigationTransaction.ts +61 -0
  75. package/src/lib/hooks/tabPanel/refresh.ts +109 -0
  76. package/src/lib/hooks/tabPanel/scroll.ts +57 -36
  77. package/src/lib/hooks/tabPanel/session.ts +84 -22
  78. package/src/lib/hooks/tabPanel/state.ts +41 -44
  79. package/src/lib/hooks/useIframeManager.ts +253 -0
  80. package/src/lib/hooks/useTabActions.ts +62 -30
  81. package/src/lib/hooks/useTabEventBus.ts +29 -19
  82. package/src/lib/hooks/useTabLoading.ts +3 -2
  83. package/src/lib/hooks/useTabPanel.tsx +415 -213
  84. package/src/lib/hooks/useTabRouter.ts +172 -43
  85. package/src/lib/i18n/index.ts +53 -45
  86. package/src/lib/i18n/lang/en.ts +6 -1
  87. package/src/lib/i18n/lang/zh-CN.ts +6 -1
  88. package/src/lib/i18n/stackTabsLocale.ts +8 -0
  89. package/src/lib/iframe-bridge.ts +12 -0
  90. package/src/lib/index.ts +14 -19
  91. package/src/lib/model/TabModel.ts +34 -86
  92. package/src/lib/nuxt/module.d.ts +4 -0
  93. package/src/lib/nuxt/module.ts +8 -3
  94. package/src/lib/nuxt/runtime/plugin.client.ts +2 -3
  95. package/src/lib/nuxt/runtime/plugin.ts +11 -0
  96. package/src/lib/utils/iframeBridge.ts +29 -5
  97. package/src/lib/utils/scrollUtils.ts +9 -4
  98. package/src/lib/utils/stackTabsA11y.ts +12 -0
  99. package/src/lib/utils/stackTabsMessage.ts +99 -0
  100. package/src/lib/utils/tabInfoEncoder.ts +69 -42
  101. package/src/lib/utils/typeGuards.ts +3 -0
  102. package/src/lib/utils/urlParser.ts +144 -14
  103. package/src/lib/versionLogger.ts +7 -19
  104. package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +0 -1
  105. package/dist/src/lib/hooks/useTabPanel.spec.d.ts +0 -1
  106. package/dist/src/lib/model/TabModel.spec.d.ts +0 -1
  107. package/dist/src/lib/utils/iframeBridge.spec.d.ts +0 -1
  108. package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +0 -1
  109. package/dist/src/lib/utils/urlParser.spec.d.ts +0 -1
  110. package/dist/vue-stack-tabs.umd.js +0 -1
  111. package/src/lib/hooks/tabPanel/evict.spec.ts +0 -85
  112. package/src/lib/hooks/useTabPanel.spec.ts +0 -34
  113. package/src/lib/model/TabModel.spec.ts +0 -40
  114. package/src/lib/utils/iframeBridge.spec.ts +0 -37
  115. package/src/lib/utils/tabInfoEncoder.spec.ts +0 -42
  116. package/src/lib/utils/urlParser.spec.ts +0 -45
@@ -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,52 +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 = document.querySelector(sel) as HTMLElement | null
14
- if (el) {
15
- el.scrollTop = pos.top
16
- 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
+ }
17
33
  }
18
34
  }
19
- }
20
35
 
21
- /** 保存指定页面的滚动位置 */
22
- export const saveScroller = (pageCacheId: string) => {
23
- const positions = scrollPositionsByPageId.get(pageCacheId)
24
- if (!positions) return
25
- for (const sel of positions.keys()) {
26
- const el = document.querySelector(sel) as HTMLElement | null
27
- positions.set(sel, {
28
- top: el?.scrollTop ?? 0,
29
- left: el?.scrollLeft ?? 0
30
- })
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
+ }
31
47
  }
32
- }
33
48
 
34
- /** 移除指定页面的滚动记录 */
35
- export const removeScroller = (pageCacheId: string) => {
36
- const positions = scrollPositionsByPageId.get(pageCacheId)
37
- if (positions) {
49
+ const removeScroller = (pageCacheId: string): void => {
50
+ const positions = scrollPositionsByPageId.get(pageCacheId)
51
+ if (!positions) return
52
+
38
53
  positions.clear()
39
54
  scrollPositionsByPageId.delete(pageCacheId)
40
55
  }
41
- }
42
56
 
43
- /** 为页面注册需要记录滚动位置的选择器 */
44
- export const addPageScroller = (pageCacheId: string, ...selectorIds: string[]) => {
45
- if (!scrollPositionsByPageId.has(pageCacheId)) {
46
- 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
+ }
47
65
  }
48
- const positions = scrollPositionsByPageId.get(pageCacheId)!
49
- for (const sel of selectorIds) {
50
- positions.set(sel, { top: 0, left: 0 })
66
+
67
+ return {
68
+ restoreScroller,
69
+ saveScroller,
70
+ removeScroller,
71
+ addPageScroller
51
72
  }
52
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
  }
@@ -1,52 +1,49 @@
1
1
  /**
2
- * tabPanel/state - 标签页核心共享状态
2
+ * tabPanel/state - 标签页运行时状态 factory
3
3
  *
4
- * 职责:集中管理 tabs、caches、components、驱逐集合、滚动位置等
5
- * 被 useTabPanel、evict、scroll、session 导入使用
4
+ * 职责:创建 tabs、caches、components、驱逐集合、滚动位置等运行时状态。
5
+ * 注意:本文件不导出模块级可变状态;每个 Vue app 只有一个 StackTabsRuntimeContext 持有这些状态。
6
6
  */
7
+ import type { DefineComponent, Ref, ShallowRef } from 'vue'
8
+ import { ref, shallowRef } from 'vue'
7
9
  import type { ITabItem } from '../../model/TabModel'
8
- import { ref } from 'vue'
9
10
 
10
- /** 当前打开的标签列表(含每个标签的页面栈) */
11
- export const tabs = ref<ITabItem[]>([])
12
- /** 初始化时传入的默认标签(用于对比 session 恢复) */
13
- export const defaultTabs: ITabItem[] = []
14
- /** keep-alive include 数组,与 Vue 的 keep-alive :include 绑定 */
15
- export const caches = ref<string[]>([])
16
- /** 页面缓存 ID -> 包装后的 DefineComponent */
17
- export const components = new Map<string, import('vue').DefineComponent>()
18
- /** 待驱逐的页面缓存 ID 集合 */
19
- export const cacheIdsToEvict = new Set<string>()
20
- /** 待驱逐的标签 ID 集合 */
21
- export const tabIdsToEvict = new Set<string>()
22
- /** 刷新计数器,变更时触发 component :key 变化 */
23
- export const refreshKey = ref<number>(0)
24
- /** 刷新时临时加入 keep-alive exclude 的缓存 ID */
25
- export const excludedCacheIdsForRefresh = ref<string[]>([])
26
- /** pageCacheId -> Map<selector, { top, left }> 滚动位置存储 */
27
- export const scrollPositionsByPageId = new Map<string, Map<string, { top: number; left: number }>>()
28
- /** 是否已完成初始化 */
29
- export const isInitialized = ref<boolean>(false)
30
-
31
- /** sessionStorage 中存储当前激活标签的 key 前缀 */
32
- export const SESSION_TAB_NAME = 'stacktab-active-tab'
11
+ export interface ScrollPosition {
12
+ top: number
13
+ left: number
14
+ }
33
15
 
34
- /** iframe 标签 tabId -> 刷新序号,变更时触发 iframe 重载 */
35
- export const iframeRefreshKeys = ref<Record<string, number>>({})
16
+ export interface TabPanelRuntimeState {
17
+ tabs: Ref<ITabItem[]>
18
+ defaultTabs: Ref<ITabItem[]>
19
+ caches: ShallowRef<string[]>
20
+ components: Map<string, DefineComponent>
21
+ cacheIdsToEvict: Set<string>
22
+ tabIdsToEvict: Set<string>
23
+ refreshKey: Ref<number>
24
+ scrollPositionsByPageId: Map<string, Map<string, ScrollPosition>>
25
+ isInitialized: Ref<boolean>
26
+ iframeRefreshKeys: Ref<Record<string, number>>
27
+ maxTabCount: Ref<number>
28
+ useGlobalScroll: Ref<boolean>
29
+ sessionPrefix: Ref<string>
30
+ }
36
31
 
37
- /** 最大标签数量,0 表示不限制 */
38
- export let maxTabCount = 0
39
- /** 是否使用页面级滚动 */
40
- export let useGlobalScroll = false
41
- /** sessionStorage key 前缀 */
42
- export let sessionPrefix = ''
32
+ /** sessionStorage 中存储当前激活标签的 key 后缀 */
33
+ export const SESSION_TAB_NAME = 'stacktab-active-tab'
43
34
 
44
- export const setMaxTabCount = (n: number) => {
45
- maxTabCount = n
46
- }
47
- export const setUseGlobalScroll = (v: boolean) => {
48
- useGlobalScroll = v
49
- }
50
- export const setSessionPrefix = (p: string) => {
51
- sessionPrefix = p
52
- }
35
+ export const createTabPanelRuntimeState = (): TabPanelRuntimeState => ({
36
+ tabs: ref<ITabItem[]>([]) as Ref<ITabItem[]>,
37
+ defaultTabs: ref<ITabItem[]>([]) as Ref<ITabItem[]>,
38
+ caches: shallowRef<string[]>([]),
39
+ components: new Map<string, DefineComponent>(),
40
+ cacheIdsToEvict: new Set<string>(),
41
+ tabIdsToEvict: new Set<string>(),
42
+ refreshKey: ref<number>(0),
43
+ scrollPositionsByPageId: new Map<string, Map<string, ScrollPosition>>(),
44
+ isInitialized: ref<boolean>(false),
45
+ iframeRefreshKeys: ref<Record<string, number>>({}),
46
+ maxTabCount: ref<number>(0),
47
+ useGlobalScroll: ref<boolean>(false),
48
+ sessionPrefix: ref<string>('')
49
+ })