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.
- package/README.en.md +526 -0
- package/README.md +141 -39
- package/README.ru.md +526 -0
- package/dist/iframe-bridge.d.ts +8 -0
- package/dist/iframe-bridge.html +77 -0
- package/dist/iframe-bridge.mjs +20 -0
- package/dist/iframe-child.html +19 -6
- package/dist/nuxt/favicon.svg +10 -0
- package/dist/nuxt/iframe-bridge.html +77 -0
- package/dist/nuxt/iframe-child.html +88 -0
- package/dist/nuxt/module.d.ts +6 -0
- package/dist/nuxt/module.mjs +28 -0
- package/dist/nuxt/plugin.mjs +9 -0
- package/dist/nuxt/runtime/plugin.d.ts +2 -0
- package/dist/nuxt/runtime/plugin.mjs +9 -0
- package/dist/src/lib/StackTabs.vue.d.ts +30 -16
- package/dist/src/lib/components/ContextMenu/ContextMenuItem.vue.d.ts +7 -1
- package/dist/src/lib/components/ContextMenu/index.vue.d.ts +5 -4
- package/dist/src/lib/components/PageLoading.vue.d.ts +2 -1
- package/dist/src/lib/components/StackKeepAlive/StackCacheRenderer.vue.d.ts +25 -0
- package/dist/src/lib/components/StackKeepAlive/StackKeepAlive.vue.d.ts +12 -0
- package/dist/src/lib/components/TabHeader/TabHeaderButton.vue.d.ts +11 -12
- package/dist/src/lib/components/TabHeader/TabHeaderItem.vue.d.ts +5 -4
- package/dist/src/lib/components/TabHeader/TabHeaderScroll.vue.d.ts +8 -22
- package/dist/src/lib/components/TabHeader/index.vue.d.ts +11 -164
- package/dist/src/lib/hooks/stackTabsContext.d.ts +49 -0
- package/dist/src/lib/hooks/tabPanel/evict.d.ts +21 -61
- package/dist/src/lib/hooks/tabPanel/navigationTransaction.d.ts +35 -0
- package/dist/src/lib/hooks/tabPanel/refresh.d.ts +17 -0
- package/dist/src/lib/hooks/tabPanel/scroll.d.ts +8 -8
- package/dist/src/lib/hooks/tabPanel/session.d.ts +9 -8
- package/dist/src/lib/hooks/tabPanel/state.d.ts +20 -72
- package/dist/src/lib/hooks/useIframeManager.d.ts +35 -0
- package/dist/src/lib/hooks/useTabActions.d.ts +5 -46
- package/dist/src/lib/hooks/useTabEventBus.d.ts +22 -9
- package/dist/src/lib/hooks/useTabPanel.d.ts +14 -73
- package/dist/src/lib/i18n/index.d.ts +6 -4
- package/dist/src/lib/i18n/lang/en.d.ts +5 -0
- package/dist/src/lib/i18n/lang/zh-CN.d.ts +5 -0
- package/dist/src/lib/i18n/stackTabsLocale.d.ts +2 -0
- package/dist/src/lib/iframe-bridge.d.ts +8 -0
- package/dist/src/lib/iframe.vue.d.ts +2 -1
- package/dist/src/lib/index.d.ts +4 -5
- package/dist/src/lib/model/TabModel.d.ts +11 -40
- package/dist/src/lib/nuxt/module.d.ts +6 -0
- package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +2 -0
- package/dist/src/lib/nuxt/runtime/plugin.d.ts +2 -0
- package/dist/src/lib/utils/iframeBridge.d.ts +11 -3
- package/dist/src/lib/utils/stackTabsA11y.d.ts +8 -0
- package/dist/src/lib/utils/stackTabsMessage.d.ts +7 -0
- package/dist/src/lib/utils/tabInfoEncoder.d.ts +0 -15
- package/dist/src/lib/utils/typeGuards.d.ts +2 -0
- package/dist/src/lib/utils/urlParser.d.ts +24 -3
- package/dist/src/lib/versionLogger.d.ts +1 -0
- package/dist/style.css +2 -1
- package/dist/utils/iframeBridge.d.ts +39 -0
- package/dist/vue-stack-tabs.css +2 -1
- package/dist/vue-stack-tabs.es.js +1 -2220
- package/package.json +71 -50
- package/src/lib/StackTabs.vue +223 -196
- package/src/lib/assets/style/stackTab.scss +110 -13
- package/src/lib/components/ContextMenu/ContextMenuItem.vue +23 -11
- package/src/lib/components/ContextMenu/index.vue +94 -10
- package/src/lib/components/PageLoading.vue +9 -7
- package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +70 -0
- package/src/lib/components/StackKeepAlive/StackKeepAlive.vue +57 -0
- package/src/lib/components/TabHeader/TabHeaderButton.vue +19 -8
- package/src/lib/components/TabHeader/TabHeaderItem.vue +74 -17
- package/src/lib/components/TabHeader/TabHeaderScroll.vue +47 -25
- package/src/lib/components/TabHeader/index.vue +181 -13
- package/src/lib/hooks/stackTabsContext.ts +135 -0
- package/src/lib/hooks/tabPanel/evict.ts +122 -87
- package/src/lib/hooks/tabPanel/navigationTransaction.ts +61 -0
- package/src/lib/hooks/tabPanel/refresh.ts +109 -0
- package/src/lib/hooks/tabPanel/scroll.ts +57 -40
- package/src/lib/hooks/tabPanel/session.ts +84 -22
- package/src/lib/hooks/tabPanel/state.ts +41 -44
- package/src/lib/hooks/useIframeManager.ts +253 -0
- package/src/lib/hooks/useTabActions.ts +64 -30
- package/src/lib/hooks/useTabEventBus.ts +29 -19
- package/src/lib/hooks/useTabLoading.ts +3 -2
- package/src/lib/hooks/useTabPanel.tsx +290 -313
- package/src/lib/hooks/useTabRouter.ts +111 -97
- package/src/lib/i18n/index.ts +53 -45
- package/src/lib/i18n/lang/en.ts +6 -1
- package/src/lib/i18n/lang/zh-CN.ts +6 -1
- package/src/lib/i18n/stackTabsLocale.ts +8 -0
- package/src/lib/iframe-bridge.ts +12 -0
- package/src/lib/index.ts +14 -19
- package/src/lib/model/TabModel.ts +27 -82
- package/src/lib/nuxt/module.d.ts +4 -0
- package/src/lib/nuxt/module.ts +8 -3
- package/src/lib/nuxt/runtime/plugin.client.ts +1 -0
- package/src/lib/nuxt/runtime/plugin.ts +11 -0
- package/src/lib/utils/iframeBridge.ts +29 -5
- package/src/lib/utils/scrollUtils.ts +9 -4
- package/src/lib/utils/stackTabsA11y.ts +12 -0
- package/src/lib/utils/stackTabsMessage.ts +99 -0
- package/src/lib/utils/tabInfoEncoder.ts +70 -21
- package/src/lib/utils/typeGuards.ts +3 -0
- package/src/lib/utils/urlParser.ts +144 -14
- package/src/lib/versionLogger.ts +7 -19
- package/dist/src/lib/hooks/tabPanel/evict.spec.d.ts +0 -1
- package/dist/src/lib/hooks/useTabPanel.spec.d.ts +0 -1
- package/dist/src/lib/model/TabModel.spec.d.ts +0 -1
- package/dist/src/lib/utils/iframeBridge.spec.d.ts +0 -1
- package/dist/src/lib/utils/tabInfoEncoder.spec.d.ts +0 -1
- package/dist/src/lib/utils/urlParser.spec.d.ts +0 -1
- package/dist/vue-stack-tabs.umd.js +0 -1
- package/src/lib/hooks/tabPanel/evict.spec.ts +0 -85
- package/src/lib/hooks/useTabPanel.spec.ts +0 -34
- package/src/lib/model/TabModel.spec.ts +0 -40
- package/src/lib/utils/iframeBridge.spec.ts +0 -37
- package/src/lib/utils/tabInfoEncoder.spec.ts +0 -32
- 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 {
|
|
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 {
|
|
9
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
87
|
+
const targetQuery = (to.query || {}) as LocationQueryRaw
|
|
78
88
|
|
|
79
89
|
if (tab) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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.
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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)
|
|
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(
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
package/src/lib/i18n/index.ts
CHANGED
|
@@ -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
|
-
|
|
57
|
-
const getI18n = (localeI18n?:
|
|
58
|
-
const combinateMessage =
|
|
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
|
package/src/lib/i18n/lang/en.ts
CHANGED
|
@@ -11,6 +11,11 @@ export default {
|
|
|
11
11
|
restore: 'restore',
|
|
12
12
|
undefined: 'undefined',
|
|
13
13
|
loading: 'loading',
|
|
14
|
-
|
|
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
|
-
|
|
14
|
+
iframeTitle: '标签页内嵌页面',
|
|
15
|
+
iframeLoadTimeout: 'iframe 加载超时',
|
|
16
|
+
retry: '重试',
|
|
17
|
+
openInNewWindow: '在新窗口打开',
|
|
18
|
+
scrollLeft: '向左滚动',
|
|
19
|
+
scrollRight: '向右滚动'
|
|
15
20
|
}
|
|
16
21
|
}
|
|
@@ -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 '
|
|
16
|
-
import useTabEventBus from '@/lib/hooks/useTabEventBus'
|
|
17
|
+
import TabHeaderButton from './components/TabHeader/TabHeaderButton.vue'
|
|
17
18
|
import './assets/style/index.scss'
|
|
18
|
-
|
|
19
|
-
|
|
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?:
|
|
41
|
+
install(Vue: App, options?: VueStackTabsPluginOptions): void {
|
|
50
42
|
logVersion(import.meta.env.PACKAGE_VERSION)
|
|
51
|
-
|
|
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
|
}
|