vue-stack-tabs 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +526 -0
- package/README.md +139 -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 +68 -47
- package/src/lib/StackTabs.vue +223 -196
- 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 +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 +62 -30
- package/src/lib/hooks/useTabEventBus.ts +29 -19
- package/src/lib/hooks/useTabLoading.ts +3 -2
- package/src/lib/hooks/useTabPanel.tsx +266 -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,54 @@ 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
|
-
/** 从路由解析 tabInfo
|
|
192
|
+
/** 从路由解析 tabInfo,若无则创建默认信息,调用方负责把编码信息写入内部 page.query */
|
|
168
193
|
const parseTabInfoFromRoute = (route: RouteLocationNormalizedLoaded): ITabBase => {
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
194
|
+
if (typeof route.query.__tab === 'string') {
|
|
195
|
+
const tabInfo = decodeTabInfo(route.query.__tab)
|
|
196
|
+
if (tabInfo.id) return tabInfo
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const path = normalizePathForCache(route)
|
|
200
|
+
const activeTab = tabs.value.find((tab) => tab.active)
|
|
201
|
+
const activePage = activeTab?.pages.peek()
|
|
202
|
+
if (activeTab && activePage?.path === path) {
|
|
203
|
+
return {
|
|
204
|
+
id: activeTab.id,
|
|
205
|
+
title: activeTab.title,
|
|
206
|
+
closable: activeTab.closable,
|
|
207
|
+
refreshable: activeTab.refreshable,
|
|
208
|
+
iframe: activeTab.iframe,
|
|
209
|
+
iframeRefreshMode: activeTab.iframeRefreshMode
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
id: crypto.randomUUID(),
|
|
172
215
|
title: t('VueStackTab.undefined'),
|
|
173
216
|
closable: true,
|
|
174
217
|
refreshable: true,
|
|
175
218
|
iframe: false
|
|
176
219
|
}
|
|
177
|
-
route.query.__tab = encodeTabInfo(tabInfo)
|
|
178
|
-
return tabInfo
|
|
179
220
|
}
|
|
180
221
|
|
|
181
222
|
/**
|
|
182
223
|
* 查找或创建目标标签,并同步更新 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
224
|
*/
|
|
193
225
|
const findOrCreateTargetTab = (
|
|
194
226
|
tabInfo: ITabBase,
|
|
@@ -203,7 +235,6 @@ export default () => {
|
|
|
203
235
|
}
|
|
204
236
|
|
|
205
237
|
if (targetTab === null) {
|
|
206
|
-
// 全新标签:初始化 pages 栈、从 query.__src 获取 iframe url
|
|
207
238
|
const pages = new Stack<ITabPage>()
|
|
208
239
|
pages.push(page)
|
|
209
240
|
const src = route.query.__src
|
|
@@ -214,17 +245,14 @@ export default () => {
|
|
|
214
245
|
refreshable: tabInfo.refreshable!,
|
|
215
246
|
iframe: tabInfo.iframe!,
|
|
216
247
|
iframeRefreshMode: tabInfo.iframeRefreshMode ?? 'postMessage',
|
|
217
|
-
url:
|
|
248
|
+
url: decodeSafeTabUrl(src),
|
|
218
249
|
active: true,
|
|
219
250
|
pages
|
|
220
251
|
} as unknown as ITabItem
|
|
221
|
-
addTab(targetTab)
|
|
222
|
-
|
|
223
|
-
})
|
|
252
|
+
addTab(targetTab)
|
|
253
|
+
emitter.emit(TabEventType.TAB_ACTIVE, { id: tabInfo.id!, isRoute: false })
|
|
224
254
|
} else {
|
|
225
255
|
targetTab.active = true
|
|
226
|
-
// 幂等保护:page.id 与当前栈顶相同时不重复入栈
|
|
227
|
-
// 响应式重渲染(如 caches 变更)会重复调用 tabWrapper,需过滤此类重入
|
|
228
256
|
if (targetTab.pages.isEmpty() || targetTab.pages.peek()!.id !== page.id) {
|
|
229
257
|
targetTab.pages.push(page)
|
|
230
258
|
}
|
|
@@ -232,26 +260,12 @@ export default () => {
|
|
|
232
260
|
return targetTab
|
|
233
261
|
}
|
|
234
262
|
|
|
235
|
-
|
|
236
|
-
* 暴露给外部使用,用于获取指定路由对应的 cacheName
|
|
237
|
-
*/
|
|
238
|
-
const getCacheName = () => {
|
|
263
|
+
const createCacheName = () => {
|
|
239
264
|
return createPageId()
|
|
240
265
|
}
|
|
241
266
|
|
|
242
267
|
/**
|
|
243
268
|
* 根据当前路由更新/创建标签与页面状态(核心路由适配器)。
|
|
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
269
|
*/
|
|
256
270
|
const updatePageState = (
|
|
257
271
|
route: RouteLocationNormalizedLoaded
|
|
@@ -264,8 +278,7 @@ export default () => {
|
|
|
264
278
|
|
|
265
279
|
const path = normalizePathForCache(route)
|
|
266
280
|
|
|
267
|
-
|
|
268
|
-
let targetTab: ITabItem | null = tabInfo.id ? (getTab(tabInfo.id) as unknown as ITabItem) : null
|
|
281
|
+
let targetTab: ITabItem | null = tabInfo.id ? (getTab(tabInfo.id) as ITabItem | null) : null
|
|
269
282
|
let cacheName: string
|
|
270
283
|
let page: ITabPage | null = null
|
|
271
284
|
|
|
@@ -273,16 +286,11 @@ export default () => {
|
|
|
273
286
|
const top = targetTab.pages.peek()
|
|
274
287
|
if (!top) throw new Error('[vue-stack-tabs] Stack is not empty but peek() returned undefined')
|
|
275
288
|
|
|
276
|
-
// 数据栈是唯一真相源,只比对 path 即可判断当前路由是否与栈顶一致
|
|
277
289
|
if (top.path === path) {
|
|
278
290
|
cacheName = top.id
|
|
279
291
|
} else {
|
|
280
|
-
|
|
281
|
-
// URL 和栈顶对不上!我们被外部改变了。
|
|
282
|
-
const pagesList = targetTab.pages.list()
|
|
292
|
+
const pagesList = targetTab.pages.readonlyList()
|
|
283
293
|
let foundIndex = -1
|
|
284
|
-
// 从栈顶往下找,有没有曾经来过的同路由(以此判定是否为 PopState 后退)
|
|
285
|
-
// 只比对 path,因为数据栈才是唯一真相源
|
|
286
294
|
for (let i = pagesList.length - 2; i >= 0; i--) {
|
|
287
295
|
const pastPage = pagesList[i]
|
|
288
296
|
if (pastPage && pastPage.path === path) {
|
|
@@ -292,18 +300,16 @@ export default () => {
|
|
|
292
300
|
}
|
|
293
301
|
|
|
294
302
|
if (foundIndex !== -1) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
303
|
+
if (!import.meta.env.PROD) {
|
|
304
|
+
console.warn(
|
|
305
|
+
'[vue-stack-tabs] Detected native browser back navigation, repairing page stack...'
|
|
306
|
+
)
|
|
307
|
+
}
|
|
300
308
|
const stepsToPop = pagesList.length - 1 - foundIndex
|
|
301
309
|
for (let i = 0; i < stepsToPop; i++) {
|
|
302
310
|
const popped = targetTab.pages.pop()
|
|
303
|
-
// 立即驱逐被错误保留在内存的那些未来组件(在 router 的钩子里也可以,但这里既然已经渲染就直接驱)
|
|
304
311
|
if (popped) evictPageCache(popped.id)
|
|
305
312
|
}
|
|
306
|
-
// 因为我们把不要的都出栈了,现在的栈顶就是以前的这一个记录
|
|
307
313
|
const newTop = targetTab.pages.peek()
|
|
308
314
|
cacheName = newTop ? newTop.id : createPageId()
|
|
309
315
|
|
|
@@ -311,49 +317,41 @@ export default () => {
|
|
|
311
317
|
page = {
|
|
312
318
|
id: cacheName,
|
|
313
319
|
tabId: tabInfo.id!,
|
|
314
|
-
path: route.path,
|
|
315
|
-
query: route
|
|
320
|
+
path: route.path,
|
|
321
|
+
query: cloneRouteQuery(route, tabInfo)
|
|
316
322
|
}
|
|
317
323
|
}
|
|
318
324
|
} else {
|
|
319
|
-
// 在历史栈完全没找到,真的是被强行推入的“空降兵”新路由,直接补发一个新的栈记录。
|
|
320
325
|
cacheName = createPageId()
|
|
321
326
|
page = {
|
|
322
327
|
id: cacheName,
|
|
323
328
|
tabId: tabInfo.id!,
|
|
324
|
-
path: route.path,
|
|
325
|
-
query: route
|
|
329
|
+
path: route.path,
|
|
330
|
+
query: cloneRouteQuery(route, tabInfo)
|
|
326
331
|
}
|
|
327
332
|
}
|
|
328
333
|
}
|
|
329
334
|
} else {
|
|
330
|
-
// 这是一个全新创建的 Tab(被外部用空链接无中生有)
|
|
331
335
|
cacheName = createPageId()
|
|
332
336
|
page = {
|
|
333
337
|
id: cacheName,
|
|
334
338
|
tabId: tabInfo.id!,
|
|
335
339
|
path: route.path,
|
|
336
|
-
query: route
|
|
340
|
+
query: cloneRouteQuery(route, tabInfo)
|
|
337
341
|
}
|
|
338
342
|
}
|
|
339
343
|
|
|
340
|
-
// 刷新恢复场景:依旧支持软驱逐和组件复用
|
|
341
344
|
if (cacheIdsToEvict.has(cacheName)) {
|
|
342
345
|
cacheIdsToEvict.delete(cacheName)
|
|
343
346
|
const cached = components.get(cacheName)
|
|
344
347
|
if (cached) return { recovered: cached, cacheName }
|
|
345
348
|
}
|
|
346
349
|
|
|
347
|
-
// 将 page (如果上面判断它为全新的时候)塞入栈中或建新 Tab
|
|
348
350
|
if (page) {
|
|
349
351
|
targetTab = findOrCreateTargetTab(tabInfo, route, cacheName, page)
|
|
350
352
|
} else {
|
|
351
|
-
// 否则说明这只是同页渲染复用(或者是刷新回来的无变动栈),把 targetTab 直接提拔为 active
|
|
352
353
|
for (const t of tabs.value) t.active = false
|
|
353
354
|
targetTab!.active = true
|
|
354
|
-
if (!cacheName && targetTab && !targetTab.pages.isEmpty()) {
|
|
355
|
-
cacheName = targetTab.pages.peek()!.id
|
|
356
|
-
}
|
|
357
355
|
}
|
|
358
356
|
|
|
359
357
|
saveActiveTabToSession(targetTab!)
|
|
@@ -362,27 +360,6 @@ export default () => {
|
|
|
362
360
|
|
|
363
361
|
/**
|
|
364
362
|
* 获取或创建与 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
363
|
*/
|
|
387
364
|
const resolvePageComponent = (ctx: {
|
|
388
365
|
cacheName: string
|
|
@@ -391,17 +368,13 @@ export default () => {
|
|
|
391
368
|
}): DefineComponent => {
|
|
392
369
|
const { cacheName, tabInfo } = ctx
|
|
393
370
|
|
|
394
|
-
// 已存在的组件定义直接复用(keep-alive 会在内部找到缓存实例),无需重建
|
|
395
371
|
if (components.has(cacheName)) {
|
|
396
|
-
addCache(cacheName)
|
|
372
|
+
addCache(cacheName)
|
|
397
373
|
return components.get(cacheName)!
|
|
398
374
|
}
|
|
399
375
|
|
|
400
|
-
// 首次进入该缓存页:创建专属 cacheComponent 包装组件
|
|
401
376
|
const cacheComponent = defineComponent({
|
|
402
|
-
// name 与 cacheName 保持一致,作为 keep-alive include 的匹配键
|
|
403
377
|
name: cacheName,
|
|
404
|
-
// vnode 由 StackTabs.vue 的 :vnode="Component" 动态传入(解耦闭包)
|
|
405
378
|
props: {
|
|
406
379
|
vnode: {
|
|
407
380
|
type: Object as () => VNode,
|
|
@@ -411,8 +384,10 @@ export default () => {
|
|
|
411
384
|
},
|
|
412
385
|
emits: ['onLoaded'],
|
|
413
386
|
setup(props, context) {
|
|
414
|
-
// 用于在当前组件活跃期内存下回退参数,离开或重返时将更新或清理,真正实现生命周期同步!
|
|
415
387
|
const localBackParams = ref<Record<string, unknown> | null>(null)
|
|
388
|
+
const lastVnode = shallowRef<VNode | null>(null)
|
|
389
|
+
const lastCloned = shallowRef<VNode | null>(null)
|
|
390
|
+
const lastBackParams = shallowRef<Record<string, unknown> | null>(null)
|
|
416
391
|
|
|
417
392
|
const checkAndConsumeBackParams = () => {
|
|
418
393
|
try {
|
|
@@ -420,7 +395,6 @@ export default () => {
|
|
|
420
395
|
const top = tab?.pages.peek()
|
|
421
396
|
if (top && top.id === cacheName && top._backParams) {
|
|
422
397
|
localBackParams.value = { ...top._backParams }
|
|
423
|
-
// 取出后阅后即焚,防止从别处循环回退时拿到过期老参
|
|
424
398
|
delete top._backParams
|
|
425
399
|
} else {
|
|
426
400
|
localBackParams.value = null
|
|
@@ -451,18 +425,27 @@ export default () => {
|
|
|
451
425
|
return () => {
|
|
452
426
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
453
427
|
const Node = (props as any).vnode as VNode
|
|
454
|
-
const
|
|
428
|
+
const backParams = localBackParams.value
|
|
429
|
+
|
|
430
|
+
if (Node !== lastVnode.value || backParams !== lastBackParams.value) {
|
|
431
|
+
const dynamicBackProps = backParams ? { _back: backParams } : {}
|
|
432
|
+
lastCloned.value = Node
|
|
433
|
+
? cloneVNode(Node, { tId: tabInfo.id, pId: cacheName, ...dynamicBackProps })
|
|
434
|
+
: null
|
|
435
|
+
lastVnode.value = Node
|
|
436
|
+
lastBackParams.value = backParams
|
|
437
|
+
}
|
|
455
438
|
|
|
456
439
|
return (
|
|
457
440
|
<div
|
|
458
441
|
class="cache-page-wrapper"
|
|
459
442
|
id={`W-${cacheName}`}
|
|
460
|
-
style={[
|
|
443
|
+
style={[
|
|
444
|
+
runtimeContext.useGlobalScroll.value ? 'overflow:auto' : 'overflow:hidden',
|
|
445
|
+
'height: 100%'
|
|
446
|
+
]}
|
|
461
447
|
>
|
|
462
|
-
{
|
|
463
|
-
{Node
|
|
464
|
-
? cloneVNode(Node, { tId: tabInfo.id, pId: cacheName, ...(dynamicBackProps || {}) })
|
|
465
|
-
: null}
|
|
448
|
+
{lastCloned.value}
|
|
466
449
|
<PageLoading tabId={tabInfo.id!} />
|
|
467
450
|
</div>
|
|
468
451
|
)
|
|
@@ -474,45 +457,63 @@ export default () => {
|
|
|
474
457
|
return cacheComponent
|
|
475
458
|
}
|
|
476
459
|
|
|
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
460
|
/** 当前正在渲染的页面的缓存 Key(ULID),供 StackTabs.vue 的 :key 绑定使用 */
|
|
493
461
|
const activeCacheKey = ref<string>('')
|
|
462
|
+
/** 只随当前活动页面刷新变化,避免一个 Tab 刷新导致其他 Tab 的缓存 key 失效。 */
|
|
463
|
+
const activePageRefreshVersion = computed(() => {
|
|
464
|
+
const activeTab = tabs.value.find((tab) => tab.active)
|
|
465
|
+
return activeTab?.pages.peek()?.refreshVersion ?? 0
|
|
466
|
+
})
|
|
494
467
|
|
|
495
|
-
|
|
468
|
+
let lastRouteKey = ''
|
|
469
|
+
let lastAddPageResult: DefineComponent | null = null
|
|
470
|
+
|
|
471
|
+
const addPage = (
|
|
472
|
+
route: RouteLocationNormalizedLoaded,
|
|
473
|
+
component?: VNode | null
|
|
474
|
+
): DefineComponent => {
|
|
496
475
|
if (!component) return EmptyPlaceholderComponent as DefineComponent
|
|
497
476
|
|
|
477
|
+
const routeKey = route.fullPath + (route.query.__tab || '')
|
|
478
|
+
if (routeKey === lastRouteKey && lastAddPageResult) {
|
|
479
|
+
return lastAddPageResult
|
|
480
|
+
}
|
|
481
|
+
|
|
498
482
|
const state = updatePageState(route)
|
|
499
483
|
if ('empty' in state) return EmptyPlaceholderComponent as DefineComponent
|
|
500
484
|
if ('recovered' in state) {
|
|
501
|
-
// 刷新场景:组件定义仍存活,只需重新加入 include 列表即可
|
|
502
485
|
addCache(state.cacheName)
|
|
503
486
|
activeCacheKey.value = state.cacheName
|
|
487
|
+
lastRouteKey = routeKey
|
|
488
|
+
lastAddPageResult = state.recovered
|
|
504
489
|
return state.recovered
|
|
505
490
|
}
|
|
506
491
|
activeCacheKey.value = state.cacheName
|
|
507
|
-
|
|
492
|
+
const result = resolvePageComponent(state)
|
|
493
|
+
lastRouteKey = routeKey
|
|
494
|
+
lastAddPageResult = result
|
|
495
|
+
return result
|
|
508
496
|
}
|
|
509
497
|
|
|
510
498
|
const renewTab = (tab: ITabData) => {
|
|
511
499
|
const currentTab = getTab(tab.id!)
|
|
512
|
-
if (!currentTab) return
|
|
500
|
+
if (!currentTab) return undefined
|
|
501
|
+
|
|
502
|
+
const pageSnapshot = currentTab.pages.list().map((p) => clonePage(p))
|
|
503
|
+
const cacheSnapshot = [...caches.value]
|
|
504
|
+
const componentSnapshot = new Map(components)
|
|
505
|
+
|
|
513
506
|
markTabPagesForEvictionOnly(currentTab)
|
|
514
507
|
evictMarkedCaches()
|
|
515
508
|
currentTab.pages.clear()
|
|
509
|
+
|
|
510
|
+
return () => {
|
|
511
|
+
currentTab.pages.clear()
|
|
512
|
+
for (const page of pageSnapshot) currentTab.pages.push(clonePage(page))
|
|
513
|
+
caches.value = [...cacheSnapshot]
|
|
514
|
+
components.clear()
|
|
515
|
+
for (const [id, component] of componentSnapshot) components.set(id, component)
|
|
516
|
+
}
|
|
516
517
|
}
|
|
517
518
|
|
|
518
519
|
const removeTab = (id: string): string => {
|
|
@@ -543,16 +544,21 @@ export default () => {
|
|
|
543
544
|
return activeTabId
|
|
544
545
|
}
|
|
545
546
|
|
|
546
|
-
|
|
547
|
+
/** 批量关闭满足条件的可关闭标签,并统一标记其页面缓存待驱逐。 */
|
|
548
|
+
const removeTabs = (shouldRemove: (tab: ITabItem, index: number) => boolean) => {
|
|
547
549
|
const tabList = tabs.value
|
|
548
|
-
for (let
|
|
549
|
-
const tab = tabList[
|
|
550
|
-
if (tab
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
550
|
+
for (let index = tabList.length - 1; index >= 0; index--) {
|
|
551
|
+
const tab = tabList[index]
|
|
552
|
+
if (!tab || !tab.closable || !shouldRemove(tab, index)) continue
|
|
553
|
+
markTabPagesForEviction(tab)
|
|
554
|
+
tab.pages.clear()
|
|
555
|
+
tabList.splice(index, 1)
|
|
555
556
|
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const removeAllTabs = () => {
|
|
560
|
+
const tabList = tabs.value
|
|
561
|
+
removeTabs(() => true)
|
|
556
562
|
const hasActive = tabList.some((t) => t.active)
|
|
557
563
|
if (hasActive) {
|
|
558
564
|
evictMarkedCaches()
|
|
@@ -565,18 +571,8 @@ export default () => {
|
|
|
565
571
|
|
|
566
572
|
const removeOtherTabs = (id: string) => {
|
|
567
573
|
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
|
-
}
|
|
574
|
+
const activeTab = tabList.find((tab) => tab.id === id)
|
|
575
|
+
removeTabs((tab) => tab.id !== id)
|
|
580
576
|
if (activeTab && !activeTab.active) {
|
|
581
577
|
emitter.emit(TabEventType.TAB_ACTIVE, { id })
|
|
582
578
|
} else {
|
|
@@ -590,14 +586,7 @@ export default () => {
|
|
|
590
586
|
const pivot = tabList.findIndex((t) => t?.id === id)
|
|
591
587
|
if (pivot <= 0) return
|
|
592
588
|
|
|
593
|
-
|
|
594
|
-
const tab = tabList[i]
|
|
595
|
-
if (tab?.closable) {
|
|
596
|
-
markTabPagesForEviction(tab)
|
|
597
|
-
tab.pages.clear()
|
|
598
|
-
tabList.splice(i, 1)
|
|
599
|
-
}
|
|
600
|
-
}
|
|
589
|
+
removeTabs((_, index) => index < pivot)
|
|
601
590
|
if (tabList.some((t) => t.active)) {
|
|
602
591
|
evictMarkedCaches()
|
|
603
592
|
tabIdsToEvict.clear()
|
|
@@ -612,14 +601,7 @@ export default () => {
|
|
|
612
601
|
const pivot = tabList.findIndex((t) => t?.id === id)
|
|
613
602
|
if (pivot < 0 || pivot >= tabList.length - 1) return
|
|
614
603
|
|
|
615
|
-
|
|
616
|
-
const tab = tabList[i]
|
|
617
|
-
if (tab?.closable) {
|
|
618
|
-
markTabPagesForEviction(tab)
|
|
619
|
-
tab.pages.clear()
|
|
620
|
-
tabList.splice(i, 1)
|
|
621
|
-
}
|
|
622
|
-
}
|
|
604
|
+
removeTabs((_, index) => index > pivot)
|
|
623
605
|
if (tabList.some((t) => t.active)) {
|
|
624
606
|
evictMarkedCaches()
|
|
625
607
|
tabIdsToEvict.clear()
|
|
@@ -629,79 +611,16 @@ export default () => {
|
|
|
629
611
|
if (last) emitter.emit(TabEventType.TAB_ACTIVE, { id: last.id })
|
|
630
612
|
}
|
|
631
613
|
|
|
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
|
-
}
|
|
614
|
+
const { refreshTab, refreshAllTabs } = createTabPanelRefresh({
|
|
615
|
+
tabs,
|
|
616
|
+
iframeRefreshKeys,
|
|
617
|
+
getTab,
|
|
618
|
+
createPageId,
|
|
619
|
+
addCache,
|
|
620
|
+
evictPageCache,
|
|
621
|
+
replacePageCaches,
|
|
622
|
+
refreshIframeByPostMessage: (id) => emitter.emit(TabEventType.REFRESH_IFRAME_POSTMESSAGE, id)
|
|
623
|
+
})
|
|
705
624
|
|
|
706
625
|
const getComponent = (id: string) => components.get(id)
|
|
707
626
|
|
|
@@ -711,44 +630,71 @@ export default () => {
|
|
|
711
630
|
* @param route - 是否执行 router.push
|
|
712
631
|
*/
|
|
713
632
|
const active = (id: string, route = true) => {
|
|
714
|
-
emitter.emit(
|
|
633
|
+
emitter.emit(TabEventType.FORWARD)
|
|
715
634
|
const target = getTab(id)
|
|
716
|
-
if (!target) return
|
|
717
|
-
if (target.active) return
|
|
635
|
+
if (!target) return Promise.resolve()
|
|
636
|
+
if (target.active) return Promise.resolve()
|
|
718
637
|
|
|
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 })
|
|
638
|
+
const top = target.pages.peek()
|
|
639
|
+
const activateTarget = () => {
|
|
640
|
+
for (const tab of tabs.value) {
|
|
641
|
+
tab.active = tab.id === id
|
|
739
642
|
}
|
|
643
|
+
saveActiveTabToSession(target as ITabItem)
|
|
644
|
+
tabs.value = tabs.value.slice()
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (!route) {
|
|
648
|
+
activateTarget()
|
|
649
|
+
return Promise.resolve()
|
|
740
650
|
}
|
|
651
|
+
if (!top) {
|
|
652
|
+
activateTarget()
|
|
653
|
+
return Promise.resolve()
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const __tab = encodeTabInfo({
|
|
657
|
+
id: target.id,
|
|
658
|
+
title: target.title,
|
|
659
|
+
closable: target.closable,
|
|
660
|
+
refreshable: target.refreshable,
|
|
661
|
+
iframe: target.iframe
|
|
662
|
+
})
|
|
663
|
+
const query = defu({ __tab }, top.query || {})
|
|
664
|
+
|
|
665
|
+
return runNavigationTransaction({
|
|
666
|
+
apply: () => {
|
|
667
|
+
const prevActiveId = tabs.value.find((tab) => tab.active)?.id
|
|
668
|
+
const sessionSnapshot = window.sessionStorage.getItem(getSessionKey())
|
|
669
|
+
activateTarget()
|
|
670
|
+
return { prevActiveId, sessionSnapshot }
|
|
671
|
+
},
|
|
672
|
+
navigate: () => router.push({ path: top.path, query }),
|
|
673
|
+
rollback: (snapshot) => {
|
|
674
|
+
for (const tab of tabs.value) {
|
|
675
|
+
tab.active = tab.id === snapshot.prevActiveId
|
|
676
|
+
}
|
|
677
|
+
tabs.value = tabs.value.slice()
|
|
678
|
+
restoreActiveTabSession(snapshot.sessionSnapshot)
|
|
679
|
+
},
|
|
680
|
+
isFailureResult: isNavigationFailure,
|
|
681
|
+
rejectFailureResult: true
|
|
682
|
+
})
|
|
741
683
|
}
|
|
742
684
|
|
|
743
685
|
const reset = () => removeAllTabs()
|
|
744
686
|
|
|
745
687
|
const destroy = () => {
|
|
746
|
-
tabs.value
|
|
688
|
+
tabs.value = []
|
|
689
|
+
defaultTabs.value = []
|
|
747
690
|
cacheIdsToEvict.clear()
|
|
748
691
|
tabIdsToEvict.clear()
|
|
749
692
|
iframeRefreshKeys.value = {}
|
|
750
|
-
caches.value
|
|
693
|
+
caches.value = []
|
|
751
694
|
components.clear()
|
|
695
|
+
runtimeContext.scrollPositionsByPageId.clear()
|
|
696
|
+
runtimeContext.isInitialized.value = false
|
|
697
|
+
runtimeContext.refreshKey.value = 0
|
|
752
698
|
clearSession()
|
|
753
699
|
}
|
|
754
700
|
|
|
@@ -757,13 +703,16 @@ export default () => {
|
|
|
757
703
|
caches,
|
|
758
704
|
refreshKey,
|
|
759
705
|
activeCacheKey,
|
|
706
|
+
activePageRefreshVersion,
|
|
760
707
|
isInitialized,
|
|
761
|
-
setMaxSize:
|
|
708
|
+
setMaxSize: (value: number) => {
|
|
709
|
+
runtimeContext.maxTabCount.value = value
|
|
710
|
+
},
|
|
762
711
|
canAddTab,
|
|
763
712
|
initialize,
|
|
764
713
|
size,
|
|
765
714
|
active,
|
|
766
|
-
getCacheName,
|
|
715
|
+
getCacheName: createCacheName,
|
|
767
716
|
destroy,
|
|
768
717
|
reset,
|
|
769
718
|
addCache,
|
|
@@ -788,8 +737,12 @@ export default () => {
|
|
|
788
737
|
refreshAllTabs,
|
|
789
738
|
iframeRefreshKeys,
|
|
790
739
|
addPageScroller,
|
|
791
|
-
setGlobalScroll:
|
|
740
|
+
setGlobalScroll: (value: boolean) => {
|
|
741
|
+
runtimeContext.useGlobalScroll.value = value
|
|
742
|
+
},
|
|
792
743
|
clearSession,
|
|
793
|
-
setSessionPrefix
|
|
744
|
+
setSessionPrefix: (value: string) => {
|
|
745
|
+
runtimeContext.sessionPrefix.value = value
|
|
746
|
+
}
|
|
794
747
|
}
|
|
795
748
|
}
|