vue-stack-tabs 1.1.0 → 1.3.1
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 +141 -39
- 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 +19 -6
- 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 -61
- 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 -72
- 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 +14 -73
- 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 +11 -40
- 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 +0 -15
- 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 -2220
- package/package.json +71 -50
- package/src/lib/StackTabs.vue +223 -196
- package/src/lib/assets/style/stackTab.scss +110 -13
- 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 +74 -17
- package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
- package/src/lib/components/TabHeader/index.vue +181 -13
- package/src/lib/hooks/stackTabsContext.ts +135 -0
- package/src/lib/hooks/tabPanel/evict.ts +122 -87
- 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 -40
- 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 +64 -30
- package/src/lib/hooks/useTabEventBus.ts +29 -19
- package/src/lib/hooks/useTabLoading.ts +3 -2
- package/src/lib/hooks/useTabPanel.tsx +290 -313
- package/src/lib/hooks/useTabRouter.ts +111 -97
- 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 +27 -82
- 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 +1 -0
- 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 +70 -21
- 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 -32
- package/src/lib/utils/urlParser.spec.ts +0 -45
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VueStackTabs 全局运行时上下文管理。
|
|
3
|
+
*
|
|
4
|
+
* 负责创建、注册、解析全局唯一的 StackTabsRuntimeContext,
|
|
5
|
+
* 支持通过 Vue provide/inject 或模块级单例两种方式访问。
|
|
6
|
+
* 每个 Vue app 只允许注册一个 <VueStackTabs> 实例。
|
|
7
|
+
*/
|
|
8
|
+
import type { App, DefineComponent, InjectionKey, Ref, ShallowRef } from 'vue'
|
|
9
|
+
import { hasInjectionContext, inject, ref } from 'vue'
|
|
10
|
+
import type { Emitter } from 'mitt'
|
|
11
|
+
import mitt from 'mitt'
|
|
12
|
+
import type { ITabItem } from '../model/TabModel'
|
|
13
|
+
import type { TabEventPayloadMap } from './useTabEventBus'
|
|
14
|
+
import { createTabPanelRuntimeState } from './tabPanel/state'
|
|
15
|
+
import type { ScrollPosition } from './tabPanel/state'
|
|
16
|
+
|
|
17
|
+
export const STACK_TABS_CONTEXT_UNAVAILABLE_MESSAGE =
|
|
18
|
+
'VueStackTabs runtime context is not available. Render <VueStackTabs> before using vue-stack-tabs composables.'
|
|
19
|
+
|
|
20
|
+
export const STACK_TABS_DUPLICATE_INSTANCE_MESSAGE =
|
|
21
|
+
'VueStackTabs only supports one <VueStackTabs> instance per Vue app.'
|
|
22
|
+
|
|
23
|
+
export interface StackTabsRuntimeContext {
|
|
24
|
+
tabs: Ref<ITabItem[]>
|
|
25
|
+
defaultTabs: Ref<ITabItem[]>
|
|
26
|
+
caches: ShallowRef<string[]>
|
|
27
|
+
components: Map<string, DefineComponent>
|
|
28
|
+
cacheIdsToEvict: Set<string>
|
|
29
|
+
tabIdsToEvict: Set<string>
|
|
30
|
+
refreshKey: Ref<number>
|
|
31
|
+
scrollPositionsByPageId: Map<string, Map<string, ScrollPosition>>
|
|
32
|
+
isInitialized: Ref<boolean>
|
|
33
|
+
iframeRefreshKeys: Ref<Record<string, number>>
|
|
34
|
+
maxTabCount: Ref<number>
|
|
35
|
+
useGlobalScroll: Ref<boolean>
|
|
36
|
+
sessionPrefix: Ref<string>
|
|
37
|
+
iframePath: Ref<string>
|
|
38
|
+
eventBus: Emitter<TabEventPayloadMap>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CreateStackTabsRuntimeContextOptions {
|
|
42
|
+
iframePath?: string
|
|
43
|
+
maxTabCount?: number
|
|
44
|
+
useGlobalScroll?: boolean
|
|
45
|
+
sessionPrefix?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface RegisterStackTabsRuntimeContextOptions {
|
|
49
|
+
app?: App
|
|
50
|
+
isProduction?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface UnregisterStackTabsRuntimeContextOptions {
|
|
54
|
+
app?: App
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const stackTabsContextKey: InjectionKey<StackTabsRuntimeContext> = Symbol('stackTabsContext')
|
|
58
|
+
export const maximumKey: InjectionKey<Ref<boolean>> = Symbol('stackTabsMaximum')
|
|
59
|
+
|
|
60
|
+
let activeRuntimeContext: StackTabsRuntimeContext | null = null
|
|
61
|
+
let isFallbackRuntimeContextOwnerRegistered = false
|
|
62
|
+
const runtimeContextOwnersByApp = new WeakSet<App>()
|
|
63
|
+
|
|
64
|
+
/** 创建一个新的运行时上下文实例 */
|
|
65
|
+
export const createStackTabsRuntimeContext = (
|
|
66
|
+
options: CreateStackTabsRuntimeContextOptions = {}
|
|
67
|
+
): StackTabsRuntimeContext => {
|
|
68
|
+
const state = createTabPanelRuntimeState()
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
...state,
|
|
72
|
+
maxTabCount: ref(options.maxTabCount ?? state.maxTabCount.value),
|
|
73
|
+
useGlobalScroll: ref(options.useGlobalScroll ?? state.useGlobalScroll.value),
|
|
74
|
+
sessionPrefix: ref(options.sessionPrefix ?? state.sessionPrefix.value),
|
|
75
|
+
iframePath: ref(options.iframePath ?? ''),
|
|
76
|
+
eventBus: mitt<TabEventPayloadMap>()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 获取当前活跃的运行时上下文(模块级单例) */
|
|
81
|
+
export const getActiveStackTabsRuntimeContext = (): StackTabsRuntimeContext | null =>
|
|
82
|
+
activeRuntimeContext
|
|
83
|
+
|
|
84
|
+
/** 注册运行时上下文,重复注册会在开发环境抛出错误 */
|
|
85
|
+
export const registerStackTabsRuntimeContext = (
|
|
86
|
+
context: StackTabsRuntimeContext,
|
|
87
|
+
options: RegisterStackTabsRuntimeContextOptions = {}
|
|
88
|
+
): boolean => {
|
|
89
|
+
if (!activeRuntimeContext) {
|
|
90
|
+
activeRuntimeContext = context
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (options.app && !runtimeContextOwnersByApp.has(options.app)) {
|
|
94
|
+
runtimeContextOwnersByApp.add(options.app)
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
!options.app &&
|
|
100
|
+
activeRuntimeContext === context &&
|
|
101
|
+
!isFallbackRuntimeContextOwnerRegistered
|
|
102
|
+
) {
|
|
103
|
+
isFallbackRuntimeContextOwnerRegistered = true
|
|
104
|
+
return true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const isProduction = options.isProduction ?? import.meta.env.PROD
|
|
108
|
+
if (isProduction) {
|
|
109
|
+
console.warn(STACK_TABS_DUPLICATE_INSTANCE_MESSAGE)
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
throw new Error(STACK_TABS_DUPLICATE_INSTANCE_MESSAGE)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 注销运行时上下文,组件卸载时调用 */
|
|
117
|
+
export const unregisterStackTabsRuntimeContext = (
|
|
118
|
+
context: StackTabsRuntimeContext,
|
|
119
|
+
options: UnregisterStackTabsRuntimeContextOptions = {}
|
|
120
|
+
): void => {
|
|
121
|
+
if (options.app) runtimeContextOwnersByApp.delete(options.app)
|
|
122
|
+
if (!options.app && activeRuntimeContext === context)
|
|
123
|
+
isFallbackRuntimeContextOwnerRegistered = false
|
|
124
|
+
if (activeRuntimeContext === context) activeRuntimeContext = null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 解析当前可用的运行时上下文,优先使用 inject,回退到模块单例 */
|
|
128
|
+
export const resolveStackTabsRuntimeContext = (): StackTabsRuntimeContext => {
|
|
129
|
+
const injected = hasInjectionContext() ? inject(stackTabsContextKey, null) : null
|
|
130
|
+
const context = injected ?? activeRuntimeContext
|
|
131
|
+
|
|
132
|
+
if (!context) throw new Error(STACK_TABS_CONTEXT_UNAVAILABLE_MESSAGE)
|
|
133
|
+
|
|
134
|
+
return context
|
|
135
|
+
}
|
|
@@ -1,104 +1,139 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* tabPanel/evict - 缓存驱逐逻辑
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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 {
|
|
7
|
+
import { triggerRef } from 'vue'
|
|
8
|
+
import type { StackTabsRuntimeContext } from '../stackTabsContext'
|
|
22
9
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
restoreScroller,
|
|
69
|
+
saveScroller,
|
|
70
|
+
removeScroller,
|
|
71
|
+
addPageScroller
|
|
55
72
|
}
|
|
56
73
|
}
|