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
|
@@ -18,53 +18,36 @@ import {
|
|
|
18
18
|
onDeactivated,
|
|
19
19
|
onMounted,
|
|
20
20
|
onUnmounted,
|
|
21
|
+
computed,
|
|
21
22
|
ref,
|
|
23
|
+
shallowRef,
|
|
22
24
|
cloneVNode
|
|
23
25
|
} from 'vue'
|
|
24
26
|
import type { DefineComponent, VNode } from 'vue'
|
|
25
|
-
import { useRouter } from 'vue-router'
|
|
27
|
+
import { isNavigationFailure, useRouter } from 'vue-router'
|
|
26
28
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
|
27
|
-
import { ulid } from 'ulid'
|
|
28
29
|
import { encodeTabInfo, createPageId, decodeTabInfo } from '../utils/tabInfoEncoder'
|
|
29
30
|
import { defu } from 'defu'
|
|
30
31
|
import { Stack } from '../model/TabModel'
|
|
31
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
parseUrl,
|
|
34
|
+
isCrossOriginUrl,
|
|
35
|
+
omitStackTabsReservedQuery,
|
|
36
|
+
toSafeTabUrl,
|
|
37
|
+
decodeSafeTabUrl,
|
|
38
|
+
cloneLocationQuery,
|
|
39
|
+
clonePage
|
|
40
|
+
} from '../utils/urlParser'
|
|
32
41
|
import PageLoading from '../components/PageLoading.vue'
|
|
33
42
|
import { TabEventType, useTabEmitter } from './useTabEventBus'
|
|
43
|
+
import { runNavigationTransaction } from './tabPanel/navigationTransaction'
|
|
34
44
|
import { useI18n } from 'vue-i18n-lite'
|
|
35
45
|
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
tabIdsToEvict,
|
|
42
|
-
refreshKey,
|
|
43
|
-
iframeRefreshKeys,
|
|
44
|
-
isInitialized,
|
|
45
|
-
maxTabCount,
|
|
46
|
-
useGlobalScroll,
|
|
47
|
-
cacheIdsToEvict,
|
|
48
|
-
setMaxTabCount,
|
|
49
|
-
setUseGlobalScroll,
|
|
50
|
-
setSessionPrefix
|
|
51
|
-
} from './tabPanel/state'
|
|
52
|
-
import {
|
|
53
|
-
markCacheForEviction,
|
|
54
|
-
markTabPagesForEviction,
|
|
55
|
-
markTabPagesForEvictionOnly,
|
|
56
|
-
addCache,
|
|
57
|
-
removeCache,
|
|
58
|
-
evictMarkedCaches,
|
|
59
|
-
evictPageCache
|
|
60
|
-
} from './tabPanel/evict'
|
|
61
|
-
import { restoreScroller, saveScroller, removeScroller, addPageScroller } from './tabPanel/scroll'
|
|
62
|
-
import {
|
|
63
|
-
saveActiveTabToSession,
|
|
64
|
-
clearSession,
|
|
65
|
-
restoreTabFromSession,
|
|
66
|
-
getSessionKey
|
|
67
|
-
} from './tabPanel/session'
|
|
46
|
+
import { resolveStackTabsRuntimeContext } from './stackTabsContext'
|
|
47
|
+
import { createTabPanelEviction } from './tabPanel/evict'
|
|
48
|
+
import { createTabPanelScroll } from './tabPanel/scroll'
|
|
49
|
+
import { createTabPanelSession } from './tabPanel/session'
|
|
50
|
+
import { createTabPanelRefresh } from './tabPanel/refresh'
|
|
68
51
|
|
|
69
52
|
/* eslint-disable vue/one-component-per-file */
|
|
70
53
|
/** 占位组件:当路由组件为空或标签已被标记删除时返回 */
|
|
@@ -75,6 +58,11 @@ const EmptyPlaceholderComponent = defineComponent({
|
|
|
75
58
|
}
|
|
76
59
|
})
|
|
77
60
|
|
|
61
|
+
const cloneRouteQuery = (route: RouteLocationNormalizedLoaded, tabInfo: ITabBase) => ({
|
|
62
|
+
...cloneLocationQuery(route.query),
|
|
63
|
+
__tab: route.query.__tab ?? encodeTabInfo(tabInfo)
|
|
64
|
+
})
|
|
65
|
+
|
|
78
66
|
/** 规范化路径(nginx 等可能加尾部斜杠) */
|
|
79
67
|
export const normalizePathForCache = (route: RouteLocationNormalizedLoaded): string => {
|
|
80
68
|
const lastMatch = route.matched[route.matched.length - 1]
|
|
@@ -87,6 +75,37 @@ export default () => {
|
|
|
87
75
|
const router = useRouter()
|
|
88
76
|
const emitter = useTabEmitter()
|
|
89
77
|
const { t } = useI18n()
|
|
78
|
+
const runtimeContext = resolveStackTabsRuntimeContext()
|
|
79
|
+
const {
|
|
80
|
+
tabs,
|
|
81
|
+
defaultTabs,
|
|
82
|
+
caches,
|
|
83
|
+
components,
|
|
84
|
+
tabIdsToEvict,
|
|
85
|
+
refreshKey,
|
|
86
|
+
iframeRefreshKeys,
|
|
87
|
+
isInitialized,
|
|
88
|
+
cacheIdsToEvict
|
|
89
|
+
} = runtimeContext
|
|
90
|
+
const {
|
|
91
|
+
markCacheForEviction,
|
|
92
|
+
markTabPagesForEviction,
|
|
93
|
+
markTabPagesForEvictionOnly,
|
|
94
|
+
addCache,
|
|
95
|
+
removeCache,
|
|
96
|
+
evictMarkedCaches,
|
|
97
|
+
evictPageCache,
|
|
98
|
+
replacePageCaches
|
|
99
|
+
} = createTabPanelEviction(runtimeContext)
|
|
100
|
+
const { restoreScroller, saveScroller, removeScroller, addPageScroller } =
|
|
101
|
+
createTabPanelScroll(runtimeContext)
|
|
102
|
+
const {
|
|
103
|
+
saveActiveTabToSession,
|
|
104
|
+
clearSession,
|
|
105
|
+
restoreTabFromSession,
|
|
106
|
+
restoreActiveTabSession,
|
|
107
|
+
getSessionKey
|
|
108
|
+
} = createTabPanelSession(runtimeContext)
|
|
90
109
|
|
|
91
110
|
const size = (): number => tabs.value.length
|
|
92
111
|
|
|
@@ -95,33 +114,36 @@ export default () => {
|
|
|
95
114
|
* @param staticTabs - 初始标签配置
|
|
96
115
|
*/
|
|
97
116
|
const initialize = (staticTabs: ITabData[]) => {
|
|
117
|
+
const newDefaults: ITabItem[] = []
|
|
118
|
+
const newTabs: ITabItem[] = []
|
|
119
|
+
|
|
98
120
|
for (const item of staticTabs) {
|
|
99
|
-
const fullItem = defu(item, {
|
|
100
|
-
|
|
101
|
-
fullItem.id = tabId
|
|
102
|
-
const uri = parseUrl(fullItem.path)
|
|
103
|
-
const config = defu(fullItem, {
|
|
104
|
-
closable: true,
|
|
121
|
+
const fullItem = defu(item, {
|
|
122
|
+
id: crypto.randomUUID(),
|
|
105
123
|
refreshable: true,
|
|
124
|
+
closable: true,
|
|
106
125
|
iframe: false
|
|
107
126
|
})
|
|
127
|
+
const tabId = fullItem.id ?? crypto.randomUUID()
|
|
128
|
+
fullItem.id = tabId
|
|
129
|
+
const uri = parseUrl(fullItem.path)
|
|
108
130
|
const cacheName = createPageId()
|
|
109
131
|
const page: ITabPage = {
|
|
110
132
|
id: cacheName,
|
|
111
133
|
tabId,
|
|
112
134
|
path: uri.path,
|
|
113
|
-
query: defu(uri.query, { __tab: encodeTabInfo(fullItem) })
|
|
135
|
+
query: defu(omitStackTabsReservedQuery(uri.query), { __tab: encodeTabInfo(fullItem) })
|
|
114
136
|
}
|
|
115
137
|
const pages = new Stack<ITabPage>()
|
|
116
138
|
pages.push(page)
|
|
117
139
|
const tab: ITabItem = {
|
|
118
140
|
id: tabId,
|
|
119
|
-
title:
|
|
120
|
-
closable:
|
|
121
|
-
refreshable:
|
|
122
|
-
iframe:
|
|
141
|
+
title: fullItem.title ?? '',
|
|
142
|
+
closable: fullItem.closable!,
|
|
143
|
+
refreshable: fullItem.refreshable!,
|
|
144
|
+
iframe: fullItem.iframe!,
|
|
123
145
|
iframeRefreshMode:
|
|
124
|
-
(
|
|
146
|
+
(fullItem as { iframeRefreshMode?: string }).iframeRefreshMode === 'reload'
|
|
125
147
|
? 'reload'
|
|
126
148
|
: 'postMessage',
|
|
127
149
|
active: false,
|
|
@@ -129,19 +151,21 @@ export default () => {
|
|
|
129
151
|
}
|
|
130
152
|
|
|
131
153
|
addCache(cacheName)
|
|
132
|
-
|
|
133
|
-
|
|
154
|
+
newDefaults.push(tab)
|
|
155
|
+
newTabs.push({ ...tab })
|
|
134
156
|
}
|
|
135
157
|
|
|
158
|
+
defaultTabs.value = [...defaultTabs.value, ...newDefaults]
|
|
159
|
+
tabs.value = [...tabs.value, ...newTabs]
|
|
160
|
+
|
|
136
161
|
const storedTabJson = sessionStorage.getItem(getSessionKey())
|
|
137
162
|
const restoredTab = restoreTabFromSession(storedTabJson)
|
|
138
|
-
if (restoredTab && !tabs.value.some((t) => t.id === restoredTab
|
|
163
|
+
if (restoredTab && !tabs.value.some((t) => t.id === restoredTab.id)) {
|
|
164
|
+
if (restoredTab.url) restoredTab.url = toSafeTabUrl(restoredTab.url)
|
|
139
165
|
if (restoredTab.iframe && restoredTab.url && restoredTab.iframeRefreshMode === undefined) {
|
|
140
166
|
restoredTab.iframeRefreshMode = isCrossOriginUrl(restoredTab.url) ? 'reload' : 'postMessage'
|
|
141
167
|
}
|
|
142
168
|
|
|
143
|
-
// 核心修复:刷新恢复后,不仅要把 Tab 塞入列表,
|
|
144
|
-
// 更要把里面所有的 Page 的 ULID 重新注册进 keep-alive 的 includes,防止彻底沦为白板无缓存组件
|
|
145
169
|
if (restoredTab.pages && typeof restoredTab.pages.list === 'function') {
|
|
146
170
|
const pages = restoredTab.pages.list()
|
|
147
171
|
pages.forEach((page) => {
|
|
@@ -149,46 +173,77 @@ export default () => {
|
|
|
149
173
|
})
|
|
150
174
|
}
|
|
151
175
|
|
|
152
|
-
tabs.value.
|
|
176
|
+
tabs.value = [...tabs.value, restoredTab]
|
|
153
177
|
}
|
|
154
178
|
isInitialized.value = true
|
|
155
179
|
}
|
|
156
180
|
|
|
157
181
|
const hasTab = (id: string) => tabs.value.some((t) => t.id === id)
|
|
158
|
-
const canAddTab = () =>
|
|
182
|
+
const canAddTab = () =>
|
|
183
|
+
runtimeContext.maxTabCount.value <= 0 ||
|
|
184
|
+
(runtimeContext.maxTabCount.value > 0 && runtimeContext.maxTabCount.value > tabs.value.length)
|
|
159
185
|
|
|
160
|
-
const addTab = (tab: ITabItem) => {
|
|
161
|
-
tabs.value.
|
|
162
|
-
return Promise.resolve(true)
|
|
186
|
+
const addTab = (tab: ITabItem): void => {
|
|
187
|
+
tabs.value = [...tabs.value, tab]
|
|
163
188
|
}
|
|
164
189
|
|
|
165
190
|
const getTab = (id: string) => tabs.value.find((t) => t.id === id) ?? null
|
|
166
191
|
|
|
167
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* 将指定标签移动到目标索引。标签对象本身不变,因此其页面栈和缓存关联保持不受影响。
|
|
194
|
+
* @returns 是否实际调整了顺序
|
|
195
|
+
*/
|
|
196
|
+
const moveTab = (id: string, targetIndex: number): boolean => {
|
|
197
|
+
const sourceIndex = tabs.value.findIndex((tab) => tab.id === id)
|
|
198
|
+
if (
|
|
199
|
+
sourceIndex < 0 ||
|
|
200
|
+
targetIndex < 0 ||
|
|
201
|
+
targetIndex >= tabs.value.length ||
|
|
202
|
+
sourceIndex === targetIndex
|
|
203
|
+
) {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const reorderedTabs = tabs.value.slice()
|
|
208
|
+
const [tab] = reorderedTabs.splice(sourceIndex, 1)
|
|
209
|
+
if (!tab) return false
|
|
210
|
+
reorderedTabs.splice(targetIndex, 0, tab)
|
|
211
|
+
tabs.value = reorderedTabs
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 从路由解析 tabInfo,若无则创建默认信息,调用方负责把编码信息写入内部 page.query */
|
|
168
216
|
const parseTabInfoFromRoute = (route: RouteLocationNormalizedLoaded): ITabBase => {
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
217
|
+
if (typeof route.query.__tab === 'string') {
|
|
218
|
+
const tabInfo = decodeTabInfo(route.query.__tab)
|
|
219
|
+
if (tabInfo.id) return tabInfo
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const path = normalizePathForCache(route)
|
|
223
|
+
const activeTab = tabs.value.find((tab) => tab.active)
|
|
224
|
+
const activePage = activeTab?.pages.peek()
|
|
225
|
+
if (activeTab && activePage?.path === path) {
|
|
226
|
+
return {
|
|
227
|
+
id: activeTab.id,
|
|
228
|
+
title: activeTab.title,
|
|
229
|
+
closable: activeTab.closable,
|
|
230
|
+
refreshable: activeTab.refreshable,
|
|
231
|
+
iframe: activeTab.iframe,
|
|
232
|
+
iframeRefreshMode: activeTab.iframeRefreshMode
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
id: crypto.randomUUID(),
|
|
172
238
|
title: t('VueStackTab.undefined'),
|
|
173
239
|
closable: true,
|
|
174
240
|
refreshable: true,
|
|
175
241
|
iframe: false
|
|
176
242
|
}
|
|
177
|
-
route.query.__tab = encodeTabInfo(tabInfo)
|
|
178
|
-
return tabInfo
|
|
179
243
|
}
|
|
180
244
|
|
|
181
245
|
/**
|
|
182
246
|
* 查找或创建目标标签,并同步更新 active 状态与 pages 栈。
|
|
183
|
-
*
|
|
184
|
-
* 算法:
|
|
185
|
-
* 1. 遍历所有 tab,将所有 tab.active 置为 false(全部失活)
|
|
186
|
-
* 2. 同时查找 id 匹配的目标 tab(targetTab)
|
|
187
|
-
* 3-a. 若目标 tab 不存在:说明是首次打开该标签,创建新的 ITabItem,初始化 pages 栈并推入当前 page
|
|
188
|
-
* 3-b. 若目标 tab 已存在:
|
|
189
|
-
* - 设为 active
|
|
190
|
-
* - 判断当前 page 是否已在栈顶(避免重复入栈)
|
|
191
|
-
* 当路由在同一标签内多次重渲染(响应式触发)时,page.id 与 peek().id 相同,跳过 push
|
|
192
247
|
*/
|
|
193
248
|
const findOrCreateTargetTab = (
|
|
194
249
|
tabInfo: ITabBase,
|
|
@@ -203,7 +258,6 @@ export default () => {
|
|
|
203
258
|
}
|
|
204
259
|
|
|
205
260
|
if (targetTab === null) {
|
|
206
|
-
// 全新标签:初始化 pages 栈、从 query.__src 获取 iframe url
|
|
207
261
|
const pages = new Stack<ITabPage>()
|
|
208
262
|
pages.push(page)
|
|
209
263
|
const src = route.query.__src
|
|
@@ -214,17 +268,14 @@ export default () => {
|
|
|
214
268
|
refreshable: tabInfo.refreshable!,
|
|
215
269
|
iframe: tabInfo.iframe!,
|
|
216
270
|
iframeRefreshMode: tabInfo.iframeRefreshMode ?? 'postMessage',
|
|
217
|
-
url:
|
|
271
|
+
url: decodeSafeTabUrl(src),
|
|
218
272
|
active: true,
|
|
219
273
|
pages
|
|
220
274
|
} as unknown as ITabItem
|
|
221
|
-
addTab(targetTab)
|
|
222
|
-
|
|
223
|
-
})
|
|
275
|
+
addTab(targetTab)
|
|
276
|
+
emitter.emit(TabEventType.TAB_ACTIVE, { id: tabInfo.id!, isRoute: false })
|
|
224
277
|
} else {
|
|
225
278
|
targetTab.active = true
|
|
226
|
-
// 幂等保护:page.id 与当前栈顶相同时不重复入栈
|
|
227
|
-
// 响应式重渲染(如 caches 变更)会重复调用 tabWrapper,需过滤此类重入
|
|
228
279
|
if (targetTab.pages.isEmpty() || targetTab.pages.peek()!.id !== page.id) {
|
|
229
280
|
targetTab.pages.push(page)
|
|
230
281
|
}
|
|
@@ -232,26 +283,12 @@ export default () => {
|
|
|
232
283
|
return targetTab
|
|
233
284
|
}
|
|
234
285
|
|
|
235
|
-
|
|
236
|
-
* 暴露给外部使用,用于获取指定路由对应的 cacheName
|
|
237
|
-
*/
|
|
238
|
-
const getCacheName = () => {
|
|
286
|
+
const createCacheName = () => {
|
|
239
287
|
return createPageId()
|
|
240
288
|
}
|
|
241
289
|
|
|
242
290
|
/**
|
|
243
291
|
* 根据当前路由更新/创建标签与页面状态(核心路由适配器)。
|
|
244
|
-
*
|
|
245
|
-
* 该函数在每次 tabWrapper 被调用时执行(即每次 <router-view> 渲染或响应式重渲染时)。
|
|
246
|
-
* 返回三种可能的结果:
|
|
247
|
-
* - { empty: true } → 目标标签已被标记为驱逐(关闭态),返回空占位组件
|
|
248
|
-
* - { recovered, cacheName } → 页面曾被软标记驱逐(刷新场景),原组件仍存活,直接恢复
|
|
249
|
-
* - { cacheName, tabInfo, targetTab } → 正常流程,返回完整上下文供 resolvePageComponent 使用
|
|
250
|
-
*
|
|
251
|
-
* cacheName 生成算法:
|
|
252
|
-
* createPageId(tabId, normalizedPath, queryObject)
|
|
253
|
-
* 使用 tabId + 路径 + query(含 __tab)生成确定性哈希,
|
|
254
|
-
* 同一标签同一路由的 cacheName 始终相同,用作 cacheComponent.name 及 keep-alive include 的键。
|
|
255
292
|
*/
|
|
256
293
|
const updatePageState = (
|
|
257
294
|
route: RouteLocationNormalizedLoaded
|
|
@@ -264,8 +301,7 @@ export default () => {
|
|
|
264
301
|
|
|
265
302
|
const path = normalizePathForCache(route)
|
|
266
303
|
|
|
267
|
-
|
|
268
|
-
let targetTab: ITabItem | null = tabInfo.id ? (getTab(tabInfo.id) as unknown as ITabItem) : null
|
|
304
|
+
let targetTab: ITabItem | null = tabInfo.id ? (getTab(tabInfo.id) as ITabItem | null) : null
|
|
269
305
|
let cacheName: string
|
|
270
306
|
let page: ITabPage | null = null
|
|
271
307
|
|
|
@@ -273,16 +309,11 @@ export default () => {
|
|
|
273
309
|
const top = targetTab.pages.peek()
|
|
274
310
|
if (!top) throw new Error('[vue-stack-tabs] Stack is not empty but peek() returned undefined')
|
|
275
311
|
|
|
276
|
-
// 数据栈是唯一真相源,只比对 path 即可判断当前路由是否与栈顶一致
|
|
277
312
|
if (top.path === path) {
|
|
278
313
|
cacheName = top.id
|
|
279
314
|
} else {
|
|
280
|
-
|
|
281
|
-
// URL 和栈顶对不上!我们被外部改变了。
|
|
282
|
-
const pagesList = targetTab.pages.list()
|
|
315
|
+
const pagesList = targetTab.pages.readonlyList()
|
|
283
316
|
let foundIndex = -1
|
|
284
|
-
// 从栈顶往下找,有没有曾经来过的同路由(以此判定是否为 PopState 后退)
|
|
285
|
-
// 只比对 path,因为数据栈才是唯一真相源
|
|
286
317
|
for (let i = pagesList.length - 2; i >= 0; i--) {
|
|
287
318
|
const pastPage = pagesList[i]
|
|
288
319
|
if (pastPage && pastPage.path === path) {
|
|
@@ -292,18 +323,16 @@ export default () => {
|
|
|
292
323
|
}
|
|
293
324
|
|
|
294
325
|
if (foundIndex !== -1) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
326
|
+
if (!import.meta.env.PROD) {
|
|
327
|
+
console.warn(
|
|
328
|
+
'[vue-stack-tabs] Detected native browser back navigation, repairing page stack...'
|
|
329
|
+
)
|
|
330
|
+
}
|
|
300
331
|
const stepsToPop = pagesList.length - 1 - foundIndex
|
|
301
332
|
for (let i = 0; i < stepsToPop; i++) {
|
|
302
333
|
const popped = targetTab.pages.pop()
|
|
303
|
-
// 立即驱逐被错误保留在内存的那些未来组件(在 router 的钩子里也可以,但这里既然已经渲染就直接驱)
|
|
304
334
|
if (popped) evictPageCache(popped.id)
|
|
305
335
|
}
|
|
306
|
-
// 因为我们把不要的都出栈了,现在的栈顶就是以前的这一个记录
|
|
307
336
|
const newTop = targetTab.pages.peek()
|
|
308
337
|
cacheName = newTop ? newTop.id : createPageId()
|
|
309
338
|
|
|
@@ -311,49 +340,41 @@ export default () => {
|
|
|
311
340
|
page = {
|
|
312
341
|
id: cacheName,
|
|
313
342
|
tabId: tabInfo.id!,
|
|
314
|
-
path: route.path,
|
|
315
|
-
query: route
|
|
343
|
+
path: route.path,
|
|
344
|
+
query: cloneRouteQuery(route, tabInfo)
|
|
316
345
|
}
|
|
317
346
|
}
|
|
318
347
|
} else {
|
|
319
|
-
// 在历史栈完全没找到,真的是被强行推入的“空降兵”新路由,直接补发一个新的栈记录。
|
|
320
348
|
cacheName = createPageId()
|
|
321
349
|
page = {
|
|
322
350
|
id: cacheName,
|
|
323
351
|
tabId: tabInfo.id!,
|
|
324
|
-
path: route.path,
|
|
325
|
-
query: route
|
|
352
|
+
path: route.path,
|
|
353
|
+
query: cloneRouteQuery(route, tabInfo)
|
|
326
354
|
}
|
|
327
355
|
}
|
|
328
356
|
}
|
|
329
357
|
} else {
|
|
330
|
-
// 这是一个全新创建的 Tab(被外部用空链接无中生有)
|
|
331
358
|
cacheName = createPageId()
|
|
332
359
|
page = {
|
|
333
360
|
id: cacheName,
|
|
334
361
|
tabId: tabInfo.id!,
|
|
335
362
|
path: route.path,
|
|
336
|
-
query: route
|
|
363
|
+
query: cloneRouteQuery(route, tabInfo)
|
|
337
364
|
}
|
|
338
365
|
}
|
|
339
366
|
|
|
340
|
-
// 刷新恢复场景:依旧支持软驱逐和组件复用
|
|
341
367
|
if (cacheIdsToEvict.has(cacheName)) {
|
|
342
368
|
cacheIdsToEvict.delete(cacheName)
|
|
343
369
|
const cached = components.get(cacheName)
|
|
344
370
|
if (cached) return { recovered: cached, cacheName }
|
|
345
371
|
}
|
|
346
372
|
|
|
347
|
-
// 将 page (如果上面判断它为全新的时候)塞入栈中或建新 Tab
|
|
348
373
|
if (page) {
|
|
349
374
|
targetTab = findOrCreateTargetTab(tabInfo, route, cacheName, page)
|
|
350
375
|
} else {
|
|
351
|
-
// 否则说明这只是同页渲染复用(或者是刷新回来的无变动栈),把 targetTab 直接提拔为 active
|
|
352
376
|
for (const t of tabs.value) t.active = false
|
|
353
377
|
targetTab!.active = true
|
|
354
|
-
if (!cacheName && targetTab && !targetTab.pages.isEmpty()) {
|
|
355
|
-
cacheName = targetTab.pages.peek()!.id
|
|
356
|
-
}
|
|
357
378
|
}
|
|
358
379
|
|
|
359
380
|
saveActiveTabToSession(targetTab!)
|
|
@@ -362,27 +383,6 @@ export default () => {
|
|
|
362
383
|
|
|
363
384
|
/**
|
|
364
385
|
* 获取或创建与 cacheName 对应的 cacheComponent(keep-alive 包装组件)。
|
|
365
|
-
*
|
|
366
|
-
* 设计要点:
|
|
367
|
-
*
|
|
368
|
-
* 1. 「组件定义与 VNode 解耦」
|
|
369
|
-
* 旧版实现在 defineComponent 时通过 components: { DynamicComponent: component }
|
|
370
|
-
* 将页面 VNode 锁定为闭包,导致:
|
|
371
|
-
* - 组件定义本身持有旧 VNode 引用,即使路由参数变化也不更新
|
|
372
|
-
* - evictPageCache 后重建新定义,但 keep-alive 以 name 为键仍激活旧实例
|
|
373
|
-
* 新版改为通过 props: ['vnode'] + StackTabs.vue 的 :vnode="Component" 动态传入,
|
|
374
|
-
* 每次渲染从父层获取最新 VNode,彻底消除闭包缓存残留。
|
|
375
|
-
*
|
|
376
|
-
* 2. 「cloneVNode 注入 tId / pId」
|
|
377
|
-
* render 函数执行时,通过 cloneVNode(Node, { tId, pId }) 克隆页面 VNode 并附加标签标识属性。
|
|
378
|
-
* 被渲染的页面组件(如 InternalRouteDetail)通过 attrs.tId / attrs.pId 读取这两个值,
|
|
379
|
-
* 供 useTabRouter 内部定位所属标签与缓存 ID。
|
|
380
|
-
* (未声明在 defineProps 里的属性自动进入 attrs)
|
|
381
|
-
*
|
|
382
|
-
* 3. 「onActivated 延迟驱逐」
|
|
383
|
-
* 标签切换(非 backward)导致旧页面失活时,旧页面 ID 已由 markCacheForEviction 标记,
|
|
384
|
-
* 等新页面的 cacheComponent onActivated 触发,再批量执行 evictMarkedCaches 是安全的——
|
|
385
|
-
* 此时新页面已就绪,不存在重渲染重建的竞态问题。
|
|
386
386
|
*/
|
|
387
387
|
const resolvePageComponent = (ctx: {
|
|
388
388
|
cacheName: string
|
|
@@ -391,17 +391,13 @@ export default () => {
|
|
|
391
391
|
}): DefineComponent => {
|
|
392
392
|
const { cacheName, tabInfo } = ctx
|
|
393
393
|
|
|
394
|
-
// 已存在的组件定义直接复用(keep-alive 会在内部找到缓存实例),无需重建
|
|
395
394
|
if (components.has(cacheName)) {
|
|
396
|
-
addCache(cacheName)
|
|
395
|
+
addCache(cacheName)
|
|
397
396
|
return components.get(cacheName)!
|
|
398
397
|
}
|
|
399
398
|
|
|
400
|
-
// 首次进入该缓存页:创建专属 cacheComponent 包装组件
|
|
401
399
|
const cacheComponent = defineComponent({
|
|
402
|
-
// name 与 cacheName 保持一致,作为 keep-alive include 的匹配键
|
|
403
400
|
name: cacheName,
|
|
404
|
-
// vnode 由 StackTabs.vue 的 :vnode="Component" 动态传入(解耦闭包)
|
|
405
401
|
props: {
|
|
406
402
|
vnode: {
|
|
407
403
|
type: Object as () => VNode,
|
|
@@ -411,8 +407,10 @@ export default () => {
|
|
|
411
407
|
},
|
|
412
408
|
emits: ['onLoaded'],
|
|
413
409
|
setup(props, context) {
|
|
414
|
-
// 用于在当前组件活跃期内存下回退参数,离开或重返时将更新或清理,真正实现生命周期同步!
|
|
415
410
|
const localBackParams = ref<Record<string, unknown> | null>(null)
|
|
411
|
+
const lastVnode = shallowRef<VNode | null>(null)
|
|
412
|
+
const lastCloned = shallowRef<VNode | null>(null)
|
|
413
|
+
const lastBackParams = shallowRef<Record<string, unknown> | null>(null)
|
|
416
414
|
|
|
417
415
|
const checkAndConsumeBackParams = () => {
|
|
418
416
|
try {
|
|
@@ -420,7 +418,6 @@ export default () => {
|
|
|
420
418
|
const top = tab?.pages.peek()
|
|
421
419
|
if (top && top.id === cacheName && top._backParams) {
|
|
422
420
|
localBackParams.value = { ...top._backParams }
|
|
423
|
-
// 取出后阅后即焚,防止从别处循环回退时拿到过期老参
|
|
424
421
|
delete top._backParams
|
|
425
422
|
} else {
|
|
426
423
|
localBackParams.value = null
|
|
@@ -451,18 +448,27 @@ export default () => {
|
|
|
451
448
|
return () => {
|
|
452
449
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
453
450
|
const Node = (props as any).vnode as VNode
|
|
454
|
-
const
|
|
451
|
+
const backParams = localBackParams.value
|
|
452
|
+
|
|
453
|
+
if (Node !== lastVnode.value || backParams !== lastBackParams.value) {
|
|
454
|
+
const dynamicBackProps = backParams ? { _back: backParams } : {}
|
|
455
|
+
lastCloned.value = Node
|
|
456
|
+
? cloneVNode(Node, { tId: tabInfo.id, pId: cacheName, ...dynamicBackProps })
|
|
457
|
+
: null
|
|
458
|
+
lastVnode.value = Node
|
|
459
|
+
lastBackParams.value = backParams
|
|
460
|
+
}
|
|
455
461
|
|
|
456
462
|
return (
|
|
457
463
|
<div
|
|
458
464
|
class="cache-page-wrapper"
|
|
459
465
|
id={`W-${cacheName}`}
|
|
460
|
-
style={[
|
|
466
|
+
style={[
|
|
467
|
+
runtimeContext.useGlobalScroll.value ? 'overflow:auto' : 'overflow:hidden',
|
|
468
|
+
'height: 100%'
|
|
469
|
+
]}
|
|
461
470
|
>
|
|
462
|
-
{
|
|
463
|
-
{Node
|
|
464
|
-
? cloneVNode(Node, { tId: tabInfo.id, pId: cacheName, ...(dynamicBackProps || {}) })
|
|
465
|
-
: null}
|
|
471
|
+
{lastCloned.value}
|
|
466
472
|
<PageLoading tabId={tabInfo.id!} />
|
|
467
473
|
</div>
|
|
468
474
|
)
|
|
@@ -474,45 +480,63 @@ export default () => {
|
|
|
474
480
|
return cacheComponent
|
|
475
481
|
}
|
|
476
482
|
|
|
477
|
-
// 刷新隔离名单功能已由全新 ID 替换法替代
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* 将路由对应的组件注册为缓存页面(由 StackTabs.vue 的 tabWrapper 调用)。
|
|
481
|
-
*
|
|
482
|
-
* 流程:
|
|
483
|
-
* 1. 若 component 为空(router-view 未匹配到路由),返回空占位组件
|
|
484
|
-
* 2. updatePageState 解析路由状态:
|
|
485
|
-
* - empty → 标签已关闭,返回空占位
|
|
486
|
-
* - recovered → 刷新恢复,直接 addCache 并返回原组件
|
|
487
|
-
* - 正常 → 调用 resolvePageComponent 获取/创建 cacheComponent
|
|
488
|
-
*
|
|
489
|
-
* @param component StackTabs.vue 传入的原始 VNode(来自 <router-view> 的 Component 插槽)
|
|
490
|
-
* @returns 用于 <component :is> 的 cacheComponent 包装定义
|
|
491
|
-
*/
|
|
492
483
|
/** 当前正在渲染的页面的缓存 Key(ULID),供 StackTabs.vue 的 :key 绑定使用 */
|
|
493
484
|
const activeCacheKey = ref<string>('')
|
|
485
|
+
/** 只随当前活动页面刷新变化,避免一个 Tab 刷新导致其他 Tab 的缓存 key 失效。 */
|
|
486
|
+
const activePageRefreshVersion = computed(() => {
|
|
487
|
+
const activeTab = tabs.value.find((tab) => tab.active)
|
|
488
|
+
return activeTab?.pages.peek()?.refreshVersion ?? 0
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
let lastRouteKey = ''
|
|
492
|
+
let lastAddPageResult: DefineComponent | null = null
|
|
494
493
|
|
|
495
|
-
const addPage = (
|
|
494
|
+
const addPage = (
|
|
495
|
+
route: RouteLocationNormalizedLoaded,
|
|
496
|
+
component?: VNode | null
|
|
497
|
+
): DefineComponent => {
|
|
496
498
|
if (!component) return EmptyPlaceholderComponent as DefineComponent
|
|
497
499
|
|
|
500
|
+
const routeKey = route.fullPath + (route.query.__tab || '')
|
|
501
|
+
if (routeKey === lastRouteKey && lastAddPageResult) {
|
|
502
|
+
return lastAddPageResult
|
|
503
|
+
}
|
|
504
|
+
|
|
498
505
|
const state = updatePageState(route)
|
|
499
506
|
if ('empty' in state) return EmptyPlaceholderComponent as DefineComponent
|
|
500
507
|
if ('recovered' in state) {
|
|
501
|
-
// 刷新场景:组件定义仍存活,只需重新加入 include 列表即可
|
|
502
508
|
addCache(state.cacheName)
|
|
503
509
|
activeCacheKey.value = state.cacheName
|
|
510
|
+
lastRouteKey = routeKey
|
|
511
|
+
lastAddPageResult = state.recovered
|
|
504
512
|
return state.recovered
|
|
505
513
|
}
|
|
506
514
|
activeCacheKey.value = state.cacheName
|
|
507
|
-
|
|
515
|
+
const result = resolvePageComponent(state)
|
|
516
|
+
lastRouteKey = routeKey
|
|
517
|
+
lastAddPageResult = result
|
|
518
|
+
return result
|
|
508
519
|
}
|
|
509
520
|
|
|
510
521
|
const renewTab = (tab: ITabData) => {
|
|
511
522
|
const currentTab = getTab(tab.id!)
|
|
512
|
-
if (!currentTab) return
|
|
523
|
+
if (!currentTab) return undefined
|
|
524
|
+
|
|
525
|
+
const pageSnapshot = currentTab.pages.list().map((p) => clonePage(p))
|
|
526
|
+
const cacheSnapshot = [...caches.value]
|
|
527
|
+
const componentSnapshot = new Map(components)
|
|
528
|
+
|
|
513
529
|
markTabPagesForEvictionOnly(currentTab)
|
|
514
530
|
evictMarkedCaches()
|
|
515
531
|
currentTab.pages.clear()
|
|
532
|
+
|
|
533
|
+
return () => {
|
|
534
|
+
currentTab.pages.clear()
|
|
535
|
+
for (const page of pageSnapshot) currentTab.pages.push(clonePage(page))
|
|
536
|
+
caches.value = [...cacheSnapshot]
|
|
537
|
+
components.clear()
|
|
538
|
+
for (const [id, component] of componentSnapshot) components.set(id, component)
|
|
539
|
+
}
|
|
516
540
|
}
|
|
517
541
|
|
|
518
542
|
const removeTab = (id: string): string => {
|
|
@@ -543,16 +567,21 @@ export default () => {
|
|
|
543
567
|
return activeTabId
|
|
544
568
|
}
|
|
545
569
|
|
|
546
|
-
|
|
570
|
+
/** 批量关闭满足条件的可关闭标签,并统一标记其页面缓存待驱逐。 */
|
|
571
|
+
const removeTabs = (shouldRemove: (tab: ITabItem, index: number) => boolean) => {
|
|
547
572
|
const tabList = tabs.value
|
|
548
|
-
for (let
|
|
549
|
-
const tab = tabList[
|
|
550
|
-
if (tab
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
573
|
+
for (let index = tabList.length - 1; index >= 0; index--) {
|
|
574
|
+
const tab = tabList[index]
|
|
575
|
+
if (!tab || !tab.closable || !shouldRemove(tab, index)) continue
|
|
576
|
+
markTabPagesForEviction(tab)
|
|
577
|
+
tab.pages.clear()
|
|
578
|
+
tabList.splice(index, 1)
|
|
555
579
|
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const removeAllTabs = () => {
|
|
583
|
+
const tabList = tabs.value
|
|
584
|
+
removeTabs(() => true)
|
|
556
585
|
const hasActive = tabList.some((t) => t.active)
|
|
557
586
|
if (hasActive) {
|
|
558
587
|
evictMarkedCaches()
|
|
@@ -565,18 +594,8 @@ export default () => {
|
|
|
565
594
|
|
|
566
595
|
const removeOtherTabs = (id: string) => {
|
|
567
596
|
const tabList = tabs.value
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const tab = tabList[i]
|
|
571
|
-
if (!tab) continue
|
|
572
|
-
if (tab.id === id) {
|
|
573
|
-
activeTab = tab as ITabItem
|
|
574
|
-
} else if (tab.closable) {
|
|
575
|
-
markTabPagesForEviction(tab)
|
|
576
|
-
tab.pages.clear()
|
|
577
|
-
tabList.splice(i, 1)
|
|
578
|
-
}
|
|
579
|
-
}
|
|
597
|
+
const activeTab = tabList.find((tab) => tab.id === id)
|
|
598
|
+
removeTabs((tab) => tab.id !== id)
|
|
580
599
|
if (activeTab && !activeTab.active) {
|
|
581
600
|
emitter.emit(TabEventType.TAB_ACTIVE, { id })
|
|
582
601
|
} else {
|
|
@@ -590,14 +609,7 @@ export default () => {
|
|
|
590
609
|
const pivot = tabList.findIndex((t) => t?.id === id)
|
|
591
610
|
if (pivot <= 0) return
|
|
592
611
|
|
|
593
|
-
|
|
594
|
-
const tab = tabList[i]
|
|
595
|
-
if (tab?.closable) {
|
|
596
|
-
markTabPagesForEviction(tab)
|
|
597
|
-
tab.pages.clear()
|
|
598
|
-
tabList.splice(i, 1)
|
|
599
|
-
}
|
|
600
|
-
}
|
|
612
|
+
removeTabs((_, index) => index < pivot)
|
|
601
613
|
if (tabList.some((t) => t.active)) {
|
|
602
614
|
evictMarkedCaches()
|
|
603
615
|
tabIdsToEvict.clear()
|
|
@@ -612,14 +624,7 @@ export default () => {
|
|
|
612
624
|
const pivot = tabList.findIndex((t) => t?.id === id)
|
|
613
625
|
if (pivot < 0 || pivot >= tabList.length - 1) return
|
|
614
626
|
|
|
615
|
-
|
|
616
|
-
const tab = tabList[i]
|
|
617
|
-
if (tab?.closable) {
|
|
618
|
-
markTabPagesForEviction(tab)
|
|
619
|
-
tab.pages.clear()
|
|
620
|
-
tabList.splice(i, 1)
|
|
621
|
-
}
|
|
622
|
-
}
|
|
627
|
+
removeTabs((_, index) => index > pivot)
|
|
623
628
|
if (tabList.some((t) => t.active)) {
|
|
624
629
|
evictMarkedCaches()
|
|
625
630
|
tabIdsToEvict.clear()
|
|
@@ -629,79 +634,16 @@ export default () => {
|
|
|
629
634
|
if (last) emitter.emit(TabEventType.TAB_ACTIVE, { id: last.id })
|
|
630
635
|
}
|
|
631
636
|
|
|
632
|
-
const refreshTab = (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
} else {
|
|
643
|
-
;(emitter as { emit: (t: string, p: string) => void }).emit(
|
|
644
|
-
'REFRESH_IFRAME_POSTMESSAGE',
|
|
645
|
-
id
|
|
646
|
-
)
|
|
647
|
-
}
|
|
648
|
-
return
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const currentPage = tab.pages.peek()
|
|
652
|
-
if (!currentPage) return
|
|
653
|
-
|
|
654
|
-
// 新架构:真正的刷新 = 把栈顶的旧组件数据连根拔起并注销,让它换个新 ID 重新走生命周期
|
|
655
|
-
const oldId = currentPage.id
|
|
656
|
-
evictPageCache(oldId) // 清除旧缓存
|
|
657
|
-
|
|
658
|
-
// 生成全新 ID,相当于把原页面配方注入了一个崭新的组件身份
|
|
659
|
-
currentPage.id = createPageId()
|
|
660
|
-
addCache(currentPage.id)
|
|
661
|
-
|
|
662
|
-
if (tab.active) {
|
|
663
|
-
// 激活状态下,增加 refreshKey 强制触发当前 Router 的重排,
|
|
664
|
-
// 由于 `<keep-alive>` 中之前记录的名字已经被驱逐出境,它会自动使用新 ID 创建一个全新页面。
|
|
665
|
-
refreshKey.value++
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
const refreshAllTabs = () => {
|
|
670
|
-
let needsActiveRefresh = false
|
|
671
|
-
|
|
672
|
-
for (const tab of tabs.value) {
|
|
673
|
-
if (!tab?.refreshable) continue
|
|
674
|
-
if (tab.iframe) {
|
|
675
|
-
if (tab.iframeRefreshMode === 'reload') {
|
|
676
|
-
iframeRefreshKeys.value = {
|
|
677
|
-
...iframeRefreshKeys.value,
|
|
678
|
-
[tab.id]: (iframeRefreshKeys.value[tab.id] ?? 0) + 1
|
|
679
|
-
}
|
|
680
|
-
} else {
|
|
681
|
-
;(emitter as { emit: (t: string, p: string) => void }).emit(
|
|
682
|
-
'REFRESH_IFRAME_POSTMESSAGE',
|
|
683
|
-
tab.id
|
|
684
|
-
)
|
|
685
|
-
}
|
|
686
|
-
} else {
|
|
687
|
-
const currentPage = tab.pages.peek()
|
|
688
|
-
if (currentPage) {
|
|
689
|
-
const oldId = currentPage.id
|
|
690
|
-
evictPageCache(oldId)
|
|
691
|
-
currentPage.id = createPageId()
|
|
692
|
-
addCache(currentPage.id)
|
|
693
|
-
|
|
694
|
-
if (tab.active) {
|
|
695
|
-
needsActiveRefresh = true
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
if (needsActiveRefresh) {
|
|
702
|
-
refreshKey.value++
|
|
703
|
-
}
|
|
704
|
-
}
|
|
637
|
+
const { refreshTab, refreshAllTabs } = createTabPanelRefresh({
|
|
638
|
+
tabs,
|
|
639
|
+
iframeRefreshKeys,
|
|
640
|
+
getTab,
|
|
641
|
+
createPageId,
|
|
642
|
+
addCache,
|
|
643
|
+
evictPageCache,
|
|
644
|
+
replacePageCaches,
|
|
645
|
+
refreshIframeByPostMessage: (id) => emitter.emit(TabEventType.REFRESH_IFRAME_POSTMESSAGE, id)
|
|
646
|
+
})
|
|
705
647
|
|
|
706
648
|
const getComponent = (id: string) => components.get(id)
|
|
707
649
|
|
|
@@ -711,44 +653,71 @@ export default () => {
|
|
|
711
653
|
* @param route - 是否执行 router.push
|
|
712
654
|
*/
|
|
713
655
|
const active = (id: string, route = true) => {
|
|
714
|
-
emitter.emit(
|
|
656
|
+
emitter.emit(TabEventType.FORWARD)
|
|
715
657
|
const target = getTab(id)
|
|
716
|
-
if (!target) return
|
|
717
|
-
if (target.active) return
|
|
658
|
+
if (!target) return Promise.resolve()
|
|
659
|
+
if (target.active) return Promise.resolve()
|
|
718
660
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
// 强制触发 tabs 响应式更新,确保 TabHeader 高亮与 iframe 进入动画 timing 正确
|
|
724
|
-
tabs.value = tabs.value.slice()
|
|
725
|
-
if (route) {
|
|
726
|
-
const top = target.pages.peek()
|
|
727
|
-
if (top) {
|
|
728
|
-
// 切换 Tab 走路由时,必须将 __tab 编码信息注入 query,
|
|
729
|
-
// 否则 parseTabInfoFromRoute 找不到对应 Tab,会新建匿名标签!
|
|
730
|
-
const __tab = encodeTabInfo({
|
|
731
|
-
id: target.id,
|
|
732
|
-
title: target.title,
|
|
733
|
-
closable: target.closable,
|
|
734
|
-
refreshable: target.refreshable,
|
|
735
|
-
iframe: target.iframe
|
|
736
|
-
})
|
|
737
|
-
const query = defu({ __tab }, top.query || {})
|
|
738
|
-
router.push({ path: top.path, query })
|
|
661
|
+
const top = target.pages.peek()
|
|
662
|
+
const activateTarget = () => {
|
|
663
|
+
for (const tab of tabs.value) {
|
|
664
|
+
tab.active = tab.id === id
|
|
739
665
|
}
|
|
666
|
+
saveActiveTabToSession(target as ITabItem)
|
|
667
|
+
tabs.value = tabs.value.slice()
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (!route) {
|
|
671
|
+
activateTarget()
|
|
672
|
+
return Promise.resolve()
|
|
740
673
|
}
|
|
674
|
+
if (!top) {
|
|
675
|
+
activateTarget()
|
|
676
|
+
return Promise.resolve()
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const __tab = encodeTabInfo({
|
|
680
|
+
id: target.id,
|
|
681
|
+
title: target.title,
|
|
682
|
+
closable: target.closable,
|
|
683
|
+
refreshable: target.refreshable,
|
|
684
|
+
iframe: target.iframe
|
|
685
|
+
})
|
|
686
|
+
const query = defu({ __tab }, top.query || {})
|
|
687
|
+
|
|
688
|
+
return runNavigationTransaction({
|
|
689
|
+
apply: () => {
|
|
690
|
+
const prevActiveId = tabs.value.find((tab) => tab.active)?.id
|
|
691
|
+
const sessionSnapshot = window.sessionStorage.getItem(getSessionKey())
|
|
692
|
+
activateTarget()
|
|
693
|
+
return { prevActiveId, sessionSnapshot }
|
|
694
|
+
},
|
|
695
|
+
navigate: () => router.push({ path: top.path, query }),
|
|
696
|
+
rollback: (snapshot) => {
|
|
697
|
+
for (const tab of tabs.value) {
|
|
698
|
+
tab.active = tab.id === snapshot.prevActiveId
|
|
699
|
+
}
|
|
700
|
+
tabs.value = tabs.value.slice()
|
|
701
|
+
restoreActiveTabSession(snapshot.sessionSnapshot)
|
|
702
|
+
},
|
|
703
|
+
isFailureResult: isNavigationFailure,
|
|
704
|
+
rejectFailureResult: true
|
|
705
|
+
})
|
|
741
706
|
}
|
|
742
707
|
|
|
743
708
|
const reset = () => removeAllTabs()
|
|
744
709
|
|
|
745
710
|
const destroy = () => {
|
|
746
|
-
tabs.value
|
|
711
|
+
tabs.value = []
|
|
712
|
+
defaultTabs.value = []
|
|
747
713
|
cacheIdsToEvict.clear()
|
|
748
714
|
tabIdsToEvict.clear()
|
|
749
715
|
iframeRefreshKeys.value = {}
|
|
750
|
-
caches.value
|
|
716
|
+
caches.value = []
|
|
751
717
|
components.clear()
|
|
718
|
+
runtimeContext.scrollPositionsByPageId.clear()
|
|
719
|
+
runtimeContext.isInitialized.value = false
|
|
720
|
+
runtimeContext.refreshKey.value = 0
|
|
752
721
|
clearSession()
|
|
753
722
|
}
|
|
754
723
|
|
|
@@ -757,18 +726,22 @@ export default () => {
|
|
|
757
726
|
caches,
|
|
758
727
|
refreshKey,
|
|
759
728
|
activeCacheKey,
|
|
729
|
+
activePageRefreshVersion,
|
|
760
730
|
isInitialized,
|
|
761
|
-
setMaxSize:
|
|
731
|
+
setMaxSize: (value: number) => {
|
|
732
|
+
runtimeContext.maxTabCount.value = value
|
|
733
|
+
},
|
|
762
734
|
canAddTab,
|
|
763
735
|
initialize,
|
|
764
736
|
size,
|
|
765
737
|
active,
|
|
766
|
-
getCacheName,
|
|
738
|
+
getCacheName: createCacheName,
|
|
767
739
|
destroy,
|
|
768
740
|
reset,
|
|
769
741
|
addCache,
|
|
770
742
|
removeCache,
|
|
771
743
|
getTab,
|
|
744
|
+
moveTab,
|
|
772
745
|
addTab,
|
|
773
746
|
removeTab,
|
|
774
747
|
removeAllTabs,
|
|
@@ -788,8 +761,12 @@ export default () => {
|
|
|
788
761
|
refreshAllTabs,
|
|
789
762
|
iframeRefreshKeys,
|
|
790
763
|
addPageScroller,
|
|
791
|
-
setGlobalScroll:
|
|
764
|
+
setGlobalScroll: (value: boolean) => {
|
|
765
|
+
runtimeContext.useGlobalScroll.value = value
|
|
766
|
+
},
|
|
792
767
|
clearSession,
|
|
793
|
-
setSessionPrefix
|
|
768
|
+
setSessionPrefix: (value: string) => {
|
|
769
|
+
runtimeContext.sessionPrefix.value = value
|
|
770
|
+
}
|
|
794
771
|
}
|
|
795
772
|
}
|