gc_i18n 1.4.5 → 1.4.7
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/.qoder/repowiki/zh/content//346/240/270/345/277/203/345/272/223/345/256/236/347/216/260.md +396 -527
- package/.qoder/repowiki/zh/content//351/205/215/347/275/256/351/200/211/351/241/271.md +62 -4
- package/.qoder/repowiki/zh/content//351/253/230/347/272/247/347/224/250/346/263/225//344/276/235/350/265/226/347/256/241/347/220/206/347/255/226/347/225/245.md +53 -32
- package/.qoder/repowiki/zh/content//351/253/230/347/272/247/347/224/250/346/263/225//351/253/230/347/272/247/347/224/250/346/263/225.md +293 -15
- package/.qoder/repowiki/zh/meta/repowiki-metadata.json +1 -1
- package/lib/gc_i18n.es.js +1317 -1081
- package/lib/gc_i18n.umd.js +51 -51
- package/package.json +2 -1
- package/packages/components/earth.js +2 -1
- package/packages/index.js +88 -35
- package/packages/libs/textEditMode.js +2 -1
package/.qoder/repowiki/zh/content//346/240/270/345/277/203/345/272/223/345/256/236/347/216/260.md
CHANGED
|
@@ -2,48 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
<cite>
|
|
4
4
|
**本文档中引用的文件**
|
|
5
|
-
- [packages/index.js](file://packages/index.js) - *核心I18n
|
|
6
|
-
- [packages/libs/service.js](file://packages/libs/service.js) - *核心服务逻辑变更*
|
|
7
|
-
- [packages/libs/utils.js](file://packages/libs/utils.js) - *工具函数库*
|
|
8
|
-
- [packages/libs/i18nUtils.ts](file://packages/libs/i18nUtils.ts) - *翻译键提取工具*
|
|
9
|
-
- [packages/components/earth.js](file://packages/components/earth.js) - *地球组件*
|
|
5
|
+
- [packages/index.js](file://packages/index.js) - *核心I18n类实现*
|
|
10
6
|
- [packages/libs/textEditMode.js](file://packages/libs/textEditMode.js) - *文本编辑模式模块*
|
|
11
7
|
- [packages/libs/textEditMode.css](file://packages/libs/textEditMode.css) - *文本编辑模式样式*
|
|
12
8
|
- [packages/swal.css](file://packages/swal.css) - *SweetAlert2样式集成*
|
|
13
|
-
- [
|
|
9
|
+
- [packages/libs/service.js](file://packages/libs/service.js) - *HTTP服务封装*
|
|
14
10
|
- [lib/gc_i18n.es.js](file://lib/gc_i18n.es.js) - *ES模块构建*
|
|
15
11
|
- [lib/gc_i18n.umd.js](file://lib/gc_i18n.umd.js) - *UMD构建*
|
|
16
12
|
- [lang/index.js](file://lang/index.js) - *语言包入口*
|
|
17
13
|
- [lang/index.json](file://lang/index.json) - *语言资源文件*
|
|
18
|
-
- [src/main.js](file://src/main.js) - *应用入口*
|
|
19
|
-
- [src/App.vue](file://src/App.vue) - *根组件*
|
|
20
14
|
- [package.json](file://package.json) - *项目配置*
|
|
21
15
|
- [vite.config.js](file://vite.config.js) - *构建配置*
|
|
22
16
|
</cite>
|
|
23
17
|
|
|
24
18
|
## 更新摘要
|
|
25
19
|
**变更内容**
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- **新增翻译函数默认值参数支持**,当翻译不存在时返回默认值
|
|
34
|
-
- **新增全局键集合跟踪机制**,通过__I18N_ALL_KEYS__全局变量记录所有调用过的键
|
|
35
|
-
- **增强文本编辑模式核心功能**,改进键值映射和查找机制
|
|
36
|
-
- 新增JWT令牌生成和管理功能
|
|
37
|
-
- 完善缓存策略,支持本地存储和远程增量更新
|
|
38
|
-
- **新增RTL方向管理功能**,支持从右到左语言的自动检测和文档方向切换
|
|
39
|
-
- **新增文本编辑模式功能**,支持可视化翻译编辑和快捷键绑定
|
|
40
|
-
- **新增editKeyboard配置选项**,允许自定义文本编辑模式快捷键
|
|
41
|
-
- **集成$keyValueMap映射机制**,为文本编辑模式提供键值映射支持
|
|
42
|
-
- **HTTP客户端现代化**,使用独立axios实例避免全局污染
|
|
43
|
-
- **安全机制改进**,新增JWT令牌验证和权限控制
|
|
44
|
-
- **SweetAlert2集成优化**,改进变量命名和代码组织结构,提升可读性和维护性
|
|
45
|
-
- **新增智能语言列表缓存机制**,通过getLanguagesWithCache提供缓存和自动失效功能
|
|
46
|
-
- **新增postMessage事件处理功能**,扩展了原有的updateLanguage事件处理机制,允许外部应用程序通过postMessage事件编程关闭模态框
|
|
20
|
+
- 版本从1.4.5升级到1.4.6,核心库实现重大重构
|
|
21
|
+
- 新增Axios请求拦截器,自动为所有HTTP请求添加Bearer令牌
|
|
22
|
+
- SweetAlert2集成得到改进,移除了复杂的模态框创建系统
|
|
23
|
+
- 简化为直接的SweetAlert2集成,提升可维护性
|
|
24
|
+
- 优化了事件通知机制,改进closeModal事件处理
|
|
25
|
+
- 增强了文本编辑模式的SweetAlert2集成
|
|
26
|
+
- 改进了HTTP客户端的集中式认证管理
|
|
47
27
|
|
|
48
28
|
## 目录
|
|
49
29
|
1. [项目结构分析](#项目结构分析)
|
|
@@ -52,44 +32,33 @@
|
|
|
52
32
|
4. [公共API接口文档](#公共api接口文档)
|
|
53
33
|
5. [RTL方向管理功能](#rtl方向管理功能)
|
|
54
34
|
6. [中文语言模式兼容性](#中文语言模式兼容性)
|
|
55
|
-
7. [
|
|
56
|
-
8. [
|
|
57
|
-
9. [
|
|
58
|
-
10. [样式文件作用](#样式文件作用)
|
|
35
|
+
7. [文本编辑模式功能](#文本编辑模式功能)
|
|
36
|
+
8. [非组件环境使用](#非组件环境使用)
|
|
37
|
+
9. [样式文件作用](#样式文件作用)
|
|
59
38
|
|
|
60
39
|
## 项目结构分析
|
|
61
40
|
|
|
62
41
|
```mermaid
|
|
63
42
|
graph TB
|
|
64
43
|
subgraph "核心模块"
|
|
65
|
-
packages[index.js<br/>
|
|
44
|
+
packages[index.js<br/>textEditMode.js<br/>textEditMode.css<br/>swal.css<br/>service.js]
|
|
66
45
|
end
|
|
67
46
|
subgraph "构建产物"
|
|
68
47
|
lib[lib]
|
|
69
48
|
gc_i18n_es[gc_i18n.es.js]
|
|
70
49
|
gc_i18n_umd[gc_i18n.umd.js]
|
|
71
|
-
gc_i18n_css[gc_i18n.css]
|
|
72
|
-
text_edit_css[textEditMode.css]
|
|
73
|
-
swal_css[swal.css]
|
|
74
50
|
end
|
|
75
51
|
subgraph "语言资源"
|
|
76
52
|
lang[index.js<br/>index.json]
|
|
77
53
|
end
|
|
78
|
-
subgraph "示例应用"
|
|
79
|
-
src[src]
|
|
80
|
-
main_js[main.js]
|
|
81
|
-
app_vue[App.vue]
|
|
82
|
-
end
|
|
83
54
|
subgraph "依赖管理"
|
|
84
55
|
package_json[package.json]
|
|
85
56
|
vite_config[vite.config.js]
|
|
86
57
|
dependencies[依赖配置]
|
|
87
|
-
devDependencies[开发依赖]
|
|
88
58
|
end
|
|
89
59
|
packages --> lib
|
|
90
60
|
lib --> packages
|
|
91
61
|
lang --> packages
|
|
92
|
-
src --> packages
|
|
93
62
|
package_json --> dependencies
|
|
94
63
|
vite_config --> dependencies
|
|
95
64
|
```
|
|
@@ -97,6 +66,7 @@ vite_config --> dependencies
|
|
|
97
66
|
**图示来源**
|
|
98
67
|
- [packages/index.js](file://packages/index.js)
|
|
99
68
|
- [packages/libs/textEditMode.js](file://packages/libs/textEditMode.js)
|
|
69
|
+
- [packages/libs/service.js](file://packages/libs/service.js)
|
|
100
70
|
- [lib/gc_i18n.es.js](file://lib/gc_i18n.es.js)
|
|
101
71
|
- [lang/index.js](file://lang/index.js)
|
|
102
72
|
- [package.json](file://package.json)
|
|
@@ -109,193 +79,27 @@ vite_config --> dependencies
|
|
|
109
79
|
|
|
110
80
|
## 核心逻辑实现
|
|
111
81
|
|
|
112
|
-
###
|
|
113
|
-
|
|
114
|
-
核心I18n类引入了createI18nCompat函数来处理Vue 2和Vue 3的API差异:
|
|
115
|
-
|
|
116
|
-
```javascript
|
|
117
|
-
function createI18nCompat(options) {
|
|
118
|
-
if (vueI18n.createI18n) {
|
|
119
|
-
return vueI18n.createI18n(options);
|
|
120
|
-
}
|
|
121
|
-
const VueI18n = vueI18n.default || vueI18n.VueI18n;
|
|
122
|
-
// Vue 2 下必须先 Vue.use(VueI18n),否则 new VueI18n() 会报 config undefined
|
|
123
|
-
const Vue = options.vue;
|
|
124
|
-
if (Vue) {
|
|
125
|
-
Vue.use(VueI18n);
|
|
126
|
-
}
|
|
127
|
-
const { vue, ...rest } = options;
|
|
128
|
-
const opts = {
|
|
129
|
-
locale: options.locale,
|
|
130
|
-
messages: options.messages || {},
|
|
131
|
-
silentTranslationWarn: options.silentTranslationWarn,
|
|
132
|
-
silentFallbackWarn: options.silentFallbackWarn,
|
|
133
|
-
missingWarn: options.missingWarn,
|
|
134
|
-
fallbackWarn: options.fallbackWarn,
|
|
135
|
-
...rest
|
|
136
|
-
};
|
|
137
|
-
const instance = new VueI18n(opts);
|
|
138
|
-
instance.global = {
|
|
139
|
-
t: instance.t.bind(instance),
|
|
140
|
-
setLocaleMessage: instance.setLocaleMessage.bind(instance),
|
|
141
|
-
locale: {
|
|
142
|
-
get value() {
|
|
143
|
-
return instance.locale;
|
|
144
|
-
},
|
|
145
|
-
set value(v) {
|
|
146
|
-
instance.locale = v;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
return instance;
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### 语言切换机制
|
|
155
|
-
|
|
156
|
-
语言切换通过增强的翻译查找机制实现,支持路由特定键和通用键的优先级处理:
|
|
157
|
-
|
|
158
|
-
```javascript
|
|
159
|
-
globalThis.$t = this.i18n.global.t = (key, comment) => {
|
|
160
|
-
// 记录本次调用的原始 key(字符串场景)
|
|
161
|
-
if (typeof key === "string") {
|
|
162
|
-
this.translationKeySet.add(key);
|
|
163
|
-
}
|
|
164
|
-
// Vue Router 3 (Vue2) currentRoute 为普通对象,Vue Router 4 (Vue3) 为 ref
|
|
165
|
-
const route = this.router?.currentRoute;
|
|
166
|
-
const routeName = (route?.value ?? route)?.name;
|
|
167
|
-
if (routeName) {
|
|
168
|
-
const routerKey = `${routeName}.${key}`;
|
|
169
|
-
const routeTranslation = originalT(routerKey);
|
|
170
|
-
if (routeTranslation !== routerKey && !_.isEmpty(routeTranslation)) {
|
|
171
|
-
return routeTranslation;
|
|
172
|
-
} else {
|
|
173
|
-
const commonKey = `common.${key}`;
|
|
174
|
-
const commonTranslation = originalT(commonKey);
|
|
175
|
-
if (commonTranslation !== commonKey) {
|
|
176
|
-
return commonTranslation;
|
|
177
|
-
} else {
|
|
178
|
-
return originalT(key, comment);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
return originalT(key, comment);
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### 路由守卫机制
|
|
82
|
+
### Axios请求拦截器集成
|
|
188
83
|
|
|
189
|
-
|
|
84
|
+
**更新** 核心I18n类现在集成了Axios请求拦截器,实现了集中式的认证管理:
|
|
190
85
|
|
|
191
86
|
```javascript
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
I18n._routerGuardRegistered = true;
|
|
195
|
-
this.router.beforeEach(async (to, from, next) => {
|
|
196
|
-
const { language, token } = to.query;
|
|
197
|
-
this.configInstance && this.configInstance.closeModal();
|
|
198
|
-
this.name = to.name;
|
|
199
|
-
// 新路由开始前重置本次页面已收集到的 key
|
|
200
|
-
this.translationKeySet = new Set();
|
|
201
|
-
token && this.setToken(token);
|
|
202
|
-
|
|
203
|
-
if (this.loadI18n) {
|
|
204
|
-
await this.setLanguage(language || this.locale);
|
|
205
|
-
}
|
|
206
|
-
this.loadI18n = false;
|
|
207
|
-
|
|
208
|
-
// 动态加载异步组件并提取翻译键
|
|
209
|
-
if (to.matched.length > 0) {
|
|
210
|
-
try {
|
|
211
|
-
const allTranslationKeys = [];
|
|
212
|
-
for (const match of to.matched) {
|
|
213
|
-
let component = match.components?.default;
|
|
214
|
-
if (
|
|
215
|
-
typeof component === "function" &&
|
|
216
|
-
component.toString().includes("import(")
|
|
217
|
-
) {
|
|
218
|
-
component = await component();
|
|
219
|
-
component = component.default;
|
|
220
|
-
}
|
|
221
|
-
if (component) {
|
|
222
|
-
const keys = extractTranslationKeys(component);
|
|
223
|
-
allTranslationKeys.push(...keys);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
this.translationKeys = allTranslationKeys;
|
|
227
|
-
} catch (error) {
|
|
228
|
-
console.error("加载异步组件时出错:", error);
|
|
229
|
-
next();
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
next();
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// 在每次路由完成后,同步一次运行时收集到的 keys
|
|
237
|
-
if (!I18n._afterEachGuardRegistered) {
|
|
238
|
-
I18n._afterEachGuardRegistered = true;
|
|
239
|
-
this.router.afterEach(async () => {
|
|
240
|
-
await nextTick();
|
|
241
|
-
// 合并静态扫描与运行时收集,去重
|
|
242
|
-
const runtimeKeys = Array.from(this.translationKeySet);
|
|
243
|
-
const merged = Array.from(
|
|
244
|
-
new Set([...(this.translationKeys || []), ...runtimeKeys])
|
|
245
|
-
);
|
|
246
|
-
this.translationKeys = merged;
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
### 翻译查找机制
|
|
253
|
-
|
|
254
|
-
翻译查找通过增强的消息解析器实现,支持嵌套键值查找和路由特定键处理:
|
|
87
|
+
// 独立 Axios 实例,避免宿主环境污染全局 axios
|
|
88
|
+
const http = axios.create();
|
|
255
89
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
for (; r < a; ) {
|
|
264
|
-
const o = n[r]
|
|
265
|
-
if (ca.includes(o) && le(s)) return null
|
|
266
|
-
const l = s[o]
|
|
267
|
-
if (l === void 0 || x(s)) return null
|
|
268
|
-
s = l, r++
|
|
90
|
+
// 添加请求拦截器,自动带上 token
|
|
91
|
+
http.interceptors.request.use((config) => {
|
|
92
|
+
// 从 I18N_CONFIG 中获取 token
|
|
93
|
+
const i18nConfig = store2.get("I18N_CONFIG") || {};
|
|
94
|
+
const token = i18nConfig.token;
|
|
95
|
+
if (token) {
|
|
96
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
269
97
|
}
|
|
270
|
-
return
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### 事件通知机制
|
|
275
|
-
|
|
276
|
-
**更新** 事件通知通过自定义事件系统实现,使用mitt事件发射器,新增了closeModal事件处理功能:
|
|
277
|
-
|
|
278
|
-
```javascript
|
|
279
|
-
install(app, opts = {}) {
|
|
280
|
-
app.use(this.i18n);
|
|
281
|
-
// 自动注册全局组件
|
|
282
|
-
// app.component("i18n-earth", earthVue);
|
|
283
|
-
|
|
284
|
-
window.addEventListener("message", (event) => {
|
|
285
|
-
if (event.data.type === "updateLanguage") {
|
|
286
|
-
location.reload();
|
|
287
|
-
}
|
|
288
|
-
// 新增:监听关闭弹窗消息
|
|
289
|
-
if (event.data.type === "closeModal") {
|
|
290
|
-
this.configInstance && this.configInstance.closeModal();
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
}
|
|
98
|
+
return config;
|
|
99
|
+
});
|
|
294
100
|
```
|
|
295
101
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
**更新** 新增独立axios实例,避免宿主环境污染全局axios配置:
|
|
102
|
+
**更新** HTTP客户端现代化,新增独立axios实例,避免宿主环境污染全局axios配置:
|
|
299
103
|
|
|
300
104
|
```javascript
|
|
301
105
|
// 独立 Axios 实例,避免宿主环境污染全局 axios
|
|
@@ -309,7 +113,6 @@ export const getTranslate = async ({
|
|
|
309
113
|
routerName
|
|
310
114
|
}) => {
|
|
311
115
|
return new Promise(async (resolve, reject) => {
|
|
312
|
-
// const appCodeStore = i18nStore.get(appCode);
|
|
313
116
|
const languageStore = store2.namespace(`I18N_${_.toUpper(appCode)}`);
|
|
314
117
|
const options = {
|
|
315
118
|
baseUrl,
|
|
@@ -357,36 +160,71 @@ export const getTranslate = async ({
|
|
|
357
160
|
};
|
|
358
161
|
```
|
|
359
162
|
|
|
360
|
-
###
|
|
163
|
+
### SweetAlert2集成重构
|
|
361
164
|
|
|
362
|
-
**更新**
|
|
165
|
+
**更新** 核心I18n类现在使用直接的SweetAlert2集成,移除了复杂的模态框创建系统:
|
|
363
166
|
|
|
364
167
|
```javascript
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
168
|
+
// 直接使用 SweetAlert2 创建配置弹窗
|
|
169
|
+
keyboardJS.bind(this.keyboard, (e) => {
|
|
170
|
+
const iframeSrc = `${this.baseUrl}/i18n-admin-front/#/config?name=${
|
|
171
|
+
this.name
|
|
172
|
+
}&appCode=${this.appCode}&env=${
|
|
173
|
+
this.env
|
|
174
|
+
}&translationKeys=${this.translationKeys.join(",")}`;
|
|
175
|
+
this.swalInstance = Swal.fire({
|
|
176
|
+
titleText: "多语言管理中心",
|
|
177
|
+
padding: "0px",
|
|
178
|
+
showCloseButton: true,
|
|
179
|
+
showConfirmButton: false,
|
|
180
|
+
width: "1000px",
|
|
181
|
+
customClass: {
|
|
182
|
+
container: "gc_i18n_container",
|
|
183
|
+
title: "gc_i18n_title",
|
|
184
|
+
closeButton: "gc_i18n_close_button",
|
|
185
|
+
htmlContainer: "gc_i18n_html_container"
|
|
186
|
+
},
|
|
187
|
+
html: `<iframe src="${iframeSrc}" style="width: 100%; height: 100%;border: none;min-height: 480px;"></iframe>`
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
```
|
|
379
191
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
192
|
+
### 事件通知机制改进
|
|
193
|
+
|
|
194
|
+
**更新** 事件通知通过自定义事件系统实现,使用mitt事件发射器,改进了closeModal事件处理功能:
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
install(app, opts = {}) {
|
|
198
|
+
app.use(this.i18n);
|
|
199
|
+
// 自动注册全局组件
|
|
200
|
+
app.component("i18n-earth", {
|
|
201
|
+
...earthVue,
|
|
202
|
+
props: {
|
|
203
|
+
...earthVue.props,
|
|
204
|
+
baseUrl: {
|
|
205
|
+
type: String,
|
|
206
|
+
default: this.baseUrl
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
window.addEventListener("message", (event) => {
|
|
212
|
+
if (event.data.type === "updateLanguage") {
|
|
213
|
+
location.reload();
|
|
214
|
+
}
|
|
215
|
+
// 新增:监听关闭弹窗消息
|
|
216
|
+
if (event.data.type === "closeModal") {
|
|
217
|
+
if (this.swalInstance) {
|
|
218
|
+
this.swalInstance.close();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
384
222
|
}
|
|
385
223
|
```
|
|
386
224
|
|
|
387
|
-
###
|
|
225
|
+
### 翻译查找机制
|
|
388
226
|
|
|
389
|
-
|
|
227
|
+
翻译查找通过增强的消息解析器实现,支持嵌套键值查找和路由特定键处理:
|
|
390
228
|
|
|
391
229
|
```javascript
|
|
392
230
|
globalThis.$t = this.i18n.global.t = (key, defaultValue) => {
|
|
@@ -441,88 +279,38 @@ globalThis.$t = this.i18n.global.t = (key, defaultValue) => {
|
|
|
441
279
|
};
|
|
442
280
|
```
|
|
443
281
|
|
|
444
|
-
###
|
|
445
|
-
|
|
446
|
-
**更新** 新增全局键集合跟踪机制,通过__I18N_ALL_KEYS__全局变量记录所有调用过的键:
|
|
447
|
-
|
|
448
|
-
```javascript
|
|
449
|
-
// 用于存储所有调用过的 key,供文本编辑模式使用
|
|
450
|
-
globalThis.__I18N_ALL_KEYS__ = new Set();
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
### 智能语言列表缓存机制
|
|
282
|
+
### 安全机制改进
|
|
454
283
|
|
|
455
|
-
|
|
284
|
+
新增JWT令牌生成和验证机制:
|
|
456
285
|
|
|
457
286
|
```javascript
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}) => {
|
|
473
|
-
// 先从缓存获取
|
|
474
|
-
const cached = store2.get("I18N_LANGUAGES");
|
|
475
|
-
if (cached && Array.isArray(cached) && cached.length > 0 && !isRemote) {
|
|
476
|
-
return cached.map((l) => ({
|
|
477
|
-
langCode: l.language || l.langCode || l,
|
|
478
|
-
langName: l.langName || l.language || l
|
|
479
|
-
}));
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// 从 API 获取
|
|
483
|
-
if (!appCode) {
|
|
484
|
-
return [];
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
try {
|
|
488
|
-
const res = await http.get(`${baseUrl}/i18n-web/language/languages`, {
|
|
489
|
-
headers: {
|
|
490
|
-
appCode,
|
|
491
|
-
Authorization: token
|
|
492
|
-
}
|
|
493
|
-
});
|
|
494
|
-
if (res.data && res.data.result === 0) {
|
|
495
|
-
const langs = _.get(res.data, "retVal", []);
|
|
496
|
-
const formattedLangs = langs.map((l) => ({
|
|
497
|
-
langCode: l.language || l.langCode || l,
|
|
498
|
-
langName: l.langName || l.language || l
|
|
499
|
-
}));
|
|
500
|
-
store2.set("I18N_LANGUAGES", langs);
|
|
501
|
-
return formattedLangs;
|
|
502
|
-
}
|
|
503
|
-
} catch (error) {
|
|
504
|
-
console.error("获取语言列表失败:", error);
|
|
505
|
-
}
|
|
287
|
+
// 生成 JWT 的函数
|
|
288
|
+
function generateJWT(orgCode) {
|
|
289
|
+
const header = {
|
|
290
|
+
alg: "HS512" // 算法
|
|
291
|
+
};
|
|
292
|
+
const payload = {
|
|
293
|
+
orgCode
|
|
294
|
+
};
|
|
295
|
+
const sHeader = JSON.stringify(header);
|
|
296
|
+
const sPayload = JSON.stringify(payload);
|
|
297
|
+
const secret = "";
|
|
298
|
+
// 同步生成 JWT
|
|
299
|
+
return jws.JWS.sign("HS512", sHeader, sPayload, secret);
|
|
300
|
+
}
|
|
506
301
|
|
|
507
|
-
|
|
508
|
-
|
|
302
|
+
// 设置令牌
|
|
303
|
+
setToken(token) {
|
|
304
|
+
this.token = token;
|
|
305
|
+
store2.set("I18N_TOKEN", token);
|
|
306
|
+
}
|
|
509
307
|
```
|
|
510
308
|
|
|
511
|
-
### 缓存策略和自动失效
|
|
512
|
-
|
|
513
|
-
智能缓存机制具有以下特点:
|
|
514
|
-
|
|
515
|
-
1. **本地缓存优先**:首次调用时从store2本地存储获取缓存数据
|
|
516
|
-
2. **智能判断**:检查缓存数据的有效性(数组类型、长度大于0)
|
|
517
|
-
3. **强制刷新**:通过isRemote参数强制从远程API获取最新数据
|
|
518
|
-
4. **自动存储**:成功获取后自动存储到本地缓存
|
|
519
|
-
5. **格式化输出**:统一返回标准化的语言列表格式
|
|
520
|
-
|
|
521
309
|
**本节来源**
|
|
522
|
-
- [packages/index.js:
|
|
523
|
-
- [packages/
|
|
524
|
-
- [packages/
|
|
525
|
-
- [packages/
|
|
310
|
+
- [packages/index.js:51-62](file://packages/index.js#L51-L62)
|
|
311
|
+
- [packages/index.js:133-152](file://packages/index.js#L133-L152)
|
|
312
|
+
- [packages/index.js:519-530](file://packages/index.js#L519-L530)
|
|
313
|
+
- [packages/index.js:327-376](file://packages/index.js#L327-L376)
|
|
526
314
|
|
|
527
315
|
## Vue插件安装机制
|
|
528
316
|
|
|
@@ -532,15 +320,26 @@ I18n类作为Vue插件,通过install方法进行安装:
|
|
|
532
320
|
install(app, opts = {}) {
|
|
533
321
|
app.use(this.i18n);
|
|
534
322
|
// 自动注册全局组件
|
|
535
|
-
|
|
323
|
+
app.component("i18n-earth", {
|
|
324
|
+
...earthVue,
|
|
325
|
+
props: {
|
|
326
|
+
...earthVue.props,
|
|
327
|
+
baseUrl: {
|
|
328
|
+
type: String,
|
|
329
|
+
default: this.baseUrl
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
});
|
|
536
333
|
|
|
537
334
|
window.addEventListener("message", (event) => {
|
|
538
335
|
if (event.data.type === "updateLanguage") {
|
|
539
336
|
location.reload();
|
|
540
337
|
}
|
|
541
|
-
//
|
|
338
|
+
// 监听关闭弹窗消息
|
|
542
339
|
if (event.data.type === "closeModal") {
|
|
543
|
-
|
|
340
|
+
if (this.swalInstance) {
|
|
341
|
+
this.swalInstance.close();
|
|
342
|
+
}
|
|
544
343
|
}
|
|
545
344
|
});
|
|
546
345
|
}
|
|
@@ -565,7 +364,7 @@ this.i18n = createI18nCompat({
|
|
|
565
364
|
|
|
566
365
|
const originalT = this.i18n.global.t;
|
|
567
366
|
|
|
568
|
-
globalThis.$t = this.i18n.global.t = (key,
|
|
367
|
+
globalThis.$t = this.i18n.global.t = (key, defaultValue) => {
|
|
569
368
|
// 记录本次调用的原始 key(字符串场景)
|
|
570
369
|
if (typeof key === "string") {
|
|
571
370
|
this.translationKeySet.add(key);
|
|
@@ -573,29 +372,37 @@ globalThis.$t = this.i18n.global.t = (key, comment) => {
|
|
|
573
372
|
// Vue Router 3 (Vue2) currentRoute 为普通对象,Vue Router 4 (Vue3) 为 ref
|
|
574
373
|
const route = this.router?.currentRoute;
|
|
575
374
|
const routeName = (route?.value ?? route)?.name;
|
|
375
|
+
let result;
|
|
576
376
|
if (routeName) {
|
|
577
377
|
const routerKey = `${routeName}.${key}`;
|
|
578
378
|
const routeTranslation = originalT(routerKey);
|
|
579
379
|
if (routeTranslation !== routerKey && !_.isEmpty(routeTranslation)) {
|
|
580
|
-
|
|
380
|
+
result = routeTranslation;
|
|
581
381
|
} else {
|
|
582
382
|
const commonKey = `common.${key}`;
|
|
583
383
|
const commonTranslation = originalT(commonKey);
|
|
584
384
|
if (commonTranslation !== commonKey) {
|
|
585
|
-
|
|
385
|
+
result = commonTranslation;
|
|
586
386
|
} else {
|
|
587
|
-
|
|
387
|
+
result = originalT(key);
|
|
588
388
|
}
|
|
589
389
|
}
|
|
590
390
|
} else {
|
|
591
|
-
|
|
391
|
+
result = originalT(key);
|
|
592
392
|
}
|
|
393
|
+
|
|
394
|
+
// 如果翻译不存在(返回的是 key 本身),则使用默认值
|
|
395
|
+
if (result === key && typeof defaultValue === "string" && defaultValue) {
|
|
396
|
+
result = defaultValue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return result;
|
|
593
400
|
};
|
|
594
401
|
```
|
|
595
402
|
|
|
596
403
|
**本节来源**
|
|
597
|
-
- [packages/index.js:
|
|
598
|
-
- [packages/index.js:
|
|
404
|
+
- [packages/index.js:505-530](file://packages/index.js#L505-L530)
|
|
405
|
+
- [packages/index.js:306-408](file://packages/index.js#L306-L408)
|
|
599
406
|
|
|
600
407
|
## 公共API接口文档
|
|
601
408
|
|
|
@@ -735,9 +542,8 @@ const latestLanguages = await $getLanguages(true)
|
|
|
735
542
|
```
|
|
736
543
|
|
|
737
544
|
**本节来源**
|
|
738
|
-
- [packages/index.js:
|
|
739
|
-
- [packages/index.js:
|
|
740
|
-
- [packages/index.js:412-420](file://packages/index.js#L412-L420)
|
|
545
|
+
- [packages/index.js:395-408](file://packages/index.js#L395-L408)
|
|
546
|
+
- [packages/index.js:440-448](file://packages/index.js#L440-L448)
|
|
741
547
|
|
|
742
548
|
## RTL方向管理功能
|
|
743
549
|
|
|
@@ -913,15 +719,13 @@ async setLanguage(language = "zh-CN") {
|
|
|
913
719
|
```
|
|
914
720
|
|
|
915
721
|
**本节来源**
|
|
916
|
-
- [packages/index.js:
|
|
917
|
-
- [packages/index.js:310-343](file://packages/index.js#L310-L343)
|
|
918
|
-
- [packages/libs/utils.js:3-22](file://packages/libs/utils.js#L3-L22)
|
|
722
|
+
- [packages/index.js:410-435](file://packages/index.js#L410-L435)
|
|
919
723
|
|
|
920
724
|
## 中文语言模式兼容性
|
|
921
725
|
|
|
922
726
|
### 语言代码规范化机制
|
|
923
727
|
|
|
924
|
-
|
|
728
|
+
I18n类引入了normalizeLocale方法,专门处理中文语言模式的兼容性支持:
|
|
925
729
|
|
|
926
730
|
```javascript
|
|
927
731
|
/**
|
|
@@ -931,7 +735,19 @@ async setLanguage(language = "zh-CN") {
|
|
|
931
735
|
*/
|
|
932
736
|
normalizeLocale(locale) {
|
|
933
737
|
if (!locale) return "zh-CN";
|
|
934
|
-
//
|
|
738
|
+
// 保留繁体中文变体
|
|
739
|
+
if (locale === "zh-TW" || locale === "zh-HK" || locale === "zh-MO") {
|
|
740
|
+
return locale;
|
|
741
|
+
}
|
|
742
|
+
// zh-SG 等其它 zh- 开头映射为 zh-CN
|
|
743
|
+
if (locale.startsWith("zh-")) {
|
|
744
|
+
return "zh-CN";
|
|
745
|
+
}
|
|
746
|
+
// en-* 全部映射为 en-US
|
|
747
|
+
if (locale.startsWith("en-")) {
|
|
748
|
+
return "en-US";
|
|
749
|
+
}
|
|
750
|
+
// 处理完全匹配形式
|
|
935
751
|
const localeMap = {
|
|
936
752
|
zh: "zh-CN",
|
|
937
753
|
en: "en-US"
|
|
@@ -942,7 +758,7 @@ normalizeLocale(locale) {
|
|
|
942
758
|
|
|
943
759
|
### 中文简体和繁体支持
|
|
944
760
|
|
|
945
|
-
|
|
761
|
+
完整的中文语言模式支持,包括简体中文(zh-CN)和繁体中文(zh-TW):
|
|
946
762
|
|
|
947
763
|
```javascript
|
|
948
764
|
// 语言资源文件包含简体和繁体中文翻译
|
|
@@ -968,7 +784,7 @@ normalizeLocale(locale) {
|
|
|
968
784
|
|
|
969
785
|
### 自动语言检测和回退机制
|
|
970
786
|
|
|
971
|
-
|
|
787
|
+
语言加载时的智能回退机制:
|
|
972
788
|
|
|
973
789
|
```javascript
|
|
974
790
|
async setLanguage(language = "zh-CN") {
|
|
@@ -1003,7 +819,7 @@ async setLanguage(language = "zh-CN") {
|
|
|
1003
819
|
|
|
1004
820
|
### 语言资源文件结构
|
|
1005
821
|
|
|
1006
|
-
|
|
822
|
+
语言资源文件采用JSON格式,支持多语言键值对:
|
|
1007
823
|
|
|
1008
824
|
```json
|
|
1009
825
|
{
|
|
@@ -1021,7 +837,7 @@ async setLanguage(language = "zh-CN") {
|
|
|
1021
837
|
|
|
1022
838
|
### 中文语言模式最佳实践
|
|
1023
839
|
|
|
1024
|
-
|
|
840
|
+
使用中文语言模式的推荐做法:
|
|
1025
841
|
|
|
1026
842
|
```javascript
|
|
1027
843
|
// 1. 使用规范化的语言代码
|
|
@@ -1039,135 +855,8 @@ await $changeLocale('zh-CN'); // 切换到简体中文
|
|
|
1039
855
|
```
|
|
1040
856
|
|
|
1041
857
|
**本节来源**
|
|
1042
|
-
- [packages/index.js:168-
|
|
1043
|
-
- [packages/index.js:
|
|
1044
|
-
- [lang/index.json:1-130](file://lang/index.json#L1-L130)
|
|
1045
|
-
|
|
1046
|
-
## 单元代码支持功能
|
|
1047
|
-
|
|
1048
|
-
### 单位代码配置机制
|
|
1049
|
-
|
|
1050
|
-
**新增** I18n类引入了unitCode属性,用于支持单元代码(组织代码)的配置和管理:
|
|
1051
|
-
|
|
1052
|
-
```javascript
|
|
1053
|
-
constructor(options = {}) {
|
|
1054
|
-
// ... 其他初始化代码
|
|
1055
|
-
this.unitCode = options.unitCode || "GREENCLOUD";
|
|
1056
|
-
// ... 其他初始化代码
|
|
1057
|
-
}
|
|
1058
|
-
```
|
|
1059
|
-
|
|
1060
|
-
### 默认值设置和环境检测
|
|
1061
|
-
|
|
1062
|
-
**更新** 单元代码的默认值设置,结合环境检测机制:
|
|
1063
|
-
|
|
1064
|
-
```javascript
|
|
1065
|
-
// 在语言包入口中,根据环境模式设置单元代码
|
|
1066
|
-
const i18n = new gc_i18n({
|
|
1067
|
-
// ... 其他配置
|
|
1068
|
-
orgCode: import.meta.env.MODE === "unit" ? "DD" : "GREENCLOUD",
|
|
1069
|
-
// ...
|
|
1070
|
-
});
|
|
1071
|
-
```
|
|
1072
|
-
|
|
1073
|
-
### 文本编辑模式中的单元代码集成
|
|
1074
|
-
|
|
1075
|
-
**新增** 文本编辑模式中集成了unitCode支持,用于翻译数据的保存和检索:
|
|
1076
|
-
|
|
1077
|
-
```javascript
|
|
1078
|
-
async saveTranslations(key, values, originalData) {
|
|
1079
|
-
const original = originalData[0];
|
|
1080
|
-
const unitCode = this.unitCode || "GREENCLOUD";
|
|
1081
|
-
|
|
1082
|
-
const requestData = [];
|
|
1083
|
-
Object.keys(values).forEach(lang => {
|
|
1084
|
-
requestData.push({
|
|
1085
|
-
appCode: this.appCode,
|
|
1086
|
-
page: original.page,
|
|
1087
|
-
key: key,
|
|
1088
|
-
unitCode: unitCode,
|
|
1089
|
-
lang: lang,
|
|
1090
|
-
value: values[lang],
|
|
1091
|
-
fromAi: "F"
|
|
1092
|
-
});
|
|
1093
|
-
});
|
|
1094
|
-
|
|
1095
|
-
const response = await saveTranslate({
|
|
1096
|
-
data: { data: requestData, isLocal: "F" },
|
|
1097
|
-
token: this.token,
|
|
1098
|
-
baseUrl: this.baseUrl
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
return response;
|
|
1102
|
-
}
|
|
1103
|
-
```
|
|
1104
|
-
|
|
1105
|
-
### 单元代码在配置系统中的应用
|
|
1106
|
-
|
|
1107
|
-
**新增** 单元代码在配置系统中的完整应用流程:
|
|
1108
|
-
|
|
1109
|
-
```javascript
|
|
1110
|
-
/**
|
|
1111
|
-
* 初始化配置
|
|
1112
|
-
*/
|
|
1113
|
-
async initConfig() {
|
|
1114
|
-
const i18nConfig = store2.get("I18N_CONFIG") || {};
|
|
1115
|
-
const env = i18nConfig.env || "local";
|
|
1116
|
-
|
|
1117
|
-
// 根据环境设置 baseUrl
|
|
1118
|
-
this.baseUrl =
|
|
1119
|
-
!env || env === "local"
|
|
1120
|
-
? ""
|
|
1121
|
-
: env === "dev"
|
|
1122
|
-
? "https://test.ihotel.cn"
|
|
1123
|
-
: "https://trans.ihotel.cn";
|
|
1124
|
-
|
|
1125
|
-
this.appCode = i18nConfig.appCode || "";
|
|
1126
|
-
this.unitCode = i18nConfig.unitCode || "GREENCLOUD";
|
|
1127
|
-
this.token = i18nConfig.token || store2.get("I18N_TOKEN") || "";
|
|
1128
|
-
this.currentLocale = i18nConfig.locale || store2.get("I18N_LANGUAGE") || "zh-CN";
|
|
1129
|
-
|
|
1130
|
-
await this.loadLanguages();
|
|
1131
|
-
}
|
|
1132
|
-
```
|
|
1133
|
-
|
|
1134
|
-
### 单元代码的默认值策略
|
|
1135
|
-
|
|
1136
|
-
**新增** 单元代码的默认值策略,确保在不同环境下的一致性:
|
|
1137
|
-
|
|
1138
|
-
```javascript
|
|
1139
|
-
// 默认单元代码为 GREENCLOUD
|
|
1140
|
-
this.unitCode = options.unitCode || "GREENCLOUD";
|
|
1141
|
-
|
|
1142
|
-
// 在语言包入口中,根据模式覆盖默认值
|
|
1143
|
-
const unitCode = import.meta.env.MODE === "unit" ? "DD" : "GREENCLOUD";
|
|
1144
|
-
```
|
|
1145
|
-
|
|
1146
|
-
### 单元代码支持的最佳实践
|
|
1147
|
-
|
|
1148
|
-
**新增** 使用单元代码支持的推荐做法:
|
|
1149
|
-
|
|
1150
|
-
```javascript
|
|
1151
|
-
// 1. 在创建I18n实例时显式设置unitCode
|
|
1152
|
-
const i18n = new I18n({
|
|
1153
|
-
unitCode: 'CUSTOM_UNIT_CODE',
|
|
1154
|
-
appCode: 'MY_APP',
|
|
1155
|
-
orgCode: 'ORG123'
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
|
-
// 2. 在不同环境使用不同的unitCode
|
|
1159
|
-
const unitCode = import.meta.env.MODE === "unit" ? "DD" : "GREENCLOUD";
|
|
1160
|
-
|
|
1161
|
-
// 3. 在文本编辑模式中自动使用配置的unitCode
|
|
1162
|
-
await textEditMode.toggle();
|
|
1163
|
-
// 编辑的翻译数据将使用配置的unitCode保存
|
|
1164
|
-
```
|
|
1165
|
-
|
|
1166
|
-
**本节来源**
|
|
1167
|
-
- [packages/index.js:70-125](file://packages/index.js#L70-L125)
|
|
1168
|
-
- [packages/libs/textEditMode.js:29-50](file://packages/libs/textEditMode.js#L29-L50)
|
|
1169
|
-
- [lang/index.js:10](file://lang/index.js#L10)
|
|
1170
|
-
- [lib/gc_i18n.es.js:1964-1966](file://lib/gc_i18n.es.js#L1964-L1966)
|
|
858
|
+
- [packages/index.js:168-188](file://packages/index.js#L168-L188)
|
|
859
|
+
- [packages/index.js:449-504](file://packages/index.js#L449-L504)
|
|
1171
860
|
|
|
1172
861
|
## 文本编辑模式功能
|
|
1173
862
|
|
|
@@ -1192,6 +881,7 @@ handleElementClick[处理元素点击]
|
|
|
1192
881
|
openEditModal[打开编辑弹窗]
|
|
1193
882
|
fetchTranslations[获取翻译数据]
|
|
1194
883
|
saveTranslations[保存翻译数据]
|
|
884
|
+
updateTranslatedText[更新翻译文本]
|
|
1195
885
|
end
|
|
1196
886
|
subgraph "快捷键绑定"
|
|
1197
887
|
keyboardJS[keyboardJS库]
|
|
@@ -1214,6 +904,7 @@ textEditMode --> handleElementClick
|
|
|
1214
904
|
textEditMode --> openEditModal
|
|
1215
905
|
textEditMode --> fetchTranslations
|
|
1216
906
|
textEditMode --> saveTranslations
|
|
907
|
+
textEditMode --> updateTranslatedText
|
|
1217
908
|
keyboardJS --> editKeyboard
|
|
1218
909
|
textEditModeCSS --> i18nEditableText
|
|
1219
910
|
textEditModeCSS --> i18nEditModeActive
|
|
@@ -1221,9 +912,9 @@ swalCSS --> swal2Container
|
|
|
1221
912
|
```
|
|
1222
913
|
|
|
1223
914
|
**图示来源**
|
|
1224
|
-
- [packages/libs/textEditMode.js:14-
|
|
1225
|
-
- [packages/index.js:
|
|
1226
|
-
- [packages/swal.css:1-
|
|
915
|
+
- [packages/libs/textEditMode.js:14-624](file://packages/libs/textEditMode.js#L14-L624)
|
|
916
|
+
- [packages/index.js:154-158](file://packages/index.js#L154-L158)
|
|
917
|
+
- [packages/swal.css:1-23](file://packages/swal.css#L1-L23)
|
|
1227
918
|
|
|
1228
919
|
### 文本编辑模式快捷键配置
|
|
1229
920
|
|
|
@@ -1269,7 +960,7 @@ enableEditMode() {
|
|
|
1269
960
|
// 显示提示
|
|
1270
961
|
Swal.fire({
|
|
1271
962
|
title: "文本编辑模式已开启",
|
|
1272
|
-
text: "
|
|
963
|
+
text: "点击带外框的文本即可编辑翻译内容,按 Ctrl+Shift+E 退出",
|
|
1273
964
|
icon: "info",
|
|
1274
965
|
timer: 2000,
|
|
1275
966
|
showConfirmButton: false,
|
|
@@ -1310,6 +1001,9 @@ scanTextNodes(rootElement) {
|
|
|
1310
1001
|
const key = this.findKeyByValue(text);
|
|
1311
1002
|
if (key) {
|
|
1312
1003
|
this.wrapTextNode(textNode, key);
|
|
1004
|
+
} else {
|
|
1005
|
+
// 尝试从文本中提取可识别的翻译部分
|
|
1006
|
+
this.wrapPartialTextNode(textNode);
|
|
1313
1007
|
}
|
|
1314
1008
|
});
|
|
1315
1009
|
}
|
|
@@ -1317,7 +1011,7 @@ scanTextNodes(rootElement) {
|
|
|
1317
1011
|
|
|
1318
1012
|
#### 查找翻译键
|
|
1319
1013
|
|
|
1320
|
-
|
|
1014
|
+
增强的键查找机制,支持更全面的键值映射:
|
|
1321
1015
|
|
|
1322
1016
|
```javascript
|
|
1323
1017
|
findKeyByValue(value) {
|
|
@@ -1344,6 +1038,15 @@ findKeyByValue(value) {
|
|
|
1344
1038
|
}
|
|
1345
1039
|
}
|
|
1346
1040
|
|
|
1041
|
+
// 从当前路由的翻译中查找(如果存在)
|
|
1042
|
+
const routeMessagesKey = `I18N_MESSAGES_${this.currentLocale}`;
|
|
1043
|
+
const routeMessages = store2.get(routeMessagesKey) || {};
|
|
1044
|
+
for (const [key, val] of Object.entries(routeMessages)) {
|
|
1045
|
+
if (val === value) {
|
|
1046
|
+
return key;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1347
1050
|
// 如果找不到,返回 null
|
|
1348
1051
|
return null;
|
|
1349
1052
|
}
|
|
@@ -1351,6 +1054,8 @@ findKeyByValue(value) {
|
|
|
1351
1054
|
|
|
1352
1055
|
#### 打开编辑弹窗
|
|
1353
1056
|
|
|
1057
|
+
**更新** 使用SweetAlert2直接集成,简化了弹窗创建过程:
|
|
1058
|
+
|
|
1354
1059
|
```javascript
|
|
1355
1060
|
async openEditModal(key) {
|
|
1356
1061
|
try {
|
|
@@ -1358,7 +1063,7 @@ async openEditModal(key) {
|
|
|
1358
1063
|
if (key.startsWith("i.") || key.startsWith("pro.")) {
|
|
1359
1064
|
// 显示 tips 提示
|
|
1360
1065
|
Swal.fire({
|
|
1361
|
-
text: "
|
|
1066
|
+
text: "系统字段不支持修改",
|
|
1362
1067
|
icon: "info",
|
|
1363
1068
|
timer: 2000,
|
|
1364
1069
|
showConfirmButton: false,
|
|
@@ -1372,6 +1077,20 @@ async openEditModal(key) {
|
|
|
1372
1077
|
const translations = await this.fetchTranslations(key);
|
|
1373
1078
|
console.log("translations", translations);
|
|
1374
1079
|
const value = translations[0] || "";
|
|
1080
|
+
|
|
1081
|
+
// 如果没有数据,显示提示
|
|
1082
|
+
if (!value) {
|
|
1083
|
+
Swal.fire({
|
|
1084
|
+
text: "暂无翻译数据",
|
|
1085
|
+
icon: "info",
|
|
1086
|
+
timer: 2000,
|
|
1087
|
+
showConfirmButton: false,
|
|
1088
|
+
position: "top",
|
|
1089
|
+
toast: true
|
|
1090
|
+
});
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1375
1094
|
// 构建编辑表单
|
|
1376
1095
|
const langInputs =
|
|
1377
1096
|
value &&
|
|
@@ -1393,9 +1112,10 @@ async openEditModal(key) {
|
|
|
1393
1112
|
.join("");
|
|
1394
1113
|
|
|
1395
1114
|
const { value: formValues } = await Swal.fire({
|
|
1396
|
-
title:
|
|
1115
|
+
title: `原值参考:${value["zh-CN_raw"]}`,
|
|
1397
1116
|
html: `
|
|
1398
1117
|
<div class="i18n-edit-form">
|
|
1118
|
+
<div id="i18n-save-error" style="display: none; margin-bottom: 15px; padding: 10px 15px; background: #fff1f0; border: 1px solid #ffa39e; border-radius: 4px; color: #cf1322; font-size: 13px;"></div>
|
|
1399
1119
|
${langInputs}
|
|
1400
1120
|
</div>
|
|
1401
1121
|
`,
|
|
@@ -1409,18 +1129,50 @@ async openEditModal(key) {
|
|
|
1409
1129
|
title: "i18n-edit-modal-title",
|
|
1410
1130
|
htmlContainer: "i18n-edit-modal-content"
|
|
1411
1131
|
},
|
|
1412
|
-
preConfirm: () => {
|
|
1132
|
+
preConfirm: async () => {
|
|
1413
1133
|
const inputs = document.querySelectorAll(".i18n-edit-input");
|
|
1414
1134
|
const values = {};
|
|
1415
1135
|
inputs.forEach((input) => {
|
|
1416
1136
|
values[input.getAttribute("data-lang")] = input.value;
|
|
1417
1137
|
});
|
|
1418
|
-
|
|
1138
|
+
|
|
1139
|
+
// 隐藏之前的错误信息
|
|
1140
|
+
const errorDiv = document.getElementById("i18n-save-error");
|
|
1141
|
+
if (errorDiv) {
|
|
1142
|
+
errorDiv.style.display = "none";
|
|
1143
|
+
errorDiv.textContent = "";
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// 执行保存
|
|
1147
|
+
try {
|
|
1148
|
+
await this.saveTranslations(key, values, translations);
|
|
1149
|
+
return values;
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
// 显示错误信息在弹窗顶部
|
|
1152
|
+
if (errorDiv) {
|
|
1153
|
+
errorDiv.textContent =
|
|
1154
|
+
error.message || "保存失败,请检查网络连接或权限";
|
|
1155
|
+
errorDiv.style.display = "block";
|
|
1156
|
+
}
|
|
1157
|
+
// 返回 false 阻止弹窗关闭
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1419
1160
|
}
|
|
1420
1161
|
});
|
|
1421
1162
|
|
|
1422
1163
|
if (formValues) {
|
|
1423
|
-
|
|
1164
|
+
// 保存成功后会自动关闭弹窗
|
|
1165
|
+
this.updateTranslatedText(key, formValues);
|
|
1166
|
+
|
|
1167
|
+
// 显示成功提示
|
|
1168
|
+
Swal.fire({
|
|
1169
|
+
text: "保存成功",
|
|
1170
|
+
icon: "success",
|
|
1171
|
+
timer: 1500,
|
|
1172
|
+
showConfirmButton: false,
|
|
1173
|
+
position: "top",
|
|
1174
|
+
toast: true
|
|
1175
|
+
});
|
|
1424
1176
|
}
|
|
1425
1177
|
} catch (error) {
|
|
1426
1178
|
console.error("打开编辑弹窗失败:", error);
|
|
@@ -1446,8 +1198,7 @@ async openEditModal(key) {
|
|
|
1446
1198
|
/* 可编辑文本的外框样式 */
|
|
1447
1199
|
.i18n-editable-text {
|
|
1448
1200
|
position: relative;
|
|
1449
|
-
outline:
|
|
1450
|
-
outline-offset: 2px;
|
|
1201
|
+
outline: 1px dashed #2d8cf0 !important;
|
|
1451
1202
|
cursor: pointer !important;
|
|
1452
1203
|
background-color: rgba(45, 140, 240, 0.05);
|
|
1453
1204
|
border-radius: 2px;
|
|
@@ -1461,8 +1212,8 @@ async openEditModal(key) {
|
|
|
1461
1212
|
box-shadow: 0 0 8px rgba(45, 140, 240, 0.3);
|
|
1462
1213
|
}
|
|
1463
1214
|
|
|
1464
|
-
/* 编辑模式提示标签 */
|
|
1465
|
-
.i18n-editable-text::after {
|
|
1215
|
+
/* 编辑模式提示标签 - 已禁用 */
|
|
1216
|
+
/* .i18n-editable-text::after {
|
|
1466
1217
|
content: attr(data-i18n-key);
|
|
1467
1218
|
position: absolute;
|
|
1468
1219
|
top: -20px;
|
|
@@ -1483,12 +1234,12 @@ async openEditModal(key) {
|
|
|
1483
1234
|
.i18n-editable-text:hover::after {
|
|
1484
1235
|
opacity: 1;
|
|
1485
1236
|
visibility: visible;
|
|
1486
|
-
}
|
|
1237
|
+
} */
|
|
1487
1238
|
```
|
|
1488
1239
|
|
|
1489
1240
|
### SweetAlert2样式集成优化
|
|
1490
1241
|
|
|
1491
|
-
|
|
1242
|
+
SweetAlert2样式集成经过优化,提供更好的视觉体验:
|
|
1492
1243
|
|
|
1493
1244
|
```css
|
|
1494
1245
|
/* SweetAlert2 容器样式 */
|
|
@@ -1515,6 +1266,11 @@ async openEditModal(key) {
|
|
|
1515
1266
|
div:where(.swal2-container) button:where(.swal2-close):focus-visible {
|
|
1516
1267
|
box-shadow: none !important;
|
|
1517
1268
|
}
|
|
1269
|
+
|
|
1270
|
+
/* 全局 SweetAlert2 弹窗 z-index 设置,确保不被其他弹窗遮盖 */
|
|
1271
|
+
.swal2-container {
|
|
1272
|
+
z-index: 99999 !important;
|
|
1273
|
+
}
|
|
1518
1274
|
```
|
|
1519
1275
|
|
|
1520
1276
|
### 非组件环境使用
|
|
@@ -1552,10 +1308,10 @@ if ($isRTL()) {
|
|
|
1552
1308
|
```
|
|
1553
1309
|
|
|
1554
1310
|
**本节来源**
|
|
1555
|
-
- [packages/libs/textEditMode.js:
|
|
1311
|
+
- [packages/libs/textEditMode.js:14-624](file://packages/libs/textEditMode.js#L14-L624)
|
|
1556
1312
|
- [packages/libs/textEditMode.css:1-166](file://packages/libs/textEditMode.css#L1-L166)
|
|
1557
|
-
- [packages/index.js:
|
|
1558
|
-
- [packages/swal.css:1-
|
|
1313
|
+
- [packages/index.js:154-158](file://packages/index.js#L154-L158)
|
|
1314
|
+
- [packages/swal.css:1-23](file://packages/swal.css#L1-L23)
|
|
1559
1315
|
|
|
1560
1316
|
## 非组件环境使用
|
|
1561
1317
|
|
|
@@ -1684,8 +1440,7 @@ div:where(.swal2-container) button:where(.swal2-close):focus-visible {
|
|
|
1684
1440
|
/* 可编辑文本的外框样式 */
|
|
1685
1441
|
.i18n-editable-text {
|
|
1686
1442
|
position: relative;
|
|
1687
|
-
outline:
|
|
1688
|
-
outline-offset: 2px;
|
|
1443
|
+
outline: 1px dashed #2d8cf0 !important;
|
|
1689
1444
|
cursor: pointer !important;
|
|
1690
1445
|
background-color: rgba(45, 140, 240, 0.05);
|
|
1691
1446
|
border-radius: 2px;
|
|
@@ -1698,35 +1453,11 @@ div:where(.swal2-container) button:where(.swal2-close):focus-visible {
|
|
|
1698
1453
|
background-color: rgba(45, 140, 240, 0.1);
|
|
1699
1454
|
box-shadow: 0 0 8px rgba(45, 140, 240, 0.3);
|
|
1700
1455
|
}
|
|
1701
|
-
|
|
1702
|
-
/* 编辑模式提示标签 */
|
|
1703
|
-
.i18n-editable-text::after {
|
|
1704
|
-
content: attr(data-i18n-key);
|
|
1705
|
-
position: absolute;
|
|
1706
|
-
top: -20px;
|
|
1707
|
-
left: 0;
|
|
1708
|
-
background: #2d8cf0;
|
|
1709
|
-
color: white;
|
|
1710
|
-
font-size: 11px;
|
|
1711
|
-
padding: 2px 6px;
|
|
1712
|
-
border-radius: 3px;
|
|
1713
|
-
white-space: nowrap;
|
|
1714
|
-
opacity: 0;
|
|
1715
|
-
visibility: hidden;
|
|
1716
|
-
transition: all 0.2s ease;
|
|
1717
|
-
z-index: 9999;
|
|
1718
|
-
pointer-events: none;
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
.i18n-editable-text:hover::after {
|
|
1722
|
-
opacity: 1;
|
|
1723
|
-
visibility: visible;
|
|
1724
|
-
}
|
|
1725
1456
|
```
|
|
1726
1457
|
|
|
1727
1458
|
### SweetAlert2样式集成
|
|
1728
1459
|
|
|
1729
|
-
|
|
1460
|
+
SweetAlert2样式集成优化,提供更好的用户体验:
|
|
1730
1461
|
|
|
1731
1462
|
```css
|
|
1732
1463
|
/* SweetAlert2 容器样式 */
|
|
@@ -1753,6 +1484,11 @@ div:where(.swal2-container) button:where(.swal2-close):focus-visible {
|
|
|
1753
1484
|
div:where(.swal2-container) button:where(.swal2-close):focus-visible {
|
|
1754
1485
|
box-shadow: none !important;
|
|
1755
1486
|
}
|
|
1487
|
+
|
|
1488
|
+
/* 全局 SweetAlert2 弹窗 z-index 设置,确保不被其他弹窗遮盖 */
|
|
1489
|
+
.swal2-container {
|
|
1490
|
+
z-index: 99999 !important;
|
|
1491
|
+
}
|
|
1756
1492
|
```
|
|
1757
1493
|
|
|
1758
1494
|
### 样式特性
|
|
@@ -1768,12 +1504,11 @@ div:where(.swal2-container) button:where(.swal2-close):focus-visible {
|
|
|
1768
1504
|
**本节来源**
|
|
1769
1505
|
- [lib/gc_i18n.css](file://lib/gc_i18n.css)
|
|
1770
1506
|
- [packages/libs/textEditMode.css:1-166](file://packages/libs/textEditMode.css#L1-L166)
|
|
1771
|
-
- [packages/swal.css:1-
|
|
1772
|
-
- [packages/index.js:338-343](file://packages/index.js#L338-L343)
|
|
1507
|
+
- [packages/swal.css:1-23](file://packages/swal.css#L1-L23)
|
|
1773
1508
|
|
|
1774
1509
|
## PostMessage事件处理功能
|
|
1775
1510
|
|
|
1776
|
-
|
|
1511
|
+
I18n类现在支持通过postMessage事件编程控制模态框的关闭,扩展了原有的updateLanguage事件处理机制。
|
|
1777
1512
|
|
|
1778
1513
|
### 事件处理机制
|
|
1779
1514
|
|
|
@@ -1786,7 +1521,9 @@ window.addEventListener("message", (event) => {
|
|
|
1786
1521
|
}
|
|
1787
1522
|
// 新增:监听关闭弹窗消息
|
|
1788
1523
|
if (event.data.type === "closeModal") {
|
|
1789
|
-
|
|
1524
|
+
if (this.swalInstance) {
|
|
1525
|
+
this.swalInstance.close();
|
|
1526
|
+
}
|
|
1790
1527
|
}
|
|
1791
1528
|
});
|
|
1792
1529
|
```
|
|
@@ -1803,7 +1540,7 @@ window.addEventListener("message", (event) => {
|
|
|
1803
1540
|
|
|
1804
1541
|
- **用途**: 编程式关闭模态框
|
|
1805
1542
|
- **触发条件**: 接收到type为"closeModal"的消息
|
|
1806
|
-
- **行为**: 调用
|
|
1543
|
+
- **行为**: 调用this.swalInstance.close()关闭当前打开的配置模态框
|
|
1807
1544
|
- **适用场景**: 外部应用程序需要程序化控制模态框的关闭
|
|
1808
1545
|
|
|
1809
1546
|
### 外部应用程序集成
|
|
@@ -1843,6 +1580,138 @@ const handleMessage = (event) => {
|
|
|
1843
1580
|
4. **时机控制**: 在模态框真正打开后再发送closeModal事件
|
|
1844
1581
|
|
|
1845
1582
|
**本节来源**
|
|
1846
|
-
- [packages/index.js:
|
|
1847
|
-
|
|
1848
|
-
|
|
1583
|
+
- [packages/index.js:519-530](file://packages/index.js#L519-L530)
|
|
1584
|
+
|
|
1585
|
+
## HTTP客户端集中式认证管理
|
|
1586
|
+
|
|
1587
|
+
**新增** I18n类集成了Axios请求拦截器,实现了HTTP请求的集中式认证管理:
|
|
1588
|
+
|
|
1589
|
+
### 请求拦截器实现
|
|
1590
|
+
|
|
1591
|
+
```javascript
|
|
1592
|
+
// 独立 Axios 实例,避免宿主环境污染全局 axios
|
|
1593
|
+
const http = axios.create();
|
|
1594
|
+
|
|
1595
|
+
// 添加请求拦截器,自动带上 token
|
|
1596
|
+
http.interceptors.request.use((config) => {
|
|
1597
|
+
// 从 I18N_CONFIG 中获取 token
|
|
1598
|
+
const i18nConfig = store2.get("I18N_CONFIG") || {};
|
|
1599
|
+
const token = i18nConfig.token;
|
|
1600
|
+
if (token) {
|
|
1601
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
1602
|
+
}
|
|
1603
|
+
return config;
|
|
1604
|
+
});
|
|
1605
|
+
```
|
|
1606
|
+
|
|
1607
|
+
### 认证令牌管理
|
|
1608
|
+
|
|
1609
|
+
**更新** 新增了独立的Axios实例,避免宿主环境对全局axios配置的污染:
|
|
1610
|
+
|
|
1611
|
+
```javascript
|
|
1612
|
+
// 独立 Axios 实例,避免宿主环境污染全局 axios
|
|
1613
|
+
const http = axios.create();
|
|
1614
|
+
|
|
1615
|
+
export const getTranslate = async ({
|
|
1616
|
+
baseUrl,
|
|
1617
|
+
appCode,
|
|
1618
|
+
language = "zh-CN",
|
|
1619
|
+
token,
|
|
1620
|
+
routerName
|
|
1621
|
+
}) => {
|
|
1622
|
+
return new Promise(async (resolve, reject) => {
|
|
1623
|
+
const languageStore = store2.namespace(`I18N_${_.toUpper(appCode)}`);
|
|
1624
|
+
const options = {
|
|
1625
|
+
baseUrl,
|
|
1626
|
+
appCode,
|
|
1627
|
+
language,
|
|
1628
|
+
token
|
|
1629
|
+
};
|
|
1630
|
+
const lastData = languageStore.get(language);
|
|
1631
|
+
|
|
1632
|
+
if (!lastData || !lastData.lastPullDate) {
|
|
1633
|
+
// 如果是第一次获取
|
|
1634
|
+
const res = await fetchTranslate(options);
|
|
1635
|
+
if (res) {
|
|
1636
|
+
languageStore.set(language, res);
|
|
1637
|
+
resolve(res.translatesDTOs);
|
|
1638
|
+
}
|
|
1639
|
+
} else {
|
|
1640
|
+
const { lastPullDate } = lastData;
|
|
1641
|
+
const res = await fetchTranslate({
|
|
1642
|
+
...options,
|
|
1643
|
+
lastPullDate
|
|
1644
|
+
});
|
|
1645
|
+
if (res) {
|
|
1646
|
+
// 合并增量数据
|
|
1647
|
+
const langData = _.get(lastData, "translatesDTOs");
|
|
1648
|
+
if (!_.isEmpty(res.translatesDTOs)) {
|
|
1649
|
+
const data = mergeArraysByKey(
|
|
1650
|
+
langData,
|
|
1651
|
+
res.translatesDTOs,
|
|
1652
|
+
routerName
|
|
1653
|
+
);
|
|
1654
|
+
const saveData = {
|
|
1655
|
+
lastPullDate: res.lastPullDate,
|
|
1656
|
+
translatesDTOs: data
|
|
1657
|
+
};
|
|
1658
|
+
const languageStore = store2.namespace(`I18N_${_.toUpper(appCode)}`);
|
|
1659
|
+
languageStore.set(language, saveData, ":"); // Only update the specific key
|
|
1660
|
+
resolve(data);
|
|
1661
|
+
} else {
|
|
1662
|
+
resolve(langData);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
};
|
|
1668
|
+
```
|
|
1669
|
+
|
|
1670
|
+
### 服务层HTTP封装
|
|
1671
|
+
|
|
1672
|
+
**更新** service.js文件也采用了相同的集中式认证管理模式:
|
|
1673
|
+
|
|
1674
|
+
```javascript
|
|
1675
|
+
// 独立 Axios 实例,避免宿主环境拦截或修改全局 axios
|
|
1676
|
+
const http = axios.create();
|
|
1677
|
+
|
|
1678
|
+
export const getLanguagesWithCache = async ({
|
|
1679
|
+
baseUrl,
|
|
1680
|
+
token,
|
|
1681
|
+
appCode,
|
|
1682
|
+
isRemote = false
|
|
1683
|
+
}) => {
|
|
1684
|
+
// 先从缓存获取
|
|
1685
|
+
const cached = store2.get("I18N_LANGUAGES");
|
|
1686
|
+
if (cached && Array.isArray(cached) && cached.length > 0 && !isRemote) {
|
|
1687
|
+
return cached.map((l) => ({
|
|
1688
|
+
langCode: l.language || l.langCode || l,
|
|
1689
|
+
langName: l.langName || l.language || l
|
|
1690
|
+
}));
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// 从 API 获取
|
|
1694
|
+
if (!appCode) {
|
|
1695
|
+
return [];
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
try {
|
|
1699
|
+
const res = await http.get(`${baseUrl}/i18n-web//app/getsupportedlangs`, {
|
|
1700
|
+
headers: {
|
|
1701
|
+
appCode,
|
|
1702
|
+
Authorization: token
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
```
|
|
1706
|
+
|
|
1707
|
+
### 认证流程优势
|
|
1708
|
+
|
|
1709
|
+
1. **集中管理**: 所有HTTP请求都通过统一的拦截器处理认证
|
|
1710
|
+
2. **自动添加**: 无需在每个请求中手动添加Authorization头
|
|
1711
|
+
3. **环境隔离**: 独立的axios实例避免了宿主环境的干扰
|
|
1712
|
+
4. **令牌持久化**: 通过store2存储和管理认证令牌
|
|
1713
|
+
5. **错误处理**: 统一的认证错误处理机制
|
|
1714
|
+
|
|
1715
|
+
**本节来源**
|
|
1716
|
+
- [packages/index.js:51-62](file://packages/index.js#L51-L62)
|
|
1717
|
+
- [packages/libs/service.js:7-45](file://packages/libs/service.js#L7-L45)
|