vue-stack-tabs 1.1.0 → 1.3.1

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 (115) hide show
  1. package/README.en.md +526 -0
  2. package/README.md +141 -39
  3. package/README.ru.md +526 -0
  4. package/dist/iframe-bridge.d.ts +8 -0
  5. package/dist/iframe-bridge.html +77 -0
  6. package/dist/iframe-bridge.mjs +20 -0
  7. package/dist/iframe-child.html +19 -6
  8. package/dist/nuxt/favicon.svg +10 -0
  9. package/dist/nuxt/iframe-bridge.html +77 -0
  10. package/dist/nuxt/iframe-child.html +88 -0
  11. package/dist/nuxt/module.d.ts +6 -0
  12. package/dist/nuxt/module.mjs +28 -0
  13. package/dist/nuxt/plugin.mjs +9 -0
  14. package/dist/nuxt/runtime/plugin.d.ts +2 -0
  15. package/dist/nuxt/runtime/plugin.mjs +9 -0
  16. package/dist/src/lib/StackTabs.vue.d.ts +30 -16
  17. package/dist/src/lib/components/ContextMenu/ContextMenuItem.vue.d.ts +7 -1
  18. package/dist/src/lib/components/ContextMenu/index.vue.d.ts +5 -4
  19. package/dist/src/lib/components/PageLoading.vue.d.ts +2 -1
  20. package/dist/src/lib/components/StackKeepAlive/StackCacheRenderer.vue.d.ts +25 -0
  21. package/dist/src/lib/components/StackKeepAlive/StackKeepAlive.vue.d.ts +12 -0
  22. package/dist/src/lib/components/TabHeader/TabHeaderButton.vue.d.ts +11 -12
  23. package/dist/src/lib/components/TabHeader/TabHeaderItem.vue.d.ts +5 -4
  24. package/dist/src/lib/components/TabHeader/TabHeaderScroll.vue.d.ts +8 -22
  25. package/dist/src/lib/components/TabHeader/index.vue.d.ts +11 -164
  26. package/dist/src/lib/hooks/stackTabsContext.d.ts +49 -0
  27. package/dist/src/lib/hooks/tabPanel/evict.d.ts +21 -61
  28. package/dist/src/lib/hooks/tabPanel/navigationTransaction.d.ts +35 -0
  29. package/dist/src/lib/hooks/tabPanel/refresh.d.ts +17 -0
  30. package/dist/src/lib/hooks/tabPanel/scroll.d.ts +8 -8
  31. package/dist/src/lib/hooks/tabPanel/session.d.ts +9 -8
  32. package/dist/src/lib/hooks/tabPanel/state.d.ts +20 -72
  33. package/dist/src/lib/hooks/useIframeManager.d.ts +35 -0
  34. package/dist/src/lib/hooks/useTabActions.d.ts +5 -46
  35. package/dist/src/lib/hooks/useTabEventBus.d.ts +22 -9
  36. package/dist/src/lib/hooks/useTabPanel.d.ts +14 -73
  37. package/dist/src/lib/i18n/index.d.ts +6 -4
  38. package/dist/src/lib/i18n/lang/en.d.ts +5 -0
  39. package/dist/src/lib/i18n/lang/zh-CN.d.ts +5 -0
  40. package/dist/src/lib/i18n/stackTabsLocale.d.ts +2 -0
  41. package/dist/src/lib/iframe-bridge.d.ts +8 -0
  42. package/dist/src/lib/iframe.vue.d.ts +2 -1
  43. package/dist/src/lib/index.d.ts +4 -5
  44. package/dist/src/lib/model/TabModel.d.ts +11 -40
  45. package/dist/src/lib/nuxt/module.d.ts +6 -0
  46. package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +2 -0
  47. package/dist/src/lib/nuxt/runtime/plugin.d.ts +2 -0
  48. package/dist/src/lib/utils/iframeBridge.d.ts +11 -3
  49. package/dist/src/lib/utils/stackTabsA11y.d.ts +8 -0
  50. package/dist/src/lib/utils/stackTabsMessage.d.ts +7 -0
  51. package/dist/src/lib/utils/tabInfoEncoder.d.ts +0 -15
  52. package/dist/src/lib/utils/typeGuards.d.ts +2 -0
  53. package/dist/src/lib/utils/urlParser.d.ts +24 -3
  54. package/dist/src/lib/versionLogger.d.ts +1 -0
  55. package/dist/style.css +2 -1
  56. package/dist/utils/iframeBridge.d.ts +39 -0
  57. package/dist/vue-stack-tabs.css +2 -1
  58. package/dist/vue-stack-tabs.es.js +1 -2220
  59. package/package.json +71 -50
  60. package/src/lib/StackTabs.vue +223 -196
  61. package/src/lib/assets/style/stackTab.scss +110 -13
  62. package/src/lib/components/ContextMenu/ContextMenuItem.vue +23 -11
  63. package/src/lib/components/ContextMenu/index.vue +94 -10
  64. package/src/lib/components/PageLoading.vue +9 -7
  65. package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +70 -0
  66. package/src/lib/components/StackKeepAlive/StackKeepAlive.vue +57 -0
  67. package/src/lib/components/TabHeader/TabHeaderButton.vue +19 -8
  68. package/src/lib/components/TabHeader/TabHeaderItem.vue +74 -17
  69. package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
  70. package/src/lib/components/TabHeader/index.vue +181 -13
  71. package/src/lib/hooks/stackTabsContext.ts +135 -0
  72. package/src/lib/hooks/tabPanel/evict.ts +122 -87
  73. package/src/lib/hooks/tabPanel/navigationTransaction.ts +61 -0
  74. package/src/lib/hooks/tabPanel/refresh.ts +109 -0
  75. package/src/lib/hooks/tabPanel/scroll.ts +57 -40
  76. package/src/lib/hooks/tabPanel/session.ts +84 -22
  77. package/src/lib/hooks/tabPanel/state.ts +41 -44
  78. package/src/lib/hooks/useIframeManager.ts +253 -0
  79. package/src/lib/hooks/useTabActions.ts +64 -30
  80. package/src/lib/hooks/useTabEventBus.ts +29 -19
  81. package/src/lib/hooks/useTabLoading.ts +3 -2
  82. package/src/lib/hooks/useTabPanel.tsx +290 -313
  83. package/src/lib/hooks/useTabRouter.ts +111 -97
  84. package/src/lib/i18n/index.ts +53 -45
  85. package/src/lib/i18n/lang/en.ts +6 -1
  86. package/src/lib/i18n/lang/zh-CN.ts +6 -1
  87. package/src/lib/i18n/stackTabsLocale.ts +8 -0
  88. package/src/lib/iframe-bridge.ts +12 -0
  89. package/src/lib/index.ts +14 -19
  90. package/src/lib/model/TabModel.ts +27 -82
  91. package/src/lib/nuxt/module.d.ts +4 -0
  92. package/src/lib/nuxt/module.ts +8 -3
  93. package/src/lib/nuxt/runtime/plugin.client.ts +1 -0
  94. package/src/lib/nuxt/runtime/plugin.ts +11 -0
  95. package/src/lib/utils/iframeBridge.ts +29 -5
  96. package/src/lib/utils/scrollUtils.ts +9 -4
  97. package/src/lib/utils/stackTabsA11y.ts +12 -0
  98. package/src/lib/utils/stackTabsMessage.ts +99 -0
  99. package/src/lib/utils/tabInfoEncoder.ts +70 -21
  100. package/src/lib/utils/typeGuards.ts +3 -0
  101. package/src/lib/utils/urlParser.ts +144 -14
  102. package/src/lib/versionLogger.ts +7 -19
  103. package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +0 -1
  104. package/dist/src/lib/hooks/useTabPanel.spec.d.ts +0 -1
  105. package/dist/src/lib/model/TabModel.spec.d.ts +0 -1
  106. package/dist/src/lib/utils/iframeBridge.spec.d.ts +0 -1
  107. package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +0 -1
  108. package/dist/src/lib/utils/urlParser.spec.d.ts +0 -1
  109. package/dist/vue-stack-tabs.umd.js +0 -1
  110. package/src/lib/hooks/tabPanel/evict.spec.ts +0 -85
  111. package/src/lib/hooks/useTabPanel.spec.ts +0 -34
  112. package/src/lib/model/TabModel.spec.ts +0 -40
  113. package/src/lib/utils/iframeBridge.spec.ts +0 -37
  114. package/src/lib/utils/tabInfoEncoder.spec.ts +0 -32
  115. package/src/lib/utils/urlParser.spec.ts +0 -45
@@ -6,50 +6,61 @@
6
6
  - 包裹在 router-view 外层,将路由渲染到对应标签的缓存页面中
7
7
 
8
8
  数据流:
9
- - useTabPanel:tabs/caches/excludedCacheIdsForRefresh/refreshKey
9
+ - useTabPanel:tabs、缓存列表与页面级刷新版本
10
10
  - useTabActions:openTab/closeTab 等,需 setIFramePath
11
11
  - keep-alive include=caches,exclude=excludedCacheIdsForRefresh(刷新时排除)
12
12
  -->
13
13
  <script lang="ts" setup>
14
- import { computed, onBeforeMount, onBeforeUnmount, provide, reactive, ref, watch } from 'vue'
15
- import type { TransitionProps, DefineComponent, VNode } from 'vue'
16
- import { type RouteLocationNormalizedLoaded } from 'vue-router'
14
+ import {
15
+ computed,
16
+ getCurrentInstance,
17
+ inject,
18
+ onBeforeMount,
19
+ onBeforeUnmount,
20
+ provide,
21
+ ref,
22
+ watch
23
+ } from 'vue'
24
+ import type { TransitionProps } from 'vue'
17
25
  import { getMaxZIndex } from './utils/scrollUtils'
18
- import { isInvalidIframeUrl } from './utils/urlParser'
26
+ import { isAllowedTabUrl } from './utils/urlParser'
27
+ import { applyStackTabsLocale } from './i18n/stackTabsLocale'
19
28
  import { type ITabData, TabScrollMode } from './model/TabModel'
20
29
  import TabHeader from './components/TabHeader/index.vue'
30
+ import StackKeepAlive from './components/StackKeepAlive/StackKeepAlive.vue'
21
31
  import useTabPanel from './hooks/useTabPanel'
22
32
  import useTabActions from './hooks/useTabActions'
23
33
  import { useI18n } from 'vue-i18n-lite'
24
- import { useTabEmitter } from '@/lib/hooks/useTabEventBus'
25
- const {
26
- tabs,
27
- caches,
28
- iframeRefreshKeys,
29
- activeCacheKey,
30
- destroy,
31
- addPage,
32
- initialize,
33
- setMaxSize,
34
- setGlobalScroll,
35
- setSessionPrefix
36
- } = useTabPanel()
37
- /** keep-alive 的 include 列表;excludedCacheIdsForRefresh 为刷新时临时 exclude,触发重挂载 */
34
+ import { TabEventType, tabEmitterKey } from './hooks/useTabEventBus'
35
+ import {
36
+ createStackTabsRuntimeContext,
37
+ maximumKey,
38
+ registerStackTabsRuntimeContext,
39
+ stackTabsContextKey,
40
+ unregisterStackTabsRuntimeContext
41
+ } from './hooks/stackTabsContext'
42
+ import { getStackTabPanelId, getStackTabTabId } from './utils/stackTabsA11y'
43
+ import { useIframeManager } from './hooks/useIframeManager'
44
+ type StackTabsIframeReferrerPolicy =
45
+ | ''
46
+ | 'no-referrer'
47
+ | 'no-referrer-when-downgrade'
48
+ | 'origin'
49
+ | 'origin-when-cross-origin'
50
+ | 'same-origin'
51
+ | 'strict-origin'
52
+ | 'strict-origin-when-cross-origin'
53
+ | 'unsafe-url'
54
+
38
55
  const emit = defineEmits(['onActive', 'onPageLoaded'])
39
56
  const props = withDefaults(
40
57
  defineProps<{
41
- // 初始页签数据
42
58
  defaultTabs?: ITabData[]
43
- // 最大打开数量
44
59
  max?: number
45
- // 右键菜单
46
60
  contextmenu?: boolean | object
47
- // 页面转场效果
48
61
  pageTransition?: string
49
62
  pageTransitionBack?: string
50
- // 标签转场效果
51
63
  tabTransition?: string | TransitionProps
52
- // tab 滚动效果
53
64
  tabScrollMode?: TabScrollMode
54
65
  width?: string
55
66
  height?: string
@@ -58,8 +69,11 @@ const props = withDefaults(
58
69
  globalScroll?: boolean
59
70
  iframePath: string
60
71
  sessionPrefix?: string
61
- /** 允许通过 postMessage 调用 openTab 的 iframe 来源,默认仅同源 */
62
72
  iframeAllowedOrigins?: string[]
73
+ iframeSandbox?: string
74
+ iframeReferrerPolicy?: StackTabsIframeReferrerPolicy
75
+ iframeAllow?: string
76
+ iframeLoadTimeout?: number
63
77
  }>(),
64
78
  {
65
79
  defaultTabs: () => [],
@@ -71,205 +85,169 @@ const props = withDefaults(
71
85
  tabScrollMode: TabScrollMode.BOTH,
72
86
  width: '100%',
73
87
  height: '100%',
74
- i18n: 'zh-CN',
88
+ i18n: undefined,
75
89
  space: 300,
76
90
  globalScroll: false,
77
91
  sessionPrefix: '',
78
- iframeAllowedOrigins: () => []
92
+ iframeAllowedOrigins: () => [],
93
+ iframeSandbox: 'allow-scripts allow-forms allow-popups allow-downloads allow-same-origin',
94
+ iframeReferrerPolicy: 'strict-origin-when-cross-origin' as StackTabsIframeReferrerPolicy,
95
+ iframeAllow: '',
96
+ iframeLoadTimeout: 15000
79
97
  }
80
98
  )
81
99
  const { changeLocale, t } = useI18n()
82
- /** 是否最大化显示,通过 provide 向下传递 */
100
+ const runtimeContext =
101
+ inject(stackTabsContextKey, null) ??
102
+ createStackTabsRuntimeContext({
103
+ iframePath: props.iframePath,
104
+ maxTabCount: props.max,
105
+ useGlobalScroll: props.globalScroll,
106
+ sessionPrefix: props.sessionPrefix
107
+ })
108
+ const stackTabsApp = getCurrentInstance()?.appContext.app
109
+ const isRuntimeContextOwner = registerStackTabsRuntimeContext(
110
+ runtimeContext,
111
+ stackTabsApp ? { app: stackTabsApp } : undefined
112
+ )
113
+ if (isRuntimeContextOwner) {
114
+ provide(stackTabsContextKey, runtimeContext)
115
+ provide(tabEmitterKey, runtimeContext.eventBus)
116
+ }
83
117
  const maximum = ref<boolean>(false)
84
- provide('maximum', maximum)
85
- const { setIFramePath, openTab } = useTabActions()
86
- setIFramePath(props.iframePath)
87
- setGlobalScroll(props.globalScroll)
88
- changeLocale(props.i18n)
89
- /** 将 router-view 的 Component 包装为带缓存 id 的页面组件,交给 addPage 注册到对应标签 */
90
- const tabWrapper = (route: RouteLocationNormalizedLoaded, component: VNode): DefineComponent => {
91
- return addPage(route, component)
118
+ const maximumZIndex = ref<number>()
119
+ if (isRuntimeContextOwner) {
120
+ provide(maximumKey, maximum)
92
121
  }
93
- /** 标签激活时向外转发 */
122
+ watch(maximum, (isMaximum) => {
123
+ maximumZIndex.value = isMaximum ? getMaxZIndex('body *:not(.stack-tab,.stack-tab *)') : undefined
124
+ })
125
+ const panelApi = isRuntimeContextOwner
126
+ ? useTabPanel()
127
+ : {
128
+ tabs: runtimeContext.tabs,
129
+ iframeRefreshKeys: runtimeContext.iframeRefreshKeys,
130
+ destroy: () => undefined,
131
+ initialize: () => undefined,
132
+ setMaxSize: () => undefined,
133
+ setGlobalScroll: () => undefined,
134
+ setSessionPrefix: () => undefined
135
+ }
136
+ const {
137
+ tabs,
138
+ iframeRefreshKeys,
139
+ destroy,
140
+ initialize,
141
+ setMaxSize,
142
+ setGlobalScroll,
143
+ setSessionPrefix
144
+ } = panelApi
145
+ const tabActions = isRuntimeContextOwner
146
+ ? useTabActions()
147
+ : {
148
+ setIFramePath: () => undefined,
149
+ openTab: () => Promise.resolve(undefined),
150
+ openInNewWindow: () => undefined
151
+ }
152
+ const { setIFramePath, openTab, openInNewWindow } = tabActions
153
+ if (isRuntimeContextOwner) {
154
+ setIFramePath(props.iframePath)
155
+ setGlobalScroll(props.globalScroll)
156
+ setMaxSize(props.max)
157
+ applyStackTabsLocale(changeLocale, props.i18n)
158
+ }
159
+
94
160
  const onTabActive = (id: string) => {
95
161
  emit('onActive', id)
96
162
  }
97
- setMaxSize(props.max)
98
- /** 页面组件加载完成时向外转发 */
99
163
  const onComponentLoaded = () => {
100
164
  emit('onPageLoaded')
101
165
  }
102
- /** 当前页面转场动画名,前进/后退时由 useTabRouter 发出 FORWARD/BACKWARD 事件切换 */
166
+
103
167
  const pageSwitch = ref<string>(props.pageTransition)
104
- const emitter = useTabEmitter()
105
- /** 前进时使用 pageTransition */
168
+ const emitter = runtimeContext.eventBus
106
169
  const forwardHandler = () => {
107
170
  pageSwitch.value = props.pageTransition
108
171
  }
109
- /** 后退时使用 pageTransitionBack */
110
172
  const backwardHandler = () => {
111
173
  pageSwitch.value = props.pageTransitionBack
112
174
  }
113
- emitter.on('FORWARD', forwardHandler)
114
- emitter.on('BACKWARD', backwardHandler)
115
-
116
- /** iframe 标签列表,computed 避免重复 filter */
117
- const iframeTabs = computed(() => tabs.value.filter((item) => item.iframe))
118
-
119
- /** 已激活过的 iframe 使用真实 URL(保留内容),未激活过的用 about:blank(按需加载,切换不重载) */
120
- const iframeEverActivated = reactive<Record<string, boolean>>({})
121
- /** 当前激活且有有效 URL 的 iframe 列表(供 watch 复用,减少重复 filter) */
122
- const activeIframesWithUrl = computed(() =>
123
- iframeTabs.value.filter((f) => f.active && (f.url ?? '') && !isInvalidIframeUrl(f.url ?? ''))
124
- )
125
- watch(
126
- activeIframesWithUrl,
127
- (frames) => {
128
- for (const f of frames) iframeEverActivated[f.id] = true
129
- },
130
- { immediate: true, deep: true }
131
- )
132
- /**
133
- * 获取 iframe 的 src:未激活过的用 about:blank 延迟加载,已激活的保留真实 URL
134
- * @param frame - iframe 标签项
135
- * @returns 实际加载的 URL
136
- */
137
- const getIframeSrc = (frame: { id: string; url?: string }) => {
138
- const url = frame.url ?? ''
139
- if (isInvalidIframeUrl(url)) return 'about:blank'
140
- return iframeEverActivated[frame.id] ? url : 'about:blank'
141
- }
142
-
143
- /** iframe 的 key:postMessage 模式仅用 id(不重建,动画正常);reload 模式含 refreshKeys */
144
- const getIframeKey = (frame: { id: string; iframeRefreshMode?: string }) =>
145
- frame.iframeRefreshMode === 'reload'
146
- ? `${frame.id}-${iframeRefreshKeys.value[frame.id] ?? 0}`
147
- : frame.id
148
-
149
- /** 各 iframe 是否已加载过(切回时不再显示 loading,因 load 不会再次触发) */
150
- const iframeHasLoaded = reactive<Record<string, boolean>>({})
151
- /** 各 iframe 加载状态,仅在首次加载时显示 */
152
- const iframeLoadingStates = reactive<Record<string, boolean>>({})
153
- /** iframe 加载超时定时器 Map,用于兜底清理 loading 状态 */
154
- const loadTimeoutIds = new Map<string, ReturnType<typeof setTimeout>>()
155
- /** 加载超时时间(ms),超时后强制隐藏 loading */
156
- const LOAD_TIMEOUT_MS = 5000
157
-
158
- /** 标记 iframe 已加载完成,清除超时并隐藏 loading */
159
- const setIframeLoaded = (frameId: string) => {
160
- const tid = loadTimeoutIds.get(frameId)
161
- if (tid) {
162
- clearTimeout(tid)
163
- loadTimeoutIds.delete(frameId)
164
- }
165
- iframeHasLoaded[frameId] = true
166
- iframeLoadingStates[frameId] = false
167
- }
168
-
169
- /** 为 iframe 显示 loading 状态,已加载过的切回时不重复显示;设置超时兜底 */
170
- const setIframeLoading = (frameId: string) => {
171
- if (iframeHasLoaded[frameId]) return // 已加载过,切回时不显示 loading
172
- iframeLoadingStates[frameId] = true
173
- const tid = loadTimeoutIds.get(frameId)
174
- if (tid) clearTimeout(tid)
175
- loadTimeoutIds.set(
176
- frameId,
177
- setTimeout(() => {
178
- loadTimeoutIds.delete(frameId)
179
- iframeLoadingStates[frameId] = false
180
- }, LOAD_TIMEOUT_MS)
181
- )
175
+ if (isRuntimeContextOwner) {
176
+ emitter.on(TabEventType.FORWARD, forwardHandler)
177
+ emitter.on(TabEventType.BACKWARD, backwardHandler)
182
178
  }
183
179
 
184
- // 当 iframe 标签变为激活且有真实 URL 时,显示加载状态(仅首次)
185
- watch(
186
- activeIframesWithUrl,
187
- (frames) => {
188
- for (const f of frames) setIframeLoading(f.id)
189
- },
190
- { immediate: true, deep: true }
191
- )
192
-
193
- /** iframe 元素 refs,key 为 tabId,用于 postMessage 刷新时获取 contentWindow */
194
- const iframeElRefs = reactive<Record<string, HTMLIFrameElement | null>>({})
195
- /** 注册/注销 iframe 的 DOM 引用 */
196
- const setIframeRef = (id: string, el: HTMLIFrameElement | null) => {
197
- if (el) {
198
- iframeElRefs[id] = el
199
- } else {
200
- delete iframeElRefs[id]
201
- }
202
- }
203
-
204
- /** postMessage 刷新:向 iframe 发送消息,由其自行刷新 */
205
- const handleRefreshIframePostMessage = (payload: unknown) => {
206
- const tabId = payload as string
207
- const iframe = iframeElRefs[tabId]
208
- if (iframe?.contentWindow) {
209
- try {
210
- console.log(
211
- `[vue-stack-tabs] Sending 'vue-stack-tabs:refresh' postMessage to iframe: ${tabId}`
212
- )
213
- iframe.contentWindow.postMessage({ type: 'vue-stack-tabs:refresh' }, '*')
214
- } catch (e) {
215
- console.warn(`[vue-stack-tabs] Failed to send refresh postMessage to iframe: ${tabId}`, e)
216
- }
217
- } else {
218
- console.warn(
219
- `[vue-stack-tabs] IFrame element not found for tabId: ${tabId} when trying to refresh.`
220
- )
221
- }
222
- }
180
+ const {
181
+ iframeTabs,
182
+ activeNonIframeTab,
183
+ hasActiveNonIframeTab,
184
+ iframeSrcMap,
185
+ getIframeRefreshKey,
186
+ getIframeKey,
187
+ getIframeLoadState,
188
+ shouldShowIframeLoading,
189
+ shouldShowIframeError,
190
+ retryIframe,
191
+ setIframeRef,
192
+ handleIframeLoad,
193
+ handleRefreshIframePostMessage,
194
+ createMessageHandler
195
+ } = useIframeManager({
196
+ tabs,
197
+ iframeRefreshKeys,
198
+ iframeLoadTimeout: props.iframeLoadTimeout,
199
+ t
200
+ })
223
201
 
224
- /** 允许通过 postMessage 调用 openTab 的来源白名单 */
225
202
  const allowedOrigins = computed(() => {
226
203
  const origin = typeof window !== 'undefined' ? window.location.origin : ''
227
204
  const extra = props.iframeAllowedOrigins ?? []
228
205
  return [origin, ...extra].filter(Boolean)
229
206
  })
230
- /** 处理 iframe 发来的 postMessage,校验 origin 后调用 openTab */
231
- const handleMessage = (ev: MessageEvent) => {
232
- if (!allowedOrigins.value.includes(ev.origin)) return
233
- const data = ev.data
234
- if (!data || typeof data !== 'object' || data.type !== 'vue-stack-tabs:openTab') return
235
- const payload = data.payload
236
- if (payload && typeof payload === 'object' && payload.title && payload.path) {
237
- openTab(payload)
238
- }
239
- }
207
+ const handleMessage = createMessageHandler(allowedOrigins, openTab)
240
208
 
241
- ;(emitter as { on: (t: string, h: (p: unknown) => void) => void }).on(
242
- 'REFRESH_IFRAME_POSTMESSAGE',
243
- handleRefreshIframePostMessage
244
- )
209
+ if (isRuntimeContextOwner) {
210
+ emitter.on(TabEventType.REFRESH_IFRAME_POSTMESSAGE, handleRefreshIframePostMessage)
211
+ }
245
212
 
246
213
  onBeforeMount(() => {
214
+ if (!isRuntimeContextOwner) return
247
215
  setSessionPrefix(props.sessionPrefix)
248
216
  initialize(props.defaultTabs)
249
217
  window.addEventListener('message', handleMessage)
250
218
  })
251
219
  onBeforeUnmount(() => {
220
+ if (!isRuntimeContextOwner) return
252
221
  window.removeEventListener('message', handleMessage)
253
- ;(emitter as { off: (t: string, h: (p: unknown) => void) => void }).off(
254
- 'REFRESH_IFRAME_POSTMESSAGE',
255
- handleRefreshIframePostMessage
256
- )
257
- emitter.off('FORWARD', forwardHandler)
258
- emitter.off('BACKWARD', backwardHandler)
222
+ emitter.off(TabEventType.REFRESH_IFRAME_POSTMESSAGE, handleRefreshIframePostMessage)
223
+ emitter.off(TabEventType.FORWARD, forwardHandler)
224
+ emitter.off(TabEventType.BACKWARD, backwardHandler)
259
225
  destroy()
226
+ unregisterStackTabsRuntimeContext(
227
+ runtimeContext,
228
+ stackTabsApp ? { app: stackTabsApp } : undefined
229
+ )
260
230
  })
261
231
  </script>
262
232
  <template>
263
233
  <div
234
+ v-if="isRuntimeContextOwner"
264
235
  class="stack-tab"
265
236
  :style="{
266
237
  width: width,
267
238
  height: height,
268
- 'z-index': maximum ? getMaxZIndex('body *:not(.stack-tab,.stack-tab *)') : undefined
239
+ 'z-index': maximumZIndex
269
240
  }"
270
241
  :class="{ 'stack-tab__maximum': maximum }"
271
242
  >
272
- <tab-header :space="space" :tab-transition="tabTransition" :max="max" @active="onTabActive">
243
+ <tab-header
244
+ :space="space"
245
+ :tab-transition="tabTransition"
246
+ :tab-scroll-mode="tabScrollMode"
247
+ :contextmenu="contextmenu"
248
+ :max="max"
249
+ @active="onTabActive"
250
+ >
273
251
  <template #leftButton>
274
252
  <slot name="leftButton" />
275
253
  </template>
@@ -278,40 +256,89 @@ onBeforeUnmount(() => {
278
256
  </template>
279
257
  </tab-header>
280
258
  <div class="stack-tab__container">
281
- <router-view v-slot="{ Component, route }">
282
- <transition :name="pageSwitch" appear mode="out-in">
283
- <keep-alive :include="caches">
284
- <component
285
- :is="tabWrapper(route, Component)"
286
- :key="activeCacheKey"
287
- :vnode="Component"
288
- @on-loaded="onComponentLoaded"
289
- />
290
- </keep-alive>
291
- </transition>
292
- </router-view>
293
- <transition-group :name="pageTransition" tag="div" class="stack-tab__iframes" appear>
259
+ <Transition :name="pageSwitch" appear>
260
+ <div
261
+ v-show="hasActiveNonIframeTab"
262
+ :id="activeNonIframeTab ? getStackTabPanelId(activeNonIframeTab.id) : undefined"
263
+ :key="'keep-alive'"
264
+ class="stack-tab__keep-alive-panel"
265
+ :role="activeNonIframeTab ? 'tabpanel' : undefined"
266
+ :aria-labelledby="
267
+ activeNonIframeTab ? getStackTabTabId(activeNonIframeTab.id) : undefined
268
+ "
269
+ :aria-hidden="activeNonIframeTab ? undefined : 'true'"
270
+ :hidden="!hasActiveNonIframeTab"
271
+ >
272
+ <StackKeepAlive :transition-name="pageSwitch" @loaded="onComponentLoaded" />
273
+ </div>
274
+ </Transition>
275
+ <div class="stack-tab__iframes">
294
276
  <Transition
295
277
  v-for="frame of iframeTabs"
296
278
  :key="getIframeKey(frame)"
297
- :name="pageTransition"
279
+ :name="pageSwitch"
298
280
  appear
299
281
  >
300
- <div v-show="frame.active" class="stack-tab__iframe-wrapper">
301
- <div v-show="iframeLoadingStates[frame.id] !== false" class="stack-tab__iframe-loading">
302
- <span class="stack-tab__iframe-loading-text">{{ t('VueStackTab.loading') }}</span>
282
+ <div
283
+ v-show="frame.active"
284
+ :id="getStackTabPanelId(frame.id)"
285
+ :key="getIframeKey(frame)"
286
+ class="stack-tab__iframe-wrapper"
287
+ role="tabpanel"
288
+ :aria-labelledby="getStackTabTabId(frame.id)"
289
+ >
290
+ <div
291
+ v-if="shouldShowIframeLoading(frame.id)"
292
+ class="stack-tab__iframe-loading"
293
+ role="status"
294
+ aria-live="polite"
295
+ :aria-label="t('VueStackTab.loading')"
296
+ >
297
+ <slot name="iframeLoading" :tab="frame">
298
+ <span class="stack-tab__iframe-loading-text">{{ t('VueStackTab.loading') }}</span>
299
+ </slot>
300
+ </div>
301
+ <div
302
+ v-if="shouldShowIframeError(frame.id)"
303
+ class="stack-tab__iframe-error"
304
+ role="alert"
305
+ >
306
+ <slot name="iframeError" :tab="frame" :retry="retryIframe">
307
+ <span class="stack-tab__iframe-error-text">
308
+ {{ getIframeLoadState(frame.id).message ?? t('VueStackTab.iframeLoadTimeout') }}
309
+ </span>
310
+ <button
311
+ type="button"
312
+ class="stack-tab__iframe-error-retry"
313
+ @click="retryIframe(frame.id)"
314
+ >
315
+ {{ t('VueStackTab.retry') }}
316
+ </button>
317
+ <button
318
+ v-if="frame.url && isAllowedTabUrl(frame.url)"
319
+ type="button"
320
+ class="stack-tab__iframe-error-open"
321
+ @click="openInNewWindow(frame.id)"
322
+ >
323
+ {{ t('VueStackTab.openInNewWindow') }}
324
+ </button>
325
+ </slot>
303
326
  </div>
304
327
  <iframe
305
- :key="iframeRefreshKeys[frame.id] || 0"
306
328
  :ref="(el) => setIframeRef(frame.id, el as HTMLIFrameElement | null)"
307
329
  class="stack-tab__iframe"
308
- :src="getIframeSrc(frame)"
330
+ :src="iframeSrcMap[frame.id] || 'about:blank'"
331
+ :data-refresh-key="getIframeRefreshKey(frame.id)"
332
+ :title="frame.title || t('VueStackTab.iframeTitle') || 'Stack tab iframe'"
333
+ :sandbox="iframeSandbox || undefined"
334
+ :referrerpolicy="iframeReferrerPolicy"
335
+ :allow="iframeAllow || undefined"
309
336
  frameborder="0"
310
- @load="setIframeLoaded(frame.id)"
337
+ @load="handleIframeLoad(frame, $event)"
311
338
  />
312
339
  </div>
313
340
  </Transition>
314
- </transition-group>
341
+ </div>
315
342
  </div>
316
343
  </div>
317
344
  </template>