vue-stack-tabs 1.3.1 → 1.4.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 +119 -52
- package/README.md +103 -42
- package/README.ru.md +125 -54
- package/dist/{src/lib/StackTabs.vue.d.ts → StackTabs.vue.d.ts} +5 -5
- package/dist/{src/lib/components → components}/ContextMenu/index.vue.d.ts +1 -1
- package/dist/components/StackKeepAlive/emptyPlaceholder.d.ts +3 -0
- package/dist/{src/lib/components → components}/TabHeader/TabHeaderItem.vue.d.ts +9 -0
- package/dist/{src/lib/components → components}/TabHeader/index.vue.d.ts +3 -3
- package/dist/{src/lib/hooks → hooks}/stackTabsContext.d.ts +5 -2
- package/dist/hooks/tabPanel/pageComponentFactory.d.ts +20 -0
- package/dist/{src/lib/hooks → hooks}/tabPanel/scroll.d.ts +2 -0
- package/dist/hooks/useScrollShadow.d.ts +20 -0
- package/dist/{src/lib/hooks → hooks}/useTabActions.d.ts +7 -2
- package/dist/{src/lib/hooks → hooks}/useTabEventBus.d.ts +2 -1
- package/dist/{src/lib/hooks → hooks}/useTabPanel.d.ts +3 -1
- package/dist/iframe-bridge.mjs +1 -1
- package/dist/index.d.ts +22 -6
- package/dist/{src/lib/model → model}/TabModel.d.ts +21 -0
- package/dist/nuxt/module.mjs +10 -6
- package/dist/nuxt/runtime/plugin.mjs +1600 -4
- package/dist/style.css +1 -1
- package/dist/utils/iframeBridge.d.ts +7 -1
- package/dist/{src/lib/utils → utils}/scrollUtils.d.ts +7 -1
- package/dist/{src/lib/versionLogger.d.ts → versionLogger.d.ts} +1 -1
- package/dist/vue-stack-tabs.css +1 -1
- package/dist/vue-stack-tabs.es.js +1 -1
- package/package.json +64 -40
- package/src/lib/StackTabs.vue +16 -3
- package/src/lib/assets/style/common/var.scss +2 -0
- package/src/lib/assets/style/stackTab.scss +17 -3
- package/src/lib/components/ContextMenu/index.vue +12 -3
- package/src/lib/components/PageLoading.vue +10 -2
- package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +3 -9
- package/src/lib/components/StackKeepAlive/emptyPlaceholder.tsx +18 -0
- package/src/lib/components/TabHeader/index.vue +4 -3
- package/src/lib/hooks/stackTabsContext.ts +23 -18
- package/src/lib/hooks/tabPanel/pageComponentFactory.tsx +142 -0
- package/src/lib/hooks/tabPanel/refresh.ts +3 -0
- package/src/lib/hooks/tabPanel/scroll.ts +110 -7
- package/src/lib/hooks/tabPanel/session.ts +29 -3
- package/src/lib/hooks/useContextMenu.ts +57 -11
- package/src/lib/hooks/useIframeManager.ts +18 -2
- package/src/lib/hooks/useScrollShadow.ts +124 -0
- package/src/lib/hooks/useTabActions.ts +18 -8
- package/src/lib/hooks/useTabEventBus.ts +5 -3
- package/src/lib/hooks/useTabPanel.tsx +108 -180
- package/src/lib/hooks/useTabRouter.ts +11 -2
- package/src/lib/i18n/index.ts +3 -3
- package/src/lib/model/TabModel.ts +40 -1
- package/src/lib/nuxt/module.ts +10 -3
- package/src/lib/nuxt/runtime/plugin.ts +13 -2
- package/src/lib/utils/iframeBridge.ts +9 -1
- package/src/lib/utils/scrollUtils.ts +35 -2
- package/src/lib/utils/tabInfoEncoder.ts +21 -8
- package/src/lib/utils/urlParser.ts +3 -1
- package/src/lib/versionLogger.ts +6 -2
- package/dist/favicon.svg +0 -10
- package/dist/iframe-bridge.html +0 -77
- package/dist/iframe-child.html +0 -88
- package/dist/nuxt/favicon.svg +0 -10
- package/dist/nuxt/iframe-bridge.html +0 -77
- package/dist/nuxt/iframe-child.html +0 -88
- package/dist/nuxt/plugin.mjs +0 -9
- package/dist/src/lib/iframe-bridge.d.ts +0 -8
- package/dist/src/lib/index.d.ts +0 -22
- package/dist/src/lib/nuxt/module.d.ts +0 -6
- package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +0 -2
- package/dist/src/lib/nuxt/runtime/plugin.d.ts +0 -2
- package/dist/src/lib/utils/iframeBridge.d.ts +0 -39
- package/src/lib/nuxt/runtime/plugin.client.ts +0 -10
- /package/dist/{src/lib/components → components}/ContextMenu/ContextMenuItem.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/PageLoading.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/StackKeepAlive/StackCacheRenderer.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/StackKeepAlive/StackKeepAlive.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/TabHeader/TabHeaderButton.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/TabHeader/TabHeaderScroll.vue.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/evict.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/navigationTransaction.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/refresh.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/session.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/state.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useContextMenu.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useIframeManager.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useTabLoading.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useTabRouter.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/index.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/lang/en.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/lang/zh-CN.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/stackTabsLocale.d.ts +0 -0
- /package/dist/{src/lib/iframe.vue.d.ts → iframe.vue.d.ts} +0 -0
- /package/dist/{src/lib/utils → utils}/stackTabsA11y.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/stackTabsMessage.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/tabInfoEncoder.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/typeGuards.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/urlParser.d.ts +0 -0
|
@@ -91,13 +91,46 @@ export const getScrollbarWidth = (function () {
|
|
|
91
91
|
|
|
92
92
|
const Z_INDEX_CEILING = 89999
|
|
93
93
|
|
|
94
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* 缓存条目:保存某选择器下最近一次的最大 z-index 结果。
|
|
96
|
+
* 在 Z_INDEX_CACHE_TTL 毫秒内的重复请求直接复用,避免反复遍历 DOM。
|
|
97
|
+
*/
|
|
98
|
+
interface ZIndexCacheEntry {
|
|
99
|
+
value: number
|
|
100
|
+
timestamp: number
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const Z_INDEX_CACHE_TTL = 100
|
|
104
|
+
const zIndexCache = new Map<string, ZIndexCacheEntry>()
|
|
105
|
+
|
|
106
|
+
/** 在用户主动布局变化后调用,使缓存失效 */
|
|
107
|
+
export const invalidateZIndexCache = (selector?: string): void => {
|
|
108
|
+
if (selector === undefined) {
|
|
109
|
+
zIndexCache.clear()
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
zIndexCache.delete(selector)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 获取指定选择器下元素的最大 z-index + 1,用于最大化时置顶。
|
|
117
|
+
* 同一选择器在 Z_INDEX_CACHE_TTL 毫秒内的重复请求会复用上次结果,避免重复 querySelectorAll / getComputedStyle。
|
|
118
|
+
* 当 DOM 结构发生显著变化时,请调用 invalidateZIndexCache 让缓存失效。
|
|
119
|
+
*/
|
|
95
120
|
export const getMaxZIndex = (key = '.stack-tab__container *'): number => {
|
|
121
|
+
const now = Date.now()
|
|
122
|
+
const cached = zIndexCache.get(key)
|
|
123
|
+
if (cached && now - cached.timestamp < Z_INDEX_CACHE_TTL) {
|
|
124
|
+
return cached.value
|
|
125
|
+
}
|
|
126
|
+
|
|
96
127
|
const elements = document.querySelectorAll(key)
|
|
97
128
|
let max = 0
|
|
98
129
|
for (let i = 0; i < elements.length; i++) {
|
|
99
130
|
const z = +window.getComputedStyle(elements[i]!).zIndex || 0
|
|
100
131
|
if (z < Z_INDEX_CEILING && z > max) max = z
|
|
101
132
|
}
|
|
102
|
-
|
|
133
|
+
const value = max + 1
|
|
134
|
+
zIndexCache.set(key, { value, timestamp: now })
|
|
135
|
+
return value
|
|
103
136
|
}
|
|
@@ -20,9 +20,14 @@ interface EncodedTabInfoV1 {
|
|
|
20
20
|
const MAX_DECODED_TAB_INFO_LENGTH = 2048
|
|
21
21
|
const MAX_ENCODED_TAB_INFO_LENGTH = 4096
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
/**
|
|
24
|
+
* 把任意 UTF-8 字符串安全地编码为 base64(避免中文/特殊字符直接 btoa 抛错)。
|
|
25
|
+
* 使用 encodeURIComponent + unescape 是经典跨浏览器方案,比手动管理 TextEncoder 更简洁。
|
|
26
|
+
*/
|
|
27
|
+
const encodeBase64 = (value: string): string => btoa(unescape(encodeURIComponent(value)))
|
|
28
|
+
|
|
29
|
+
/** encodeBase64 的逆操作,非合法 base64 时抛错,由调用方 try/catch。 */
|
|
30
|
+
const decodeBase64 = (encoded: string): string => decodeURIComponent(escape(atob(encoded)))
|
|
26
31
|
|
|
27
32
|
const DEFAULT_TAB_INFO: ITabBase = {
|
|
28
33
|
id: '',
|
|
@@ -39,7 +44,7 @@ const toTabInfo = (payload: unknown): ITabBase => {
|
|
|
39
44
|
if (!isRecord(payload)) return { ...DEFAULT_TAB_INFO }
|
|
40
45
|
|
|
41
46
|
return {
|
|
42
|
-
id: typeof payload.id === 'string' ? payload.id :
|
|
47
|
+
id: typeof payload.id === 'string' ? payload.id : undefined,
|
|
43
48
|
title: typeof payload.title === 'string' ? payload.title : DEFAULT_TAB_INFO.title,
|
|
44
49
|
iframe: typeof payload.iframe === 'boolean' ? payload.iframe : DEFAULT_TAB_INFO.iframe,
|
|
45
50
|
closable: typeof payload.closable === 'boolean' ? payload.closable : DEFAULT_TAB_INFO.closable,
|
|
@@ -76,13 +81,21 @@ export const encodeTabInfo = (tabData: ITabBase): string => {
|
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
export const decodeTabInfo = (encoded: string): ITabBase => {
|
|
79
|
-
if (encoded.length > MAX_ENCODED_TAB_INFO_LENGTH)
|
|
84
|
+
if (typeof encoded !== 'string' || encoded.length > MAX_ENCODED_TAB_INFO_LENGTH) {
|
|
85
|
+
return { ...DEFAULT_TAB_INFO }
|
|
86
|
+
}
|
|
80
87
|
|
|
88
|
+
let tabString: string
|
|
81
89
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
tabString = decodeBase64(encoded)
|
|
91
|
+
} catch {
|
|
92
|
+
return { ...DEFAULT_TAB_INFO }
|
|
93
|
+
}
|
|
85
94
|
|
|
95
|
+
if (tabString.length > MAX_DECODED_TAB_INFO_LENGTH) return { ...DEFAULT_TAB_INFO }
|
|
96
|
+
if (!tabString.trim().startsWith('{')) return decodeLegacyTabInfo(tabString)
|
|
97
|
+
|
|
98
|
+
try {
|
|
86
99
|
return toTabInfo(JSON.parse(tabString))
|
|
87
100
|
} catch {
|
|
88
101
|
return { ...DEFAULT_TAB_INFO }
|
|
@@ -137,7 +137,9 @@ export function isSameQueryIgnoringReserved(
|
|
|
137
137
|
const result: Record<string, string | string[]> = {}
|
|
138
138
|
for (const [key, value] of Object.entries(q)) {
|
|
139
139
|
if (STACK_TABS_RESERVED_QUERY_KEYS.has(key) || key === '_back') continue
|
|
140
|
-
result[key] = Array.isArray(value)
|
|
140
|
+
result[key] = Array.isArray(value)
|
|
141
|
+
? value.map((item) => String(item ?? ''))
|
|
142
|
+
: String(value ?? '')
|
|
141
143
|
}
|
|
142
144
|
return result
|
|
143
145
|
}
|
package/src/lib/versionLogger.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/** 在浏览器控制台输出带样式的库版本标识 */
|
|
2
|
-
const logVersion = (version
|
|
2
|
+
const logVersion = (version?: string) => {
|
|
3
3
|
if (typeof window === 'undefined') return
|
|
4
4
|
|
|
5
|
+
// 兜底:Vitest / SSR 等环境下 import.meta.env.PACKAGE_VERSION 缺失,
|
|
6
|
+
// 避免打印出 "vundefined" 这种丑陋的版本号。
|
|
7
|
+
const displayVersion = typeof version === 'string' && version.length > 0 ? version : 'dev'
|
|
8
|
+
|
|
5
9
|
window.console.log(
|
|
6
|
-
'%c vue-stack-tabs %c v' +
|
|
10
|
+
'%c vue-stack-tabs %c v' + displayVersion + ' %c',
|
|
7
11
|
'background:#306fff; padding:2px 8px; border-radius:3px 0 0 3px; color:#fff;',
|
|
8
12
|
'background:#00d797; padding:2px 8px; border-radius:0 3px 3px 0; color:#fff;',
|
|
9
13
|
'background:transparent'
|
package/dist/favicon.svg
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 199.49 199.67">
|
|
3
|
-
<path d="M79.24,19.73h108.91v29.7h-108.91v-29.7Z" style="fill: #79a7ff; stroke-width: 0px;"/>
|
|
4
|
-
<path d="M179.24,199.67H20.26C9.05,199.6,0,190.5,0,179.29V20.39C0,9.18,9.05.07,20.26,0h59.49c3.51.07,6.34,2.9,6.41,6.42v44.36c0,3.54-2.87,6.41-6.41,6.41-3.54,0-6.41-2.86-6.42-6.4,0,0,0,0,0,0V12.36H20.25c-4.13.07-7.44,3.44-7.44,7.56v159.41c0,4.13,3.31,7.49,7.44,7.56h158.98c4.13-.07,7.44-3.44,7.44-7.56V51.84c0-3.54,2.87-6.41,6.41-6.42,3.54,0,6.42,2.87,6.42,6.42h0v127.45c0,11.21-9.05,20.32-20.26,20.39h0Z" style="fill: #377dff; stroke-width: 0px;"/>
|
|
5
|
-
<path d="M192.8,60.77c-3.66-.07-6.61-3.02-6.68-6.68v-18.23c.09-4.29-3.32-7.83-7.6-7.92-.05,0-.11,0-.16,0h-96.27c-3.69,0-6.69-2.99-6.69-6.69s2.99-6.69,6.69-6.69h96.27c11.69.08,21.13,9.58,21.13,21.27v18.25c0,3.69-2.99,6.68-6.68,6.68,0,0,0,0,0,0h0Z" style="fill: #377dff; stroke-width: 0px;"/>
|
|
6
|
-
<path d="M193.08,58.67H10.15c-3.61-.08-6.48-3.07-6.41-6.68-.08-3.62,2.79-6.61,6.41-6.69h182.93c3.62.08,6.48,3.08,6.41,6.69.07,3.61-2.79,6.6-6.41,6.68Z" style="fill: #377dff; stroke-width: 0px;"/>
|
|
7
|
-
<path d="M137.2,58.67c-3.42.24-6.4-2.32-6.68-5.74v-28.15c.55-3.69,3.99-6.24,7.69-5.69,2.94.44,5.25,2.75,5.69,5.69v28.15c-.28,3.42-3.26,5.98-6.68,5.74h0Z" style="fill: #377dff; stroke-width: 0px;"/>
|
|
8
|
-
<path d="M99.78,100.48l-12.83-22.22h-18.39l31.22,54.05,31.22-54.05h-18.39l-12.83,22.22Z" style="fill: #00d797; stroke-width: 0px;"/>
|
|
9
|
-
<path d="M135.28,78.26l-35.49,61.43-35.44-61.43h-20.11l55.55,96.2,55.55-96.2h-20.05Z" style="fill: #306fff; stroke-width: 0px;"/>
|
|
10
|
-
</svg>
|
package/dist/iframe-bridge.html
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Iframe Bridge 测试页面</title>
|
|
7
|
-
<style>
|
|
8
|
-
body {
|
|
9
|
-
font-family: system-ui, sans-serif;
|
|
10
|
-
padding: 20px;
|
|
11
|
-
background: #f0f2f5;
|
|
12
|
-
}
|
|
13
|
-
.card {
|
|
14
|
-
background: white;
|
|
15
|
-
padding: 20px;
|
|
16
|
-
border-radius: 8px;
|
|
17
|
-
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
18
|
-
}
|
|
19
|
-
button {
|
|
20
|
-
padding: 10px 20px;
|
|
21
|
-
background: #67c23a;
|
|
22
|
-
color: white;
|
|
23
|
-
border: none;
|
|
24
|
-
border-radius: 4px;
|
|
25
|
-
cursor: pointer;
|
|
26
|
-
}
|
|
27
|
-
.status {
|
|
28
|
-
margin-top: 10px;
|
|
29
|
-
color: #606266;
|
|
30
|
-
}
|
|
31
|
-
</style>
|
|
32
|
-
</head>
|
|
33
|
-
<body>
|
|
34
|
-
<div class="card">
|
|
35
|
-
<h2>此页面演示使用封装好的 Bridge 对接方式</h2>
|
|
36
|
-
<p>页面加载时间: <span id="time"></span></p>
|
|
37
|
-
<p>
|
|
38
|
-
使用 <code>iframeBridge.ts</code> 中提供的 <code>postOpenTab</code> 和
|
|
39
|
-
<code>onRefreshRequest</code>。
|
|
40
|
-
</p>
|
|
41
|
-
|
|
42
|
-
<div style="margin: 20px 0">
|
|
43
|
-
<button id="btnOpen">使用 Bridge 方法请求打开新 Tab</button>
|
|
44
|
-
</div>
|
|
45
|
-
<div class="status" id="status"></div>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<!-- 注意:在实际项目中,如果是原生 HTML 需要引入编译后的 Bridge 脚本,或者直接使用 type="module" -->
|
|
49
|
-
<script type="module">
|
|
50
|
-
// 这里的导入路径仅为本地调试示意,实际推荐将 Bridge 打包为公共库或 CDN 引用
|
|
51
|
-
import { postOpenTab, onRefreshRequest } from './src/lib/utils/iframeBridge.ts'
|
|
52
|
-
|
|
53
|
-
const timeEl = document.getElementById('time')
|
|
54
|
-
timeEl.innerText = new Date().toLocaleTimeString()
|
|
55
|
-
|
|
56
|
-
// 1. 使用 Bridge 封装的方法打开 Tab
|
|
57
|
-
document.getElementById('btnOpen').onclick = () => {
|
|
58
|
-
postOpenTab({
|
|
59
|
-
id: 'bridge_triggered_tab',
|
|
60
|
-
title: 'Bridge 弹出的新页签',
|
|
61
|
-
path: '/demo/page1',
|
|
62
|
-
closable: true
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 2. 使用 Bridge 封装的方法监听刷新请求
|
|
67
|
-
onRefreshRequest(() => {
|
|
68
|
-
console.log('[Bridge Child] Received refresh request from parent')
|
|
69
|
-
const statusEl = document.getElementById('status')
|
|
70
|
-
statusEl.innerText = '正在执行刷新 logic...'
|
|
71
|
-
setTimeout(() => {
|
|
72
|
-
location.reload()
|
|
73
|
-
}, 500)
|
|
74
|
-
})
|
|
75
|
-
</script>
|
|
76
|
-
</body>
|
|
77
|
-
</html>
|
package/dist/iframe-child.html
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Iframe 子页面测试</title>
|
|
7
|
-
<style>
|
|
8
|
-
body {
|
|
9
|
-
font-family: system-ui, sans-serif;
|
|
10
|
-
padding: 20px;
|
|
11
|
-
background: #f0f9eb;
|
|
12
|
-
}
|
|
13
|
-
.card {
|
|
14
|
-
background: white;
|
|
15
|
-
padding: 20px;
|
|
16
|
-
border-radius: 8px;
|
|
17
|
-
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
18
|
-
}
|
|
19
|
-
button {
|
|
20
|
-
padding: 10px 20px;
|
|
21
|
-
background: #409eff;
|
|
22
|
-
color: white;
|
|
23
|
-
border: none;
|
|
24
|
-
border-radius: 4px;
|
|
25
|
-
cursor: pointer;
|
|
26
|
-
}
|
|
27
|
-
input {
|
|
28
|
-
padding: 8px;
|
|
29
|
-
width: 200px;
|
|
30
|
-
margin-right: 10px;
|
|
31
|
-
border: 1px solid #dcdfe6;
|
|
32
|
-
border-radius: 4px;
|
|
33
|
-
}
|
|
34
|
-
</style>
|
|
35
|
-
</head>
|
|
36
|
-
<body>
|
|
37
|
-
<div class="card">
|
|
38
|
-
<h2>此页面运行在隔离的 iframe 中</h2>
|
|
39
|
-
<p>页面加载时间: <span id="time"></span></p>
|
|
40
|
-
<p>
|
|
41
|
-
用于测试 iframe 页面的加载、刷新隔离,以及通过 `postMessage` 向外层通信请求打开新 Tab
|
|
42
|
-
的功能。
|
|
43
|
-
</p>
|
|
44
|
-
|
|
45
|
-
<div style="margin: 20px 0">
|
|
46
|
-
<label>隔离缓存测试输入框:</label>
|
|
47
|
-
<input type="text" id="ipt" placeholder="切走该页签是否会销毁内容?" />
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<div style="margin: 20px 0">
|
|
51
|
-
<button onclick="requestNewTab()">向父级发送 postMessage 请求在外部打开新 Tab</button>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<script>
|
|
56
|
-
const timeEl = document.getElementById('time')
|
|
57
|
-
const updateTime = () => {
|
|
58
|
-
timeEl.innerText = new Date().toLocaleTimeString()
|
|
59
|
-
}
|
|
60
|
-
updateTime()
|
|
61
|
-
|
|
62
|
-
// 1. 原生对接:向父窗口请求打开新标签
|
|
63
|
-
function requestNewTab() {
|
|
64
|
-
window.parent.postMessage(
|
|
65
|
-
{
|
|
66
|
-
type: 'vue-stack-tabs:openTab', // 推荐使用该标准类型,StackTabs 已支持兼容 'openTab'
|
|
67
|
-
payload: {
|
|
68
|
-
id: 'native_triggered_tab',
|
|
69
|
-
title: '原生弹出的新页签',
|
|
70
|
-
path: '/demo/page2',
|
|
71
|
-
closable: true
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
'*'
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// 2. 原生对接:监听父窗口发送的刷新请求
|
|
79
|
-
window.addEventListener('message', (ev) => {
|
|
80
|
-
if (ev.data?.type === 'vue-stack-tabs:refresh') {
|
|
81
|
-
console.log('[Native Child] Received refresh request from parent')
|
|
82
|
-
// 执行刷新逻辑(例如重新加载页面或局部数据更新)
|
|
83
|
-
location.reload()
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
|
-
</script>
|
|
87
|
-
</body>
|
|
88
|
-
</html>
|
package/dist/nuxt/favicon.svg
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 199.49 199.67">
|
|
3
|
-
<path d="M79.24,19.73h108.91v29.7h-108.91v-29.7Z" style="fill: #79a7ff; stroke-width: 0px;"/>
|
|
4
|
-
<path d="M179.24,199.67H20.26C9.05,199.6,0,190.5,0,179.29V20.39C0,9.18,9.05.07,20.26,0h59.49c3.51.07,6.34,2.9,6.41,6.42v44.36c0,3.54-2.87,6.41-6.41,6.41-3.54,0-6.41-2.86-6.42-6.4,0,0,0,0,0,0V12.36H20.25c-4.13.07-7.44,3.44-7.44,7.56v159.41c0,4.13,3.31,7.49,7.44,7.56h158.98c4.13-.07,7.44-3.44,7.44-7.56V51.84c0-3.54,2.87-6.41,6.41-6.42,3.54,0,6.42,2.87,6.42,6.42h0v127.45c0,11.21-9.05,20.32-20.26,20.39h0Z" style="fill: #377dff; stroke-width: 0px;"/>
|
|
5
|
-
<path d="M192.8,60.77c-3.66-.07-6.61-3.02-6.68-6.68v-18.23c.09-4.29-3.32-7.83-7.6-7.92-.05,0-.11,0-.16,0h-96.27c-3.69,0-6.69-2.99-6.69-6.69s2.99-6.69,6.69-6.69h96.27c11.69.08,21.13,9.58,21.13,21.27v18.25c0,3.69-2.99,6.68-6.68,6.68,0,0,0,0,0,0h0Z" style="fill: #377dff; stroke-width: 0px;"/>
|
|
6
|
-
<path d="M193.08,58.67H10.15c-3.61-.08-6.48-3.07-6.41-6.68-.08-3.62,2.79-6.61,6.41-6.69h182.93c3.62.08,6.48,3.08,6.41,6.69.07,3.61-2.79,6.6-6.41,6.68Z" style="fill: #377dff; stroke-width: 0px;"/>
|
|
7
|
-
<path d="M137.2,58.67c-3.42.24-6.4-2.32-6.68-5.74v-28.15c.55-3.69,3.99-6.24,7.69-5.69,2.94.44,5.25,2.75,5.69,5.69v28.15c-.28,3.42-3.26,5.98-6.68,5.74h0Z" style="fill: #377dff; stroke-width: 0px;"/>
|
|
8
|
-
<path d="M99.78,100.48l-12.83-22.22h-18.39l31.22,54.05,31.22-54.05h-18.39l-12.83,22.22Z" style="fill: #00d797; stroke-width: 0px;"/>
|
|
9
|
-
<path d="M135.28,78.26l-35.49,61.43-35.44-61.43h-20.11l55.55,96.2,55.55-96.2h-20.05Z" style="fill: #306fff; stroke-width: 0px;"/>
|
|
10
|
-
</svg>
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Iframe Bridge 测试页面</title>
|
|
7
|
-
<style>
|
|
8
|
-
body {
|
|
9
|
-
font-family: system-ui, sans-serif;
|
|
10
|
-
padding: 20px;
|
|
11
|
-
background: #f0f2f5;
|
|
12
|
-
}
|
|
13
|
-
.card {
|
|
14
|
-
background: white;
|
|
15
|
-
padding: 20px;
|
|
16
|
-
border-radius: 8px;
|
|
17
|
-
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
18
|
-
}
|
|
19
|
-
button {
|
|
20
|
-
padding: 10px 20px;
|
|
21
|
-
background: #67c23a;
|
|
22
|
-
color: white;
|
|
23
|
-
border: none;
|
|
24
|
-
border-radius: 4px;
|
|
25
|
-
cursor: pointer;
|
|
26
|
-
}
|
|
27
|
-
.status {
|
|
28
|
-
margin-top: 10px;
|
|
29
|
-
color: #606266;
|
|
30
|
-
}
|
|
31
|
-
</style>
|
|
32
|
-
</head>
|
|
33
|
-
<body>
|
|
34
|
-
<div class="card">
|
|
35
|
-
<h2>此页面演示使用封装好的 Bridge 对接方式</h2>
|
|
36
|
-
<p>页面加载时间: <span id="time"></span></p>
|
|
37
|
-
<p>
|
|
38
|
-
使用 <code>iframeBridge.ts</code> 中提供的 <code>postOpenTab</code> 和
|
|
39
|
-
<code>onRefreshRequest</code>。
|
|
40
|
-
</p>
|
|
41
|
-
|
|
42
|
-
<div style="margin: 20px 0">
|
|
43
|
-
<button id="btnOpen">使用 Bridge 方法请求打开新 Tab</button>
|
|
44
|
-
</div>
|
|
45
|
-
<div class="status" id="status"></div>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<!-- 注意:在实际项目中,如果是原生 HTML 需要引入编译后的 Bridge 脚本,或者直接使用 type="module" -->
|
|
49
|
-
<script type="module">
|
|
50
|
-
// 这里的导入路径仅为本地调试示意,实际推荐将 Bridge 打包为公共库或 CDN 引用
|
|
51
|
-
import { postOpenTab, onRefreshRequest } from './src/lib/utils/iframeBridge.ts'
|
|
52
|
-
|
|
53
|
-
const timeEl = document.getElementById('time')
|
|
54
|
-
timeEl.innerText = new Date().toLocaleTimeString()
|
|
55
|
-
|
|
56
|
-
// 1. 使用 Bridge 封装的方法打开 Tab
|
|
57
|
-
document.getElementById('btnOpen').onclick = () => {
|
|
58
|
-
postOpenTab({
|
|
59
|
-
id: 'bridge_triggered_tab',
|
|
60
|
-
title: 'Bridge 弹出的新页签',
|
|
61
|
-
path: '/demo/page1',
|
|
62
|
-
closable: true
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 2. 使用 Bridge 封装的方法监听刷新请求
|
|
67
|
-
onRefreshRequest(() => {
|
|
68
|
-
console.log('[Bridge Child] Received refresh request from parent')
|
|
69
|
-
const statusEl = document.getElementById('status')
|
|
70
|
-
statusEl.innerText = '正在执行刷新 logic...'
|
|
71
|
-
setTimeout(() => {
|
|
72
|
-
location.reload()
|
|
73
|
-
}, 500)
|
|
74
|
-
})
|
|
75
|
-
</script>
|
|
76
|
-
</body>
|
|
77
|
-
</html>
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Iframe 子页面测试</title>
|
|
7
|
-
<style>
|
|
8
|
-
body {
|
|
9
|
-
font-family: system-ui, sans-serif;
|
|
10
|
-
padding: 20px;
|
|
11
|
-
background: #f0f9eb;
|
|
12
|
-
}
|
|
13
|
-
.card {
|
|
14
|
-
background: white;
|
|
15
|
-
padding: 20px;
|
|
16
|
-
border-radius: 8px;
|
|
17
|
-
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
18
|
-
}
|
|
19
|
-
button {
|
|
20
|
-
padding: 10px 20px;
|
|
21
|
-
background: #409eff;
|
|
22
|
-
color: white;
|
|
23
|
-
border: none;
|
|
24
|
-
border-radius: 4px;
|
|
25
|
-
cursor: pointer;
|
|
26
|
-
}
|
|
27
|
-
input {
|
|
28
|
-
padding: 8px;
|
|
29
|
-
width: 200px;
|
|
30
|
-
margin-right: 10px;
|
|
31
|
-
border: 1px solid #dcdfe6;
|
|
32
|
-
border-radius: 4px;
|
|
33
|
-
}
|
|
34
|
-
</style>
|
|
35
|
-
</head>
|
|
36
|
-
<body>
|
|
37
|
-
<div class="card">
|
|
38
|
-
<h2>此页面运行在隔离的 iframe 中</h2>
|
|
39
|
-
<p>页面加载时间: <span id="time"></span></p>
|
|
40
|
-
<p>
|
|
41
|
-
用于测试 iframe 页面的加载、刷新隔离,以及通过 `postMessage` 向外层通信请求打开新 Tab
|
|
42
|
-
的功能。
|
|
43
|
-
</p>
|
|
44
|
-
|
|
45
|
-
<div style="margin: 20px 0">
|
|
46
|
-
<label>隔离缓存测试输入框:</label>
|
|
47
|
-
<input type="text" id="ipt" placeholder="切走该页签是否会销毁内容?" />
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<div style="margin: 20px 0">
|
|
51
|
-
<button onclick="requestNewTab()">向父级发送 postMessage 请求在外部打开新 Tab</button>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<script>
|
|
56
|
-
const timeEl = document.getElementById('time')
|
|
57
|
-
const updateTime = () => {
|
|
58
|
-
timeEl.innerText = new Date().toLocaleTimeString()
|
|
59
|
-
}
|
|
60
|
-
updateTime()
|
|
61
|
-
|
|
62
|
-
// 1. 原生对接:向父窗口请求打开新标签
|
|
63
|
-
function requestNewTab() {
|
|
64
|
-
window.parent.postMessage(
|
|
65
|
-
{
|
|
66
|
-
type: 'vue-stack-tabs:openTab', // 推荐使用该标准类型,StackTabs 已支持兼容 'openTab'
|
|
67
|
-
payload: {
|
|
68
|
-
id: 'native_triggered_tab',
|
|
69
|
-
title: '原生弹出的新页签',
|
|
70
|
-
path: '/demo/page2',
|
|
71
|
-
closable: true
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
'*'
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// 2. 原生对接:监听父窗口发送的刷新请求
|
|
79
|
-
window.addEventListener('message', (ev) => {
|
|
80
|
-
if (ev.data?.type === 'vue-stack-tabs:refresh') {
|
|
81
|
-
console.log('[Native Child] Received refresh request from parent')
|
|
82
|
-
// 执行刷新逻辑(例如重新加载页面或局部数据更新)
|
|
83
|
-
location.reload()
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
|
-
</script>
|
|
87
|
-
</body>
|
|
88
|
-
</html>
|
package/dist/nuxt/plugin.mjs
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { defineNuxtPlugin as e, useRuntimeConfig as t } from "nuxt/app";
|
|
2
|
-
import n from "vue-stack-tabs";
|
|
3
|
-
//#region src/lib/nuxt/runtime/plugin.ts
|
|
4
|
-
var r = e((e) => {
|
|
5
|
-
let r = t().public.vueStackTabs;
|
|
6
|
-
e.vueApp.use(n, [{ locale: r?.locale ?? "zh-CN" }]);
|
|
7
|
-
});
|
|
8
|
-
//#endregion
|
|
9
|
-
export { r as default };
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* iframe-bridge 子入口(vue-stack-tabs/iframe-bridge)。
|
|
3
|
-
*
|
|
4
|
-
* 供 iframe 内部页面引用,不包含 Vue 组件和样式等副作用,
|
|
5
|
-
* 仅导出 postMessage 通信工具。
|
|
6
|
-
*/
|
|
7
|
-
export { MSG_OPEN_TAB, MSG_REFRESH, onRefreshRequest, postOpenTab } from './utils/iframeBridge';
|
|
8
|
-
export type { IframeBridgeOptions, IframeOpenTabPayload, RefreshRequestOptions } from './utils/iframeBridge';
|
package/dist/src/lib/index.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { App } from 'vue';
|
|
2
|
-
import { LocaleMessageOption } from './i18n';
|
|
3
|
-
import { default as StackTab } from './StackTabs.vue';
|
|
4
|
-
import { default as IFrame } from './iframe.vue';
|
|
5
|
-
import { default as useTabLoading } from './hooks/useTabLoading';
|
|
6
|
-
import { default as useTabRouter } from './hooks/useTabRouter';
|
|
7
|
-
import { default as useTabActions } from './hooks/useTabActions';
|
|
8
|
-
import { default as TabHeaderButton } from './components/TabHeader/TabHeaderButton.vue';
|
|
9
|
-
export type VueStackTabsPluginOptions = LocaleMessageOption[];
|
|
10
|
-
export * from './model/TabModel';
|
|
11
|
-
declare module '@vue/runtime-core' {
|
|
12
|
-
interface GlobalComponents {
|
|
13
|
-
VueStackTabs: typeof StackTab;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
export { IFrame, useTabActions, useTabLoading, useTabRouter, TabHeaderButton, StackTab, StackTab as VueStackTabs };
|
|
17
|
-
export { postOpenTab, onRefreshRequest, MSG_REFRESH, MSG_OPEN_TAB } from './utils/iframeBridge';
|
|
18
|
-
export type { IframeOpenTabPayload } from './utils/iframeBridge';
|
|
19
|
-
declare const _default: {
|
|
20
|
-
install(Vue: App, options?: VueStackTabsPluginOptions): void;
|
|
21
|
-
};
|
|
22
|
-
export default _default;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { LocationQueryRaw } from 'vue-router';
|
|
2
|
-
/**
|
|
3
|
-
* iframe 与父窗口的 postMessage 桥接
|
|
4
|
-
*
|
|
5
|
-
* 在 iframe 内的页面中引入,用于:
|
|
6
|
-
* - 接收父窗口的刷新指令并执行
|
|
7
|
-
* - 向父窗口发送打开新标签的请求
|
|
8
|
-
*/
|
|
9
|
-
/** 刷新消息:父 -> iframe,iframe 内页面需监听并执行 location.reload() 或自身刷新逻辑 */
|
|
10
|
-
export declare const MSG_REFRESH = "vue-stack-tabs:refresh";
|
|
11
|
-
/** 打开标签消息:iframe -> 父,payload 为 ITabData */
|
|
12
|
-
export declare const MSG_OPEN_TAB = "vue-stack-tabs:openTab";
|
|
13
|
-
export interface IframeBridgeOptions {
|
|
14
|
-
targetOrigin?: string;
|
|
15
|
-
}
|
|
16
|
-
export interface RefreshRequestOptions {
|
|
17
|
-
allowedOrigins?: string[];
|
|
18
|
-
}
|
|
19
|
-
export interface IframeOpenTabPayload {
|
|
20
|
-
id?: string;
|
|
21
|
-
title: string;
|
|
22
|
-
path: string;
|
|
23
|
-
query?: LocationQueryRaw;
|
|
24
|
-
closable?: boolean;
|
|
25
|
-
refreshable?: boolean;
|
|
26
|
-
iframe?: boolean;
|
|
27
|
-
iframeRefreshMode?: 'postMessage' | 'reload';
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* 在 iframe 内调用:向父窗口请求打开新标签
|
|
31
|
-
* @param payload 标签数据,至少需 title、path
|
|
32
|
-
*/
|
|
33
|
-
export declare function postOpenTab(payload: IframeOpenTabPayload, options?: IframeBridgeOptions): void;
|
|
34
|
-
/**
|
|
35
|
-
* 在 iframe 内调用:注册刷新监听,收到父窗口刷新指令时执行 callback
|
|
36
|
-
* @param callback 默认 location.reload(),可自定义
|
|
37
|
-
* @returns 取消监听的函数
|
|
38
|
-
*/
|
|
39
|
-
export declare function onRefreshRequest(callback?: () => void, options?: RefreshRequestOptions): () => void;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { defineNuxtPlugin, useRuntimeConfig } from 'nuxt/app'
|
|
2
|
-
import VueStackTabs from 'vue-stack-tabs'
|
|
3
|
-
|
|
4
|
-
export default defineNuxtPlugin((nuxtApp) => {
|
|
5
|
-
const config = useRuntimeConfig().public.vueStackTabs as {
|
|
6
|
-
locale?: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
nuxtApp.vueApp.use(VueStackTabs, [{ locale: config?.locale ?? 'zh-CN' }])
|
|
10
|
-
})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|