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.
- package/README.en.md +526 -0
- package/README.md +141 -39
- package/README.ru.md +526 -0
- package/dist/iframe-bridge.d.ts +8 -0
- package/dist/iframe-bridge.html +77 -0
- package/dist/iframe-bridge.mjs +20 -0
- package/dist/iframe-child.html +19 -6
- package/dist/nuxt/favicon.svg +10 -0
- package/dist/nuxt/iframe-bridge.html +77 -0
- package/dist/nuxt/iframe-child.html +88 -0
- package/dist/nuxt/module.d.ts +6 -0
- package/dist/nuxt/module.mjs +28 -0
- package/dist/nuxt/plugin.mjs +9 -0
- package/dist/nuxt/runtime/plugin.d.ts +2 -0
- package/dist/nuxt/runtime/plugin.mjs +9 -0
- package/dist/src/lib/StackTabs.vue.d.ts +30 -16
- package/dist/src/lib/components/ContextMenu/ContextMenuItem.vue.d.ts +7 -1
- package/dist/src/lib/components/ContextMenu/index.vue.d.ts +5 -4
- package/dist/src/lib/components/PageLoading.vue.d.ts +2 -1
- package/dist/src/lib/components/StackKeepAlive/StackCacheRenderer.vue.d.ts +25 -0
- package/dist/src/lib/components/StackKeepAlive/StackKeepAlive.vue.d.ts +12 -0
- package/dist/src/lib/components/TabHeader/TabHeaderButton.vue.d.ts +11 -12
- package/dist/src/lib/components/TabHeader/TabHeaderItem.vue.d.ts +5 -4
- package/dist/src/lib/components/TabHeader/TabHeaderScroll.vue.d.ts +8 -22
- package/dist/src/lib/components/TabHeader/index.vue.d.ts +11 -164
- package/dist/src/lib/hooks/stackTabsContext.d.ts +49 -0
- package/dist/src/lib/hooks/tabPanel/evict.d.ts +21 -61
- package/dist/src/lib/hooks/tabPanel/navigationTransaction.d.ts +35 -0
- package/dist/src/lib/hooks/tabPanel/refresh.d.ts +17 -0
- package/dist/src/lib/hooks/tabPanel/scroll.d.ts +8 -8
- package/dist/src/lib/hooks/tabPanel/session.d.ts +9 -8
- package/dist/src/lib/hooks/tabPanel/state.d.ts +20 -72
- package/dist/src/lib/hooks/useIframeManager.d.ts +35 -0
- package/dist/src/lib/hooks/useTabActions.d.ts +5 -46
- package/dist/src/lib/hooks/useTabEventBus.d.ts +22 -9
- package/dist/src/lib/hooks/useTabPanel.d.ts +14 -73
- package/dist/src/lib/i18n/index.d.ts +6 -4
- package/dist/src/lib/i18n/lang/en.d.ts +5 -0
- package/dist/src/lib/i18n/lang/zh-CN.d.ts +5 -0
- package/dist/src/lib/i18n/stackTabsLocale.d.ts +2 -0
- package/dist/src/lib/iframe-bridge.d.ts +8 -0
- package/dist/src/lib/iframe.vue.d.ts +2 -1
- package/dist/src/lib/index.d.ts +4 -5
- package/dist/src/lib/model/TabModel.d.ts +11 -40
- package/dist/src/lib/nuxt/module.d.ts +6 -0
- package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +2 -0
- package/dist/src/lib/nuxt/runtime/plugin.d.ts +2 -0
- package/dist/src/lib/utils/iframeBridge.d.ts +11 -3
- package/dist/src/lib/utils/stackTabsA11y.d.ts +8 -0
- package/dist/src/lib/utils/stackTabsMessage.d.ts +7 -0
- package/dist/src/lib/utils/tabInfoEncoder.d.ts +0 -15
- package/dist/src/lib/utils/typeGuards.d.ts +2 -0
- package/dist/src/lib/utils/urlParser.d.ts +24 -3
- package/dist/src/lib/versionLogger.d.ts +1 -0
- package/dist/style.css +2 -1
- package/dist/utils/iframeBridge.d.ts +39 -0
- package/dist/vue-stack-tabs.css +2 -1
- package/dist/vue-stack-tabs.es.js +1 -2220
- package/package.json +71 -50
- package/src/lib/StackTabs.vue +223 -196
- package/src/lib/assets/style/stackTab.scss +110 -13
- package/src/lib/components/ContextMenu/ContextMenuItem.vue +23 -11
- package/src/lib/components/ContextMenu/index.vue +94 -10
- package/src/lib/components/PageLoading.vue +9 -7
- package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +70 -0
- package/src/lib/components/StackKeepAlive/StackKeepAlive.vue +57 -0
- package/src/lib/components/TabHeader/TabHeaderButton.vue +19 -8
- package/src/lib/components/TabHeader/TabHeaderItem.vue +74 -17
- package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
- package/src/lib/components/TabHeader/index.vue +181 -13
- package/src/lib/hooks/stackTabsContext.ts +135 -0
- package/src/lib/hooks/tabPanel/evict.ts +122 -87
- package/src/lib/hooks/tabPanel/navigationTransaction.ts +61 -0
- package/src/lib/hooks/tabPanel/refresh.ts +109 -0
- package/src/lib/hooks/tabPanel/scroll.ts +57 -40
- package/src/lib/hooks/tabPanel/session.ts +84 -22
- package/src/lib/hooks/tabPanel/state.ts +41 -44
- package/src/lib/hooks/useIframeManager.ts +253 -0
- package/src/lib/hooks/useTabActions.ts +64 -30
- package/src/lib/hooks/useTabEventBus.ts +29 -19
- package/src/lib/hooks/useTabLoading.ts +3 -2
- package/src/lib/hooks/useTabPanel.tsx +290 -313
- package/src/lib/hooks/useTabRouter.ts +111 -97
- package/src/lib/i18n/index.ts +53 -45
- package/src/lib/i18n/lang/en.ts +6 -1
- package/src/lib/i18n/lang/zh-CN.ts +6 -1
- package/src/lib/i18n/stackTabsLocale.ts +8 -0
- package/src/lib/iframe-bridge.ts +12 -0
- package/src/lib/index.ts +14 -19
- package/src/lib/model/TabModel.ts +27 -82
- package/src/lib/nuxt/module.d.ts +4 -0
- package/src/lib/nuxt/module.ts +8 -3
- package/src/lib/nuxt/runtime/plugin.client.ts +1 -0
- package/src/lib/nuxt/runtime/plugin.ts +11 -0
- package/src/lib/utils/iframeBridge.ts +29 -5
- package/src/lib/utils/scrollUtils.ts +9 -4
- package/src/lib/utils/stackTabsA11y.ts +12 -0
- package/src/lib/utils/stackTabsMessage.ts +99 -0
- package/src/lib/utils/tabInfoEncoder.ts +70 -21
- package/src/lib/utils/typeGuards.ts +3 -0
- package/src/lib/utils/urlParser.ts +144 -14
- package/src/lib/versionLogger.ts +7 -19
- package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +0 -1
- package/dist/src/lib/hooks/useTabPanel.spec.d.ts +0 -1
- package/dist/src/lib/model/TabModel.spec.d.ts +0 -1
- package/dist/src/lib/utils/iframeBridge.spec.d.ts +0 -1
- package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +0 -1
- package/dist/src/lib/utils/urlParser.spec.d.ts +0 -1
- package/dist/vue-stack-tabs.umd.js +0 -1
- package/src/lib/hooks/tabPanel/evict.spec.ts +0 -85
- package/src/lib/hooks/useTabPanel.spec.ts +0 -34
- package/src/lib/model/TabModel.spec.ts +0 -40
- package/src/lib/utils/iframeBridge.spec.ts +0 -37
- package/src/lib/utils/tabInfoEncoder.spec.ts +0 -32
- package/src/lib/utils/urlParser.spec.ts +0 -45
package/src/lib/StackTabs.vue
CHANGED
|
@@ -6,50 +6,61 @@
|
|
|
6
6
|
- 包裹在 router-view 外层,将路由渲染到对应标签的缓存页面中
|
|
7
7
|
|
|
8
8
|
数据流:
|
|
9
|
-
- useTabPanel:tabs
|
|
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 {
|
|
15
|
-
|
|
16
|
-
|
|
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 {
|
|
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 {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
166
|
+
|
|
103
167
|
const pageSwitch = ref<string>(props.pageTransition)
|
|
104
|
-
const emitter =
|
|
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
|
-
|
|
114
|
-
emitter.on(
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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':
|
|
239
|
+
'z-index': maximumZIndex
|
|
269
240
|
}"
|
|
270
241
|
:class="{ 'stack-tab__maximum': maximum }"
|
|
271
242
|
>
|
|
272
|
-
<tab-header
|
|
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
|
-
<
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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="
|
|
279
|
+
:name="pageSwitch"
|
|
298
280
|
appear
|
|
299
281
|
>
|
|
300
|
-
<div
|
|
301
|
-
|
|
302
|
-
|
|
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="
|
|
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="
|
|
337
|
+
@load="handleIframeLoad(frame, $event)"
|
|
311
338
|
/>
|
|
312
339
|
</div>
|
|
313
340
|
</Transition>
|
|
314
|
-
</
|
|
341
|
+
</div>
|
|
315
342
|
</div>
|
|
316
343
|
</div>
|
|
317
344
|
</template>
|