vue-stack-tabs 0.1.51 → 1.0.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.md +717 -3
- package/dist/index.d.ts +3 -0
- package/dist/src/lib/StackTabs.vue.d.ts +33 -161
- package/dist/src/lib/components/ContextMenu/ContextMenuItem.vue.d.ts +6 -6
- package/dist/src/lib/components/ContextMenu/index.vue.d.ts +14 -14
- package/dist/src/lib/components/PageLoading.vue.d.ts +5 -5
- package/dist/src/lib/components/TabHeader/TabHeaderButton.vue.d.ts +24 -15
- package/dist/src/lib/components/TabHeader/TabHeaderItem.vue.d.ts +10 -8
- package/dist/src/lib/components/TabHeader/TabHeaderScroll.vue.d.ts +31 -13
- package/dist/src/lib/components/TabHeader/index.vue.d.ts +172 -20
- package/dist/src/lib/hooks/tabPanel/evict.d.ts +24 -0
- package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +1 -0
- package/dist/src/lib/hooks/tabPanel/scroll.d.ts +8 -0
- package/dist/src/lib/hooks/tabPanel/session.d.ts +9 -0
- package/dist/src/lib/hooks/tabPanel/state.d.ts +77 -0
- package/dist/src/lib/hooks/useContextMenu.d.ts +6 -1
- package/dist/src/lib/hooks/useTabActions.d.ts +68 -0
- package/dist/src/lib/hooks/{useTabMitt.d.ts → useTabEventBus.d.ts} +4 -3
- package/dist/src/lib/hooks/useTabLoading.d.ts +6 -0
- package/dist/src/lib/hooks/{useTabpanel.d.ts → useTabPanel.d.ts} +20 -14
- package/dist/src/lib/hooks/useTabPanel.spec.d.ts +1 -0
- package/dist/src/lib/hooks/useTabRouter.d.ts +14 -5
- package/dist/src/lib/i18n/index.d.ts +1 -1
- package/dist/src/lib/i18n/lang/en.d.ts +1 -0
- package/dist/src/lib/i18n/lang/zh-CN.d.ts +1 -0
- package/dist/src/lib/index.d.ts +5 -4
- package/dist/src/lib/model/TabModel.d.ts +20 -3
- package/dist/src/lib/model/TabModel.spec.d.ts +1 -0
- package/dist/src/lib/utils/iframeBridge.d.ts +31 -0
- package/dist/src/lib/utils/iframeBridge.spec.d.ts +1 -0
- package/dist/src/lib/utils/scrollUtils.d.ts +28 -0
- package/dist/src/lib/utils/tabInfoEncoder.d.ts +19 -0
- package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +1 -0
- package/dist/src/lib/utils/urlParser.d.ts +19 -0
- package/dist/src/lib/utils/urlParser.spec.d.ts +1 -0
- package/dist/src/lib/versionLogger.d.ts +2 -0
- package/dist/style.css +1 -1
- package/dist/vue-stack-tabs.css +1 -0
- package/dist/vue-stack-tabs.es.js +1795 -1679
- package/dist/vue-stack-tabs.umd.js +1 -5
- package/package.json +76 -39
- package/src/lib/StackTabs.vue +222 -47
- package/src/lib/assets/image/open.svg +1 -0
- package/src/lib/assets/style/common/var.scss +1 -1
- package/src/lib/assets/style/icon.scss +6 -0
- package/src/lib/assets/style/stackTab.scss +44 -3
- package/src/lib/assets/style/transition.scss +63 -62
- package/src/lib/components/ContextMenu/ContextMenuItem.vue +7 -0
- package/src/lib/components/ContextMenu/index.vue +50 -38
- package/src/lib/components/PageLoading.vue +30 -10
- package/src/lib/components/TabHeader/TabHeaderButton.vue +12 -3
- package/src/lib/components/TabHeader/TabHeaderItem.vue +31 -27
- package/src/lib/components/TabHeader/TabHeaderScroll.vue +83 -77
- package/src/lib/components/TabHeader/index.vue +45 -40
- package/src/lib/hooks/tabPanel/evict.spec.ts +85 -0
- package/src/lib/hooks/tabPanel/evict.ts +57 -0
- package/src/lib/hooks/tabPanel/scroll.ts +52 -0
- package/src/lib/hooks/tabPanel/session.ts +33 -0
- package/src/lib/hooks/tabPanel/state.ts +52 -0
- package/src/lib/hooks/useContextMenu.ts +22 -20
- package/src/lib/hooks/useTabActions.ts +148 -0
- package/src/lib/hooks/useTabEventBus.ts +31 -0
- package/src/lib/hooks/useTabLoading.ts +12 -4
- package/src/lib/hooks/useTabPanel.spec.ts +34 -0
- package/src/lib/hooks/useTabPanel.tsx +546 -0
- package/src/lib/hooks/useTabRouter.ts +114 -103
- package/src/lib/i18n/index.ts +14 -18
- package/src/lib/i18n/lang/en.ts +2 -1
- package/src/lib/i18n/lang/zh-CN.ts +2 -1
- package/src/lib/iframe.vue +6 -0
- package/src/lib/index.ts +27 -8
- package/src/lib/model/TabModel.spec.ts +40 -0
- package/src/lib/model/TabModel.ts +22 -9
- package/src/lib/nuxt/module.d.ts +19 -0
- package/src/lib/nuxt/module.ts +34 -0
- package/src/lib/nuxt/runtime/plugin.client.ts +11 -0
- package/src/lib/utils/iframeBridge.spec.ts +37 -0
- package/src/lib/utils/iframeBridge.ts +52 -0
- package/src/lib/utils/{TabScrollHelper.ts → scrollUtils.ts} +23 -29
- package/src/lib/utils/tabInfoEncoder.spec.ts +42 -0
- package/src/lib/utils/tabInfoEncoder.ts +65 -0
- package/src/lib/utils/urlParser.spec.ts +45 -0
- package/src/lib/utils/urlParser.ts +42 -0
- package/src/lib/{banner.ts → versionLogger.ts} +4 -5
- package/dist/src/lib/banner.d.ts +0 -2
- package/dist/src/lib/hooks/useStackTab.d.ts +0 -11
- package/dist/src/lib/utils/TabIdHelper.d.ts +0 -11
- package/dist/src/lib/utils/TabScrollHelper.d.ts +0 -33
- package/dist/src/lib/utils/UriHelper.d.ts +0 -4
- package/src/lib/hooks/useStackTab.ts +0 -119
- package/src/lib/hooks/useTabMitt.ts +0 -30
- package/src/lib/hooks/useTabpanel.tsx +0 -708
- package/src/lib/utils/TabIdHelper.ts +0 -70
- package/src/lib/utils/UriHelper.ts +0 -22
package/src/lib/StackTabs.vue
CHANGED
|
@@ -1,16 +1,41 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
StackTabs - 堆叠式标签页主组件
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
- 标签页容器,集成 TabHeader、router-view、keep-alive、iframe 展示
|
|
6
|
+
- 包裹在 router-view 外层,将路由渲染到对应标签的缓存页面中
|
|
7
|
+
|
|
8
|
+
数据流:
|
|
9
|
+
- useTabPanel:tabs/caches/excludedCacheIdsForRefresh/refreshKey
|
|
10
|
+
- useTabActions:openTab/closeTab 等,需 setIFramePath
|
|
11
|
+
- keep-alive include=caches,exclude=excludedCacheIdsForRefresh(刷新时排除)
|
|
12
|
+
-->
|
|
1
13
|
<script lang="ts" setup>
|
|
2
|
-
import { onBeforeMount, onBeforeUnmount, provide, ref } from 'vue'
|
|
14
|
+
import { computed, onBeforeMount, onBeforeUnmount, provide, reactive, ref, watch } from 'vue'
|
|
3
15
|
import type { TransitionProps, DefineComponent, VNode } from 'vue'
|
|
4
16
|
import { type RouteLocationNormalizedLoaded } from 'vue-router'
|
|
5
|
-
import { getMaxZIndex } from './utils/
|
|
17
|
+
import { getMaxZIndex } from './utils/scrollUtils'
|
|
18
|
+
import { isInvalidIframeUrl } from './utils/urlParser'
|
|
6
19
|
import { type ITabData, TabScrollMode } from './model/TabModel'
|
|
7
20
|
import TabHeader from './components/TabHeader/index.vue'
|
|
8
|
-
import
|
|
9
|
-
import
|
|
21
|
+
import useTabPanel from './hooks/useTabPanel'
|
|
22
|
+
import useTabActions from './hooks/useTabActions'
|
|
10
23
|
import { useI18n } from 'vue-i18n-lite'
|
|
11
|
-
import {
|
|
12
|
-
const {
|
|
13
|
-
|
|
24
|
+
import { useTabEmitter } from '@/lib/hooks/useTabEventBus'
|
|
25
|
+
const {
|
|
26
|
+
tabs,
|
|
27
|
+
caches,
|
|
28
|
+
refreshKey,
|
|
29
|
+
excludedCacheIdsForRefresh,
|
|
30
|
+
iframeRefreshKeys,
|
|
31
|
+
destroy,
|
|
32
|
+
addPage,
|
|
33
|
+
initialize,
|
|
34
|
+
setMaxSize,
|
|
35
|
+
setGlobalScroll,
|
|
36
|
+
setSessionPrefix
|
|
37
|
+
} = useTabPanel()
|
|
38
|
+
/** keep-alive 的 include 列表;excludedCacheIdsForRefresh 为刷新时临时 exclude,触发重挂载 */
|
|
14
39
|
const emit = defineEmits(['onActive', 'onPageLoaded'])
|
|
15
40
|
const props = withDefaults(
|
|
16
41
|
defineProps<{
|
|
@@ -18,8 +43,6 @@ const props = withDefaults(
|
|
|
18
43
|
defaultTabs?: ITabData[]
|
|
19
44
|
// 最大打开数量
|
|
20
45
|
max?: number
|
|
21
|
-
// 新页加入哪个位置
|
|
22
|
-
append?: string
|
|
23
46
|
// 右键菜单
|
|
24
47
|
contextmenu?: boolean | object
|
|
25
48
|
// 页面转场效果
|
|
@@ -33,14 +56,15 @@ const props = withDefaults(
|
|
|
33
56
|
height?: string
|
|
34
57
|
i18n?: string
|
|
35
58
|
space?: number
|
|
36
|
-
globalScroll
|
|
59
|
+
globalScroll?: boolean
|
|
37
60
|
iframePath: string
|
|
38
61
|
sessionPrefix?: string
|
|
62
|
+
/** 允许通过 postMessage 调用 openTab 的 iframe 来源,默认仅同源 */
|
|
63
|
+
iframeAllowedOrigins?: string[]
|
|
39
64
|
}>(),
|
|
40
65
|
{
|
|
41
66
|
defaultTabs: () => [],
|
|
42
67
|
max: 20,
|
|
43
|
-
append: 'last',
|
|
44
68
|
contextmenu: true,
|
|
45
69
|
tabTransition: 'stack-tab-zoom',
|
|
46
70
|
pageTransition: 'stack-tab-swap',
|
|
@@ -52,51 +76,181 @@ const props = withDefaults(
|
|
|
52
76
|
space: 300,
|
|
53
77
|
globalScroll: false,
|
|
54
78
|
sessionPrefix: '',
|
|
79
|
+
iframeAllowedOrigins: () => []
|
|
55
80
|
}
|
|
56
81
|
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const { changeLocale } = useI18n()
|
|
82
|
+
const { changeLocale, t } = useI18n()
|
|
83
|
+
/** 是否最大化显示,通过 provide 向下传递 */
|
|
60
84
|
const maximum = ref<boolean>(false)
|
|
61
85
|
provide('maximum', maximum)
|
|
62
|
-
const { setIFramePath } =
|
|
86
|
+
const { setIFramePath, openTab } = useTabActions()
|
|
63
87
|
setIFramePath(props.iframePath)
|
|
64
88
|
setGlobalScroll(props.globalScroll)
|
|
65
89
|
changeLocale(props.i18n)
|
|
66
|
-
|
|
67
|
-
console.log('on Before mount')
|
|
68
|
-
setSessionPrefix(props.sessionPrefix)
|
|
69
|
-
initial(props.defaultTabs)
|
|
70
|
-
|
|
71
|
-
})
|
|
90
|
+
/** 将 router-view 的 Component 包装为带缓存 id 的页面组件,交给 addPage 注册到对应标签 */
|
|
72
91
|
const tabWrapper = (route: RouteLocationNormalizedLoaded, component: VNode): DefineComponent => {
|
|
73
|
-
// return defineAsyncComponent(() => addPage(route, component))
|
|
74
|
-
// if(!initialed.value){
|
|
75
|
-
// setSessionPrefix(props.sessionPrefix)
|
|
76
|
-
// initial(props.defaultTabs)
|
|
77
|
-
// }
|
|
78
92
|
return addPage(route, component)
|
|
79
93
|
}
|
|
94
|
+
/** 标签激活时向外转发 */
|
|
80
95
|
const onTabActive = (id: string) => {
|
|
81
96
|
emit('onActive', id)
|
|
82
97
|
}
|
|
83
98
|
setMaxSize(props.max)
|
|
84
|
-
|
|
85
|
-
destroy()
|
|
86
|
-
})
|
|
99
|
+
/** 页面组件加载完成时向外转发 */
|
|
87
100
|
const onComponentLoaded = () => {
|
|
88
101
|
emit('onPageLoaded')
|
|
89
102
|
}
|
|
103
|
+
/** 当前页面转场动画名,前进/后退时由 useTabRouter 发出 FORWARD/BACKWARD 事件切换 */
|
|
90
104
|
const pageSwitch = ref<string>(props.pageTransition)
|
|
91
|
-
const emitter =
|
|
92
|
-
|
|
105
|
+
const emitter = useTabEmitter()
|
|
106
|
+
/** 前进时使用 pageTransition */
|
|
107
|
+
const forwardHandler = () => {
|
|
93
108
|
pageSwitch.value = props.pageTransition
|
|
94
|
-
}
|
|
95
|
-
|
|
109
|
+
}
|
|
110
|
+
/** 后退时使用 pageTransitionBack */
|
|
111
|
+
const backwardHandler = () => {
|
|
96
112
|
pageSwitch.value = props.pageTransitionBack
|
|
113
|
+
}
|
|
114
|
+
emitter.on('FORWARD', forwardHandler)
|
|
115
|
+
emitter.on('BACKWARD', backwardHandler)
|
|
116
|
+
|
|
117
|
+
/** iframe 标签列表,computed 避免重复 filter */
|
|
118
|
+
const iframeTabs = computed(() => tabs.value.filter((item) => item.iframe))
|
|
119
|
+
|
|
120
|
+
/** 已激活过的 iframe 使用真实 URL(保留内容),未激活过的用 about:blank(按需加载,切换不重载) */
|
|
121
|
+
const iframeEverActivated = reactive<Record<string, boolean>>({})
|
|
122
|
+
/** 当前激活且有有效 URL 的 iframe 列表(供 watch 复用,减少重复 filter) */
|
|
123
|
+
const activeIframesWithUrl = computed(() =>
|
|
124
|
+
iframeTabs.value.filter((f) => f.active && (f.url ?? '') && !isInvalidIframeUrl(f.url ?? ''))
|
|
125
|
+
)
|
|
126
|
+
watch(
|
|
127
|
+
activeIframesWithUrl,
|
|
128
|
+
(frames) => {
|
|
129
|
+
for (const f of frames) iframeEverActivated[f.id] = true
|
|
130
|
+
},
|
|
131
|
+
{ immediate: true, deep: true }
|
|
132
|
+
)
|
|
133
|
+
/**
|
|
134
|
+
* 获取 iframe 的 src:未激活过的用 about:blank 延迟加载,已激活的保留真实 URL
|
|
135
|
+
* @param frame - iframe 标签项
|
|
136
|
+
* @returns 实际加载的 URL
|
|
137
|
+
*/
|
|
138
|
+
const getIframeSrc = (frame: { id: string; url?: string }) => {
|
|
139
|
+
const url = frame.url ?? ''
|
|
140
|
+
if (isInvalidIframeUrl(url)) return 'about:blank'
|
|
141
|
+
return iframeEverActivated[frame.id] ? url : 'about:blank'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** iframe 的 key:postMessage 模式仅用 id(不重建,动画正常);reload 模式含 refreshKeys */
|
|
145
|
+
const getIframeKey = (frame: { id: string; iframeRefreshMode?: string }) =>
|
|
146
|
+
frame.iframeRefreshMode === 'reload'
|
|
147
|
+
? `${frame.id}-${iframeRefreshKeys.value[frame.id] ?? 0}`
|
|
148
|
+
: frame.id
|
|
149
|
+
|
|
150
|
+
/** 各 iframe 是否已加载过(切回时不再显示 loading,因 load 不会再次触发) */
|
|
151
|
+
const iframeHasLoaded = reactive<Record<string, boolean>>({})
|
|
152
|
+
/** 各 iframe 加载状态,仅在首次加载时显示 */
|
|
153
|
+
const iframeLoadingStates = reactive<Record<string, boolean>>({})
|
|
154
|
+
/** iframe 加载超时定时器 Map,用于兜底清理 loading 状态 */
|
|
155
|
+
const loadTimeoutIds = new Map<string, ReturnType<typeof setTimeout>>()
|
|
156
|
+
/** 加载超时时间(ms),超时后强制隐藏 loading */
|
|
157
|
+
const LOAD_TIMEOUT_MS = 5000
|
|
158
|
+
|
|
159
|
+
/** 标记 iframe 已加载完成,清除超时并隐藏 loading */
|
|
160
|
+
const setIframeLoaded = (frameId: string) => {
|
|
161
|
+
const tid = loadTimeoutIds.get(frameId)
|
|
162
|
+
if (tid) {
|
|
163
|
+
clearTimeout(tid)
|
|
164
|
+
loadTimeoutIds.delete(frameId)
|
|
165
|
+
}
|
|
166
|
+
iframeHasLoaded[frameId] = true
|
|
167
|
+
iframeLoadingStates[frameId] = false
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 为 iframe 显示 loading 状态,已加载过的切回时不重复显示;设置超时兜底 */
|
|
171
|
+
const setIframeLoading = (frameId: string) => {
|
|
172
|
+
if (iframeHasLoaded[frameId]) return // 已加载过,切回时不显示 loading
|
|
173
|
+
iframeLoadingStates[frameId] = true
|
|
174
|
+
const tid = loadTimeoutIds.get(frameId)
|
|
175
|
+
if (tid) clearTimeout(tid)
|
|
176
|
+
loadTimeoutIds.set(
|
|
177
|
+
frameId,
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
loadTimeoutIds.delete(frameId)
|
|
180
|
+
iframeLoadingStates[frameId] = false
|
|
181
|
+
}, LOAD_TIMEOUT_MS)
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 当 iframe 标签变为激活且有真实 URL 时,显示加载状态(仅首次)
|
|
186
|
+
watch(
|
|
187
|
+
activeIframesWithUrl,
|
|
188
|
+
(frames) => {
|
|
189
|
+
for (const f of frames) setIframeLoading(f.id)
|
|
190
|
+
},
|
|
191
|
+
{ immediate: true, deep: true }
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
/** iframe 元素 refs,key 为 tabId,用于 postMessage 刷新时获取 contentWindow */
|
|
195
|
+
const iframeElRefs = reactive<Record<string, HTMLIFrameElement | null>>({})
|
|
196
|
+
/** 注册/注销 iframe 的 DOM 引用 */
|
|
197
|
+
const setIframeRef = (id: string, el: HTMLIFrameElement | null) => {
|
|
198
|
+
if (el) {
|
|
199
|
+
iframeElRefs[id] = el
|
|
200
|
+
} else {
|
|
201
|
+
delete iframeElRefs[id]
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** postMessage 刷新:向 iframe 发送消息,由其自行刷新 */
|
|
206
|
+
const handleRefreshIframePostMessage = (payload: unknown) => {
|
|
207
|
+
const tabId = payload as string
|
|
208
|
+
const iframe = iframeElRefs[tabId]
|
|
209
|
+
if (iframe?.contentWindow) {
|
|
210
|
+
try {
|
|
211
|
+
iframe.contentWindow.postMessage({ type: 'vue-stack-tabs:refresh' }, '*')
|
|
212
|
+
} catch {
|
|
213
|
+
/* 跨域时可能失败,忽略 */
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 允许通过 postMessage 调用 openTab 的来源白名单 */
|
|
219
|
+
const allowedOrigins = computed(() => {
|
|
220
|
+
const origin = typeof window !== 'undefined' ? window.location.origin : ''
|
|
221
|
+
const extra = props.iframeAllowedOrigins ?? []
|
|
222
|
+
return [origin, ...extra].filter(Boolean)
|
|
223
|
+
})
|
|
224
|
+
/** 处理 iframe 发来的 postMessage,校验 origin 后调用 openTab */
|
|
225
|
+
const handleMessage = (ev: MessageEvent) => {
|
|
226
|
+
if (!allowedOrigins.value.includes(ev.origin)) return
|
|
227
|
+
const data = ev.data
|
|
228
|
+
if (!data || typeof data !== 'object' || data.type !== 'vue-stack-tabs:openTab') return
|
|
229
|
+
const payload = data.payload
|
|
230
|
+
if (payload && typeof payload === 'object' && payload.title && payload.path) {
|
|
231
|
+
openTab(payload)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
;(emitter as { on: (t: string, h: (p: unknown) => void) => void }).on(
|
|
236
|
+
'REFRESH_IFRAME_POSTMESSAGE',
|
|
237
|
+
handleRefreshIframePostMessage
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
onBeforeMount(() => {
|
|
241
|
+
setSessionPrefix(props.sessionPrefix)
|
|
242
|
+
initialize(props.defaultTabs)
|
|
243
|
+
window.addEventListener('message', handleMessage)
|
|
97
244
|
})
|
|
98
245
|
onBeforeUnmount(() => {
|
|
99
|
-
|
|
246
|
+
window.removeEventListener('message', handleMessage)
|
|
247
|
+
;(emitter as { off: (t: string, h: (p: unknown) => void) => void }).off(
|
|
248
|
+
'REFRESH_IFRAME_POSTMESSAGE',
|
|
249
|
+
handleRefreshIframePostMessage
|
|
250
|
+
)
|
|
251
|
+
emitter.off('FORWARD', forwardHandler)
|
|
252
|
+
emitter.off('BACKWARD', backwardHandler)
|
|
253
|
+
destroy()
|
|
100
254
|
})
|
|
101
255
|
</script>
|
|
102
256
|
<template>
|
|
@@ -109,7 +263,7 @@ onBeforeUnmount(() => {
|
|
|
109
263
|
}"
|
|
110
264
|
:class="{ 'stack-tab__maximum': maximum }"
|
|
111
265
|
>
|
|
112
|
-
<tab-header :space="space"
|
|
266
|
+
<tab-header :space="space" :tab-transition="tabTransition" :max="max" @active="onTabActive">
|
|
113
267
|
<template #leftButton>
|
|
114
268
|
<slot name="leftButton" />
|
|
115
269
|
</template>
|
|
@@ -120,25 +274,46 @@ onBeforeUnmount(() => {
|
|
|
120
274
|
<div class="stack-tab__container">
|
|
121
275
|
<router-view v-slot="{ Component, route }">
|
|
122
276
|
<transition :name="pageSwitch" appear mode="out-in">
|
|
123
|
-
<keep-alive :include="caches">
|
|
277
|
+
<keep-alive :include="caches" :exclude="excludedCacheIdsForRefresh">
|
|
124
278
|
<component
|
|
125
279
|
:is="tabWrapper(route, Component)"
|
|
126
|
-
|
|
127
|
-
:key="`${route.name}_${route.fullPath}`"
|
|
280
|
+
:key="`${route.fullPath}_${refreshKey}`"
|
|
128
281
|
@on-loaded="onComponentLoaded"
|
|
129
282
|
/>
|
|
130
283
|
</keep-alive>
|
|
131
284
|
</transition>
|
|
132
285
|
</router-view>
|
|
133
|
-
<transition-group
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
286
|
+
<transition-group
|
|
287
|
+
:name="pageTransition"
|
|
288
|
+
tag="div"
|
|
289
|
+
class="stack-tab__iframes"
|
|
290
|
+
appear
|
|
291
|
+
>
|
|
292
|
+
<Transition
|
|
293
|
+
v-for="frame of iframeTabs"
|
|
294
|
+
:key="getIframeKey(frame)"
|
|
295
|
+
:name="pageTransition"
|
|
296
|
+
appear
|
|
297
|
+
>
|
|
298
|
+
<div
|
|
299
|
+
v-show="frame.active"
|
|
300
|
+
class="stack-tab__iframe-wrapper"
|
|
301
|
+
>
|
|
302
|
+
<div
|
|
303
|
+
v-show="iframeLoadingStates[frame.id] !== false"
|
|
304
|
+
class="stack-tab__iframe-loading"
|
|
305
|
+
>
|
|
306
|
+
<span class="stack-tab__iframe-loading-text">{{ t('VueStackTab.loading') }}</span>
|
|
307
|
+
</div>
|
|
308
|
+
<iframe
|
|
309
|
+
:ref="(el) => setIframeRef(frame.id, el as HTMLIFrameElement | null)"
|
|
310
|
+
class="stack-tab__iframe"
|
|
311
|
+
:src="getIframeSrc(frame)"
|
|
312
|
+
frameborder="0"
|
|
313
|
+
@load="setIframeLoaded(frame.id)"
|
|
314
|
+
/>
|
|
315
|
+
</div>
|
|
316
|
+
</Transition>
|
|
142
317
|
</transition-group>
|
|
143
318
|
</div>
|
|
144
319
|
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="200" height="200"><path d="M640 384v-85.333H213.333v597.334h597.334V384H640zm0-128h128v128h128V256H128v640h85.333V384H640V256z" fill="#444444"/></svg>
|
|
@@ -293,17 +293,58 @@
|
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
&__iframes {
|
|
297
|
+
position: absolute;
|
|
298
|
+
top: 0;
|
|
299
|
+
left: 0;
|
|
300
|
+
width: 100%;
|
|
301
|
+
height: 100%;
|
|
302
|
+
/* 进入的 iframe 置于上层;延迟进入,等待其他 tab 离开动画完成后再播放入场 */
|
|
303
|
+
.stack-tab-swap-enter-active {
|
|
304
|
+
z-index: 1;
|
|
305
|
+
transition-duration: 0.5s;
|
|
306
|
+
transition-delay: 0.3s;
|
|
307
|
+
}
|
|
308
|
+
.stack-tab-swap-leave-active {
|
|
309
|
+
z-index: 0;
|
|
310
|
+
position: absolute;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
&__iframe-wrapper {
|
|
315
|
+
position: absolute;
|
|
316
|
+
top: 0;
|
|
317
|
+
left: 0;
|
|
318
|
+
width: 100%;
|
|
319
|
+
height: 100%;
|
|
320
|
+
}
|
|
321
|
+
|
|
296
322
|
&__iframe {
|
|
297
323
|
position: absolute;
|
|
298
324
|
top: 0;
|
|
299
325
|
left: 0;
|
|
300
326
|
width: 100%;
|
|
301
327
|
height: 100%;
|
|
302
|
-
/*
|
|
303
|
-
新浏览器使用这个来消除frame的边框
|
|
304
|
-
*/
|
|
305
328
|
border: 0;
|
|
306
329
|
}
|
|
330
|
+
|
|
331
|
+
&__iframe-loading {
|
|
332
|
+
position: absolute;
|
|
333
|
+
top: 0;
|
|
334
|
+
left: 0;
|
|
335
|
+
z-index: 1;
|
|
336
|
+
display: flex;
|
|
337
|
+
align-items: center;
|
|
338
|
+
justify-content: center;
|
|
339
|
+
width: 100%;
|
|
340
|
+
height: 100%;
|
|
341
|
+
background: var(--stack-tab-iframe-loading-bg, #f5f5f5);
|
|
342
|
+
|
|
343
|
+
&-text {
|
|
344
|
+
color: var(--stack-tab-iframe-loading-color, #999);
|
|
345
|
+
font-size: var(--stack-tab-font-size, 14px);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
307
348
|
/*
|
|
308
349
|
右键菜单
|
|
309
350
|
*/
|
|
@@ -1,62 +1,63 @@
|
|
|
1
|
-
/*
|
|
2
|
-
transition 过渡样式
|
|
3
|
-
*/
|
|
4
|
-
.stack-tab-zoom {
|
|
5
|
-
&-enter-active,
|
|
6
|
-
&-leave-active {
|
|
7
|
-
transition: all 0.3s ease-in-out;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
&-enter-from,
|
|
11
|
-
&-leave-to {
|
|
12
|
-
transform: scale(0);
|
|
13
|
-
opacity: 0;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/*
|
|
18
|
-
页面交换
|
|
19
|
-
*/
|
|
20
|
-
.stack-tab-swap {
|
|
21
|
-
$trans: 30px;
|
|
22
|
-
|
|
23
|
-
&-
|
|
24
|
-
&-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
&-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
&-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
&-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
transition 过渡样式
|
|
3
|
+
*/
|
|
4
|
+
.stack-tab-zoom {
|
|
5
|
+
&-enter-active,
|
|
6
|
+
&-leave-active {
|
|
7
|
+
transition: all 0.3s ease-in-out;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
&-enter-from,
|
|
11
|
+
&-leave-to {
|
|
12
|
+
transform: scale(0);
|
|
13
|
+
opacity: 0;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
页面交换
|
|
19
|
+
*/
|
|
20
|
+
.stack-tab-swap {
|
|
21
|
+
$trans: 30px;
|
|
22
|
+
|
|
23
|
+
&-move,
|
|
24
|
+
&-enter-active,
|
|
25
|
+
&-leave-active {
|
|
26
|
+
transition: all 0.3s ease-in-out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&-enter-from,
|
|
30
|
+
&-leave-to {
|
|
31
|
+
opacity: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&-enter-from {
|
|
35
|
+
transform: translateX($trans);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&-leave-to {
|
|
39
|
+
transform: translateX(-$trans);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.stack-tab-swap-back {
|
|
44
|
+
$trans: 30px;
|
|
45
|
+
|
|
46
|
+
&-enter-active,
|
|
47
|
+
&-leave-active {
|
|
48
|
+
transition: all 0.3s ease-in-out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&-enter-from,
|
|
52
|
+
&-leave-to {
|
|
53
|
+
opacity: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&-enter-from {
|
|
57
|
+
transform: translateX(-$trans);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&-leave-to {
|
|
61
|
+
transform: translateX($trans);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
ContextMenuItem - 右键菜单单项
|
|
3
|
+
|
|
4
|
+
职责:展示图标和标题,支持 disabled 状态
|
|
5
|
+
-->
|
|
1
6
|
<template>
|
|
2
7
|
<div class="stack-tab__contextmenu-item">
|
|
3
8
|
<div class="stack-tab__contextmenu-icon" :class="icon"></div>
|
|
@@ -7,7 +12,9 @@
|
|
|
7
12
|
|
|
8
13
|
<script setup lang="ts">
|
|
9
14
|
defineProps<{
|
|
15
|
+
/** 图标 class,如 stack-tab__icon-reload svg-mask */
|
|
10
16
|
icon?: string
|
|
17
|
+
/** 显示文本 */
|
|
11
18
|
title: string
|
|
12
19
|
}>()
|
|
13
20
|
</script>
|