vue-stack-tabs 1.4.1 → 2.2.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 +1 -0
- package/README.md +1 -0
- package/README.ru.md +1 -0
- package/dist/StackTabs.vue.d.ts +3 -0
- package/dist/components/ContextMenu/ContextMenuItem.vue.d.ts +1 -1
- package/dist/hooks/tabPanel/pageComponentFactory.d.ts +5 -2
- package/dist/hooks/tabPanel/refresh.d.ts +8 -3
- package/dist/hooks/tabPanel/tabStore.d.ts +80 -0
- package/dist/hooks/useTabActions.d.ts +1 -0
- package/dist/hooks/useTabPanel.d.ts +27 -5
- package/dist/model/TabModel.d.ts +5 -0
- package/dist/style.css +1 -1
- package/dist/utils/tabInfoEncoder.d.ts +10 -0
- package/dist/utils/urlParser.d.ts +14 -3
- package/dist/vue-stack-tabs.es.js +1 -1
- package/package.json +4 -7
- package/src/lib/StackTabs.vue +24 -2
- package/src/lib/assets/image/close_all.svg +1 -9
- package/src/lib/assets/image/close_lefts.svg +1 -1
- package/src/lib/assets/image/close_other.svg +1 -1
- package/src/lib/assets/image/close_rights.svg +1 -1
- package/src/lib/assets/image/reload.svg +1 -3
- package/src/lib/assets/image/reload_all.svg +1 -6
- package/src/lib/assets/style/common/mixins.scss +58 -19
- package/src/lib/assets/style/common/var.scss +100 -60
- package/src/lib/assets/style/icon.scss +31 -98
- package/src/lib/assets/style/index.scss +2 -2
- package/src/lib/assets/style/loading.scss +45 -0
- package/src/lib/assets/style/{stackTab.scss → stack-tab.scss} +105 -177
- package/src/lib/assets/style/transition.scss +12 -17
- package/src/lib/components/ContextMenu/ContextMenuItem.vue +5 -5
- package/src/lib/components/ContextMenu/index.vue +22 -19
- package/src/lib/components/PageLoading.vue +5 -5
- package/src/lib/components/StackKeepAlive/StackKeepAlive.vue +5 -3
- package/src/lib/components/TabHeader/TabHeaderItem.vue +13 -7
- package/src/lib/components/TabHeader/TabHeaderScroll.vue +8 -6
- package/src/lib/components/TabHeader/index.vue +16 -2
- package/src/lib/hooks/tabPanel/pageComponentFactory.tsx +96 -26
- package/src/lib/hooks/tabPanel/refresh.ts +40 -18
- package/src/lib/hooks/tabPanel/tabStore.ts +665 -0
- package/src/lib/hooks/useContextMenu.ts +1 -1
- package/src/lib/hooks/useIframeManager.ts +10 -3
- package/src/lib/hooks/useTabActions.ts +10 -6
- package/src/lib/hooks/useTabPanel.tsx +325 -195
- package/src/lib/hooks/useTabRouter.ts +50 -48
- package/src/lib/model/TabModel.ts +5 -0
- package/src/lib/utils/tabInfoEncoder.ts +101 -17
- package/src/lib/utils/urlParser.ts +35 -11
- package/dist/vue-stack-tabs.css +0 -2
- package/src/lib/assets/style/overlayer.scss +0 -46
|
@@ -16,7 +16,7 @@ import { computed, ref } from 'vue'
|
|
|
16
16
|
import type { DefineComponent, VNode } from 'vue'
|
|
17
17
|
import { isNavigationFailure, useRouter } from 'vue-router'
|
|
18
18
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
|
19
|
-
import { encodeTabInfo, createPageId, decodeTabInfo } from '../utils/tabInfoEncoder'
|
|
19
|
+
import { encodeTabInfo, createId, createPageId, decodeTabInfo } from '../utils/tabInfoEncoder'
|
|
20
20
|
import { defu } from 'defu'
|
|
21
21
|
import { Stack } from '../model/TabModel'
|
|
22
22
|
import {
|
|
@@ -26,14 +26,15 @@ import {
|
|
|
26
26
|
toSafeTabUrl,
|
|
27
27
|
decodeSafeTabUrl,
|
|
28
28
|
cloneLocationQuery,
|
|
29
|
-
|
|
29
|
+
isSameQueryIgnoringReserved,
|
|
30
|
+
normalizePathname
|
|
30
31
|
} from '../utils/urlParser'
|
|
31
32
|
import { TabEventType, useTabEmitter } from './useTabEventBus'
|
|
32
33
|
import { runNavigationTransaction } from './tabPanel/navigationTransaction'
|
|
33
34
|
import { useI18n } from 'vue-i18n-lite'
|
|
34
35
|
|
|
35
36
|
import { resolveStackTabsRuntimeContext, type StackTabsRuntimeContext } from './stackTabsContext'
|
|
36
|
-
import {
|
|
37
|
+
import { getTabStore } from './tabPanel/tabStore'
|
|
37
38
|
import { createTabPanelScroll } from './tabPanel/scroll'
|
|
38
39
|
import { createTabPanelSession } from './tabPanel/session'
|
|
39
40
|
import { createTabPanelRefresh } from './tabPanel/refresh'
|
|
@@ -45,6 +46,9 @@ const cloneRouteQuery = (route: RouteLocationNormalizedLoaded, tabInfo: ITabBase
|
|
|
45
46
|
__tab: route.query.__tab ?? encodeTabInfo(tabInfo)
|
|
46
47
|
})
|
|
47
48
|
|
|
49
|
+
/** homeTab 未显式指定 id 时使用的固定 id,保证刷新前后 __tab 与标签身份一致。 */
|
|
50
|
+
const DEFAULT_HOME_TAB_ID = '__stack_tabs_home__'
|
|
51
|
+
|
|
48
52
|
/** 规范化路径(nginx 等可能加尾部斜杠) */
|
|
49
53
|
export const normalizePathForCache = (route: RouteLocationNormalizedLoaded): string => {
|
|
50
54
|
const lastMatch = route.matched[route.matched.length - 1]
|
|
@@ -53,32 +57,57 @@ export const normalizePathForCache = (route: RouteLocationNormalizedLoaded): str
|
|
|
53
57
|
return matchPath === path ? path : route.path
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
const createTabPanelApi = (runtimeContext: StackTabsRuntimeContext) => {
|
|
57
61
|
const router = useRouter()
|
|
58
|
-
const runtimeContext = providedRuntimeContext ?? resolveStackTabsRuntimeContext()
|
|
59
62
|
const emitter = useTabEmitter(runtimeContext)
|
|
60
63
|
const { t } = useI18n()
|
|
64
|
+
const { tabs, defaultTabs, caches, refreshKey, iframeRefreshKeys, isInitialized } = runtimeContext
|
|
65
|
+
const store = getTabStore(runtimeContext)
|
|
61
66
|
const {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
getTabs,
|
|
68
|
+
getTab,
|
|
69
|
+
getActiveTab,
|
|
70
|
+
findTabByTopPagePath,
|
|
71
|
+
hasTab,
|
|
72
|
+
canAddTab,
|
|
73
|
+
getPages,
|
|
74
|
+
getTopPage,
|
|
75
|
+
getPageRefreshVersion,
|
|
76
|
+
getPageSnapshot,
|
|
77
|
+
addTab,
|
|
78
|
+
addTabs,
|
|
79
|
+
activateTab,
|
|
80
|
+
updateTabTitle: updateTabTitleInStore,
|
|
81
|
+
moveTab,
|
|
82
|
+
removeTab: removeTabFromStore,
|
|
83
|
+
removeTabs: removeTabsFromStore,
|
|
84
|
+
appendPage,
|
|
85
|
+
popPages,
|
|
86
|
+
replacePages,
|
|
87
|
+
clearPages,
|
|
88
|
+
updatePage,
|
|
89
|
+
updatePages,
|
|
90
|
+
consumeBackParams,
|
|
73
91
|
markCacheForEviction,
|
|
74
|
-
markTabPagesForEviction,
|
|
75
92
|
markTabPagesForEvictionOnly,
|
|
76
93
|
addCache,
|
|
77
94
|
removeCache,
|
|
78
95
|
evictMarkedCaches,
|
|
79
96
|
evictPageCache,
|
|
80
|
-
replacePageCaches
|
|
81
|
-
|
|
97
|
+
replacePageCaches,
|
|
98
|
+
isTabMarkedForEviction,
|
|
99
|
+
isCacheMarkedForEviction,
|
|
100
|
+
unmarkCacheForEviction,
|
|
101
|
+
clearTabEvictionMarks,
|
|
102
|
+
getComponent,
|
|
103
|
+
setComponent,
|
|
104
|
+
deleteComponent,
|
|
105
|
+
snapshotComponents,
|
|
106
|
+
restoreComponents,
|
|
107
|
+
snapshotCaches,
|
|
108
|
+
restoreCaches,
|
|
109
|
+
reset: resetStore
|
|
110
|
+
} = store
|
|
82
111
|
const { restoreScroller, saveScroller, removeScroller, addPageScroller } =
|
|
83
112
|
createTabPanelScroll(runtimeContext)
|
|
84
113
|
const {
|
|
@@ -89,32 +118,46 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
89
118
|
getSessionKey
|
|
90
119
|
} = createTabPanelSession(runtimeContext)
|
|
91
120
|
|
|
92
|
-
const size = (): number =>
|
|
121
|
+
const size = (): number => getTabs().length
|
|
93
122
|
|
|
94
123
|
/**
|
|
95
|
-
*
|
|
124
|
+
* 初始化:先创建固定首页(homeTab),再创建 defaultTabs,并尝试从 sessionStorage 恢复上次激活的标签
|
|
96
125
|
* @param staticTabs - 初始标签配置
|
|
126
|
+
* @param homeTab - 固定首页;始终位于第一位、不可关闭、不可移动
|
|
97
127
|
*/
|
|
98
|
-
const initialize = (staticTabs: ITabData[]) => {
|
|
128
|
+
const initialize = (staticTabs: ITabData[], homeTab?: ITabData) => {
|
|
129
|
+
// 重复调用会累加 defaultTabs 并重复注册标签;初始化后直接忽略。
|
|
130
|
+
if (isInitialized.value) {
|
|
131
|
+
if (!import.meta.env.PROD) {
|
|
132
|
+
console.warn('[vue-stack-tabs] initialize() called more than once; ignored.')
|
|
133
|
+
}
|
|
134
|
+
return
|
|
135
|
+
}
|
|
99
136
|
const newDefaults: ITabItem[] = []
|
|
100
137
|
const newTabs: ITabItem[] = []
|
|
138
|
+
let homeTabId = ''
|
|
101
139
|
|
|
102
|
-
|
|
140
|
+
/** 由 ITabData 构建运行时标签;homeTab 通过 options 强制固定属性 */
|
|
141
|
+
const buildTab = (
|
|
142
|
+
item: ITabData,
|
|
143
|
+
options: { pinned?: boolean; closable?: boolean } = {}
|
|
144
|
+
): ITabItem => {
|
|
103
145
|
// 先用默认值填充非空缺字段,避免在循环里提前求值生成用不到的 UUID。
|
|
104
146
|
const fullItem = defu(item, {
|
|
105
147
|
refreshable: true,
|
|
106
148
|
closable: true,
|
|
107
149
|
iframe: false
|
|
108
150
|
})
|
|
109
|
-
// id
|
|
110
|
-
fullItem.id = item.id ??
|
|
151
|
+
// homeTab 未指定 id 时使用固定 id,避免每次初始化生成新 id 后与 session 恢复标签重复。
|
|
152
|
+
fullItem.id = item.id ?? (options.pinned ? DEFAULT_HOME_TAB_ID : createId())
|
|
153
|
+
if (options.closable !== undefined) fullItem.closable = options.closable
|
|
111
154
|
const tabId = fullItem.id
|
|
112
155
|
const uri = parseUrl(fullItem.path)
|
|
113
156
|
const cacheName = createPageId()
|
|
114
157
|
const page: ITabPage = {
|
|
115
158
|
id: cacheName,
|
|
116
159
|
tabId,
|
|
117
|
-
path: uri.path,
|
|
160
|
+
path: normalizePathname(uri.path),
|
|
118
161
|
query: defu(omitStackTabsReservedQuery(uri.query), { __tab: encodeTabInfo(fullItem) })
|
|
119
162
|
}
|
|
120
163
|
const pages = new Stack<ITabPage>()
|
|
@@ -130,98 +173,76 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
130
173
|
? 'reload'
|
|
131
174
|
: 'postMessage',
|
|
132
175
|
active: false,
|
|
133
|
-
pages
|
|
176
|
+
pages,
|
|
177
|
+
pinned: options.pinned
|
|
134
178
|
}
|
|
135
179
|
|
|
136
180
|
addCache(cacheName)
|
|
181
|
+
return tab
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (homeTab) {
|
|
185
|
+
const home = buildTab(homeTab, { pinned: true, closable: false })
|
|
186
|
+
homeTabId = home.id
|
|
187
|
+
newDefaults.push(home)
|
|
188
|
+
newTabs.push({ ...home })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const item of staticTabs) {
|
|
192
|
+
// home 已单独创建;相同 id 的配置跳过,避免出现两个固定首页。
|
|
193
|
+
if (homeTabId && item.id === homeTabId) {
|
|
194
|
+
if (!import.meta.env.PROD) {
|
|
195
|
+
console.warn(
|
|
196
|
+
'[vue-stack-tabs] defaultTabs contains the same id as homeTab; skipped:',
|
|
197
|
+
item.id
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
const tab = buildTab(item)
|
|
137
203
|
newDefaults.push(tab)
|
|
138
204
|
newTabs.push({ ...tab })
|
|
139
205
|
}
|
|
140
206
|
|
|
141
207
|
defaultTabs.value = [...defaultTabs.value, ...newDefaults]
|
|
142
|
-
|
|
208
|
+
addTabs(newTabs)
|
|
143
209
|
|
|
144
210
|
// SSR 环境无 sessionStorage,直接跳过恢复。
|
|
145
211
|
const storedTabJson =
|
|
146
212
|
typeof window !== 'undefined' ? sessionStorage.getItem(getSessionKey()) : null
|
|
147
213
|
const restoredTab = restoreTabFromSession(storedTabJson)
|
|
148
|
-
if (restoredTab && !
|
|
214
|
+
if (restoredTab && !hasTab(restoredTab.id)) {
|
|
149
215
|
if (restoredTab.url) restoredTab.url = toSafeTabUrl(restoredTab.url)
|
|
150
216
|
if (restoredTab.iframe && restoredTab.url && restoredTab.iframeRefreshMode === undefined) {
|
|
151
217
|
restoredTab.iframeRefreshMode = isCrossOriginUrl(restoredTab.url) ? 'reload' : 'postMessage'
|
|
152
218
|
}
|
|
153
219
|
|
|
154
220
|
if (restoredTab.pages && typeof restoredTab.pages.list === 'function') {
|
|
155
|
-
|
|
156
|
-
|
|
221
|
+
addTab(restoredTab)
|
|
222
|
+
for (const page of getPages(restoredTab.id)) {
|
|
157
223
|
if (page && page.id) addCache(page.id)
|
|
158
|
-
}
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
addTab(restoredTab)
|
|
159
227
|
}
|
|
160
|
-
|
|
161
|
-
tabs.value = [...tabs.value, restoredTab]
|
|
162
228
|
}
|
|
163
229
|
isInitialized.value = true
|
|
164
230
|
}
|
|
165
231
|
|
|
166
|
-
const hasTab = (id: string) => tabs.value.some((t) => t.id === id)
|
|
167
|
-
const canAddTab = () =>
|
|
168
|
-
runtimeContext.maxTabCount.value <= 0 ||
|
|
169
|
-
(runtimeContext.maxTabCount.value > 0 && runtimeContext.maxTabCount.value > tabs.value.length)
|
|
170
|
-
|
|
171
|
-
const addTab = (tab: ITabItem): void => {
|
|
172
|
-
tabs.value = [...tabs.value, tab]
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const getTab = (id: string) => tabs.value.find((t) => t.id === id) ?? null
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* 唯一的标签激活状态写入口。即使标签对象保持不变,也替换列表引用,
|
|
179
|
-
* 让 TabHeader 与 iframe 视图在同一个响应式提交中观察到一致状态。
|
|
180
|
-
*/
|
|
181
|
-
const setActiveTab = (id: string): ITabItem | null => {
|
|
182
|
-
let activeTab: ITabItem | null = null
|
|
183
|
-
tabs.value = tabs.value.map((tab) => {
|
|
184
|
-
tab.active = tab.id === id
|
|
185
|
-
if (tab.active) activeTab = tab
|
|
186
|
-
return tab
|
|
187
|
-
})
|
|
188
|
-
return activeTab
|
|
189
|
-
}
|
|
190
|
-
|
|
191
232
|
const { resolvePageComponent } = createPageComponentFactory({
|
|
192
233
|
runtimeContext,
|
|
193
|
-
|
|
234
|
+
consumeBackParams,
|
|
235
|
+
getComponent,
|
|
236
|
+
setComponent,
|
|
194
237
|
addCache,
|
|
195
238
|
evictMarkedCaches,
|
|
239
|
+
clearTabEvictionMarks,
|
|
196
240
|
addPageScroller,
|
|
197
241
|
saveScroller,
|
|
198
242
|
restoreScroller,
|
|
199
243
|
removeScroller
|
|
200
244
|
})
|
|
201
245
|
|
|
202
|
-
/**
|
|
203
|
-
* 将指定标签移动到目标索引。标签对象本身不变,因此其页面栈和缓存关联保持不受影响。
|
|
204
|
-
* @returns 是否实际调整了顺序
|
|
205
|
-
*/
|
|
206
|
-
const moveTab = (id: string, targetIndex: number): boolean => {
|
|
207
|
-
const sourceIndex = tabs.value.findIndex((tab) => tab.id === id)
|
|
208
|
-
if (
|
|
209
|
-
sourceIndex < 0 ||
|
|
210
|
-
targetIndex < 0 ||
|
|
211
|
-
targetIndex >= tabs.value.length ||
|
|
212
|
-
sourceIndex === targetIndex
|
|
213
|
-
) {
|
|
214
|
-
return false
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const reorderedTabs = tabs.value.slice()
|
|
218
|
-
const [tab] = reorderedTabs.splice(sourceIndex, 1)
|
|
219
|
-
if (!tab) return false
|
|
220
|
-
reorderedTabs.splice(targetIndex, 0, tab)
|
|
221
|
-
tabs.value = reorderedTabs
|
|
222
|
-
return true
|
|
223
|
-
}
|
|
224
|
-
|
|
225
246
|
/** 把已有标签的对外字段提取为 ITabBase(用于让路由归属回该标签) */
|
|
226
247
|
const toTabBase = (tab: ITabItem): ITabBase => ({
|
|
227
248
|
id: tab.id,
|
|
@@ -232,21 +253,101 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
232
253
|
iframeRefreshMode: tab.iframeRefreshMode
|
|
233
254
|
})
|
|
234
255
|
|
|
256
|
+
/**
|
|
257
|
+
* 动态更新标签标题。
|
|
258
|
+
*
|
|
259
|
+
* 同步 TabRecord、对外视图与 session;激活标签还会把新的 __tab 写回当前 URL,
|
|
260
|
+
* 避免刷新或分享链接后标题回退。
|
|
261
|
+
*/
|
|
262
|
+
const updateTabTitle = (tabId: string, title: string): boolean => {
|
|
263
|
+
if (!updateTabTitleInStore(tabId, title)) return false
|
|
264
|
+
const tab = getTab(tabId)
|
|
265
|
+
if (!tab) return false
|
|
266
|
+
|
|
267
|
+
if (tab.active) {
|
|
268
|
+
saveActiveTabToSession(tab)
|
|
269
|
+
const top = getTopPage(tabId)
|
|
270
|
+
if (top) {
|
|
271
|
+
void Promise.resolve(
|
|
272
|
+
router.replace({
|
|
273
|
+
path: top.path,
|
|
274
|
+
query: { ...cloneLocationQuery(top.query), __tab: encodeTabInfo(toTabBase(tab)) },
|
|
275
|
+
hash: router.currentRoute.value.hash
|
|
276
|
+
})
|
|
277
|
+
).catch((error: unknown) => {
|
|
278
|
+
if (!import.meta.env.PROD) {
|
|
279
|
+
console.warn('[vue-stack-tabs] Failed to sync tab title to URL:', error)
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return true
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** 当前固定首页(homeTab);未配置时返回 null。 */
|
|
288
|
+
const getHomeTab = (): ITabItem | null => getTabs().find((tab) => tab.pinned) ?? null
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 无 __tab 且当前路径没有归属标签时,导航到 homeTab 的栈顶页面。
|
|
292
|
+
* 使用 replace,避免在浏览器历史里留下中间状态。
|
|
293
|
+
*/
|
|
294
|
+
const redirectToHome = (): boolean => {
|
|
295
|
+
const home = getHomeTab()
|
|
296
|
+
const top = home ? getTopPage(home.id) : null
|
|
297
|
+
if (!home || !top) return false
|
|
298
|
+
|
|
299
|
+
const query = {
|
|
300
|
+
...cloneLocationQuery(top.query),
|
|
301
|
+
__tab: encodeTabInfo(toTabBase(home))
|
|
302
|
+
}
|
|
303
|
+
void Promise.resolve(
|
|
304
|
+
router.replace({
|
|
305
|
+
path: top.path,
|
|
306
|
+
query,
|
|
307
|
+
hash: ''
|
|
308
|
+
})
|
|
309
|
+
).catch((error: unknown) => {
|
|
310
|
+
if (!import.meta.env.PROD) {
|
|
311
|
+
console.warn('[vue-stack-tabs] Failed to redirect to home tab:', error)
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
return true
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 是否应回落到 homeTab:仅限 URL 没有 __tab,且当前路径没有被任何标签的栈顶承载。
|
|
319
|
+
* 带 __tab 的地址即使页面不存在也不回落,交给应用自己的 404 路由处理。
|
|
320
|
+
*/
|
|
321
|
+
const shouldRedirectToHome = (route: RouteLocationNormalizedLoaded): boolean => {
|
|
322
|
+
if (typeof route.query.__tab === 'string') return false
|
|
323
|
+
|
|
324
|
+
const path = normalizePathForCache(route)
|
|
325
|
+
const activeTab = getActiveTab()
|
|
326
|
+
if (activeTab && getTopPage(activeTab.id)?.path === path) return false
|
|
327
|
+
if (findTabByTopPagePath(path)) return false
|
|
328
|
+
|
|
329
|
+
const home = getHomeTab()
|
|
330
|
+
return Boolean(home && getTopPage(home.id))
|
|
331
|
+
}
|
|
332
|
+
|
|
235
333
|
/**
|
|
236
334
|
* 从路由解析 tabInfo,若无则创建默认信息,调用方负责把编码信息写入内部 page.query。
|
|
237
335
|
*
|
|
238
336
|
* 首次进入应用时 URL 上通常还没有 __tab:此时优先接管 path 匹配的默认标签(defaultTabs),
|
|
239
|
-
* 由调用方把 __tab 写回地址,保证「URL
|
|
337
|
+
* 由调用方把 __tab 写回地址,保证「URL 指向默认标签」;确实找不到归属且配置了 homeTab 时
|
|
338
|
+
* 返回 null,由调用方重定向到首页;未配置 homeTab 时保持原有兜底标签行为。
|
|
240
339
|
*/
|
|
241
|
-
const parseTabInfoFromRoute = (route: RouteLocationNormalizedLoaded): ITabBase => {
|
|
242
|
-
|
|
243
|
-
|
|
340
|
+
const parseTabInfoFromRoute = (route: RouteLocationNormalizedLoaded): ITabBase | null => {
|
|
341
|
+
const encodedTab = route.query.__tab
|
|
342
|
+
const hasEncodedTab = typeof encodedTab === 'string'
|
|
343
|
+
if (hasEncodedTab) {
|
|
344
|
+
const tabInfo = decodeTabInfo(encodedTab)
|
|
244
345
|
if (tabInfo.id) return tabInfo
|
|
245
346
|
}
|
|
246
347
|
|
|
247
348
|
const path = normalizePathForCache(route)
|
|
248
|
-
const activeTab =
|
|
249
|
-
const activePage = activeTab
|
|
349
|
+
const activeTab = getActiveTab()
|
|
350
|
+
const activePage = activeTab ? getTopPage(activeTab.id) : null
|
|
250
351
|
if (activeTab && activePage?.path === path) {
|
|
251
352
|
return toTabBase(activeTab)
|
|
252
353
|
}
|
|
@@ -254,11 +355,15 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
254
355
|
// 当前激活标签与路由不一致:首屏 URL 无 __tab,或 session 恢复了上次激活的其它标签
|
|
255
356
|
//(演示应用在 iframe 中重载后 hash 为空即属此类)。此时接管栈顶 path 与当前路由一致的
|
|
256
357
|
// 标签(默认标签或已有标签),避免同一个页面既留下已有标签、又生成一个「未命名」的兜底标签。
|
|
257
|
-
const pathMatchedTab =
|
|
358
|
+
const pathMatchedTab = findTabByTopPagePath(path)
|
|
258
359
|
if (pathMatchedTab) return toTabBase(pathMatchedTab)
|
|
259
360
|
|
|
361
|
+
// 无 __tab 且无任何标签承载当前路径:配置了 homeTab 时交给调用方重定向。
|
|
362
|
+
// 注意:__tab 存在但无效时仍保持原行为(新建兜底标签),不把错误地址变成首页。
|
|
363
|
+
if (!hasEncodedTab && getHomeTab()) return null
|
|
364
|
+
|
|
260
365
|
return {
|
|
261
|
-
id:
|
|
366
|
+
id: createId(),
|
|
262
367
|
title: t('VueStackTab.undefined'),
|
|
263
368
|
closable: true,
|
|
264
369
|
refreshable: true,
|
|
@@ -291,14 +396,15 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
291
396
|
url: decodeSafeTabUrl(src),
|
|
292
397
|
active: true,
|
|
293
398
|
pages
|
|
294
|
-
}
|
|
295
|
-
|
|
399
|
+
}
|
|
400
|
+
activateTab('')
|
|
296
401
|
addTab(targetTab)
|
|
297
402
|
emitter.emit(TabEventType.TAB_ACTIVE, { id: tabInfo.id!, isRoute: false })
|
|
298
403
|
} else {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
404
|
+
activateTab(targetTab.id)
|
|
405
|
+
const topPage = getTopPage(targetTab.id)
|
|
406
|
+
if (!topPage || topPage.id !== page.id) {
|
|
407
|
+
appendPage(targetTab.id, page)
|
|
302
408
|
}
|
|
303
409
|
}
|
|
304
410
|
return targetTab
|
|
@@ -317,27 +423,46 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
317
423
|
| { cacheName: string; tabInfo: ITabBase; targetTab: ITabItem }
|
|
318
424
|
| { empty: true }
|
|
319
425
|
| { recovered: DefineComponent; cacheName: string } => {
|
|
320
|
-
const
|
|
321
|
-
if (
|
|
426
|
+
const resolvedTabInfo = parseTabInfoFromRoute(route)
|
|
427
|
+
if (resolvedTabInfo === null) {
|
|
428
|
+
// 无 __tab 且没有标签承载当前路径:回落到 homeTab。
|
|
429
|
+
redirectToHome()
|
|
430
|
+
return { empty: true }
|
|
431
|
+
}
|
|
432
|
+
const tabInfo = resolvedTabInfo
|
|
433
|
+
if (isTabMarkedForEviction(tabInfo.id!)) return { empty: true }
|
|
322
434
|
|
|
323
435
|
const path = normalizePathForCache(route)
|
|
436
|
+
const routeQuery = cloneRouteQuery(route, tabInfo)
|
|
437
|
+
/** path 与业务 query 都一致才算同一个页面(忽略 __tab 等保留参数) */
|
|
438
|
+
const isSameLocation = (candidate: ITabPage): boolean =>
|
|
439
|
+
candidate.path === path && isSameQueryIgnoringReserved(routeQuery, candidate.query || {})
|
|
324
440
|
|
|
325
441
|
let targetTab: ITabItem | null = tabInfo.id ? (getTab(tabInfo.id) as ITabItem | null) : null
|
|
326
442
|
let cacheName: string
|
|
327
443
|
let page: ITabPage | null = null
|
|
328
444
|
|
|
329
|
-
if (targetTab &&
|
|
330
|
-
const top = targetTab.
|
|
445
|
+
if (targetTab && getTopPage(targetTab.id)) {
|
|
446
|
+
const top = getTopPage(targetTab.id)!
|
|
331
447
|
if (!top) throw new Error('[vue-stack-tabs] Stack is not empty but peek() returned undefined')
|
|
332
448
|
|
|
333
|
-
if (top
|
|
449
|
+
if (isSameLocation(top)) {
|
|
334
450
|
cacheName = top.id
|
|
451
|
+
} else if (top.path === path) {
|
|
452
|
+
// 同 path、不同 query:按产品语义作为新页面入栈,backward 才能按 query 精确回退。
|
|
453
|
+
cacheName = createPageId()
|
|
454
|
+
page = {
|
|
455
|
+
id: cacheName,
|
|
456
|
+
tabId: tabInfo.id!,
|
|
457
|
+
path,
|
|
458
|
+
query: cloneLocationQuery(routeQuery)
|
|
459
|
+
}
|
|
335
460
|
} else {
|
|
336
|
-
const pagesList = targetTab.
|
|
461
|
+
const pagesList = getPages(targetTab.id)
|
|
337
462
|
let foundIndex = -1
|
|
338
463
|
for (let i = pagesList.length - 2; i >= 0; i--) {
|
|
339
464
|
const pastPage = pagesList[i]
|
|
340
|
-
if (pastPage && pastPage
|
|
465
|
+
if (pastPage && isSameLocation(pastPage)) {
|
|
341
466
|
foundIndex = i
|
|
342
467
|
break
|
|
343
468
|
}
|
|
@@ -350,21 +475,19 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
350
475
|
)
|
|
351
476
|
}
|
|
352
477
|
const stepsToPop = pagesList.length - 1 - foundIndex
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
const newTop = targetTab.pages.peek()
|
|
478
|
+
const poppedPages = popPages(targetTab.id, stepsToPop)
|
|
479
|
+
// 仅标记待驱逐,由 onActivated 或下一拍 evictMarkedCaches 统一清理,
|
|
480
|
+
// 避免 watcher 同步执行与守卫拒绝路由之间产生不一致。
|
|
481
|
+
for (const popped of poppedPages) markCacheForEviction(popped.id)
|
|
482
|
+
const newTop = getTopPage(targetTab.id)
|
|
360
483
|
cacheName = newTop ? newTop.id : createPageId()
|
|
361
484
|
|
|
362
485
|
if (!newTop) {
|
|
363
486
|
page = {
|
|
364
487
|
id: cacheName,
|
|
365
488
|
tabId: tabInfo.id!,
|
|
366
|
-
path
|
|
367
|
-
query:
|
|
489
|
+
path,
|
|
490
|
+
query: cloneLocationQuery(routeQuery)
|
|
368
491
|
}
|
|
369
492
|
}
|
|
370
493
|
} else {
|
|
@@ -372,8 +495,8 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
372
495
|
page = {
|
|
373
496
|
id: cacheName,
|
|
374
497
|
tabId: tabInfo.id!,
|
|
375
|
-
path
|
|
376
|
-
query:
|
|
498
|
+
path,
|
|
499
|
+
query: cloneLocationQuery(routeQuery)
|
|
377
500
|
}
|
|
378
501
|
}
|
|
379
502
|
}
|
|
@@ -382,21 +505,21 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
382
505
|
page = {
|
|
383
506
|
id: cacheName,
|
|
384
507
|
tabId: tabInfo.id!,
|
|
385
|
-
path
|
|
386
|
-
query:
|
|
508
|
+
path,
|
|
509
|
+
query: cloneLocationQuery(routeQuery)
|
|
387
510
|
}
|
|
388
511
|
}
|
|
389
512
|
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
const cached =
|
|
513
|
+
if (isCacheMarkedForEviction(cacheName)) {
|
|
514
|
+
unmarkCacheForEviction(cacheName)
|
|
515
|
+
const cached = getComponent(cacheName)
|
|
393
516
|
if (cached) return { recovered: cached, cacheName }
|
|
394
517
|
}
|
|
395
518
|
|
|
396
519
|
if (page) {
|
|
397
520
|
targetTab = findOrCreateTargetTab(tabInfo, route, cacheName, page)
|
|
398
521
|
} else {
|
|
399
|
-
|
|
522
|
+
activateTab(targetTab!.id)
|
|
400
523
|
}
|
|
401
524
|
|
|
402
525
|
// 首屏 URL 往往没有 __tab(用户直接打开默认路由):把解析出的标签编码写回地址,
|
|
@@ -417,8 +540,8 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
417
540
|
const activeCacheKey = ref<string>('')
|
|
418
541
|
/** 只随当前活动页面刷新变化,避免一个 Tab 刷新导致其他 Tab 的缓存 key 失效。 */
|
|
419
542
|
const activePageRefreshVersion = computed(() => {
|
|
420
|
-
const activeTab =
|
|
421
|
-
return activeTab
|
|
543
|
+
const activeTab = getActiveTab()
|
|
544
|
+
return activeTab ? (getTopPage(activeTab.id)?.refreshVersion ?? 0) : 0
|
|
422
545
|
})
|
|
423
546
|
|
|
424
547
|
let lastRouteKey = ''
|
|
@@ -428,7 +551,12 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
428
551
|
route: RouteLocationNormalizedLoaded,
|
|
429
552
|
component?: VNode | null
|
|
430
553
|
): DefineComponent => {
|
|
431
|
-
if (!component)
|
|
554
|
+
if (!component) {
|
|
555
|
+
// 路由没有匹配到组件时不会进入 updatePageState;仍需处理无 __tab 的未知地址回落到首页。
|
|
556
|
+
// 带 __tab 的未知页面不做重定向,交给应用自己的 404 路由(或空占位)处理。
|
|
557
|
+
if (shouldRedirectToHome(route)) redirectToHome()
|
|
558
|
+
return getEmptyPlaceholderComponent()
|
|
559
|
+
}
|
|
432
560
|
|
|
433
561
|
const routeKey = route.fullPath + (route.query.__tab || '')
|
|
434
562
|
if (routeKey === lastRouteKey && lastAddPageResult) {
|
|
@@ -455,74 +583,45 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
455
583
|
const currentTab = getTab(tab.id!)
|
|
456
584
|
if (!currentTab) return undefined
|
|
457
585
|
|
|
458
|
-
const pageSnapshot = currentTab.
|
|
459
|
-
const cacheSnapshot =
|
|
460
|
-
const componentSnapshot =
|
|
586
|
+
const pageSnapshot = getPageSnapshot(currentTab.id)
|
|
587
|
+
const cacheSnapshot = snapshotCaches()
|
|
588
|
+
const componentSnapshot = snapshotComponents()
|
|
461
589
|
|
|
462
590
|
markTabPagesForEvictionOnly(currentTab)
|
|
463
591
|
evictMarkedCaches()
|
|
464
|
-
currentTab.
|
|
592
|
+
clearPages(currentTab.id)
|
|
465
593
|
|
|
466
594
|
return () => {
|
|
467
|
-
currentTab.
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
components.clear()
|
|
471
|
-
for (const [id, component] of componentSnapshot) components.set(id, component)
|
|
595
|
+
replacePages(currentTab.id, pageSnapshot)
|
|
596
|
+
restoreCaches(cacheSnapshot)
|
|
597
|
+
restoreComponents(componentSnapshot)
|
|
472
598
|
}
|
|
473
599
|
}
|
|
474
600
|
|
|
475
601
|
const removeTab = (id: string): string => {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const i = tabList.findIndex((t) => t?.id === id)
|
|
480
|
-
if (i < 0) return ''
|
|
481
|
-
|
|
482
|
-
const tab = tabList[i]!
|
|
483
|
-
if (!tab.closable) return id
|
|
484
|
-
|
|
485
|
-
markTabPagesForEviction(tab)
|
|
486
|
-
tab.pages.clear()
|
|
487
|
-
if (tabList.length > 1 && tab.active) {
|
|
488
|
-
activeTabId = (i === 0 ? tabList[1] : tabList[i - 1])?.id ?? ''
|
|
489
|
-
}
|
|
490
|
-
tabs.value = [...tabList.slice(0, i), ...tabList.slice(i + 1)]
|
|
602
|
+
const result = removeTabFromStore(id)
|
|
603
|
+
if (result.status === 'not-found') return ''
|
|
604
|
+
if (result.status === 'not-closable') return id
|
|
491
605
|
|
|
492
|
-
if (
|
|
493
|
-
emitter.emit(TabEventType.TAB_ACTIVE, { id:
|
|
494
|
-
}
|
|
495
|
-
if (!currentTab?.active) {
|
|
496
|
-
evictMarkedCaches()
|
|
497
|
-
tabIdsToEvict.clear()
|
|
606
|
+
if (result.nextActiveId) {
|
|
607
|
+
emitter.emit(TabEventType.TAB_ACTIVE, { id: result.nextActiveId })
|
|
498
608
|
}
|
|
499
609
|
|
|
500
|
-
return
|
|
610
|
+
return result.nextActiveId
|
|
501
611
|
}
|
|
502
612
|
|
|
503
613
|
/** 批量关闭满足条件的可关闭标签,并统一标记其页面缓存待驱逐。 */
|
|
504
|
-
const removeTabs = (shouldRemove: (tab: ITabItem, index: number) => boolean) =>
|
|
505
|
-
|
|
506
|
-
const remaining = tabs.value.filter((tab, index) => {
|
|
507
|
-
if (!tab.closable || !shouldRemove(tab, index)) return true
|
|
508
|
-
markTabPagesForEviction(tab)
|
|
509
|
-
tab.pages.clear()
|
|
510
|
-
removed.push(tab)
|
|
511
|
-
return false
|
|
512
|
-
})
|
|
513
|
-
tabs.value = remaining
|
|
514
|
-
return removed
|
|
515
|
-
}
|
|
614
|
+
const removeTabs = (shouldRemove: (tab: ITabItem, index: number) => boolean) =>
|
|
615
|
+
removeTabsFromStore(shouldRemove)
|
|
516
616
|
|
|
517
617
|
const removeAllTabs = () => {
|
|
518
618
|
removeTabs(() => true)
|
|
519
|
-
|
|
520
|
-
if (hasActive) {
|
|
619
|
+
if (getActiveTab()) {
|
|
521
620
|
evictMarkedCaches()
|
|
522
|
-
|
|
621
|
+
clearTabEvictionMarks()
|
|
523
622
|
return
|
|
524
623
|
}
|
|
525
|
-
const last =
|
|
624
|
+
const last = getTabs().at(-1)
|
|
526
625
|
if (last) emitter.emit(TabEventType.TAB_ACTIVE, { id: last.id })
|
|
527
626
|
}
|
|
528
627
|
|
|
@@ -533,42 +632,45 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
533
632
|
emitter.emit(TabEventType.TAB_ACTIVE, { id })
|
|
534
633
|
} else {
|
|
535
634
|
evictMarkedCaches()
|
|
536
|
-
|
|
635
|
+
clearTabEvictionMarks()
|
|
537
636
|
}
|
|
538
637
|
}
|
|
539
638
|
|
|
540
639
|
const removeLeftTabs = (id: string) => {
|
|
541
|
-
const pivot =
|
|
640
|
+
const pivot = getTabs().findIndex((t) => t?.id === id)
|
|
542
641
|
if (pivot <= 0) return
|
|
543
642
|
|
|
544
643
|
removeTabs((_, index) => index < pivot)
|
|
545
|
-
if (
|
|
644
|
+
if (getActiveTab()) {
|
|
546
645
|
evictMarkedCaches()
|
|
547
|
-
|
|
646
|
+
clearTabEvictionMarks()
|
|
548
647
|
return
|
|
549
648
|
}
|
|
550
|
-
const first =
|
|
649
|
+
const first = getTabs()[0]
|
|
551
650
|
if (first) emitter.emit(TabEventType.TAB_ACTIVE, { id: first.id })
|
|
552
651
|
}
|
|
553
652
|
|
|
554
653
|
const removeRightTabs = (id: string) => {
|
|
555
|
-
const
|
|
556
|
-
|
|
654
|
+
const tabList = getTabs()
|
|
655
|
+
const pivot = tabList.findIndex((t) => t?.id === id)
|
|
656
|
+
if (pivot < 0 || pivot >= tabList.length - 1) return
|
|
557
657
|
|
|
558
658
|
removeTabs((_, index) => index > pivot)
|
|
559
|
-
if (
|
|
659
|
+
if (getActiveTab()) {
|
|
560
660
|
evictMarkedCaches()
|
|
561
|
-
|
|
661
|
+
clearTabEvictionMarks()
|
|
562
662
|
return
|
|
563
663
|
}
|
|
564
|
-
const last =
|
|
664
|
+
const last = getTabs().at(-1)
|
|
565
665
|
if (last) emitter.emit(TabEventType.TAB_ACTIVE, { id: last.id })
|
|
566
666
|
}
|
|
567
667
|
|
|
568
668
|
const { refreshTab, refreshAllTabs } = createTabPanelRefresh({
|
|
569
|
-
|
|
669
|
+
getTabs,
|
|
670
|
+
getTopPage,
|
|
671
|
+
updatePage,
|
|
672
|
+
updatePages,
|
|
570
673
|
iframeRefreshKeys,
|
|
571
|
-
getTab,
|
|
572
674
|
createPageId,
|
|
573
675
|
addCache,
|
|
574
676
|
evictPageCache,
|
|
@@ -576,8 +678,6 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
576
678
|
refreshIframeByPostMessage: (id) => emitter.emit(TabEventType.REFRESH_IFRAME_POSTMESSAGE, id)
|
|
577
679
|
})
|
|
578
680
|
|
|
579
|
-
const getComponent = (id: string) => components.get(id)
|
|
580
|
-
|
|
581
681
|
/**
|
|
582
682
|
* 激活指定标签,更新 active 状态并可选跳转路由
|
|
583
683
|
* @param id - 标签 id
|
|
@@ -589,9 +689,9 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
589
689
|
if (!target) return Promise.resolve()
|
|
590
690
|
if (target.active) return Promise.resolve()
|
|
591
691
|
|
|
592
|
-
const top =
|
|
692
|
+
const top = getTopPage(id)
|
|
593
693
|
const activateTarget = () => {
|
|
594
|
-
const activeTab =
|
|
694
|
+
const activeTab = activateTab(id)
|
|
595
695
|
if (activeTab) saveActiveTabToSession(activeTab)
|
|
596
696
|
}
|
|
597
697
|
|
|
@@ -616,7 +716,7 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
616
716
|
|
|
617
717
|
return runNavigationTransaction({
|
|
618
718
|
apply: () => {
|
|
619
|
-
const prevActiveId =
|
|
719
|
+
const prevActiveId = getActiveTab()?.id
|
|
620
720
|
// SSR 环境无 sessionStorage;快照为 null 时 rollback 视为「无变化」。
|
|
621
721
|
const sessionSnapshot =
|
|
622
722
|
typeof window !== 'undefined' ? window.sessionStorage.getItem(getSessionKey()) : null
|
|
@@ -625,7 +725,7 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
625
725
|
},
|
|
626
726
|
navigate: () => router.push({ path: top.path, query }),
|
|
627
727
|
rollback: (snapshot) => {
|
|
628
|
-
|
|
728
|
+
activateTab(snapshot.prevActiveId ?? '')
|
|
629
729
|
restoreActiveTabSession(snapshot.sessionSnapshot)
|
|
630
730
|
},
|
|
631
731
|
isFailureResult: isNavigationFailure,
|
|
@@ -636,13 +736,9 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
636
736
|
const reset = () => removeAllTabs()
|
|
637
737
|
|
|
638
738
|
const destroy = () => {
|
|
639
|
-
|
|
739
|
+
resetStore()
|
|
640
740
|
defaultTabs.value = []
|
|
641
|
-
cacheIdsToEvict.clear()
|
|
642
|
-
tabIdsToEvict.clear()
|
|
643
741
|
iframeRefreshKeys.value = {}
|
|
644
|
-
caches.value = []
|
|
645
|
-
components.clear()
|
|
646
742
|
runtimeContext.scrollPositionsByPageId.clear()
|
|
647
743
|
runtimeContext.isInitialized.value = false
|
|
648
744
|
runtimeContext.refreshKey.value = 0
|
|
@@ -663,14 +759,26 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
663
759
|
initialize,
|
|
664
760
|
size,
|
|
665
761
|
active,
|
|
762
|
+
updateTabTitle,
|
|
666
763
|
getCacheName: createCacheName,
|
|
667
764
|
destroy,
|
|
668
765
|
reset,
|
|
669
766
|
addCache,
|
|
670
767
|
removeCache,
|
|
671
768
|
getTab,
|
|
769
|
+
getActiveTab,
|
|
770
|
+
getTopPage,
|
|
771
|
+
getPages,
|
|
772
|
+
getPageRefreshVersion,
|
|
773
|
+
getPageSnapshot,
|
|
672
774
|
moveTab,
|
|
673
775
|
addTab,
|
|
776
|
+
appendPage,
|
|
777
|
+
popPages,
|
|
778
|
+
replacePages,
|
|
779
|
+
clearPages,
|
|
780
|
+
updatePage,
|
|
781
|
+
updatePages,
|
|
674
782
|
removeTab,
|
|
675
783
|
removeAllTabs,
|
|
676
784
|
removeLeftTabs,
|
|
@@ -679,12 +787,13 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
679
787
|
hasTab,
|
|
680
788
|
renewTab,
|
|
681
789
|
addPage,
|
|
682
|
-
addComponent:
|
|
790
|
+
addComponent: setComponent,
|
|
683
791
|
getComponent,
|
|
684
|
-
removeComponent:
|
|
792
|
+
removeComponent: deleteComponent,
|
|
685
793
|
markCacheForEviction,
|
|
686
794
|
evictMarkedCaches,
|
|
687
795
|
evictPageCache,
|
|
796
|
+
clearTabEvictionMarks,
|
|
688
797
|
refreshTab,
|
|
689
798
|
refreshAllTabs,
|
|
690
799
|
iframeRefreshKeys,
|
|
@@ -698,3 +807,24 @@ export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
|
698
807
|
}
|
|
699
808
|
}
|
|
700
809
|
}
|
|
810
|
+
|
|
811
|
+
type TabPanelApi = ReturnType<typeof createTabPanelApi>
|
|
812
|
+
|
|
813
|
+
const panelApiByContext = new WeakMap<StackTabsRuntimeContext, TabPanelApi>()
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* 同一个运行时上下文复用同一套 panel API。
|
|
817
|
+
*
|
|
818
|
+
* StackTabs、StackKeepAlive、StackCacheRenderer、ContextMenu 与页面内的 useTabRouter
|
|
819
|
+
* 都会调用本 hook;首次调用在 setup 上下文中完成 useRouter/useI18n 注入,
|
|
820
|
+
* 后续调用直接返回缓存,避免重复创建 scroll/session/refresh 闭包与局部状态。
|
|
821
|
+
*/
|
|
822
|
+
export default (providedRuntimeContext?: StackTabsRuntimeContext): TabPanelApi => {
|
|
823
|
+
const runtimeContext = providedRuntimeContext ?? resolveStackTabsRuntimeContext()
|
|
824
|
+
const existing = panelApiByContext.get(runtimeContext)
|
|
825
|
+
if (existing) return existing
|
|
826
|
+
|
|
827
|
+
const panelApi = createTabPanelApi(runtimeContext)
|
|
828
|
+
panelApiByContext.set(runtimeContext, panelApi)
|
|
829
|
+
return panelApi
|
|
830
|
+
}
|