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.
- package/README.en.md +526 -0
- package/README.md +337 -520
- package/README.ru.md +526 -0
- package/dist/iframe-bridge.d.ts +8 -0
- package/dist/iframe-bridge.html +77 -0
- package/dist/iframe-bridge.mjs +20 -0
- package/dist/iframe-child.html +88 -0
- package/dist/nuxt/favicon.svg +10 -0
- package/dist/nuxt/iframe-bridge.html +77 -0
- package/dist/nuxt/iframe-child.html +88 -0
- package/dist/nuxt/module.d.ts +6 -0
- package/dist/nuxt/module.mjs +28 -0
- package/dist/nuxt/plugin.mjs +9 -0
- package/dist/nuxt/runtime/plugin.d.ts +2 -0
- package/dist/nuxt/runtime/plugin.mjs +9 -0
- package/dist/src/lib/StackTabs.vue.d.ts +30 -16
- package/dist/src/lib/components/ContextMenu/ContextMenuItem.vue.d.ts +7 -1
- package/dist/src/lib/components/ContextMenu/index.vue.d.ts +5 -4
- package/dist/src/lib/components/PageLoading.vue.d.ts +2 -1
- package/dist/src/lib/components/StackKeepAlive/StackCacheRenderer.vue.d.ts +25 -0
- package/dist/src/lib/components/StackKeepAlive/StackKeepAlive.vue.d.ts +12 -0
- package/dist/src/lib/components/TabHeader/TabHeaderButton.vue.d.ts +11 -12
- package/dist/src/lib/components/TabHeader/TabHeaderItem.vue.d.ts +5 -4
- package/dist/src/lib/components/TabHeader/TabHeaderScroll.vue.d.ts +8 -22
- package/dist/src/lib/components/TabHeader/index.vue.d.ts +11 -164
- package/dist/src/lib/hooks/stackTabsContext.d.ts +49 -0
- package/dist/src/lib/hooks/tabPanel/evict.d.ts +21 -23
- package/dist/src/lib/hooks/tabPanel/navigationTransaction.d.ts +35 -0
- package/dist/src/lib/hooks/tabPanel/refresh.d.ts +17 -0
- package/dist/src/lib/hooks/tabPanel/scroll.d.ts +8 -8
- package/dist/src/lib/hooks/tabPanel/session.d.ts +9 -8
- package/dist/src/lib/hooks/tabPanel/state.d.ts +20 -73
- package/dist/src/lib/hooks/useIframeManager.d.ts +35 -0
- package/dist/src/lib/hooks/useTabActions.d.ts +5 -46
- package/dist/src/lib/hooks/useTabEventBus.d.ts +22 -9
- package/dist/src/lib/hooks/useTabPanel.d.ts +17 -74
- package/dist/src/lib/hooks/useTabRouter.d.ts +5 -5
- package/dist/src/lib/i18n/index.d.ts +6 -4
- package/dist/src/lib/i18n/lang/en.d.ts +5 -0
- package/dist/src/lib/i18n/lang/zh-CN.d.ts +5 -0
- package/dist/src/lib/i18n/stackTabsLocale.d.ts +2 -0
- package/dist/src/lib/iframe-bridge.d.ts +8 -0
- package/dist/src/lib/iframe.vue.d.ts +2 -1
- package/dist/src/lib/index.d.ts +4 -5
- package/dist/src/lib/model/TabModel.d.ts +14 -33
- package/dist/src/lib/nuxt/module.d.ts +6 -0
- package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +2 -0
- package/dist/src/lib/nuxt/runtime/plugin.d.ts +2 -0
- package/dist/src/lib/utils/iframeBridge.d.ts +11 -3
- package/dist/src/lib/utils/stackTabsA11y.d.ts +8 -0
- package/dist/src/lib/utils/stackTabsMessage.d.ts +7 -0
- package/dist/src/lib/utils/tabInfoEncoder.d.ts +1 -16
- package/dist/src/lib/utils/typeGuards.d.ts +2 -0
- package/dist/src/lib/utils/urlParser.d.ts +24 -3
- package/dist/src/lib/versionLogger.d.ts +1 -0
- package/dist/style.css +2 -1
- package/dist/utils/iframeBridge.d.ts +39 -0
- package/dist/vue-stack-tabs.css +2 -1
- package/dist/vue-stack-tabs.es.js +1 -2411
- package/package.json +68 -47
- package/src/lib/StackTabs.vue +216 -192
- package/src/lib/assets/style/stackTab.scss +84 -12
- package/src/lib/components/ContextMenu/ContextMenuItem.vue +23 -11
- package/src/lib/components/ContextMenu/index.vue +94 -10
- package/src/lib/components/PageLoading.vue +9 -7
- package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +70 -0
- package/src/lib/components/StackKeepAlive/StackKeepAlive.vue +57 -0
- package/src/lib/components/TabHeader/TabHeaderButton.vue +19 -8
- package/src/lib/components/TabHeader/TabHeaderItem.vue +45 -16
- package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
- package/src/lib/components/TabHeader/index.vue +109 -12
- package/src/lib/hooks/stackTabsContext.ts +135 -0
- package/src/lib/hooks/tabPanel/evict.ts +123 -41
- package/src/lib/hooks/tabPanel/navigationTransaction.ts +61 -0
- package/src/lib/hooks/tabPanel/refresh.ts +109 -0
- package/src/lib/hooks/tabPanel/scroll.ts +57 -36
- package/src/lib/hooks/tabPanel/session.ts +84 -22
- package/src/lib/hooks/tabPanel/state.ts +41 -44
- package/src/lib/hooks/useIframeManager.ts +253 -0
- package/src/lib/hooks/useTabActions.ts +62 -30
- package/src/lib/hooks/useTabEventBus.ts +29 -19
- package/src/lib/hooks/useTabLoading.ts +3 -2
- package/src/lib/hooks/useTabPanel.tsx +415 -213
- package/src/lib/hooks/useTabRouter.ts +172 -43
- package/src/lib/i18n/index.ts +53 -45
- package/src/lib/i18n/lang/en.ts +6 -1
- package/src/lib/i18n/lang/zh-CN.ts +6 -1
- package/src/lib/i18n/stackTabsLocale.ts +8 -0
- package/src/lib/iframe-bridge.ts +12 -0
- package/src/lib/index.ts +14 -19
- package/src/lib/model/TabModel.ts +34 -86
- package/src/lib/nuxt/module.d.ts +4 -0
- package/src/lib/nuxt/module.ts +8 -3
- package/src/lib/nuxt/runtime/plugin.client.ts +2 -3
- package/src/lib/nuxt/runtime/plugin.ts +11 -0
- package/src/lib/utils/iframeBridge.ts +29 -5
- package/src/lib/utils/scrollUtils.ts +9 -4
- package/src/lib/utils/stackTabsA11y.ts +12 -0
- package/src/lib/utils/stackTabsMessage.ts +99 -0
- package/src/lib/utils/tabInfoEncoder.ts +69 -42
- package/src/lib/utils/typeGuards.ts +3 -0
- package/src/lib/utils/urlParser.ts +144 -14
- package/src/lib/versionLogger.ts +7 -19
- package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +0 -1
- package/dist/src/lib/hooks/useTabPanel.spec.d.ts +0 -1
- package/dist/src/lib/model/TabModel.spec.d.ts +0 -1
- package/dist/src/lib/utils/iframeBridge.spec.d.ts +0 -1
- package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +0 -1
- package/dist/src/lib/utils/urlParser.spec.d.ts +0 -1
- package/dist/vue-stack-tabs.umd.js +0 -1
- package/src/lib/hooks/tabPanel/evict.spec.ts +0 -85
- package/src/lib/hooks/useTabPanel.spec.ts +0 -34
- package/src/lib/model/TabModel.spec.ts +0 -40
- package/src/lib/utils/iframeBridge.spec.ts +0 -37
- package/src/lib/utils/tabInfoEncoder.spec.ts +0 -42
- 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 {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
*
|
|
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
|
|
9
|
-
import {
|
|
8
|
+
import { isRecord } from '../../utils/typeGuards'
|
|
9
|
+
import type { StackTabsRuntimeContext } from '../stackTabsContext'
|
|
10
|
+
import { SESSION_TAB_NAME } from './state'
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
/**
|
|
38
|
-
export
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
})
|