vue-stack-tabs 1.3.1 → 1.4.2
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 +119 -52
- package/README.md +103 -42
- package/README.ru.md +125 -54
- package/dist/{src/lib/StackTabs.vue.d.ts → StackTabs.vue.d.ts} +5 -5
- package/dist/{src/lib/components → components}/ContextMenu/index.vue.d.ts +1 -1
- package/dist/components/StackKeepAlive/emptyPlaceholder.d.ts +3 -0
- package/dist/{src/lib/components → components}/TabHeader/TabHeaderItem.vue.d.ts +9 -0
- package/dist/{src/lib/components → components}/TabHeader/index.vue.d.ts +3 -3
- package/dist/{src/lib/hooks → hooks}/stackTabsContext.d.ts +5 -2
- package/dist/hooks/tabPanel/pageComponentFactory.d.ts +23 -0
- package/dist/{src/lib/hooks → hooks}/tabPanel/refresh.d.ts +6 -3
- package/dist/{src/lib/hooks → hooks}/tabPanel/scroll.d.ts +2 -0
- package/dist/hooks/tabPanel/tabStore.d.ts +69 -0
- package/dist/hooks/useScrollShadow.d.ts +20 -0
- package/dist/{src/lib/hooks → hooks}/useTabActions.d.ts +7 -2
- package/dist/{src/lib/hooks → hooks}/useTabEventBus.d.ts +2 -1
- package/dist/{src/lib/hooks → hooks}/useTabPanel.d.ts +16 -4
- package/dist/iframe-bridge.mjs +1 -1
- package/dist/index.d.ts +22 -6
- package/dist/{src/lib/model → model}/TabModel.d.ts +21 -0
- package/dist/nuxt/module.mjs +10 -6
- package/dist/nuxt/runtime/plugin.mjs +1600 -4
- package/dist/style.css +1 -1
- package/dist/utils/iframeBridge.d.ts +7 -1
- package/dist/{src/lib/utils → utils}/scrollUtils.d.ts +7 -1
- package/dist/{src/lib/versionLogger.d.ts → versionLogger.d.ts} +1 -1
- package/dist/vue-stack-tabs.css +1 -1
- package/dist/vue-stack-tabs.es.js +1 -1
- package/package.json +64 -40
- package/src/lib/StackTabs.vue +16 -3
- package/src/lib/assets/style/common/var.scss +2 -0
- package/src/lib/assets/style/stackTab.scss +17 -3
- package/src/lib/components/ContextMenu/index.vue +12 -3
- package/src/lib/components/PageLoading.vue +10 -2
- package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +3 -9
- package/src/lib/components/StackKeepAlive/emptyPlaceholder.tsx +18 -0
- package/src/lib/components/TabHeader/index.vue +4 -3
- package/src/lib/hooks/stackTabsContext.ts +23 -18
- package/src/lib/hooks/tabPanel/pageComponentFactory.tsx +136 -0
- package/src/lib/hooks/tabPanel/refresh.ts +20 -13
- package/src/lib/hooks/tabPanel/scroll.ts +110 -7
- package/src/lib/hooks/tabPanel/session.ts +29 -3
- package/src/lib/hooks/tabPanel/tabStore.ts +595 -0
- package/src/lib/hooks/useContextMenu.ts +57 -11
- package/src/lib/hooks/useIframeManager.ts +18 -2
- package/src/lib/hooks/useScrollShadow.ts +124 -0
- package/src/lib/hooks/useTabActions.ts +18 -8
- package/src/lib/hooks/useTabEventBus.ts +5 -3
- package/src/lib/hooks/useTabPanel.tsx +183 -302
- package/src/lib/hooks/useTabRouter.ts +42 -38
- package/src/lib/i18n/index.ts +3 -3
- package/src/lib/model/TabModel.ts +40 -1
- package/src/lib/nuxt/module.ts +10 -3
- package/src/lib/nuxt/runtime/plugin.ts +13 -2
- package/src/lib/utils/iframeBridge.ts +9 -1
- package/src/lib/utils/scrollUtils.ts +35 -2
- package/src/lib/utils/tabInfoEncoder.ts +21 -8
- package/src/lib/utils/urlParser.ts +3 -1
- package/src/lib/versionLogger.ts +6 -2
- package/dist/favicon.svg +0 -10
- package/dist/iframe-bridge.html +0 -77
- package/dist/iframe-child.html +0 -88
- package/dist/nuxt/favicon.svg +0 -10
- package/dist/nuxt/iframe-bridge.html +0 -77
- package/dist/nuxt/iframe-child.html +0 -88
- package/dist/nuxt/plugin.mjs +0 -9
- package/dist/src/lib/iframe-bridge.d.ts +0 -8
- package/dist/src/lib/index.d.ts +0 -22
- package/dist/src/lib/nuxt/module.d.ts +0 -6
- package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +0 -2
- package/dist/src/lib/nuxt/runtime/plugin.d.ts +0 -2
- package/dist/src/lib/utils/iframeBridge.d.ts +0 -39
- package/src/lib/nuxt/runtime/plugin.client.ts +0 -10
- /package/dist/{src/lib/components → components}/ContextMenu/ContextMenuItem.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/PageLoading.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/StackKeepAlive/StackCacheRenderer.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/StackKeepAlive/StackKeepAlive.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/TabHeader/TabHeaderButton.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/TabHeader/TabHeaderScroll.vue.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/evict.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/navigationTransaction.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/session.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/state.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useContextMenu.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useIframeManager.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useTabLoading.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useTabRouter.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/index.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/lang/en.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/lang/zh-CN.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/stackTabsLocale.d.ts +0 -0
- /package/dist/{src/lib/iframe.vue.d.ts → iframe.vue.d.ts} +0 -0
- /package/dist/{src/lib/utils → utils}/stackTabsA11y.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/stackTabsMessage.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/tabInfoEncoder.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/typeGuards.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/urlParser.d.ts +0 -0
|
@@ -12,17 +12,7 @@
|
|
|
12
12
|
* 使用范围:内部 hook,由 StackTabs、useTabActions、useTabRouter、ContextMenu 调用
|
|
13
13
|
*/
|
|
14
14
|
import type { ITabBase, ITabData, ITabItem, ITabPage } from '../model/TabModel'
|
|
15
|
-
import {
|
|
16
|
-
defineComponent,
|
|
17
|
-
onActivated,
|
|
18
|
-
onDeactivated,
|
|
19
|
-
onMounted,
|
|
20
|
-
onUnmounted,
|
|
21
|
-
computed,
|
|
22
|
-
ref,
|
|
23
|
-
shallowRef,
|
|
24
|
-
cloneVNode
|
|
25
|
-
} from 'vue'
|
|
15
|
+
import { computed, ref } from 'vue'
|
|
26
16
|
import type { DefineComponent, VNode } from 'vue'
|
|
27
17
|
import { isNavigationFailure, useRouter } from 'vue-router'
|
|
28
18
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
|
@@ -35,28 +25,19 @@ import {
|
|
|
35
25
|
omitStackTabsReservedQuery,
|
|
36
26
|
toSafeTabUrl,
|
|
37
27
|
decodeSafeTabUrl,
|
|
38
|
-
cloneLocationQuery
|
|
39
|
-
clonePage
|
|
28
|
+
cloneLocationQuery
|
|
40
29
|
} from '../utils/urlParser'
|
|
41
|
-
import PageLoading from '../components/PageLoading.vue'
|
|
42
30
|
import { TabEventType, useTabEmitter } from './useTabEventBus'
|
|
43
31
|
import { runNavigationTransaction } from './tabPanel/navigationTransaction'
|
|
44
32
|
import { useI18n } from 'vue-i18n-lite'
|
|
45
33
|
|
|
46
|
-
import { resolveStackTabsRuntimeContext } from './stackTabsContext'
|
|
47
|
-
import {
|
|
34
|
+
import { resolveStackTabsRuntimeContext, type StackTabsRuntimeContext } from './stackTabsContext'
|
|
35
|
+
import { getTabStore } from './tabPanel/tabStore'
|
|
48
36
|
import { createTabPanelScroll } from './tabPanel/scroll'
|
|
49
37
|
import { createTabPanelSession } from './tabPanel/session'
|
|
50
38
|
import { createTabPanelRefresh } from './tabPanel/refresh'
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
/** 占位组件:当路由组件为空或标签已被标记删除时返回 */
|
|
54
|
-
const EmptyPlaceholderComponent = defineComponent({
|
|
55
|
-
name: 'StackTabEmptyPlaceholder',
|
|
56
|
-
setup() {
|
|
57
|
-
return () => null
|
|
58
|
-
}
|
|
59
|
-
})
|
|
39
|
+
import { createPageComponentFactory } from './tabPanel/pageComponentFactory'
|
|
40
|
+
import { getEmptyPlaceholderComponent } from '../components/StackKeepAlive/emptyPlaceholder'
|
|
60
41
|
|
|
61
42
|
const cloneRouteQuery = (route: RouteLocationNormalizedLoaded, tabInfo: ITabBase) => ({
|
|
62
43
|
...cloneLocationQuery(route.query),
|
|
@@ -71,32 +52,55 @@ export const normalizePathForCache = (route: RouteLocationNormalizedLoaded): str
|
|
|
71
52
|
return matchPath === path ? path : route.path
|
|
72
53
|
}
|
|
73
54
|
|
|
74
|
-
export default () => {
|
|
55
|
+
export default (providedRuntimeContext?: StackTabsRuntimeContext) => {
|
|
75
56
|
const router = useRouter()
|
|
76
|
-
const
|
|
57
|
+
const runtimeContext = providedRuntimeContext ?? resolveStackTabsRuntimeContext()
|
|
58
|
+
const emitter = useTabEmitter(runtimeContext)
|
|
77
59
|
const { t } = useI18n()
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
tabs,
|
|
81
|
-
defaultTabs,
|
|
82
|
-
caches,
|
|
83
|
-
components,
|
|
84
|
-
tabIdsToEvict,
|
|
85
|
-
refreshKey,
|
|
86
|
-
iframeRefreshKeys,
|
|
87
|
-
isInitialized,
|
|
88
|
-
cacheIdsToEvict
|
|
89
|
-
} = runtimeContext
|
|
60
|
+
const { tabs, defaultTabs, caches, refreshKey, iframeRefreshKeys, isInitialized } = runtimeContext
|
|
61
|
+
const store = getTabStore(runtimeContext)
|
|
90
62
|
const {
|
|
63
|
+
getTabs,
|
|
64
|
+
getTab,
|
|
65
|
+
getActiveTab,
|
|
66
|
+
findTabByTopPagePath,
|
|
67
|
+
hasTab,
|
|
68
|
+
canAddTab,
|
|
69
|
+
getPages,
|
|
70
|
+
getTopPage,
|
|
71
|
+
getPageSnapshot,
|
|
72
|
+
addTab,
|
|
73
|
+
addTabs,
|
|
74
|
+
activateTab,
|
|
75
|
+
moveTab,
|
|
76
|
+
removeTab: removeTabFromStore,
|
|
77
|
+
removeTabs: removeTabsFromStore,
|
|
78
|
+
appendPage,
|
|
79
|
+
popPages,
|
|
80
|
+
replacePages,
|
|
81
|
+
clearPages,
|
|
82
|
+
updatePage,
|
|
83
|
+
consumeBackParams,
|
|
91
84
|
markCacheForEviction,
|
|
92
|
-
markTabPagesForEviction,
|
|
93
85
|
markTabPagesForEvictionOnly,
|
|
94
86
|
addCache,
|
|
95
87
|
removeCache,
|
|
96
88
|
evictMarkedCaches,
|
|
97
89
|
evictPageCache,
|
|
98
|
-
replacePageCaches
|
|
99
|
-
|
|
90
|
+
replacePageCaches,
|
|
91
|
+
isTabMarkedForEviction,
|
|
92
|
+
isCacheMarkedForEviction,
|
|
93
|
+
unmarkCacheForEviction,
|
|
94
|
+
clearTabEvictionMarks,
|
|
95
|
+
getComponent,
|
|
96
|
+
setComponent,
|
|
97
|
+
deleteComponent,
|
|
98
|
+
snapshotComponents,
|
|
99
|
+
restoreComponents,
|
|
100
|
+
snapshotCaches,
|
|
101
|
+
restoreCaches,
|
|
102
|
+
reset: resetStore
|
|
103
|
+
} = store
|
|
100
104
|
const { restoreScroller, saveScroller, removeScroller, addPageScroller } =
|
|
101
105
|
createTabPanelScroll(runtimeContext)
|
|
102
106
|
const {
|
|
@@ -107,7 +111,7 @@ export default () => {
|
|
|
107
111
|
getSessionKey
|
|
108
112
|
} = createTabPanelSession(runtimeContext)
|
|
109
113
|
|
|
110
|
-
const size = (): number =>
|
|
114
|
+
const size = (): number => getTabs().length
|
|
111
115
|
|
|
112
116
|
/**
|
|
113
117
|
* 初始化:根据 defaultTabs 创建标签与初始页面,并尝试从 sessionStorage 恢复上次激活的标签
|
|
@@ -118,14 +122,15 @@ export default () => {
|
|
|
118
122
|
const newTabs: ITabItem[] = []
|
|
119
123
|
|
|
120
124
|
for (const item of staticTabs) {
|
|
125
|
+
// 先用默认值填充非空缺字段,避免在循环里提前求值生成用不到的 UUID。
|
|
121
126
|
const fullItem = defu(item, {
|
|
122
|
-
id: crypto.randomUUID(),
|
|
123
127
|
refreshable: true,
|
|
124
128
|
closable: true,
|
|
125
129
|
iframe: false
|
|
126
130
|
})
|
|
127
|
-
|
|
128
|
-
fullItem.id =
|
|
131
|
+
// id 单独兜底,避免每次循环都无意义地调用 crypto.randomUUID。
|
|
132
|
+
fullItem.id = item.id ?? crypto.randomUUID()
|
|
133
|
+
const tabId = fullItem.id
|
|
129
134
|
const uri = parseUrl(fullItem.path)
|
|
130
135
|
const cacheName = createPageId()
|
|
131
136
|
const page: ITabPage = {
|
|
@@ -156,63 +161,60 @@ export default () => {
|
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
defaultTabs.value = [...defaultTabs.value, ...newDefaults]
|
|
159
|
-
|
|
164
|
+
addTabs(newTabs)
|
|
160
165
|
|
|
161
|
-
|
|
166
|
+
// SSR 环境无 sessionStorage,直接跳过恢复。
|
|
167
|
+
const storedTabJson =
|
|
168
|
+
typeof window !== 'undefined' ? sessionStorage.getItem(getSessionKey()) : null
|
|
162
169
|
const restoredTab = restoreTabFromSession(storedTabJson)
|
|
163
|
-
if (restoredTab && !
|
|
170
|
+
if (restoredTab && !hasTab(restoredTab.id)) {
|
|
164
171
|
if (restoredTab.url) restoredTab.url = toSafeTabUrl(restoredTab.url)
|
|
165
172
|
if (restoredTab.iframe && restoredTab.url && restoredTab.iframeRefreshMode === undefined) {
|
|
166
173
|
restoredTab.iframeRefreshMode = isCrossOriginUrl(restoredTab.url) ? 'reload' : 'postMessage'
|
|
167
174
|
}
|
|
168
175
|
|
|
169
176
|
if (restoredTab.pages && typeof restoredTab.pages.list === 'function') {
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
addTab(restoredTab)
|
|
178
|
+
for (const page of getPages(restoredTab.id)) {
|
|
172
179
|
if (page && page.id) addCache(page.id)
|
|
173
|
-
}
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
addTab(restoredTab)
|
|
174
183
|
}
|
|
175
|
-
|
|
176
|
-
tabs.value = [...tabs.value, restoredTab]
|
|
177
184
|
}
|
|
178
185
|
isInitialized.value = true
|
|
179
186
|
}
|
|
180
187
|
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
const { resolvePageComponent } = createPageComponentFactory({
|
|
189
|
+
runtimeContext,
|
|
190
|
+
consumeBackParams,
|
|
191
|
+
getComponent,
|
|
192
|
+
setComponent,
|
|
193
|
+
addCache,
|
|
194
|
+
evictMarkedCaches,
|
|
195
|
+
clearTabEvictionMarks,
|
|
196
|
+
addPageScroller,
|
|
197
|
+
saveScroller,
|
|
198
|
+
restoreScroller,
|
|
199
|
+
removeScroller
|
|
200
|
+
})
|
|
189
201
|
|
|
190
|
-
|
|
202
|
+
/** 把已有标签的对外字段提取为 ITabBase(用于让路由归属回该标签) */
|
|
203
|
+
const toTabBase = (tab: ITabItem): ITabBase => ({
|
|
204
|
+
id: tab.id,
|
|
205
|
+
title: tab.title,
|
|
206
|
+
closable: tab.closable,
|
|
207
|
+
refreshable: tab.refreshable,
|
|
208
|
+
iframe: tab.iframe,
|
|
209
|
+
iframeRefreshMode: tab.iframeRefreshMode
|
|
210
|
+
})
|
|
191
211
|
|
|
192
212
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
213
|
+
* 从路由解析 tabInfo,若无则创建默认信息,调用方负责把编码信息写入内部 page.query。
|
|
214
|
+
*
|
|
215
|
+
* 首次进入应用时 URL 上通常还没有 __tab:此时优先接管 path 匹配的默认标签(defaultTabs),
|
|
216
|
+
* 由调用方把 __tab 写回地址,保证「URL 指向默认标签」;确实找不到归属时才新建兜底标签。
|
|
195
217
|
*/
|
|
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 */
|
|
216
218
|
const parseTabInfoFromRoute = (route: RouteLocationNormalizedLoaded): ITabBase => {
|
|
217
219
|
if (typeof route.query.__tab === 'string') {
|
|
218
220
|
const tabInfo = decodeTabInfo(route.query.__tab)
|
|
@@ -220,19 +222,18 @@ export default () => {
|
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
const path = normalizePathForCache(route)
|
|
223
|
-
const activeTab =
|
|
224
|
-
const activePage = activeTab
|
|
225
|
+
const activeTab = getActiveTab()
|
|
226
|
+
const activePage = activeTab ? getTopPage(activeTab.id) : null
|
|
225
227
|
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
|
-
}
|
|
228
|
+
return toTabBase(activeTab)
|
|
234
229
|
}
|
|
235
230
|
|
|
231
|
+
// 当前激活标签与路由不一致:首屏 URL 无 __tab,或 session 恢复了上次激活的其它标签
|
|
232
|
+
//(演示应用在 iframe 中重载后 hash 为空即属此类)。此时接管栈顶 path 与当前路由一致的
|
|
233
|
+
// 标签(默认标签或已有标签),避免同一个页面既留下已有标签、又生成一个「未命名」的兜底标签。
|
|
234
|
+
const pathMatchedTab = findTabByTopPagePath(path)
|
|
235
|
+
if (pathMatchedTab) return toTabBase(pathMatchedTab)
|
|
236
|
+
|
|
236
237
|
return {
|
|
237
238
|
id: crypto.randomUUID(),
|
|
238
239
|
title: t('VueStackTab.undefined'),
|
|
@@ -251,11 +252,7 @@ export default () => {
|
|
|
251
252
|
cacheName: string,
|
|
252
253
|
page: ITabPage
|
|
253
254
|
): ITabItem => {
|
|
254
|
-
let targetTab
|
|
255
|
-
for (const tab of tabs.value) {
|
|
256
|
-
tab.active = false
|
|
257
|
-
if (tab.id === tabInfo.id) targetTab = tab as ITabItem
|
|
258
|
-
}
|
|
255
|
+
let targetTab = tabInfo.id ? getTab(tabInfo.id) : null
|
|
259
256
|
|
|
260
257
|
if (targetTab === null) {
|
|
261
258
|
const pages = new Stack<ITabPage>()
|
|
@@ -272,12 +269,14 @@ export default () => {
|
|
|
272
269
|
active: true,
|
|
273
270
|
pages
|
|
274
271
|
} as unknown as ITabItem
|
|
272
|
+
activateTab('')
|
|
275
273
|
addTab(targetTab)
|
|
276
274
|
emitter.emit(TabEventType.TAB_ACTIVE, { id: tabInfo.id!, isRoute: false })
|
|
277
275
|
} else {
|
|
278
|
-
targetTab.
|
|
279
|
-
|
|
280
|
-
|
|
276
|
+
activateTab(targetTab.id)
|
|
277
|
+
const topPage = getTopPage(targetTab.id)
|
|
278
|
+
if (!topPage || topPage.id !== page.id) {
|
|
279
|
+
appendPage(targetTab.id, page)
|
|
281
280
|
}
|
|
282
281
|
}
|
|
283
282
|
return targetTab
|
|
@@ -297,7 +296,7 @@ export default () => {
|
|
|
297
296
|
| { empty: true }
|
|
298
297
|
| { recovered: DefineComponent; cacheName: string } => {
|
|
299
298
|
const tabInfo = parseTabInfoFromRoute(route)
|
|
300
|
-
if (
|
|
299
|
+
if (isTabMarkedForEviction(tabInfo.id!)) return { empty: true }
|
|
301
300
|
|
|
302
301
|
const path = normalizePathForCache(route)
|
|
303
302
|
|
|
@@ -305,14 +304,14 @@ export default () => {
|
|
|
305
304
|
let cacheName: string
|
|
306
305
|
let page: ITabPage | null = null
|
|
307
306
|
|
|
308
|
-
if (targetTab &&
|
|
309
|
-
const top = targetTab.
|
|
307
|
+
if (targetTab && getTopPage(targetTab.id)) {
|
|
308
|
+
const top = getTopPage(targetTab.id)!
|
|
310
309
|
if (!top) throw new Error('[vue-stack-tabs] Stack is not empty but peek() returned undefined')
|
|
311
310
|
|
|
312
311
|
if (top.path === path) {
|
|
313
312
|
cacheName = top.id
|
|
314
313
|
} else {
|
|
315
|
-
const pagesList = targetTab.
|
|
314
|
+
const pagesList = getPages(targetTab.id)
|
|
316
315
|
let foundIndex = -1
|
|
317
316
|
for (let i = pagesList.length - 2; i >= 0; i--) {
|
|
318
317
|
const pastPage = pagesList[i]
|
|
@@ -329,11 +328,11 @@ export default () => {
|
|
|
329
328
|
)
|
|
330
329
|
}
|
|
331
330
|
const stepsToPop = pagesList.length - 1 - foundIndex
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const newTop = targetTab.
|
|
331
|
+
const poppedPages = popPages(targetTab.id, stepsToPop)
|
|
332
|
+
// 仅标记待驱逐,由 onActivated 或下一拍 evictMarkedCaches 统一清理,
|
|
333
|
+
// 避免 watcher 同步执行与守卫拒绝路由之间产生不一致。
|
|
334
|
+
for (const popped of poppedPages) markCacheForEviction(popped.id)
|
|
335
|
+
const newTop = getTopPage(targetTab.id)
|
|
337
336
|
cacheName = newTop ? newTop.id : createPageId()
|
|
338
337
|
|
|
339
338
|
if (!newTop) {
|
|
@@ -364,128 +363,38 @@ export default () => {
|
|
|
364
363
|
}
|
|
365
364
|
}
|
|
366
365
|
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
const cached =
|
|
366
|
+
if (isCacheMarkedForEviction(cacheName)) {
|
|
367
|
+
unmarkCacheForEviction(cacheName)
|
|
368
|
+
const cached = getComponent(cacheName)
|
|
370
369
|
if (cached) return { recovered: cached, cacheName }
|
|
371
370
|
}
|
|
372
371
|
|
|
373
372
|
if (page) {
|
|
374
373
|
targetTab = findOrCreateTargetTab(tabInfo, route, cacheName, page)
|
|
375
374
|
} else {
|
|
376
|
-
|
|
377
|
-
targetTab!.active = true
|
|
375
|
+
activateTab(targetTab!.id)
|
|
378
376
|
}
|
|
379
377
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
cacheName: string
|
|
389
|
-
tabInfo: ITabBase
|
|
390
|
-
targetTab: ITabItem
|
|
391
|
-
}): DefineComponent => {
|
|
392
|
-
const { cacheName, tabInfo } = ctx
|
|
393
|
-
|
|
394
|
-
if (components.has(cacheName)) {
|
|
395
|
-
addCache(cacheName)
|
|
396
|
-
return components.get(cacheName)!
|
|
378
|
+
// 首屏 URL 往往没有 __tab(用户直接打开默认路由):把解析出的标签编码写回地址,
|
|
379
|
+
// 保证 URL 始终指向当前标签;之后的导航各自会带上自己的 __tab。
|
|
380
|
+
if (typeof route.query.__tab !== 'string') {
|
|
381
|
+
void router.replace({
|
|
382
|
+
path: route.path,
|
|
383
|
+
query: { ...cloneLocationQuery(route.query), __tab: encodeTabInfo(tabInfo) },
|
|
384
|
+
hash: route.hash
|
|
385
|
+
})
|
|
397
386
|
}
|
|
398
387
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
props: {
|
|
402
|
-
vnode: {
|
|
403
|
-
type: Object as () => VNode,
|
|
404
|
-
required: false,
|
|
405
|
-
default: undefined
|
|
406
|
-
}
|
|
407
|
-
},
|
|
408
|
-
emits: ['onLoaded'],
|
|
409
|
-
setup(props, context) {
|
|
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)
|
|
414
|
-
|
|
415
|
-
const checkAndConsumeBackParams = () => {
|
|
416
|
-
try {
|
|
417
|
-
const tab = getTab(tabInfo.id!)
|
|
418
|
-
const top = tab?.pages.peek()
|
|
419
|
-
if (top && top.id === cacheName && top._backParams) {
|
|
420
|
-
localBackParams.value = { ...top._backParams }
|
|
421
|
-
delete top._backParams
|
|
422
|
-
} else {
|
|
423
|
-
localBackParams.value = null
|
|
424
|
-
}
|
|
425
|
-
} catch {
|
|
426
|
-
localBackParams.value = null
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
onMounted(() => {
|
|
431
|
-
context.emit('onLoaded')
|
|
432
|
-
addPageScroller(cacheName, `#W-${cacheName}`)
|
|
433
|
-
checkAndConsumeBackParams()
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
onDeactivated(() => saveScroller(cacheName))
|
|
437
|
-
|
|
438
|
-
onActivated(() => {
|
|
439
|
-
context.emit('onLoaded')
|
|
440
|
-
restoreScroller(cacheName)
|
|
441
|
-
evictMarkedCaches()
|
|
442
|
-
tabIdsToEvict.clear()
|
|
443
|
-
checkAndConsumeBackParams()
|
|
444
|
-
})
|
|
445
|
-
|
|
446
|
-
onUnmounted(() => removeScroller(cacheName))
|
|
447
|
-
|
|
448
|
-
return () => {
|
|
449
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
450
|
-
const Node = (props as any).vnode as VNode
|
|
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
|
-
}
|
|
461
|
-
|
|
462
|
-
return (
|
|
463
|
-
<div
|
|
464
|
-
class="cache-page-wrapper"
|
|
465
|
-
id={`W-${cacheName}`}
|
|
466
|
-
style={[
|
|
467
|
-
runtimeContext.useGlobalScroll.value ? 'overflow:auto' : 'overflow:hidden',
|
|
468
|
-
'height: 100%'
|
|
469
|
-
]}
|
|
470
|
-
>
|
|
471
|
-
{lastCloned.value}
|
|
472
|
-
<PageLoading tabId={tabInfo.id!} />
|
|
473
|
-
</div>
|
|
474
|
-
)
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}) as DefineComponent
|
|
478
|
-
components.set(cacheName, cacheComponent)
|
|
479
|
-
addCache(cacheName)
|
|
480
|
-
return cacheComponent
|
|
388
|
+
saveActiveTabToSession(targetTab!)
|
|
389
|
+
return { cacheName, tabInfo, targetTab: targetTab! }
|
|
481
390
|
}
|
|
482
391
|
|
|
483
392
|
/** 当前正在渲染的页面的缓存 Key(ULID),供 StackTabs.vue 的 :key 绑定使用 */
|
|
484
393
|
const activeCacheKey = ref<string>('')
|
|
485
394
|
/** 只随当前活动页面刷新变化,避免一个 Tab 刷新导致其他 Tab 的缓存 key 失效。 */
|
|
486
395
|
const activePageRefreshVersion = computed(() => {
|
|
487
|
-
const activeTab =
|
|
488
|
-
return activeTab
|
|
396
|
+
const activeTab = getActiveTab()
|
|
397
|
+
return activeTab ? (getTopPage(activeTab.id)?.refreshVersion ?? 0) : 0
|
|
489
398
|
})
|
|
490
399
|
|
|
491
400
|
let lastRouteKey = ''
|
|
@@ -495,7 +404,7 @@ export default () => {
|
|
|
495
404
|
route: RouteLocationNormalizedLoaded,
|
|
496
405
|
component?: VNode | null
|
|
497
406
|
): DefineComponent => {
|
|
498
|
-
if (!component) return
|
|
407
|
+
if (!component) return getEmptyPlaceholderComponent()
|
|
499
408
|
|
|
500
409
|
const routeKey = route.fullPath + (route.query.__tab || '')
|
|
501
410
|
if (routeKey === lastRouteKey && lastAddPageResult) {
|
|
@@ -503,7 +412,7 @@ export default () => {
|
|
|
503
412
|
}
|
|
504
413
|
|
|
505
414
|
const state = updatePageState(route)
|
|
506
|
-
if ('empty' in state) return
|
|
415
|
+
if ('empty' in state) return getEmptyPlaceholderComponent()
|
|
507
416
|
if ('recovered' in state) {
|
|
508
417
|
addCache(state.cacheName)
|
|
509
418
|
activeCacheKey.value = state.cacheName
|
|
@@ -522,122 +431,93 @@ export default () => {
|
|
|
522
431
|
const currentTab = getTab(tab.id!)
|
|
523
432
|
if (!currentTab) return undefined
|
|
524
433
|
|
|
525
|
-
const pageSnapshot = currentTab.
|
|
526
|
-
const cacheSnapshot =
|
|
527
|
-
const componentSnapshot =
|
|
434
|
+
const pageSnapshot = getPageSnapshot(currentTab.id)
|
|
435
|
+
const cacheSnapshot = snapshotCaches()
|
|
436
|
+
const componentSnapshot = snapshotComponents()
|
|
528
437
|
|
|
529
438
|
markTabPagesForEvictionOnly(currentTab)
|
|
530
439
|
evictMarkedCaches()
|
|
531
|
-
currentTab.
|
|
440
|
+
clearPages(currentTab.id)
|
|
532
441
|
|
|
533
442
|
return () => {
|
|
534
|
-
currentTab.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
components.clear()
|
|
538
|
-
for (const [id, component] of componentSnapshot) components.set(id, component)
|
|
443
|
+
replacePages(currentTab.id, pageSnapshot)
|
|
444
|
+
restoreCaches(cacheSnapshot)
|
|
445
|
+
restoreComponents(componentSnapshot)
|
|
539
446
|
}
|
|
540
447
|
}
|
|
541
448
|
|
|
542
449
|
const removeTab = (id: string): string => {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const i = tabList.findIndex((t) => t?.id === id)
|
|
547
|
-
if (i < 0) return ''
|
|
548
|
-
|
|
549
|
-
const tab = tabList[i]!
|
|
550
|
-
if (!tab.closable) return id
|
|
551
|
-
|
|
552
|
-
markTabPagesForEviction(tab)
|
|
553
|
-
tab.pages.clear()
|
|
554
|
-
if (tabList.length > 1 && tab.active) {
|
|
555
|
-
activeTabId = (i === 0 ? tabList[1] : tabList[i - 1])?.id ?? ''
|
|
556
|
-
}
|
|
557
|
-
tabList.splice(i, 1)
|
|
450
|
+
const result = removeTabFromStore(id)
|
|
451
|
+
if (result.status === 'not-found') return ''
|
|
452
|
+
if (result.status === 'not-closable') return id
|
|
558
453
|
|
|
559
|
-
if (
|
|
560
|
-
emitter.emit(TabEventType.TAB_ACTIVE, { id:
|
|
561
|
-
}
|
|
562
|
-
if (!currentTab?.active) {
|
|
563
|
-
evictMarkedCaches()
|
|
564
|
-
tabIdsToEvict.clear()
|
|
454
|
+
if (result.nextActiveId) {
|
|
455
|
+
emitter.emit(TabEventType.TAB_ACTIVE, { id: result.nextActiveId })
|
|
565
456
|
}
|
|
566
457
|
|
|
567
|
-
return
|
|
458
|
+
return result.nextActiveId
|
|
568
459
|
}
|
|
569
460
|
|
|
570
461
|
/** 批量关闭满足条件的可关闭标签,并统一标记其页面缓存待驱逐。 */
|
|
571
|
-
const removeTabs = (shouldRemove: (tab: ITabItem, index: number) => boolean) =>
|
|
572
|
-
|
|
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)
|
|
579
|
-
}
|
|
580
|
-
}
|
|
462
|
+
const removeTabs = (shouldRemove: (tab: ITabItem, index: number) => boolean) =>
|
|
463
|
+
removeTabsFromStore(shouldRemove)
|
|
581
464
|
|
|
582
465
|
const removeAllTabs = () => {
|
|
583
|
-
const tabList = tabs.value
|
|
584
466
|
removeTabs(() => true)
|
|
585
|
-
|
|
586
|
-
if (hasActive) {
|
|
467
|
+
if (getActiveTab()) {
|
|
587
468
|
evictMarkedCaches()
|
|
588
|
-
|
|
469
|
+
clearTabEvictionMarks()
|
|
589
470
|
return
|
|
590
471
|
}
|
|
591
|
-
const last =
|
|
472
|
+
const last = getTabs().at(-1)
|
|
592
473
|
if (last) emitter.emit(TabEventType.TAB_ACTIVE, { id: last.id })
|
|
593
474
|
}
|
|
594
475
|
|
|
595
476
|
const removeOtherTabs = (id: string) => {
|
|
596
|
-
const
|
|
597
|
-
const activeTab = tabList.find((tab) => tab.id === id)
|
|
477
|
+
const activeTab = getTab(id)
|
|
598
478
|
removeTabs((tab) => tab.id !== id)
|
|
599
479
|
if (activeTab && !activeTab.active) {
|
|
600
480
|
emitter.emit(TabEventType.TAB_ACTIVE, { id })
|
|
601
481
|
} else {
|
|
602
482
|
evictMarkedCaches()
|
|
603
|
-
|
|
483
|
+
clearTabEvictionMarks()
|
|
604
484
|
}
|
|
605
485
|
}
|
|
606
486
|
|
|
607
487
|
const removeLeftTabs = (id: string) => {
|
|
608
|
-
const
|
|
609
|
-
const pivot = tabList.findIndex((t) => t?.id === id)
|
|
488
|
+
const pivot = getTabs().findIndex((t) => t?.id === id)
|
|
610
489
|
if (pivot <= 0) return
|
|
611
490
|
|
|
612
491
|
removeTabs((_, index) => index < pivot)
|
|
613
|
-
if (
|
|
492
|
+
if (getActiveTab()) {
|
|
614
493
|
evictMarkedCaches()
|
|
615
|
-
|
|
494
|
+
clearTabEvictionMarks()
|
|
616
495
|
return
|
|
617
496
|
}
|
|
618
|
-
const first =
|
|
497
|
+
const first = getTabs()[0]
|
|
619
498
|
if (first) emitter.emit(TabEventType.TAB_ACTIVE, { id: first.id })
|
|
620
499
|
}
|
|
621
500
|
|
|
622
501
|
const removeRightTabs = (id: string) => {
|
|
623
|
-
const tabList =
|
|
502
|
+
const tabList = getTabs()
|
|
624
503
|
const pivot = tabList.findIndex((t) => t?.id === id)
|
|
625
504
|
if (pivot < 0 || pivot >= tabList.length - 1) return
|
|
626
505
|
|
|
627
506
|
removeTabs((_, index) => index > pivot)
|
|
628
|
-
if (
|
|
507
|
+
if (getActiveTab()) {
|
|
629
508
|
evictMarkedCaches()
|
|
630
|
-
|
|
509
|
+
clearTabEvictionMarks()
|
|
631
510
|
return
|
|
632
511
|
}
|
|
633
|
-
const last =
|
|
512
|
+
const last = getTabs().at(-1)
|
|
634
513
|
if (last) emitter.emit(TabEventType.TAB_ACTIVE, { id: last.id })
|
|
635
514
|
}
|
|
636
515
|
|
|
637
516
|
const { refreshTab, refreshAllTabs } = createTabPanelRefresh({
|
|
638
|
-
|
|
517
|
+
getTabs,
|
|
518
|
+
getTopPage,
|
|
519
|
+
updatePage,
|
|
639
520
|
iframeRefreshKeys,
|
|
640
|
-
getTab,
|
|
641
521
|
createPageId,
|
|
642
522
|
addCache,
|
|
643
523
|
evictPageCache,
|
|
@@ -645,8 +525,6 @@ export default () => {
|
|
|
645
525
|
refreshIframeByPostMessage: (id) => emitter.emit(TabEventType.REFRESH_IFRAME_POSTMESSAGE, id)
|
|
646
526
|
})
|
|
647
527
|
|
|
648
|
-
const getComponent = (id: string) => components.get(id)
|
|
649
|
-
|
|
650
528
|
/**
|
|
651
529
|
* 激活指定标签,更新 active 状态并可选跳转路由
|
|
652
530
|
* @param id - 标签 id
|
|
@@ -658,13 +536,10 @@ export default () => {
|
|
|
658
536
|
if (!target) return Promise.resolve()
|
|
659
537
|
if (target.active) return Promise.resolve()
|
|
660
538
|
|
|
661
|
-
const top =
|
|
539
|
+
const top = getTopPage(id)
|
|
662
540
|
const activateTarget = () => {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
saveActiveTabToSession(target as ITabItem)
|
|
667
|
-
tabs.value = tabs.value.slice()
|
|
541
|
+
const activeTab = activateTab(id)
|
|
542
|
+
if (activeTab) saveActiveTabToSession(activeTab)
|
|
668
543
|
}
|
|
669
544
|
|
|
670
545
|
if (!route) {
|
|
@@ -683,21 +558,21 @@ export default () => {
|
|
|
683
558
|
refreshable: target.refreshable,
|
|
684
559
|
iframe: target.iframe
|
|
685
560
|
})
|
|
686
|
-
|
|
561
|
+
// __tab 是兜底标识,目标栈顶已带 __tab 时优先沿用,否则用我们刚编码的版本。
|
|
562
|
+
const query = defu(top.query || {}, { __tab })
|
|
687
563
|
|
|
688
564
|
return runNavigationTransaction({
|
|
689
565
|
apply: () => {
|
|
690
|
-
const prevActiveId =
|
|
691
|
-
|
|
566
|
+
const prevActiveId = getActiveTab()?.id
|
|
567
|
+
// SSR 环境无 sessionStorage;快照为 null 时 rollback 视为「无变化」。
|
|
568
|
+
const sessionSnapshot =
|
|
569
|
+
typeof window !== 'undefined' ? window.sessionStorage.getItem(getSessionKey()) : null
|
|
692
570
|
activateTarget()
|
|
693
571
|
return { prevActiveId, sessionSnapshot }
|
|
694
572
|
},
|
|
695
573
|
navigate: () => router.push({ path: top.path, query }),
|
|
696
574
|
rollback: (snapshot) => {
|
|
697
|
-
|
|
698
|
-
tab.active = tab.id === snapshot.prevActiveId
|
|
699
|
-
}
|
|
700
|
-
tabs.value = tabs.value.slice()
|
|
575
|
+
activateTab(snapshot.prevActiveId ?? '')
|
|
701
576
|
restoreActiveTabSession(snapshot.sessionSnapshot)
|
|
702
577
|
},
|
|
703
578
|
isFailureResult: isNavigationFailure,
|
|
@@ -708,13 +583,9 @@ export default () => {
|
|
|
708
583
|
const reset = () => removeAllTabs()
|
|
709
584
|
|
|
710
585
|
const destroy = () => {
|
|
711
|
-
|
|
586
|
+
resetStore()
|
|
712
587
|
defaultTabs.value = []
|
|
713
|
-
cacheIdsToEvict.clear()
|
|
714
|
-
tabIdsToEvict.clear()
|
|
715
588
|
iframeRefreshKeys.value = {}
|
|
716
|
-
caches.value = []
|
|
717
|
-
components.clear()
|
|
718
589
|
runtimeContext.scrollPositionsByPageId.clear()
|
|
719
590
|
runtimeContext.isInitialized.value = false
|
|
720
591
|
runtimeContext.refreshKey.value = 0
|
|
@@ -741,8 +612,17 @@ export default () => {
|
|
|
741
612
|
addCache,
|
|
742
613
|
removeCache,
|
|
743
614
|
getTab,
|
|
615
|
+
getActiveTab,
|
|
616
|
+
getTopPage,
|
|
617
|
+
getPages,
|
|
618
|
+
getPageSnapshot,
|
|
744
619
|
moveTab,
|
|
745
620
|
addTab,
|
|
621
|
+
appendPage,
|
|
622
|
+
popPages,
|
|
623
|
+
replacePages,
|
|
624
|
+
clearPages,
|
|
625
|
+
updatePage,
|
|
746
626
|
removeTab,
|
|
747
627
|
removeAllTabs,
|
|
748
628
|
removeLeftTabs,
|
|
@@ -751,12 +631,13 @@ export default () => {
|
|
|
751
631
|
hasTab,
|
|
752
632
|
renewTab,
|
|
753
633
|
addPage,
|
|
754
|
-
addComponent:
|
|
634
|
+
addComponent: setComponent,
|
|
755
635
|
getComponent,
|
|
756
|
-
removeComponent:
|
|
636
|
+
removeComponent: deleteComponent,
|
|
757
637
|
markCacheForEviction,
|
|
758
638
|
evictMarkedCaches,
|
|
759
639
|
evictPageCache,
|
|
640
|
+
clearTabEvictionMarks,
|
|
760
641
|
refreshTab,
|
|
761
642
|
refreshAllTabs,
|
|
762
643
|
iframeRefreshKeys,
|