vue-stack-tabs 1.0.1 → 1.3.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.en.md +526 -0
- package/README.md +337 -520
- 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 +88 -0
- 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 -23
- 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 -73
- 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 +17 -74
- package/dist/src/lib/hooks/useTabRouter.d.ts +5 -5
- 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 +14 -33
- 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 +1 -16
- 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 -2411
- package/package.json +68 -47
- package/src/lib/StackTabs.vue +216 -192
- package/src/lib/assets/style/stackTab.scss +84 -12
- 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 +45 -16
- package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
- package/src/lib/components/TabHeader/index.vue +109 -12
- package/src/lib/hooks/stackTabsContext.ts +135 -0
- package/src/lib/hooks/tabPanel/evict.ts +123 -41
- 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 -36
- 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 +62 -30
- package/src/lib/hooks/useTabEventBus.ts +29 -19
- package/src/lib/hooks/useTabLoading.ts +3 -2
- package/src/lib/hooks/useTabPanel.tsx +415 -213
- package/src/lib/hooks/useTabRouter.ts +172 -43
- 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 +34 -86
- 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 +2 -3
- 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 +69 -42
- 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 -42
- package/src/lib/utils/urlParser.spec.ts +0 -45
package/src/lib/StackTabs.vue
CHANGED
|
@@ -6,51 +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
|
-
|
|
38
|
-
|
|
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
|
+
|
|
39
55
|
const emit = defineEmits(['onActive', 'onPageLoaded'])
|
|
40
56
|
const props = withDefaults(
|
|
41
57
|
defineProps<{
|
|
42
|
-
// 初始页签数据
|
|
43
58
|
defaultTabs?: ITabData[]
|
|
44
|
-
// 最大打开数量
|
|
45
59
|
max?: number
|
|
46
|
-
// 右键菜单
|
|
47
60
|
contextmenu?: boolean | object
|
|
48
|
-
// 页面转场效果
|
|
49
61
|
pageTransition?: string
|
|
50
62
|
pageTransitionBack?: string
|
|
51
|
-
// 标签转场效果
|
|
52
63
|
tabTransition?: string | TransitionProps
|
|
53
|
-
// tab 滚动效果
|
|
54
64
|
tabScrollMode?: TabScrollMode
|
|
55
65
|
width?: string
|
|
56
66
|
height?: string
|
|
@@ -59,8 +69,11 @@ const props = withDefaults(
|
|
|
59
69
|
globalScroll?: boolean
|
|
60
70
|
iframePath: string
|
|
61
71
|
sessionPrefix?: string
|
|
62
|
-
/** 允许通过 postMessage 调用 openTab 的 iframe 来源,默认仅同源 */
|
|
63
72
|
iframeAllowedOrigins?: string[]
|
|
73
|
+
iframeSandbox?: string
|
|
74
|
+
iframeReferrerPolicy?: StackTabsIframeReferrerPolicy
|
|
75
|
+
iframeAllow?: string
|
|
76
|
+
iframeLoadTimeout?: number
|
|
64
77
|
}>(),
|
|
65
78
|
{
|
|
66
79
|
defaultTabs: () => [],
|
|
@@ -72,198 +85,169 @@ const props = withDefaults(
|
|
|
72
85
|
tabScrollMode: TabScrollMode.BOTH,
|
|
73
86
|
width: '100%',
|
|
74
87
|
height: '100%',
|
|
75
|
-
i18n:
|
|
88
|
+
i18n: undefined,
|
|
76
89
|
space: 300,
|
|
77
90
|
globalScroll: false,
|
|
78
91
|
sessionPrefix: '',
|
|
79
|
-
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
|
|
80
97
|
}
|
|
81
98
|
)
|
|
82
99
|
const { changeLocale, t } = useI18n()
|
|
83
|
-
|
|
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
|
+
}
|
|
84
117
|
const maximum = ref<boolean>(false)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
118
|
+
const maximumZIndex = ref<number>()
|
|
119
|
+
if (isRuntimeContextOwner) {
|
|
120
|
+
provide(maximumKey, maximum)
|
|
121
|
+
}
|
|
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)
|
|
93
158
|
}
|
|
94
|
-
|
|
159
|
+
|
|
95
160
|
const onTabActive = (id: string) => {
|
|
96
161
|
emit('onActive', id)
|
|
97
162
|
}
|
|
98
|
-
setMaxSize(props.max)
|
|
99
|
-
/** 页面组件加载完成时向外转发 */
|
|
100
163
|
const onComponentLoaded = () => {
|
|
101
164
|
emit('onPageLoaded')
|
|
102
165
|
}
|
|
103
|
-
|
|
166
|
+
|
|
104
167
|
const pageSwitch = ref<string>(props.pageTransition)
|
|
105
|
-
const emitter =
|
|
106
|
-
/** 前进时使用 pageTransition */
|
|
168
|
+
const emitter = runtimeContext.eventBus
|
|
107
169
|
const forwardHandler = () => {
|
|
108
170
|
pageSwitch.value = props.pageTransition
|
|
109
171
|
}
|
|
110
|
-
/** 后退时使用 pageTransitionBack */
|
|
111
172
|
const backwardHandler = () => {
|
|
112
173
|
pageSwitch.value = props.pageTransitionBack
|
|
113
174
|
}
|
|
114
|
-
|
|
115
|
-
emitter.on(
|
|
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
|
|
175
|
+
if (isRuntimeContextOwner) {
|
|
176
|
+
emitter.on(TabEventType.FORWARD, forwardHandler)
|
|
177
|
+
emitter.on(TabEventType.BACKWARD, backwardHandler)
|
|
168
178
|
}
|
|
169
179
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
}
|
|
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
|
+
})
|
|
217
201
|
|
|
218
|
-
/** 允许通过 postMessage 调用 openTab 的来源白名单 */
|
|
219
202
|
const allowedOrigins = computed(() => {
|
|
220
203
|
const origin = typeof window !== 'undefined' ? window.location.origin : ''
|
|
221
204
|
const extra = props.iframeAllowedOrigins ?? []
|
|
222
205
|
return [origin, ...extra].filter(Boolean)
|
|
223
206
|
})
|
|
224
|
-
|
|
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
|
-
}
|
|
207
|
+
const handleMessage = createMessageHandler(allowedOrigins, openTab)
|
|
234
208
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
209
|
+
if (isRuntimeContextOwner) {
|
|
210
|
+
emitter.on(TabEventType.REFRESH_IFRAME_POSTMESSAGE, handleRefreshIframePostMessage)
|
|
211
|
+
}
|
|
239
212
|
|
|
240
213
|
onBeforeMount(() => {
|
|
214
|
+
if (!isRuntimeContextOwner) return
|
|
241
215
|
setSessionPrefix(props.sessionPrefix)
|
|
242
216
|
initialize(props.defaultTabs)
|
|
243
217
|
window.addEventListener('message', handleMessage)
|
|
244
218
|
})
|
|
245
219
|
onBeforeUnmount(() => {
|
|
220
|
+
if (!isRuntimeContextOwner) return
|
|
246
221
|
window.removeEventListener('message', handleMessage)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
)
|
|
251
|
-
emitter.off('FORWARD', forwardHandler)
|
|
252
|
-
emitter.off('BACKWARD', backwardHandler)
|
|
222
|
+
emitter.off(TabEventType.REFRESH_IFRAME_POSTMESSAGE, handleRefreshIframePostMessage)
|
|
223
|
+
emitter.off(TabEventType.FORWARD, forwardHandler)
|
|
224
|
+
emitter.off(TabEventType.BACKWARD, backwardHandler)
|
|
253
225
|
destroy()
|
|
226
|
+
unregisterStackTabsRuntimeContext(
|
|
227
|
+
runtimeContext,
|
|
228
|
+
stackTabsApp ? { app: stackTabsApp } : undefined
|
|
229
|
+
)
|
|
254
230
|
})
|
|
255
231
|
</script>
|
|
256
232
|
<template>
|
|
257
233
|
<div
|
|
234
|
+
v-if="isRuntimeContextOwner"
|
|
258
235
|
class="stack-tab"
|
|
259
236
|
:style="{
|
|
260
237
|
width: width,
|
|
261
238
|
height: height,
|
|
262
|
-
'z-index':
|
|
239
|
+
'z-index': maximumZIndex
|
|
263
240
|
}"
|
|
264
241
|
:class="{ 'stack-tab__maximum': maximum }"
|
|
265
242
|
>
|
|
266
|
-
<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
|
+
>
|
|
267
251
|
<template #leftButton>
|
|
268
252
|
<slot name="leftButton" />
|
|
269
253
|
</template>
|
|
@@ -272,49 +256,89 @@ onBeforeUnmount(() => {
|
|
|
272
256
|
</template>
|
|
273
257
|
</tab-header>
|
|
274
258
|
<div class="stack-tab__container">
|
|
275
|
-
<
|
|
276
|
-
<
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
>
|
|
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">
|
|
292
276
|
<Transition
|
|
293
277
|
v-for="frame of iframeTabs"
|
|
294
278
|
:key="getIframeKey(frame)"
|
|
295
|
-
:name="
|
|
279
|
+
:name="pageSwitch"
|
|
296
280
|
appear
|
|
297
281
|
>
|
|
298
282
|
<div
|
|
299
283
|
v-show="frame.active"
|
|
284
|
+
:id="getStackTabPanelId(frame.id)"
|
|
285
|
+
:key="getIframeKey(frame)"
|
|
300
286
|
class="stack-tab__iframe-wrapper"
|
|
287
|
+
role="tabpanel"
|
|
288
|
+
:aria-labelledby="getStackTabTabId(frame.id)"
|
|
301
289
|
>
|
|
302
290
|
<div
|
|
303
|
-
v-
|
|
291
|
+
v-if="shouldShowIframeLoading(frame.id)"
|
|
304
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
305
|
>
|
|
306
|
-
<
|
|
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>
|
|
307
326
|
</div>
|
|
308
327
|
<iframe
|
|
309
328
|
:ref="(el) => setIframeRef(frame.id, el as HTMLIFrameElement | null)"
|
|
310
329
|
class="stack-tab__iframe"
|
|
311
|
-
: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"
|
|
312
336
|
frameborder="0"
|
|
313
|
-
@load="
|
|
337
|
+
@load="handleIframeLoad(frame, $event)"
|
|
314
338
|
/>
|
|
315
339
|
</div>
|
|
316
340
|
</Transition>
|
|
317
|
-
</
|
|
341
|
+
</div>
|
|
318
342
|
</div>
|
|
319
343
|
</div>
|
|
320
344
|
</template>
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
position: relative;
|
|
15
15
|
overflow: hidden;
|
|
16
16
|
}
|
|
17
|
+
&__keep-alive-panel {
|
|
18
|
+
position: absolute;
|
|
19
|
+
top: 0;
|
|
20
|
+
left: 0;
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: 100%;
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
/*缓存页面wrapper*/
|
|
18
26
|
.cache-page-wrapper {
|
|
19
27
|
width: 100%;
|
|
@@ -75,6 +83,12 @@
|
|
|
75
83
|
cursor: pointer;
|
|
76
84
|
display: flex;
|
|
77
85
|
align-items: center;
|
|
86
|
+
border: 0;
|
|
87
|
+
padding: 0;
|
|
88
|
+
color: inherit;
|
|
89
|
+
font: inherit;
|
|
90
|
+
text-align: inherit;
|
|
91
|
+
appearance: none;
|
|
78
92
|
|
|
79
93
|
//按钮禁用样式
|
|
80
94
|
&[disabled] {
|
|
@@ -216,6 +230,22 @@
|
|
|
216
230
|
}
|
|
217
231
|
}
|
|
218
232
|
|
|
233
|
+
&-tab {
|
|
234
|
+
display: flex;
|
|
235
|
+
flex: 1 1 auto;
|
|
236
|
+
min-width: 0;
|
|
237
|
+
height: 100%;
|
|
238
|
+
padding: 0;
|
|
239
|
+
color: inherit;
|
|
240
|
+
cursor: inherit;
|
|
241
|
+
background: transparent;
|
|
242
|
+
border: 0;
|
|
243
|
+
font: inherit;
|
|
244
|
+
text-align: inherit;
|
|
245
|
+
appearance: none;
|
|
246
|
+
align-items: center;
|
|
247
|
+
}
|
|
248
|
+
|
|
219
249
|
/*
|
|
220
250
|
页签标题
|
|
221
251
|
*/
|
|
@@ -247,6 +277,10 @@
|
|
|
247
277
|
width: 0;
|
|
248
278
|
background-color: $stack-tab-text-color;
|
|
249
279
|
transition: $stack-tab__trans;
|
|
280
|
+
border: 0;
|
|
281
|
+
font: inherit;
|
|
282
|
+
appearance: none;
|
|
283
|
+
padding: 0;
|
|
250
284
|
|
|
251
285
|
&:hover {
|
|
252
286
|
background-color: $stack-tab-color-primary;
|
|
@@ -299,17 +333,7 @@
|
|
|
299
333
|
left: 0;
|
|
300
334
|
width: 100%;
|
|
301
335
|
height: 100%;
|
|
302
|
-
pointer-events: none;
|
|
303
|
-
/* 进入的 iframe 置于上层;延迟进入,等待其他 tab 离开动画完成后再播放入场 */
|
|
304
|
-
.stack-tab-swap-enter-active {
|
|
305
|
-
z-index: 1;
|
|
306
|
-
transition-duration: 0.5s;
|
|
307
|
-
transition-delay: 0.3s;
|
|
308
|
-
}
|
|
309
|
-
.stack-tab-swap-leave-active {
|
|
310
|
-
z-index: 0;
|
|
311
|
-
position: absolute;
|
|
312
|
-
}
|
|
336
|
+
pointer-events: none;
|
|
313
337
|
}
|
|
314
338
|
|
|
315
339
|
&__iframe-wrapper {
|
|
@@ -347,6 +371,39 @@
|
|
|
347
371
|
font-size: var(--stack-tab-font-size, 14px);
|
|
348
372
|
}
|
|
349
373
|
}
|
|
374
|
+
|
|
375
|
+
&__iframe-error {
|
|
376
|
+
position: absolute;
|
|
377
|
+
top: 0;
|
|
378
|
+
left: 0;
|
|
379
|
+
z-index: 2;
|
|
380
|
+
display: flex;
|
|
381
|
+
gap: 8px;
|
|
382
|
+
align-items: center;
|
|
383
|
+
justify-content: center;
|
|
384
|
+
width: 100%;
|
|
385
|
+
height: 100%;
|
|
386
|
+
padding: 16px;
|
|
387
|
+
pointer-events: auto;
|
|
388
|
+
background: var(--stack-tab-iframe-error-bg, rgb(255 255 255 / 92%));
|
|
389
|
+
box-sizing: border-box;
|
|
390
|
+
|
|
391
|
+
&-text {
|
|
392
|
+
color: var(--stack-tab-iframe-error-color, #666);
|
|
393
|
+
font-size: var(--stack-tab-font-size, 14px);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
&-retry,
|
|
397
|
+
&-open {
|
|
398
|
+
border: $stack-tab-border;
|
|
399
|
+
padding: 4px 10px;
|
|
400
|
+
color: $stack-tab-text-color;
|
|
401
|
+
cursor: pointer;
|
|
402
|
+
background: $stack-tab-background-color-base;
|
|
403
|
+
border-radius: 6px;
|
|
404
|
+
font: inherit;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
350
407
|
/*
|
|
351
408
|
右键菜单
|
|
352
409
|
*/
|
|
@@ -367,16 +424,27 @@
|
|
|
367
424
|
display: flex;
|
|
368
425
|
position: relative;
|
|
369
426
|
align-items: center;
|
|
427
|
+
justify-content: flex-start;
|
|
428
|
+
width: 100%;
|
|
429
|
+
min-height: 30px;
|
|
430
|
+
margin: 0;
|
|
370
431
|
padding: 0 10px;
|
|
371
432
|
line-height: 30px;
|
|
433
|
+
font: inherit;
|
|
434
|
+
text-align: inherit;
|
|
435
|
+
appearance: none;
|
|
436
|
+
box-sizing: border-box;
|
|
372
437
|
color: $stack-tab-text-color;
|
|
373
438
|
cursor: pointer;
|
|
439
|
+
background: transparent;
|
|
440
|
+
border: 0;
|
|
374
441
|
border-radius: 10px 0;
|
|
375
442
|
transition: all 0.2s ease-in-out;
|
|
376
443
|
user-select: none;
|
|
377
444
|
|
|
378
445
|
&:hover,
|
|
379
|
-
&:active
|
|
446
|
+
&:active,
|
|
447
|
+
&:focus-visible {
|
|
380
448
|
color: $stack-tab-color-white;
|
|
381
449
|
background-color: $stack-tab-color-primary;
|
|
382
450
|
|
|
@@ -385,6 +453,10 @@
|
|
|
385
453
|
}
|
|
386
454
|
}
|
|
387
455
|
|
|
456
|
+
&:focus {
|
|
457
|
+
outline: none;
|
|
458
|
+
}
|
|
459
|
+
|
|
388
460
|
&[disabled] {
|
|
389
461
|
color: $stack-tab-color-disabled;
|
|
390
462
|
pointer-events: none;
|