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.
Files changed (116) hide show
  1. package/README.en.md +526 -0
  2. package/README.md +337 -520
  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 +88 -0
  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 -23
  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 -73
  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 +17 -74
  37. package/dist/src/lib/hooks/useTabRouter.d.ts +5 -5
  38. package/dist/src/lib/i18n/index.d.ts +6 -4
  39. package/dist/src/lib/i18n/lang/en.d.ts +5 -0
  40. package/dist/src/lib/i18n/lang/zh-CN.d.ts +5 -0
  41. package/dist/src/lib/i18n/stackTabsLocale.d.ts +2 -0
  42. package/dist/src/lib/iframe-bridge.d.ts +8 -0
  43. package/dist/src/lib/iframe.vue.d.ts +2 -1
  44. package/dist/src/lib/index.d.ts +4 -5
  45. package/dist/src/lib/model/TabModel.d.ts +14 -33
  46. package/dist/src/lib/nuxt/module.d.ts +6 -0
  47. package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +2 -0
  48. package/dist/src/lib/nuxt/runtime/plugin.d.ts +2 -0
  49. package/dist/src/lib/utils/iframeBridge.d.ts +11 -3
  50. package/dist/src/lib/utils/stackTabsA11y.d.ts +8 -0
  51. package/dist/src/lib/utils/stackTabsMessage.d.ts +7 -0
  52. package/dist/src/lib/utils/tabInfoEncoder.d.ts +1 -16
  53. package/dist/src/lib/utils/typeGuards.d.ts +2 -0
  54. package/dist/src/lib/utils/urlParser.d.ts +24 -3
  55. package/dist/src/lib/versionLogger.d.ts +1 -0
  56. package/dist/style.css +2 -1
  57. package/dist/utils/iframeBridge.d.ts +39 -0
  58. package/dist/vue-stack-tabs.css +2 -1
  59. package/dist/vue-stack-tabs.es.js +1 -2411
  60. package/package.json +68 -47
  61. package/src/lib/StackTabs.vue +216 -192
  62. package/src/lib/assets/style/stackTab.scss +84 -12
  63. package/src/lib/components/ContextMenu/ContextMenuItem.vue +23 -11
  64. package/src/lib/components/ContextMenu/index.vue +94 -10
  65. package/src/lib/components/PageLoading.vue +9 -7
  66. package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +70 -0
  67. package/src/lib/components/StackKeepAlive/StackKeepAlive.vue +57 -0
  68. package/src/lib/components/TabHeader/TabHeaderButton.vue +19 -8
  69. package/src/lib/components/TabHeader/TabHeaderItem.vue +45 -16
  70. package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
  71. package/src/lib/components/TabHeader/index.vue +109 -12
  72. package/src/lib/hooks/stackTabsContext.ts +135 -0
  73. package/src/lib/hooks/tabPanel/evict.ts +123 -41
  74. package/src/lib/hooks/tabPanel/navigationTransaction.ts +61 -0
  75. package/src/lib/hooks/tabPanel/refresh.ts +109 -0
  76. package/src/lib/hooks/tabPanel/scroll.ts +57 -36
  77. package/src/lib/hooks/tabPanel/session.ts +84 -22
  78. package/src/lib/hooks/tabPanel/state.ts +41 -44
  79. package/src/lib/hooks/useIframeManager.ts +253 -0
  80. package/src/lib/hooks/useTabActions.ts +62 -30
  81. package/src/lib/hooks/useTabEventBus.ts +29 -19
  82. package/src/lib/hooks/useTabLoading.ts +3 -2
  83. package/src/lib/hooks/useTabPanel.tsx +415 -213
  84. package/src/lib/hooks/useTabRouter.ts +172 -43
  85. package/src/lib/i18n/index.ts +53 -45
  86. package/src/lib/i18n/lang/en.ts +6 -1
  87. package/src/lib/i18n/lang/zh-CN.ts +6 -1
  88. package/src/lib/i18n/stackTabsLocale.ts +8 -0
  89. package/src/lib/iframe-bridge.ts +12 -0
  90. package/src/lib/index.ts +14 -19
  91. package/src/lib/model/TabModel.ts +34 -86
  92. package/src/lib/nuxt/module.d.ts +4 -0
  93. package/src/lib/nuxt/module.ts +8 -3
  94. package/src/lib/nuxt/runtime/plugin.client.ts +2 -3
  95. package/src/lib/nuxt/runtime/plugin.ts +11 -0
  96. package/src/lib/utils/iframeBridge.ts +29 -5
  97. package/src/lib/utils/scrollUtils.ts +9 -4
  98. package/src/lib/utils/stackTabsA11y.ts +12 -0
  99. package/src/lib/utils/stackTabsMessage.ts +99 -0
  100. package/src/lib/utils/tabInfoEncoder.ts +69 -42
  101. package/src/lib/utils/typeGuards.ts +3 -0
  102. package/src/lib/utils/urlParser.ts +144 -14
  103. package/src/lib/versionLogger.ts +7 -19
  104. package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +0 -1
  105. package/dist/src/lib/hooks/useTabPanel.spec.d.ts +0 -1
  106. package/dist/src/lib/model/TabModel.spec.d.ts +0 -1
  107. package/dist/src/lib/utils/iframeBridge.spec.d.ts +0 -1
  108. package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +0 -1
  109. package/dist/src/lib/utils/urlParser.spec.d.ts +0 -1
  110. package/dist/vue-stack-tabs.umd.js +0 -1
  111. package/src/lib/hooks/tabPanel/evict.spec.ts +0 -85
  112. package/src/lib/hooks/useTabPanel.spec.ts +0 -34
  113. package/src/lib/model/TabModel.spec.ts +0 -40
  114. package/src/lib/utils/iframeBridge.spec.ts +0 -37
  115. package/src/lib/utils/tabInfoEncoder.spec.ts +0 -42
  116. package/src/lib/utils/urlParser.spec.ts +0 -45
@@ -1,65 +1,151 @@
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'
10
16
 
11
- /** StackTabs 监听此事件切换前进动画 */
12
- const ROUTER_EVENT_FORWARD = 'FORWARD'
13
- /** StackTabs 监听此事件切换后退动画 */
14
- const ROUTER_EVENT_BACKWARD = 'BACKWARD'
17
+ import { runNavigationTransaction } from './tabPanel/navigationTransaction'
18
+
19
+ import { createPageId } from '../utils/tabInfoEncoder'
20
+
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
+ }
15
32
 
16
33
  /**
17
34
  * useTabRouter - 标签内路由 Hook
18
35
  *
19
36
  * 职责:在单个标签内实现「前进/后退」栈式导航,以及滚动位置的记录与恢复
20
- * 使用范围:仅在标签内的页面组件中使用,依赖 router-view 传入的 tId、pId
37
+ * 使用范围:仅在标签内的页面组件中使用,依赖 StackTabs 通过 cloneVNode 注入的 tId / pId
21
38
  *
22
39
  * 主要 API:
23
- * - forward(to) 前进:push 新页面到该标签的 pages 栈
24
- * - backward(to, backQuery?) 后退:pop 栈顶、markCacheForEviction,跳转到新栈顶
25
- * - addScrollTarget(...selectors) 注册需要记录滚动位置的选择器
40
+ * - forward(to) 前进:push 新页面到该标签的 pages 栈
41
+ * - backward(to, backQuery) 后退:pop 栈顶、驱逐缓存,跳转到新栈顶
42
+ * - addScrollTarget(...) 注册需要记录滚动位置的选择器
26
43
  */
27
44
  export default function useTabRouter() {
28
45
  const instance = getCurrentInstance() as ComponentInternalInstance
29
46
  const { attrs, props } = instance
30
- /** 当前标签 id,由 cache 组件的 dynamic-component 传入 */
47
+
48
+ /**
49
+ * 当前标签 ID。
50
+ * StackTabs 在 StackTabs.vue 的 tabWrapper 里通过 cloneVNode(Node, { tId, pId }) 注入,
51
+ * 未在子组件 setup 中声明为 prop 的属性会落入 attrs,
52
+ * 因此优先读 attrs.tId,再 fallback 到 props.tId。
53
+ */
31
54
  const tabId = (attrs.tId ?? props.tId) as string
32
- /** 当前页面缓存 id */
55
+
56
+ /**
57
+ * 当前页面的缓存 ID(即 cacheName / keep-alive include 中的名称)。
58
+ * 用于 addScrollTarget 向 useTabPanel 绑定该页面的可滚动容器选择器。
59
+ */
33
60
  const pageId = (attrs.pId ?? props.pId) as string
34
61
 
35
62
  const route = useRoute()
36
63
  const router = useRouter()
37
64
  const emitter = useTabEmitter()
38
- const { getTab, markCacheForEviction, getComponent, addPageScroller } = useTabPanel()
65
+ const { getTab, evictPageCache, addPageScroller, active: activateTab } = useTabPanel()
39
66
 
40
- /** 当前标签的编码信息,用于 push 时保持 __tab */
67
+ /**
68
+ * 当前标签的编码信息字符串,存于 URL query.__tab。
69
+ * 每次 forward 时必须将此值一并带入新页面的 query,
70
+ * 确保 StackTabs 在 updatePageState 解析路由时能识别该请求归属于哪个标签,
71
+ * 而不是错误地新建一个陌生标签。
72
+ */
41
73
  const tabInfo = route.query.__tab as string
42
74
 
43
- /** 注册需记录滚动位置的容器选择器 */
75
+ /** 注册需记录滚动位置的容器选择器,供 useTabPanel 在 deactivated/activated 时保存/恢复 */
44
76
  const addScrollTarget = (...selectors: string[]) => {
45
77
  addPageScroller(pageId, ...selectors)
46
78
  }
47
79
 
48
- /** 前进:进栈,跳转到新页面 */
80
+ let navigationVersion = 0
81
+
82
+ /**
83
+ * 前进:将新路由推入该标签的 pages 栈,并触发前进动画。
84
+ */
49
85
  const forward = (to: RouteLocationPathRaw) => {
86
+ const tab = getTab(tabId)
87
+ const targetQuery = (to.query || {}) as LocationQueryRaw
88
+
89
+ if (tab) {
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
+ }
99
+ }
100
+
50
101
  const query = defu({ __tab: tabInfo }, to.query)
51
- emitter.emit(ROUTER_EVENT_FORWARD)
52
- 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
+ })
53
136
  }
54
137
 
55
138
  /**
56
- * 后退:从该标签的 pages 栈中 pop,并跳转到新栈顶
57
- * @param to 数字=退几步,字符串=回退到首个路径匹配的页面
58
- * @param backQuery 回退后传给目标页面的 props 数据(目标页通过 props 接收)
139
+ * 后退:从该标签的 pages 栈中弹出若干层页面,跳转到新栈顶。
140
+ *
141
+ * @param to 数字 = 退几步;字符串路径 = 回退到栈中首个路径匹配的页面
142
+ * @param backQuery 可选,回退后传递给目标页面的额外参数(目标页通过 props 接收)
143
+ *
144
+ * ⚠️ 关键时序约束 — 必须先导航再驱逐(详见原注释)
59
145
  */
60
- const backward = (to: number | string, backQuery?: object) => {
146
+ const backward = (to: number | string, backQuery?: Record<string, unknown>) => {
61
147
  const tab = getTab(tabId)
62
- if (!tab) return
148
+ if (!tab) return false
63
149
 
64
150
  const stack = tab.pages
65
151
  let steps = 0
@@ -68,42 +154,85 @@ export default function useTabRouter() {
68
154
  steps = Math.abs(to)
69
155
  } else {
70
156
  const parsed = parseUrl(to)
71
- backQuery = defu(parsed.query, backQuery)
72
- const pages = stack.list()
157
+ const targetQuery = parsed.query || {}
158
+ const pages = stack.readonlyList()
73
159
  let found = false
160
+
74
161
  for (let i = stack.size() - 2; i >= 0; i--) {
75
162
  steps++
76
- if (pages[i]?.path === parsed.path) {
163
+ const page = pages[i]
164
+ if (
165
+ page?.path === parsed.path &&
166
+ isSameQueryIgnoringReserved(targetQuery, page?.query || {})
167
+ ) {
77
168
  found = true
78
169
  break
79
170
  }
80
171
  }
81
- if (!found) return
172
+ if (!found) {
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
+ }
179
+ return false
180
+ }
181
+ }
182
+
183
+ if (steps <= 0) return false
184
+
185
+ if (stack.size() <= steps) {
186
+ steps = stack.size() - 1
82
187
  }
83
188
 
84
- if (steps <= 0 || stack.size() <= steps) return
189
+ if (steps <= 0) return false
85
190
 
191
+ const originalPages = stack.readonlyList().map((p) => clonePage(p))
192
+ const recyclingBin: string[] = []
86
193
  for (let i = 0; i < steps; i++) {
87
194
  const popped = stack.pop()
88
- if (popped) markCacheForEviction(popped.id)
195
+ if (popped) recyclingBin.push(popped.id)
89
196
  }
90
197
 
91
198
  const target = stack.peek()
92
- if (!target) return
199
+ if (!target) {
200
+ restorePages(stack, originalPages)
201
+ return false
202
+ }
93
203
 
94
- const component = getComponent(target.id)
95
- if (component && backQuery) {
96
- const comp = component as {
97
- components?: { DynamicComponent?: { props?: object } }
98
- }
99
- const dynamic = comp.components?.DynamicComponent
100
- if (dynamic?.props) {
101
- dynamic.props = defu(backQuery, dynamic.props)
102
- }
204
+ if (backQuery) {
205
+ target._backParams = backQuery
103
206
  }
104
207
 
105
- emitter.emit(ROUTER_EVENT_BACKWARD)
106
- router.push({ path: target.path, query: target.query })
208
+ emitter.emit(TabEventType.BACKWARD)
209
+
210
+ const targetQueryWithTab = defu({ __tab: tabInfo }, target.query || {})
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)
233
+ }
234
+ })
235
+ return true
107
236
  }
108
237
 
109
238
  return {
@@ -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
  }