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.
Files changed (94) hide show
  1. package/README.md +717 -3
  2. package/dist/index.d.ts +3 -0
  3. package/dist/src/lib/StackTabs.vue.d.ts +33 -161
  4. package/dist/src/lib/components/ContextMenu/ContextMenuItem.vue.d.ts +6 -6
  5. package/dist/src/lib/components/ContextMenu/index.vue.d.ts +14 -14
  6. package/dist/src/lib/components/PageLoading.vue.d.ts +5 -5
  7. package/dist/src/lib/components/TabHeader/TabHeaderButton.vue.d.ts +24 -15
  8. package/dist/src/lib/components/TabHeader/TabHeaderItem.vue.d.ts +10 -8
  9. package/dist/src/lib/components/TabHeader/TabHeaderScroll.vue.d.ts +31 -13
  10. package/dist/src/lib/components/TabHeader/index.vue.d.ts +172 -20
  11. package/dist/src/lib/hooks/tabPanel/evict.d.ts +24 -0
  12. package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +1 -0
  13. package/dist/src/lib/hooks/tabPanel/scroll.d.ts +8 -0
  14. package/dist/src/lib/hooks/tabPanel/session.d.ts +9 -0
  15. package/dist/src/lib/hooks/tabPanel/state.d.ts +77 -0
  16. package/dist/src/lib/hooks/useContextMenu.d.ts +6 -1
  17. package/dist/src/lib/hooks/useTabActions.d.ts +68 -0
  18. package/dist/src/lib/hooks/{useTabMitt.d.ts → useTabEventBus.d.ts} +4 -3
  19. package/dist/src/lib/hooks/useTabLoading.d.ts +6 -0
  20. package/dist/src/lib/hooks/{useTabpanel.d.ts → useTabPanel.d.ts} +20 -14
  21. package/dist/src/lib/hooks/useTabPanel.spec.d.ts +1 -0
  22. package/dist/src/lib/hooks/useTabRouter.d.ts +14 -5
  23. package/dist/src/lib/i18n/index.d.ts +1 -1
  24. package/dist/src/lib/i18n/lang/en.d.ts +1 -0
  25. package/dist/src/lib/i18n/lang/zh-CN.d.ts +1 -0
  26. package/dist/src/lib/index.d.ts +5 -4
  27. package/dist/src/lib/model/TabModel.d.ts +20 -3
  28. package/dist/src/lib/model/TabModel.spec.d.ts +1 -0
  29. package/dist/src/lib/utils/iframeBridge.d.ts +31 -0
  30. package/dist/src/lib/utils/iframeBridge.spec.d.ts +1 -0
  31. package/dist/src/lib/utils/scrollUtils.d.ts +28 -0
  32. package/dist/src/lib/utils/tabInfoEncoder.d.ts +19 -0
  33. package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +1 -0
  34. package/dist/src/lib/utils/urlParser.d.ts +19 -0
  35. package/dist/src/lib/utils/urlParser.spec.d.ts +1 -0
  36. package/dist/src/lib/versionLogger.d.ts +2 -0
  37. package/dist/style.css +1 -1
  38. package/dist/vue-stack-tabs.css +1 -0
  39. package/dist/vue-stack-tabs.es.js +1795 -1679
  40. package/dist/vue-stack-tabs.umd.js +1 -5
  41. package/package.json +76 -39
  42. package/src/lib/StackTabs.vue +222 -47
  43. package/src/lib/assets/image/open.svg +1 -0
  44. package/src/lib/assets/style/common/var.scss +1 -1
  45. package/src/lib/assets/style/icon.scss +6 -0
  46. package/src/lib/assets/style/stackTab.scss +44 -3
  47. package/src/lib/assets/style/transition.scss +63 -62
  48. package/src/lib/components/ContextMenu/ContextMenuItem.vue +7 -0
  49. package/src/lib/components/ContextMenu/index.vue +50 -38
  50. package/src/lib/components/PageLoading.vue +30 -10
  51. package/src/lib/components/TabHeader/TabHeaderButton.vue +12 -3
  52. package/src/lib/components/TabHeader/TabHeaderItem.vue +31 -27
  53. package/src/lib/components/TabHeader/TabHeaderScroll.vue +83 -77
  54. package/src/lib/components/TabHeader/index.vue +45 -40
  55. package/src/lib/hooks/tabPanel/evict.spec.ts +85 -0
  56. package/src/lib/hooks/tabPanel/evict.ts +57 -0
  57. package/src/lib/hooks/tabPanel/scroll.ts +52 -0
  58. package/src/lib/hooks/tabPanel/session.ts +33 -0
  59. package/src/lib/hooks/tabPanel/state.ts +52 -0
  60. package/src/lib/hooks/useContextMenu.ts +22 -20
  61. package/src/lib/hooks/useTabActions.ts +148 -0
  62. package/src/lib/hooks/useTabEventBus.ts +31 -0
  63. package/src/lib/hooks/useTabLoading.ts +12 -4
  64. package/src/lib/hooks/useTabPanel.spec.ts +34 -0
  65. package/src/lib/hooks/useTabPanel.tsx +546 -0
  66. package/src/lib/hooks/useTabRouter.ts +114 -103
  67. package/src/lib/i18n/index.ts +14 -18
  68. package/src/lib/i18n/lang/en.ts +2 -1
  69. package/src/lib/i18n/lang/zh-CN.ts +2 -1
  70. package/src/lib/iframe.vue +6 -0
  71. package/src/lib/index.ts +27 -8
  72. package/src/lib/model/TabModel.spec.ts +40 -0
  73. package/src/lib/model/TabModel.ts +22 -9
  74. package/src/lib/nuxt/module.d.ts +19 -0
  75. package/src/lib/nuxt/module.ts +34 -0
  76. package/src/lib/nuxt/runtime/plugin.client.ts +11 -0
  77. package/src/lib/utils/iframeBridge.spec.ts +37 -0
  78. package/src/lib/utils/iframeBridge.ts +52 -0
  79. package/src/lib/utils/{TabScrollHelper.ts → scrollUtils.ts} +23 -29
  80. package/src/lib/utils/tabInfoEncoder.spec.ts +42 -0
  81. package/src/lib/utils/tabInfoEncoder.ts +65 -0
  82. package/src/lib/utils/urlParser.spec.ts +45 -0
  83. package/src/lib/utils/urlParser.ts +42 -0
  84. package/src/lib/{banner.ts → versionLogger.ts} +4 -5
  85. package/dist/src/lib/banner.d.ts +0 -2
  86. package/dist/src/lib/hooks/useStackTab.d.ts +0 -11
  87. package/dist/src/lib/utils/TabIdHelper.d.ts +0 -11
  88. package/dist/src/lib/utils/TabScrollHelper.d.ts +0 -33
  89. package/dist/src/lib/utils/UriHelper.d.ts +0 -4
  90. package/src/lib/hooks/useStackTab.ts +0 -119
  91. package/src/lib/hooks/useTabMitt.ts +0 -30
  92. package/src/lib/hooks/useTabpanel.tsx +0 -708
  93. package/src/lib/utils/TabIdHelper.ts +0 -70
  94. package/src/lib/utils/UriHelper.ts +0 -22
@@ -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/TabScrollHelper'
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 useTabpanel from './hooks/useTabpanel'
9
- import useStackTab from './hooks/useStackTab'
21
+ import useTabPanel from './hooks/useTabPanel'
22
+ import useTabActions from './hooks/useTabActions'
10
23
  import { useI18n } from 'vue-i18n-lite'
11
- import { useEmitter } from '@/lib/hooks/useTabMitt'
12
- const { tabs, initialed,pageShown, caches, destroy, addPage, initial, setMaxSize, setGlobalScroll,clearSession ,setSessionPrefix} =
13
- useTabpanel()
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: boolean
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
- // provide('locales', { ...props.i18n })
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 } = useStackTab()
86
+ const { setIFramePath, openTab } = useTabActions()
63
87
  setIFramePath(props.iframePath)
64
88
  setGlobalScroll(props.globalScroll)
65
89
  changeLocale(props.i18n)
66
- onBeforeMount(() => {
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
- onBeforeUnmount(() => {
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 = useEmitter()
92
- emitter.on('FORWARD', () => {
105
+ const emitter = useTabEmitter()
106
+ /** 前进时使用 pageTransition */
107
+ const forwardHandler = () => {
93
108
  pageSwitch.value = props.pageTransition
94
- })
95
- emitter.on('BACKWARD', () => {
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
- clearSession()
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" @active="onTabActive" :tab-transition="tabTransition" :max="max">
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
- v-if="pageShown"
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 :name="pageTransition" appear>
134
- <iframe
135
- v-for="frame of tabs.filter((item) => item.iframe)"
136
- v-show="frame.active"
137
- :key="frame.id"
138
- class="stack-tab__iframe"
139
- :src="/^(javascript|data):/i.test(frame.url ?? '') ? 'about:blank' : frame.url"
140
- frameborder="0"
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>
@@ -1,4 +1,4 @@
1
- @use "sass:color";
1
+ @use 'sass:color';
2
2
  /*
3
3
  主色调
4
4
  */
@@ -91,3 +91,9 @@
91
91
  mask: url('../image/right_arrow.svg') no-repeat center center;
92
92
  mask-size: contain;
93
93
  }
94
+
95
+ /* 新窗口打开 */
96
+ .stack-tab__icon-open {
97
+ mask: url('../image/open.svg') no-repeat center center;
98
+ mask-size: contain;
99
+ }
@@ -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
- &-enter-active,
24
- &-leave-active {
25
- transition: all 0.3s ease-in-out;
26
- }
27
-
28
- &-enter-from,
29
- &-leave-to {
30
- opacity: 0;
31
- }
32
-
33
- &-enter-from {
34
- transform: translateX($trans);
35
- }
36
-
37
- &-leave-to {
38
- transform: translateX(-$trans);
39
- }
40
- }
41
-
42
- .stack-tab-swap-back {
43
- $trans: 30px;
44
-
45
- &-enter-active,
46
- &-leave-active {
47
- transition: all 0.3s ease-in-out;
48
- }
49
-
50
- &-enter-from,
51
- &-leave-to {
52
- opacity: 0;
53
- }
54
-
55
- &-enter-from {
56
- transform: translateX(-$trans);
57
- }
58
-
59
- &-leave-to {
60
- transform: translateX($trans);
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>