vue-stack-tabs 1.1.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.en.md +526 -0
  2. package/README.md +141 -39
  3. package/README.ru.md +526 -0
  4. package/dist/iframe-bridge.d.ts +8 -0
  5. package/dist/iframe-bridge.html +77 -0
  6. package/dist/iframe-bridge.mjs +20 -0
  7. package/dist/iframe-child.html +19 -6
  8. package/dist/nuxt/favicon.svg +10 -0
  9. package/dist/nuxt/iframe-bridge.html +77 -0
  10. package/dist/nuxt/iframe-child.html +88 -0
  11. package/dist/nuxt/module.d.ts +6 -0
  12. package/dist/nuxt/module.mjs +28 -0
  13. package/dist/nuxt/plugin.mjs +9 -0
  14. package/dist/nuxt/runtime/plugin.d.ts +2 -0
  15. package/dist/nuxt/runtime/plugin.mjs +9 -0
  16. package/dist/src/lib/StackTabs.vue.d.ts +30 -16
  17. package/dist/src/lib/components/ContextMenu/ContextMenuItem.vue.d.ts +7 -1
  18. package/dist/src/lib/components/ContextMenu/index.vue.d.ts +5 -4
  19. package/dist/src/lib/components/PageLoading.vue.d.ts +2 -1
  20. package/dist/src/lib/components/StackKeepAlive/StackCacheRenderer.vue.d.ts +25 -0
  21. package/dist/src/lib/components/StackKeepAlive/StackKeepAlive.vue.d.ts +12 -0
  22. package/dist/src/lib/components/TabHeader/TabHeaderButton.vue.d.ts +11 -12
  23. package/dist/src/lib/components/TabHeader/TabHeaderItem.vue.d.ts +5 -4
  24. package/dist/src/lib/components/TabHeader/TabHeaderScroll.vue.d.ts +8 -22
  25. package/dist/src/lib/components/TabHeader/index.vue.d.ts +11 -164
  26. package/dist/src/lib/hooks/stackTabsContext.d.ts +49 -0
  27. package/dist/src/lib/hooks/tabPanel/evict.d.ts +21 -61
  28. package/dist/src/lib/hooks/tabPanel/navigationTransaction.d.ts +35 -0
  29. package/dist/src/lib/hooks/tabPanel/refresh.d.ts +17 -0
  30. package/dist/src/lib/hooks/tabPanel/scroll.d.ts +8 -8
  31. package/dist/src/lib/hooks/tabPanel/session.d.ts +9 -8
  32. package/dist/src/lib/hooks/tabPanel/state.d.ts +20 -72
  33. package/dist/src/lib/hooks/useIframeManager.d.ts +35 -0
  34. package/dist/src/lib/hooks/useTabActions.d.ts +5 -46
  35. package/dist/src/lib/hooks/useTabEventBus.d.ts +22 -9
  36. package/dist/src/lib/hooks/useTabPanel.d.ts +14 -73
  37. package/dist/src/lib/i18n/index.d.ts +6 -4
  38. package/dist/src/lib/i18n/lang/en.d.ts +5 -0
  39. package/dist/src/lib/i18n/lang/zh-CN.d.ts +5 -0
  40. package/dist/src/lib/i18n/stackTabsLocale.d.ts +2 -0
  41. package/dist/src/lib/iframe-bridge.d.ts +8 -0
  42. package/dist/src/lib/iframe.vue.d.ts +2 -1
  43. package/dist/src/lib/index.d.ts +4 -5
  44. package/dist/src/lib/model/TabModel.d.ts +11 -40
  45. package/dist/src/lib/nuxt/module.d.ts +6 -0
  46. package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +2 -0
  47. package/dist/src/lib/nuxt/runtime/plugin.d.ts +2 -0
  48. package/dist/src/lib/utils/iframeBridge.d.ts +11 -3
  49. package/dist/src/lib/utils/stackTabsA11y.d.ts +8 -0
  50. package/dist/src/lib/utils/stackTabsMessage.d.ts +7 -0
  51. package/dist/src/lib/utils/tabInfoEncoder.d.ts +0 -15
  52. package/dist/src/lib/utils/typeGuards.d.ts +2 -0
  53. package/dist/src/lib/utils/urlParser.d.ts +24 -3
  54. package/dist/src/lib/versionLogger.d.ts +1 -0
  55. package/dist/style.css +2 -1
  56. package/dist/utils/iframeBridge.d.ts +39 -0
  57. package/dist/vue-stack-tabs.css +2 -1
  58. package/dist/vue-stack-tabs.es.js +1 -2220
  59. package/package.json +71 -50
  60. package/src/lib/StackTabs.vue +223 -196
  61. package/src/lib/assets/style/stackTab.scss +110 -13
  62. package/src/lib/components/ContextMenu/ContextMenuItem.vue +23 -11
  63. package/src/lib/components/ContextMenu/index.vue +94 -10
  64. package/src/lib/components/PageLoading.vue +9 -7
  65. package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +70 -0
  66. package/src/lib/components/StackKeepAlive/StackKeepAlive.vue +57 -0
  67. package/src/lib/components/TabHeader/TabHeaderButton.vue +19 -8
  68. package/src/lib/components/TabHeader/TabHeaderItem.vue +74 -17
  69. package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
  70. package/src/lib/components/TabHeader/index.vue +181 -13
  71. package/src/lib/hooks/stackTabsContext.ts +135 -0
  72. package/src/lib/hooks/tabPanel/evict.ts +122 -87
  73. package/src/lib/hooks/tabPanel/navigationTransaction.ts +61 -0
  74. package/src/lib/hooks/tabPanel/refresh.ts +109 -0
  75. package/src/lib/hooks/tabPanel/scroll.ts +57 -40
  76. package/src/lib/hooks/tabPanel/session.ts +84 -22
  77. package/src/lib/hooks/tabPanel/state.ts +41 -44
  78. package/src/lib/hooks/useIframeManager.ts +253 -0
  79. package/src/lib/hooks/useTabActions.ts +64 -30
  80. package/src/lib/hooks/useTabEventBus.ts +29 -19
  81. package/src/lib/hooks/useTabLoading.ts +3 -2
  82. package/src/lib/hooks/useTabPanel.tsx +290 -313
  83. package/src/lib/hooks/useTabRouter.ts +111 -97
  84. package/src/lib/i18n/index.ts +53 -45
  85. package/src/lib/i18n/lang/en.ts +6 -1
  86. package/src/lib/i18n/lang/zh-CN.ts +6 -1
  87. package/src/lib/i18n/stackTabsLocale.ts +8 -0
  88. package/src/lib/iframe-bridge.ts +12 -0
  89. package/src/lib/index.ts +14 -19
  90. package/src/lib/model/TabModel.ts +27 -82
  91. package/src/lib/nuxt/module.d.ts +4 -0
  92. package/src/lib/nuxt/module.ts +8 -3
  93. package/src/lib/nuxt/runtime/plugin.client.ts +1 -0
  94. package/src/lib/nuxt/runtime/plugin.ts +11 -0
  95. package/src/lib/utils/iframeBridge.ts +29 -5
  96. package/src/lib/utils/scrollUtils.ts +9 -4
  97. package/src/lib/utils/stackTabsA11y.ts +12 -0
  98. package/src/lib/utils/stackTabsMessage.ts +99 -0
  99. package/src/lib/utils/tabInfoEncoder.ts +70 -21
  100. package/src/lib/utils/typeGuards.ts +3 -0
  101. package/src/lib/utils/urlParser.ts +144 -14
  102. package/src/lib/versionLogger.ts +7 -19
  103. package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +0 -1
  104. package/dist/src/lib/hooks/useTabPanel.spec.d.ts +0 -1
  105. package/dist/src/lib/model/TabModel.spec.d.ts +0 -1
  106. package/dist/src/lib/utils/iframeBridge.spec.d.ts +0 -1
  107. package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +0 -1
  108. package/dist/src/lib/utils/urlParser.spec.d.ts +0 -1
  109. package/dist/vue-stack-tabs.umd.js +0 -1
  110. package/src/lib/hooks/tabPanel/evict.spec.ts +0 -85
  111. package/src/lib/hooks/useTabPanel.spec.ts +0 -34
  112. package/src/lib/model/TabModel.spec.ts +0 -40
  113. package/src/lib/utils/iframeBridge.spec.ts +0 -37
  114. package/src/lib/utils/tabInfoEncoder.spec.ts +0 -32
  115. package/src/lib/utils/urlParser.spec.ts +0 -45
@@ -1,19 +1,34 @@
1
1
  import { defu } from 'defu'
2
2
  import { getCurrentInstance } from 'vue'
3
3
  import type { ComponentInternalInstance } from 'vue'
4
- import type { RouteLocationPathRaw } from 'vue-router'
5
- import { useRoute, useRouter } from 'vue-router'
4
+ import type { LocationQueryRaw, RouteLocationPathRaw } from 'vue-router'
5
+ import type { ITabPage } from '../model/TabModel'
6
+ import { isNavigationFailure, useRoute, useRouter } from 'vue-router'
6
7
 
7
8
  import useTabPanel from './useTabPanel'
8
- import { parseUrl } from '../utils/urlParser'
9
- import { useTabEmitter } from './useTabEventBus'
9
+ import {
10
+ parseUrl,
11
+ cloneLocationQuery,
12
+ clonePage,
13
+ isSameQueryIgnoringReserved
14
+ } from '../utils/urlParser'
15
+ import { TabEventType, useTabEmitter } from './useTabEventBus'
16
+
17
+ import { runNavigationTransaction } from './tabPanel/navigationTransaction'
10
18
 
11
19
  import { createPageId } from '../utils/tabInfoEncoder'
12
20
 
13
- /** StackTabs 监听此事件切换前进动画 */
14
- const ROUTER_EVENT_FORWARD = 'FORWARD'
15
- /** StackTabs 监听此事件切换后退动画 */
16
- const ROUTER_EVENT_BACKWARD = 'BACKWARD'
21
+ interface PageStackLike {
22
+ clear: () => void
23
+ push: (page: ITabPage) => void
24
+ }
25
+
26
+ const restorePages = (stack: PageStackLike, pages: ITabPage[]) => {
27
+ stack.clear()
28
+ for (const page of pages) {
29
+ stack.push(clonePage(page))
30
+ }
31
+ }
17
32
 
18
33
  /**
19
34
  * useTabRouter - 标签内路由 Hook
@@ -47,7 +62,7 @@ export default function useTabRouter() {
47
62
  const route = useRoute()
48
63
  const router = useRouter()
49
64
  const emitter = useTabEmitter()
50
- const { getTab, evictPageCache, addPageScroller } = useTabPanel()
65
+ const { getTab, evictPageCache, addPageScroller, active: activateTab } = useTabPanel()
51
66
 
52
67
  /**
53
68
  * 当前标签的编码信息字符串,存于 URL query.__tab。
@@ -62,35 +77,62 @@ export default function useTabRouter() {
62
77
  addPageScroller(pageId, ...selectors)
63
78
  }
64
79
 
80
+ let navigationVersion = 0
81
+
65
82
  /**
66
83
  * 前进:将新路由推入该标签的 pages 栈,并触发前进动画。
67
- *
68
- * 实现细节:
69
- * 1. 通过 defu 将 __tab 合并进 query,确保目标页面也携带正确的标签标识
70
- * (用户传入的 to.query 优先级更高,__tab 作为默认补全)
71
- * 2. 发出 FORWARD 事件,StackTabs 切换为前进方向的转场动画
72
- * 3. router.push 触发路由变更,StackTabs 的 tabWrapper → updatePageState
73
- * 会在下次渲染时把目标页面压入 tab.pages 栈
74
84
  */
75
85
  const forward = (to: RouteLocationPathRaw) => {
76
86
  const tab = getTab(tabId)
77
- const targetQuery = (to.query || {}) as Record<string, string>
87
+ const targetQuery = (to.query || {}) as LocationQueryRaw
78
88
 
79
89
  if (tab) {
80
- // 【核心重构:数据前置压栈】
81
- // 生成崭新独立的 CacheName,并推入专属页面栈
82
- const newPageId = createPageId()
83
- tab.pages.push({
84
- id: newPageId,
85
- tabId: tab.id,
86
- path: to.path,
87
- query: targetQuery
88
- })
90
+ const top = tab.pages.peek()
91
+ if (top && top.path === to.path) {
92
+ if (isSameQueryIgnoringReserved(targetQuery, top.query || {})) {
93
+ if (!tab.active) {
94
+ activateTab(tab.id)
95
+ }
96
+ return
97
+ }
98
+ }
89
99
  }
90
100
 
91
101
  const query = defu({ __tab: tabInfo }, to.query)
92
- emitter.emit(ROUTER_EVENT_FORWARD)
93
- router.push({ path: to.path, query })
102
+ const transactionVersion = ++navigationVersion
103
+ emitter.emit(TabEventType.FORWARD)
104
+
105
+ let optimisticPageId = ''
106
+ runNavigationTransaction({
107
+ apply: () => {
108
+ const snapshot = tab ? tab.pages.readonlyList().map((p) => clonePage(p)) : []
109
+ if (tab) {
110
+ optimisticPageId = createPageId()
111
+ tab.pages.push({
112
+ id: optimisticPageId,
113
+ tabId: tab.id,
114
+ path: to.path,
115
+ query: cloneLocationQuery(targetQuery)
116
+ })
117
+ }
118
+ return snapshot
119
+ },
120
+ navigate: () => router.push({ path: to.path, query }),
121
+ rollback: (snapshot) => {
122
+ if (tab) restorePages(tab.pages, snapshot)
123
+ },
124
+ isFailureResult: isNavigationFailure,
125
+ isCurrent: () => navigationVersion === transactionVersion,
126
+ cleanupStale: () => {
127
+ if (!tab || !optimisticPageId) return
128
+ const pages = tab.pages.list().filter((page) => page.id !== optimisticPageId)
129
+ restorePages(tab.pages, pages)
130
+ }
131
+ }).catch((error: unknown) => {
132
+ if (!import.meta.env.PROD) {
133
+ console.warn('[vue-stack-tabs] forward navigation failed:', error)
134
+ }
135
+ })
94
136
  }
95
137
 
96
138
  /**
@@ -99,24 +141,7 @@ export default function useTabRouter() {
99
141
  * @param to 数字 = 退几步;字符串路径 = 回退到栈中首个路径匹配的页面
100
142
  * @param backQuery 可选,回退后传递给目标页面的额外参数(目标页通过 props 接收)
101
143
  *
102
- * ───────────────────────────────────────────────────────
103
- * ⚠️ 关键时序约束 — 为什么必须先导航再驱逐?
104
- * ───────────────────────────────────────────────────────
105
- * evictPageCache 会立即同步修改响应式数组 caches.value(即 <keep-alive :include>)。
106
- * 这个修改会触发 Vue 将 StackTabs 组件标记为 "需要重渲染"。
107
- *
108
- * 由于 router.push 是异步的(微任务队列),重渲染在 push 完成前就可能发生。
109
- * 此时路由对象仍然指向被退出的页面,tabWrapper 以旧路由调用 resolvePageComponent,
110
- * 而 components 里对应的 cacheName 已被删除 → resolvePageComponent 误判为"新页面",
111
- * 立刻重建组件 + 调用 addCache 将其加回 caches。
112
- * 结果:驱逐了又原地复活,<keep-alive> 保留了旧实例(含用户输入状态)。
113
- *
114
- * ✅ 正确做法(本实现):
115
- * 1. 仅从 pages 栈中 pop,不立即驱逐
116
- * 2. router.push(target) — 导航到前一页,路由切换后重渲染以新路由为基础
117
- * 3. .then() 回调里安全执行 evictPageCache
118
- * 此时旧页面已不再是"当前渲染目标",不会被误重建
119
- * ───────────────────────────────────────────────────────
144
+ * ⚠️ 关键时序约束 — 必须先导航再驱逐(详见原注释)
120
145
  */
121
146
  const backward = (to: number | string, backQuery?: Record<string, unknown>) => {
122
147
  const tab = getTab(tabId)
@@ -126,96 +151,85 @@ export default function useTabRouter() {
126
151
  let steps = 0
127
152
 
128
153
  if (typeof to === 'number') {
129
- // 按步数退:取绝对值,防止调用方传入负数导致逻辑异常
130
154
  steps = Math.abs(to)
131
155
  } else {
132
- // 按路径退:从栈顶向下扫描,找第一个 path 完全匹配的历史页面
133
156
  const parsed = parseUrl(to)
134
157
  const targetQuery = parsed.query || {}
135
- const pages = stack.list()
158
+ const pages = stack.readonlyList()
136
159
  let found = false
137
160
 
138
- // 精简版的参数匹配:如果调用方指定的 to.query 为空,就只要 path 匹配即可
139
- // 若调用方指定了 query,则该 query 的每个键值必须在历史页面的 query 中存在且相等
140
- const isQueryMatch = (pageQuery?: Record<string, unknown>) => {
141
- const pq = { ...(pageQuery || {}) }
142
- const tq = { ...targetQuery }
143
- delete pq.__tab
144
- delete pq._back
145
- delete tq.__tab
146
- delete tq._back
147
-
148
- const tqKeys = Object.keys(tq)
149
- if (tqKeys.length === 0) return true // 回退意图只要路径对了即可,不管历史曾有啥参数
150
-
151
- return tqKeys.every((k) => String(tq[k] ?? '') === String(pq[k] ?? ''))
152
- }
153
-
154
- // 从 size-2 开始(size-1 是当前页,不算在候选范围内)
155
161
  for (let i = stack.size() - 2; i >= 0; i--) {
156
162
  steps++
157
163
  const page = pages[i]
158
- const isSamePath = page?.path === parsed.path
159
- const isSameQuery = isQueryMatch(page?.query)
160
- console.log(
161
- `[vue-stack-tabs backward] Checking pop candidate ${i}: path=${page?.path}, isSamePath=${isSamePath}, isSameQuery=${isSameQuery}`
162
- )
163
- if (isSamePath && isSameQuery) {
164
+ if (
165
+ page?.path === parsed.path &&
166
+ isSameQueryIgnoringReserved(targetQuery, page?.query || {})
167
+ ) {
164
168
  found = true
165
169
  break
166
170
  }
167
171
  }
168
- // 栈中找不到目标路径,无法后退,返回警告
169
172
  if (!found) {
170
- console.warn(
171
- `[vue-stack-tabs] backward failed: target url '${to}' not found in history stack. Current stack: `,
172
- JSON.parse(JSON.stringify(pages))
173
- )
173
+ if (!import.meta.env.PROD) {
174
+ console.warn(
175
+ `[vue-stack-tabs] backward failed: target url '${to}' not found in history stack. Current stack: `,
176
+ pages.map((page) => clonePage(page))
177
+ )
178
+ }
174
179
  return false
175
180
  }
176
181
  }
177
182
 
178
- // 边界保护:
179
- // - steps <= 0:无效步数,不操作
180
183
  if (steps <= 0) return false
181
184
 
182
- // - stack.size() <= steps:栈深不足,如果步数超限,最多只能退到首页(即首个页面,清空其他)。
183
185
  if (stack.size() <= steps) {
184
- steps = stack.size() - 1 // 保留最后一层(首个推入的根页面)
186
+ steps = stack.size() - 1
185
187
  }
186
188
 
187
- // 若修复后退之后步数仍为0,说明已经在根页面无可后退
188
189
  if (steps <= 0) return false
189
190
 
190
- // 阶段 1:仅从 pages 栈弹出,收集 ID 入回收站
191
- // (keep-alive 驱逐推迟到导航完成后,这就相当于您的回收站概念)
191
+ const originalPages = stack.readonlyList().map((p) => clonePage(p))
192
192
  const recyclingBin: string[] = []
193
193
  for (let i = 0; i < steps; i++) {
194
194
  const popped = stack.pop()
195
195
  if (popped) recyclingBin.push(popped.id)
196
196
  }
197
197
 
198
- // 弹出后,栈顶即为目标回退页
199
198
  const target = stack.peek()
200
- if (!target) return false
199
+ if (!target) {
200
+ restorePages(stack, originalPages)
201
+ return false
202
+ }
201
203
 
202
204
  if (backQuery) {
203
- // 按照需求,将回退参数挂载在 _backParams 这个只活在页表栈的内存变量上
204
- // 绝对隔离与不再污染真实 route.query 和 URL 跳转。
205
205
  target._backParams = backQuery
206
206
  }
207
207
 
208
- emitter.emit(ROUTER_EVENT_BACKWARD)
208
+ emitter.emit(TabEventType.BACKWARD)
209
209
 
210
- // 阶段 2:先导航,再清理回收站
211
- // 对于 targetQuery 只需要带一个能够给框架定锚的 __tab。其他参数其实已经在 target.query 里。
212
210
  const targetQueryWithTab = defu({ __tab: tabInfo }, target.query || {})
213
-
214
- // 用编程式导航发起回退路由
215
- router.push({ path: target.path, query: targetQueryWithTab }).then(() => {
216
- // 导航完成且微任务结束后,说明新目标已稳坐栈顶,此时清理回收站安全
217
- for (const id of recyclingBin) {
218
- evictPageCache(id)
211
+ const transactionVersion = ++navigationVersion
212
+
213
+ runNavigationTransaction({
214
+ apply: () => originalPages,
215
+ navigate: () => router.push({ path: target.path, query: targetQueryWithTab }),
216
+ rollback: (snapshot) => restorePages(stack, snapshot),
217
+ isFailureResult: isNavigationFailure,
218
+ isCurrent: () => navigationVersion === transactionVersion,
219
+ commit: () => {
220
+ for (const id of recyclingBin) {
221
+ evictPageCache(id)
222
+ }
223
+ },
224
+ cleanupStale: () => {
225
+ const activePageIds = new Set(stack.list().map((page) => page.id))
226
+ for (const id of recyclingBin) {
227
+ if (!activePageIds.has(id)) evictPageCache(id)
228
+ }
229
+ }
230
+ }).catch((error: unknown) => {
231
+ if (!import.meta.env.PROD) {
232
+ console.warn('[vue-stack-tabs] backward navigation failed:', error)
219
233
  }
220
234
  })
221
235
  return true
@@ -1,14 +1,59 @@
1
+ /**
2
+ * 国际化模块。
3
+ *
4
+ * 自动加载 lang/ 目录下的语言文件,支持用户通过 plugin options
5
+ * 传入自定义语言包并与内置语言深度合并。
6
+ */
1
7
  import { createI18n } from 'vue-i18n-lite'
2
8
 
9
+ export interface LocaleMessageOption {
10
+ locale: string
11
+ messages?: Record<string, unknown>
12
+ }
13
+
14
+ const DANGEROUS_MESSAGE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
15
+
16
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
17
+ typeof value === 'object' && value !== null && !Array.isArray(value)
18
+
19
+ const mergeMessageRecord = (
20
+ baseMessage: Record<string, unknown>,
21
+ overrideMessage: Record<string, unknown>
22
+ ): Record<string, unknown> => {
23
+ const mergedMessage = { ...baseMessage }
24
+ for (const [key, value] of Object.entries(overrideMessage)) {
25
+ if (DANGEROUS_MESSAGE_KEYS.has(key)) continue
26
+ const existing = mergedMessage[key]
27
+ mergedMessage[key] =
28
+ isRecord(existing) && isRecord(value) ? mergeMessageRecord(existing, value) : value
29
+ }
30
+ return mergedMessage
31
+ }
32
+
33
+ export const mergeLocaleMessages = (
34
+ baseMessages: Record<string, Record<string, unknown>>,
35
+ localeI18n?: LocaleMessageOption[]
36
+ ): Record<string, Record<string, unknown>> => {
37
+ const combinedMessages = { ...baseMessages }
38
+ if (!localeI18n) return combinedMessages
39
+
40
+ for (const item of localeI18n) {
41
+ if (!item.messages || DANGEROUS_MESSAGE_KEYS.has(item.locale)) continue
42
+ combinedMessages[item.locale] = mergeMessageRecord(
43
+ combinedMessages[item.locale] ?? {},
44
+ item.messages
45
+ )
46
+ }
47
+
48
+ return combinedMessages
49
+ }
50
+
3
51
  export default () => {
4
52
  // 引入lang目录下文件
5
53
  // 此处使用了 VITE 的 import.meta.globEager。非 VITE 的 可以使用 require.context
6
54
  const modules = import.meta.glob('./lang/*', { eager: true })
7
55
 
8
- /**
9
- * 获取所有语言文件
10
- * @param {Object} mList
11
- */
56
+ /** 从 glob 导入结果中提取语言文件并合并到 msg 对象 */
12
57
  function getLangFiles(
13
58
  mList: Record<string, { default?: Record<string, unknown> }>,
14
59
  msg: Record<string, Record<string, unknown>>
@@ -23,57 +68,20 @@ export default () => {
23
68
  }
24
69
  }
25
70
 
26
- /**
27
- * 注册i18n实例并引入语言文件
28
- */
29
- // const i18n = createI18n({
30
- // fallbackLocale: 'zh-CN',
31
- // // 定义默认语言为中文
32
- // locale: 'zh-CN',
33
- // legacy: false,
34
- // // 挂载到全局,不然会报错
35
- // // globalInjection: true,
36
- // messages: getAllLang(),
37
- // })
38
- /**
39
- * 局部使用i18n
40
- */
41
- // const i18n = useI18n({
42
- // useScope: 'local',
43
- // locale: 'zh-CN',
44
- // messages: getAllLang(),
45
- // })
46
-
47
- // 假设你还有其他目录下的语言文件 它的路径是 src/views/home/locales/en-US.ts
48
- // 那么你就可以 使用 :lower:(小写) :upper:(大写) 来引入文件
49
- // const viewModules = import.meta.globEager('../views/**/locales/[[:lower:]][[:lower:]]-[[:upper:]][[:upper:]].ts')
50
-
51
71
  const allLangs = (): Record<string, Record<string, unknown>> => {
52
72
  const message: Record<string, Record<string, unknown>> = {}
53
73
  getLangFiles(modules as Record<string, { default?: Record<string, unknown> }>, message)
54
74
  return message
55
75
  }
56
- // const localeI18n = inject('locales') as { locale: string; messages: object }
57
- const getI18n = (localeI18n?: { locale: string; messages: Record<string, unknown> }[]) => {
58
- const combinateMessage = { ...allLangs() }
59
- if (localeI18n) {
60
- for (const l of localeI18n) {
61
- combinateMessage[l.locale] = l.messages
62
- }
63
- // combinateMessage = { ...allLangs(), ...localeI18n.messages }
64
- }
76
+ /** 创建 i18n 实例,合并内置语言与用户自定义语言包 */
77
+ const getI18n = (localeI18n?: LocaleMessageOption[]) => {
78
+ const combinateMessage = mergeLocaleMessages(allLangs(), localeI18n)
65
79
 
66
80
  return createI18n({
67
- locale: 'zh-CN',
81
+ locale: localeI18n?.[0]?.locale ?? 'zh-CN',
68
82
  fallbackLocale: 'en',
69
83
  messages: combinateMessage as Record<string, Record<string, string>>
70
84
  })
71
- // const combinateMessage = { ...allLangs(), ...localeI18n.messages }
72
- // return useI18n({
73
- // useScope: 'local',
74
- // locale: localeI18n.locale,
75
- // messages: combinateMessage
76
- // })
77
85
  }
78
86
  return {
79
87
  getI18n
@@ -11,6 +11,11 @@ export default {
11
11
  restore: 'restore',
12
12
  undefined: 'undefined',
13
13
  loading: 'loading',
14
- openInNewWindow: 'Open in new window'
14
+ iframeTitle: 'Embedded tab page',
15
+ iframeLoadTimeout: 'IFrame loading timed out',
16
+ retry: 'Retry',
17
+ openInNewWindow: 'Open in new window',
18
+ scrollLeft: 'Scroll left',
19
+ scrollRight: 'Scroll right'
15
20
  }
16
21
  }
@@ -11,6 +11,11 @@ export default {
11
11
  restore: '还原',
12
12
  undefined: '未命名',
13
13
  loading: '加载中',
14
- openInNewWindow: '在新窗口打开'
14
+ iframeTitle: '标签页内嵌页面',
15
+ iframeLoadTimeout: 'iframe 加载超时',
16
+ retry: '重试',
17
+ openInNewWindow: '在新窗口打开',
18
+ scrollLeft: '向左滚动',
19
+ scrollRight: '向右滚动'
15
20
  }
16
21
  }
@@ -0,0 +1,8 @@
1
+ /** 将指定的 locale 应用到 vue-stack-tabs 内部 i18n 实例 */
2
+ export const applyStackTabsLocale = (
3
+ changeLocale: (locale: string) => void,
4
+ locale?: string
5
+ ): void => {
6
+ if (!locale) return
7
+ changeLocale(locale)
8
+ }
@@ -0,0 +1,12 @@
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 {
9
+ IframeBridgeOptions,
10
+ IframeOpenTabPayload,
11
+ RefreshRequestOptions
12
+ } from './utils/iframeBridge'
package/src/lib/index.ts CHANGED
@@ -5,25 +5,21 @@
5
5
  * 安装:Vue.use(plugin) 注册全局组件 VueStackTabs、事件总线、i18n
6
6
  */
7
7
  import type { App } from 'vue'
8
- import i18n from './i18n'
8
+ import i18n, { type LocaleMessageOption } from './i18n'
9
9
  import StackTab from './StackTabs.vue'
10
10
  import logVersion from './versionLogger'
11
+ import { createStackTabsRuntimeContext, stackTabsContextKey } from './hooks/stackTabsContext'
12
+ import { tabEmitterKey } from './hooks/useTabEventBus'
11
13
  import IFrame from './iframe.vue'
12
14
  import useTabLoading from './hooks/useTabLoading'
13
15
  import useTabRouter from './hooks/useTabRouter'
14
16
  import useTabActions from './hooks/useTabActions'
15
- import TabHeaderButton from '@/lib/components/TabHeader/TabHeaderButton.vue'
16
- import useTabEventBus from '@/lib/hooks/useTabEventBus'
17
+ import TabHeaderButton from './components/TabHeader/TabHeaderButton.vue'
17
18
  import './assets/style/index.scss'
18
- //
19
- // const i18n = createI18n({
20
- // legacy: false,
21
- // locale: 'zh-CN',
22
- // fallbackLocale: 'en-US'
23
- // })
19
+
20
+ export type VueStackTabsPluginOptions = LocaleMessageOption[]
24
21
 
25
22
  export * from './model/TabModel'
26
- // Cutsom type
27
23
  declare module '@vue/runtime-core' {
28
24
  export interface GlobalComponents {
29
25
  VueStackTabs: typeof StackTab
@@ -36,18 +32,17 @@ export {
36
32
  useTabLoading,
37
33
  useTabRouter,
38
34
  TabHeaderButton,
39
- StackTab
35
+ StackTab,
36
+ StackTab as VueStackTabs
40
37
  }
41
- export {
42
- postOpenTab,
43
- onRefreshRequest,
44
- MSG_REFRESH,
45
- MSG_OPEN_TAB
46
- } from './utils/iframeBridge'
38
+ export { postOpenTab, onRefreshRequest, MSG_REFRESH, MSG_OPEN_TAB } from './utils/iframeBridge'
47
39
  export type { IframeOpenTabPayload } from './utils/iframeBridge'
48
40
  export default {
49
- install(Vue: App, options?: { locale: string; messages: Record<string, unknown> }[]): void {
41
+ install(Vue: App, options?: VueStackTabsPluginOptions): void {
50
42
  logVersion(import.meta.env.PACKAGE_VERSION)
51
- Vue.component('VueStackTabs', StackTab).use(useTabEventBus).use(i18n().getI18n(options))
43
+ const runtimeContext = createStackTabsRuntimeContext()
44
+ Vue.provide(stackTabsContextKey, runtimeContext)
45
+ Vue.provide(tabEmitterKey, runtimeContext.eventBus)
46
+ Vue.component('VueStackTabs', StackTab).use(i18n().getI18n(options))
52
47
  }
53
48
  }