vue-stack-tabs 1.3.0 → 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.
Files changed (96) hide show
  1. package/README.en.md +119 -52
  2. package/README.md +104 -41
  3. package/README.ru.md +125 -54
  4. package/dist/{src/lib/StackTabs.vue.d.ts → StackTabs.vue.d.ts} +5 -5
  5. package/dist/{src/lib/components → components}/ContextMenu/index.vue.d.ts +1 -1
  6. package/dist/components/StackKeepAlive/emptyPlaceholder.d.ts +3 -0
  7. package/dist/{src/lib/components → components}/TabHeader/TabHeaderItem.vue.d.ts +9 -0
  8. package/dist/{src/lib/components → components}/TabHeader/index.vue.d.ts +3 -3
  9. package/dist/{src/lib/hooks → hooks}/stackTabsContext.d.ts +5 -2
  10. package/dist/hooks/tabPanel/pageComponentFactory.d.ts +20 -0
  11. package/dist/{src/lib/hooks → hooks}/tabPanel/scroll.d.ts +2 -0
  12. package/dist/hooks/useScrollShadow.d.ts +20 -0
  13. package/dist/{src/lib/hooks → hooks}/useTabActions.d.ts +7 -2
  14. package/dist/{src/lib/hooks → hooks}/useTabEventBus.d.ts +2 -1
  15. package/dist/{src/lib/hooks → hooks}/useTabPanel.d.ts +3 -1
  16. package/dist/iframe-bridge.mjs +1 -1
  17. package/dist/index.d.ts +22 -6
  18. package/dist/{src/lib/model → model}/TabModel.d.ts +21 -0
  19. package/dist/nuxt/module.mjs +10 -6
  20. package/dist/nuxt/runtime/plugin.mjs +1600 -4
  21. package/dist/style.css +1 -1
  22. package/dist/utils/iframeBridge.d.ts +7 -1
  23. package/dist/{src/lib/utils → utils}/scrollUtils.d.ts +7 -1
  24. package/dist/{src/lib/versionLogger.d.ts → versionLogger.d.ts} +1 -1
  25. package/dist/vue-stack-tabs.css +1 -1
  26. package/dist/vue-stack-tabs.es.js +1 -1
  27. package/package.json +83 -59
  28. package/src/lib/StackTabs.vue +16 -3
  29. package/src/lib/assets/style/common/var.scss +2 -0
  30. package/src/lib/assets/style/stackTab.scss +43 -4
  31. package/src/lib/components/ContextMenu/index.vue +12 -3
  32. package/src/lib/components/PageLoading.vue +10 -2
  33. package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +3 -9
  34. package/src/lib/components/StackKeepAlive/emptyPlaceholder.tsx +18 -0
  35. package/src/lib/components/TabHeader/TabHeaderItem.vue +30 -2
  36. package/src/lib/components/TabHeader/index.vue +76 -4
  37. package/src/lib/hooks/stackTabsContext.ts +23 -18
  38. package/src/lib/hooks/tabPanel/pageComponentFactory.tsx +142 -0
  39. package/src/lib/hooks/tabPanel/refresh.ts +3 -0
  40. package/src/lib/hooks/tabPanel/scroll.ts +110 -7
  41. package/src/lib/hooks/tabPanel/session.ts +29 -3
  42. package/src/lib/hooks/useContextMenu.ts +57 -11
  43. package/src/lib/hooks/useIframeManager.ts +18 -2
  44. package/src/lib/hooks/useScrollShadow.ts +124 -0
  45. package/src/lib/hooks/useTabActions.ts +20 -8
  46. package/src/lib/hooks/useTabEventBus.ts +5 -3
  47. package/src/lib/hooks/useTabPanel.tsx +132 -180
  48. package/src/lib/hooks/useTabRouter.ts +11 -2
  49. package/src/lib/i18n/index.ts +3 -3
  50. package/src/lib/model/TabModel.ts +40 -1
  51. package/src/lib/nuxt/module.ts +10 -3
  52. package/src/lib/nuxt/runtime/plugin.ts +13 -2
  53. package/src/lib/utils/iframeBridge.ts +9 -1
  54. package/src/lib/utils/scrollUtils.ts +35 -2
  55. package/src/lib/utils/tabInfoEncoder.ts +21 -8
  56. package/src/lib/utils/urlParser.ts +3 -1
  57. package/src/lib/versionLogger.ts +6 -2
  58. package/dist/favicon.svg +0 -10
  59. package/dist/iframe-bridge.html +0 -77
  60. package/dist/iframe-child.html +0 -88
  61. package/dist/nuxt/favicon.svg +0 -10
  62. package/dist/nuxt/iframe-bridge.html +0 -77
  63. package/dist/nuxt/iframe-child.html +0 -88
  64. package/dist/nuxt/plugin.mjs +0 -9
  65. package/dist/src/lib/iframe-bridge.d.ts +0 -8
  66. package/dist/src/lib/index.d.ts +0 -22
  67. package/dist/src/lib/nuxt/module.d.ts +0 -6
  68. package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +0 -2
  69. package/dist/src/lib/nuxt/runtime/plugin.d.ts +0 -2
  70. package/dist/src/lib/utils/iframeBridge.d.ts +0 -39
  71. package/src/lib/nuxt/runtime/plugin.client.ts +0 -10
  72. /package/dist/{src/lib/components → components}/ContextMenu/ContextMenuItem.vue.d.ts +0 -0
  73. /package/dist/{src/lib/components → components}/PageLoading.vue.d.ts +0 -0
  74. /package/dist/{src/lib/components → components}/StackKeepAlive/StackCacheRenderer.vue.d.ts +0 -0
  75. /package/dist/{src/lib/components → components}/StackKeepAlive/StackKeepAlive.vue.d.ts +0 -0
  76. /package/dist/{src/lib/components → components}/TabHeader/TabHeaderButton.vue.d.ts +0 -0
  77. /package/dist/{src/lib/components → components}/TabHeader/TabHeaderScroll.vue.d.ts +0 -0
  78. /package/dist/{src/lib/hooks → hooks}/tabPanel/evict.d.ts +0 -0
  79. /package/dist/{src/lib/hooks → hooks}/tabPanel/navigationTransaction.d.ts +0 -0
  80. /package/dist/{src/lib/hooks → hooks}/tabPanel/refresh.d.ts +0 -0
  81. /package/dist/{src/lib/hooks → hooks}/tabPanel/session.d.ts +0 -0
  82. /package/dist/{src/lib/hooks → hooks}/tabPanel/state.d.ts +0 -0
  83. /package/dist/{src/lib/hooks → hooks}/useContextMenu.d.ts +0 -0
  84. /package/dist/{src/lib/hooks → hooks}/useIframeManager.d.ts +0 -0
  85. /package/dist/{src/lib/hooks → hooks}/useTabLoading.d.ts +0 -0
  86. /package/dist/{src/lib/hooks → hooks}/useTabRouter.d.ts +0 -0
  87. /package/dist/{src/lib/i18n → i18n}/index.d.ts +0 -0
  88. /package/dist/{src/lib/i18n → i18n}/lang/en.d.ts +0 -0
  89. /package/dist/{src/lib/i18n → i18n}/lang/zh-CN.d.ts +0 -0
  90. /package/dist/{src/lib/i18n → i18n}/stackTabsLocale.d.ts +0 -0
  91. /package/dist/{src/lib/iframe.vue.d.ts → iframe.vue.d.ts} +0 -0
  92. /package/dist/{src/lib/utils → utils}/stackTabsA11y.d.ts +0 -0
  93. /package/dist/{src/lib/utils → utils}/stackTabsMessage.d.ts +0 -0
  94. /package/dist/{src/lib/utils → utils}/tabInfoEncoder.d.ts +0 -0
  95. /package/dist/{src/lib/utils → utils}/typeGuards.d.ts +0 -0
  96. /package/dist/{src/lib/utils → utils}/urlParser.d.ts +0 -0
@@ -53,8 +53,14 @@ export function postOpenTab(
53
53
  }
54
54
 
55
55
  /**
56
- * 在 iframe 内调用:注册刷新监听,收到父窗口刷新指令时执行 callback
56
+ * 在 iframe 内调用:注册刷新监听,收到父窗口刷新指令时执行 callback。
57
+ *
58
+ * **安全提示**:`options.allowedOrigins` 为空数组(默认值)时**不校验 origin**,
59
+ * 等同 `targetOrigin: '*'`,任何嵌入此 iframe 的父页面都能触发回调。
60
+ * 生产环境务必显式传入可信 origin 列表,例如 `['https://app.example.com']`。
61
+ *
57
62
  * @param callback 默认 location.reload(),可自定义
63
+ * @param options 可选配置,`allowedOrigins` 强烈建议显式传入
58
64
  * @returns 取消监听的函数
59
65
  */
60
66
  export function onRefreshRequest(
@@ -66,6 +72,8 @@ export function onRefreshRequest(
66
72
  const allowedOrigins = options.allowedOrigins ?? []
67
73
  const handler = (ev: MessageEvent) => {
68
74
  if (ev.source !== window.parent) return
75
+ // allowedOrigins 为空时跳过 origin 校验:刻意保留以便本地调试,
76
+ // 但必须在文档中向用户强调风险(见上方 JSDoc)。
69
77
  if (allowedOrigins.length > 0 && !allowedOrigins.includes(ev.origin)) return
70
78
  if (ev.data?.type === MSG_REFRESH) {
71
79
  callback()
@@ -91,13 +91,46 @@ export const getScrollbarWidth = (function () {
91
91
 
92
92
  const Z_INDEX_CEILING = 89999
93
93
 
94
- /** 获取指定选择器下元素的最大 z-index + 1,用于最大化时置顶 */
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
- return max + 1
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
- const encodeBase64 = (value: string): string => encodeURIComponent(btoa(encodeURIComponent(value)))
24
- const decodeBase64 = (encoded: string): string =>
25
- decodeURIComponent(atob(decodeURIComponent(encoded)))
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 : DEFAULT_TAB_INFO.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) return { ...DEFAULT_TAB_INFO }
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
- const tabString = decodeBase64(encoded)
83
- if (tabString.length > MAX_DECODED_TAB_INFO_LENGTH) return { ...DEFAULT_TAB_INFO }
84
- if (!tabString.trim().startsWith('{')) return decodeLegacyTabInfo(tabString)
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) ? value.map((item) => String(item ?? '')) : String(value ?? '')
140
+ result[key] = Array.isArray(value)
141
+ ? value.map((item) => String(item ?? ''))
142
+ : String(value ?? '')
141
143
  }
142
144
  return result
143
145
  }
@@ -1,9 +1,13 @@
1
1
  /** 在浏览器控制台输出带样式的库版本标识 */
2
- const logVersion = (version: string) => {
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' + version + ' %c',
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>
@@ -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>
@@ -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>
@@ -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';
@@ -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,6 +0,0 @@
1
- export interface ModuleOptions {
2
- /** 默认语言 */
3
- locale?: string;
4
- }
5
- declare const _default: import('nuxt/schema').NuxtModule<ModuleOptions, ModuleOptions, false>;
6
- export default _default;
@@ -1,2 +0,0 @@
1
- declare const _default: import('nuxt/app').Plugin<Record<string, unknown>> & import('nuxt/app').ObjectPlugin<Record<string, unknown>>;
2
- export default _default;
@@ -1,2 +0,0 @@
1
- declare const _default: import('nuxt/app').Plugin<Record<string, unknown>> & import('nuxt/app').ObjectPlugin<Record<string, unknown>>;
2
- 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
- })