@koishi-ce/client 1.0.3 → 1.1.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.
- package/README.md +2 -2
- package/app/home/home.vue +4 -1
- package/app/home/index.ts +2 -1
- package/app/home/welcome.vue +9 -26
- package/app/index.ts +8 -2
- package/app/layout/header.vue +12 -4
- package/app/layout/layout.vue +4 -1
- package/app/layout/menu-item.vue +3 -1
- package/app/settings/index.ts +3 -2
- package/app/settings/settings.vue +9 -7
- package/app/settings/theme.vue +4 -1
- package/app/status/loading.vue +7 -2
- package/app/theme/activity/button.vue +4 -1
- package/app/theme/activity/index.vue +12 -4
- package/app/theme/activity/item.vue +18 -4
- package/app/theme/activity/separator.vue +44 -12
- package/app/theme/blank.vue +7 -1
- package/app/theme/index.ts +7 -2
- package/app/theme/index.vue +6 -2
- package/app/theme/menu/menu-item.vue +10 -4
- package/app/theme/menu/menu.vue +9 -4
- package/client/components/chat/overlay.vue +38 -13
- package/client/components/common/k-hint.vue +3 -1
- package/client/components/dynamic.vue +2 -1
- package/client/components/icons/index.ts +7 -2
- package/client/components/index.ts +8 -2
- package/client/components/link.ts +5 -2
- package/client/components/perms.vue +22 -5
- package/client/components/slot.ts +19 -4
- package/client/context.ts +12 -4
- package/client/data.ts +57 -26
- package/client/index.ts +2 -1
- package/client/locales/de-DE.yml +47 -0
- package/client/locales/en-US.yml +47 -0
- package/client/locales/fr-FR.yml +47 -0
- package/client/locales/ja-JP.yml +47 -0
- package/client/locales/ru-RU.yml +47 -0
- package/client/locales/zh-CN.yml +47 -0
- package/client/locales/zh-TW.yml +47 -0
- package/client/plugins/action.ts +46 -13
- package/client/plugins/i18n.ts +118 -3
- package/client/plugins/loader.ts +67 -45
- package/client/plugins/router.ts +34 -11
- package/client/plugins/setting.ts +60 -16
- package/client/plugins/theme.ts +51 -11
- package/client/utils.ts +7 -2
- package/lib/bin.mjs +3 -3
- package/lib/{client-CaTn_gVG.mjs → client-C97iYPCr.mjs} +1 -1
- package/lib/index.d.ts +3 -4
- package/lib/index.mjs +1 -1
- package/lib/{src-DXaM1Kgh.mjs → src-BOakssV3.mjs} +2 -2
- package/package.json +3 -3
- package/src/bin.ts +10 -3
- package/src/index.ts +59 -15
- package/app/home/welcome.en-US.yml +0 -13
- package/app/home/welcome.zh-CN.yml +0 -13
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
# Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
+
|
|
5
|
+
pages:
|
|
6
|
+
home: Главная
|
|
7
|
+
settings: Настройки пользователя
|
|
8
|
+
home:
|
|
9
|
+
connecting: Подключение к серверу Koishi...
|
|
10
|
+
welcome:
|
|
11
|
+
title: Добро пожаловать в Koishi!
|
|
12
|
+
description: Ваш бот готов к работе. Выберите любой пункт ниже, чтобы начать путешествие по Koishi.
|
|
13
|
+
docs:
|
|
14
|
+
title: Документация
|
|
15
|
+
description: Начните читать официальную документацию — в ней есть всё, что вы хотите узнать.
|
|
16
|
+
forum:
|
|
17
|
+
title: Форум
|
|
18
|
+
description: Общайтесь с другими пользователями на форуме. Если возникнут проблемы, здесь можно обратиться за помощью.
|
|
19
|
+
settings:
|
|
20
|
+
general: Общие настройки
|
|
21
|
+
appearance: Оформление
|
|
22
|
+
status: Настройки строки состояния
|
|
23
|
+
loading:
|
|
24
|
+
page: Загрузка компонентов страницы
|
|
25
|
+
data: Загрузка данных...
|
|
26
|
+
blank:
|
|
27
|
+
hint: Если вы видите эту страницу, плагин консоли загрузился успешно, но расширения консоли ещё не установлены.
|
|
28
|
+
menu:
|
|
29
|
+
resetActivity: Сбросить панель активности
|
|
30
|
+
overlay:
|
|
31
|
+
zoomOut: Уменьшить
|
|
32
|
+
zoomIn: Увеличить
|
|
33
|
+
reset: Сбросить
|
|
34
|
+
rotateLeft: Повернуть против часовой стрелки
|
|
35
|
+
rotateRight: Повернуть по часовой стрелке
|
|
36
|
+
schema:
|
|
37
|
+
settings:
|
|
38
|
+
$description: Общие настройки
|
|
39
|
+
locale: Настройки языка.
|
|
40
|
+
theme:
|
|
41
|
+
$description: Настройки темы
|
|
42
|
+
mode: Предпочтительная тема.
|
|
43
|
+
mode-auto: Как в системе
|
|
44
|
+
mode-dark: Тёмная
|
|
45
|
+
mode-light: Светлая
|
|
46
|
+
dark: Тёмная тема.
|
|
47
|
+
light: Светлая тема.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
# Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
+
|
|
5
|
+
pages:
|
|
6
|
+
home: 欢迎
|
|
7
|
+
settings: 用户设置
|
|
8
|
+
home:
|
|
9
|
+
connecting: 正在连接到 Koishi 服务器……
|
|
10
|
+
welcome:
|
|
11
|
+
title: 欢迎使用 Koishi!
|
|
12
|
+
description: 你的机器人已经准备就绪。点击下面的任一选项,开启你的 Koishi 之旅吧。
|
|
13
|
+
docs:
|
|
14
|
+
title: 阅读文档
|
|
15
|
+
description: 开始阅读官方文档。官方文档里包含了你想要了解的全部内容。
|
|
16
|
+
forum:
|
|
17
|
+
title: 前往论坛
|
|
18
|
+
description: 前往论坛与其他用户交流。如果你遇到了问题,可以在这里寻求帮助。
|
|
19
|
+
settings:
|
|
20
|
+
general: 通用设置
|
|
21
|
+
appearance: 外观设置
|
|
22
|
+
status: 状态栏设置
|
|
23
|
+
loading:
|
|
24
|
+
page: 正在加载页面组件
|
|
25
|
+
data: 正在加载数据……
|
|
26
|
+
blank:
|
|
27
|
+
hint: 如果你看到了这个页面,说明控制台插件已经加载成功,但你尚未安装任何控制台扩展。
|
|
28
|
+
menu:
|
|
29
|
+
resetActivity: 重置活动栏
|
|
30
|
+
overlay:
|
|
31
|
+
zoomOut: 缩小
|
|
32
|
+
zoomIn: 放大
|
|
33
|
+
reset: 复原
|
|
34
|
+
rotateLeft: 逆时针旋转
|
|
35
|
+
rotateRight: 顺时针旋转
|
|
36
|
+
schema:
|
|
37
|
+
settings:
|
|
38
|
+
$description: 通用设置
|
|
39
|
+
locale: 语言设置。
|
|
40
|
+
theme:
|
|
41
|
+
$description: 主题设置
|
|
42
|
+
mode: 主题偏好。
|
|
43
|
+
mode-auto: 跟随系统
|
|
44
|
+
mode-dark: 深色
|
|
45
|
+
mode-light: 浅色
|
|
46
|
+
dark: 深色主题。
|
|
47
|
+
light: 浅色主题。
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
# Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
+
|
|
5
|
+
pages:
|
|
6
|
+
home: 歡迎
|
|
7
|
+
settings: 使用者設定
|
|
8
|
+
home:
|
|
9
|
+
connecting: 正在連線到 Koishi 伺服器……
|
|
10
|
+
welcome:
|
|
11
|
+
title: 歡迎使用 Koishi!
|
|
12
|
+
description: 你的機器人已經準備就緒。點擊下面的任一選項,開啟你的 Koishi 之旅吧。
|
|
13
|
+
docs:
|
|
14
|
+
title: 閱讀文件
|
|
15
|
+
description: 開始閱讀官方文件。官方文件裡包含了你想要了解的全部內容。
|
|
16
|
+
forum:
|
|
17
|
+
title: 前往論壇
|
|
18
|
+
description: 前往論壇與其他使用者交流。如果你遇到了問題,可以在這裡尋求幫助。
|
|
19
|
+
settings:
|
|
20
|
+
general: 一般設定
|
|
21
|
+
appearance: 外觀設定
|
|
22
|
+
status: 狀態列設定
|
|
23
|
+
loading:
|
|
24
|
+
page: 正在載入頁面元件
|
|
25
|
+
data: 正在載入資料……
|
|
26
|
+
blank:
|
|
27
|
+
hint: 如果你看到了這個頁面,說明控制檯插件已經載入成功,但你尚未安裝任何控制檯擴充功能。
|
|
28
|
+
menu:
|
|
29
|
+
resetActivity: 重置活動欄
|
|
30
|
+
overlay:
|
|
31
|
+
zoomOut: 縮小
|
|
32
|
+
zoomIn: 放大
|
|
33
|
+
reset: 復原
|
|
34
|
+
rotateLeft: 逆時針旋轉
|
|
35
|
+
rotateRight: 順時針旋轉
|
|
36
|
+
schema:
|
|
37
|
+
settings:
|
|
38
|
+
$description: 一般設定
|
|
39
|
+
locale: 語言設定。
|
|
40
|
+
theme:
|
|
41
|
+
$description: 主題設定
|
|
42
|
+
mode: 主題偏好。
|
|
43
|
+
mode-auto: 跟隨系統
|
|
44
|
+
mode-dark: 深色
|
|
45
|
+
mode-light: 淺色
|
|
46
|
+
dark: 深色主題。
|
|
47
|
+
light: 淺色主題。
|
package/client/plugins/action.ts
CHANGED
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
* 绑定右键菜单。作用域变量支持 "a.b" 点分键,经 Flatten 类型
|
|
12
12
|
* 与运行时代理呈现为嵌套对象。
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
type Dict,
|
|
16
|
+
type Intersect,
|
|
17
|
+
remove,
|
|
18
|
+
} from "cosmokit";
|
|
15
19
|
import {
|
|
16
20
|
type MaybeRefOrGetter,
|
|
17
21
|
markRaw,
|
|
@@ -58,7 +62,8 @@ export interface ActionOptions {
|
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/** 旧版菜单项写法(字段与 ActionOptions 部分重叠),仅为兼容保留 */
|
|
61
|
-
export type LegacyMenuItem = Partial<ActionOptions> &
|
|
65
|
+
export type LegacyMenuItem = Partial<ActionOptions> &
|
|
66
|
+
Omit<MenuItem, "id">;
|
|
62
67
|
|
|
63
68
|
/** 菜单项:label / type / icon 均支持写成随作用域变化的 getter */
|
|
64
69
|
export interface MenuItem {
|
|
@@ -70,10 +75,14 @@ export interface MenuItem {
|
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
/** 值或(以作用域为参数的)取值函数 */
|
|
73
|
-
export type MaybeGetter<T> =
|
|
78
|
+
export type MaybeGetter<T> =
|
|
79
|
+
| T
|
|
80
|
+
| ((scope: Flatten<ActionContext>) => T);
|
|
74
81
|
|
|
75
82
|
/** 作用域存储:扁平键到「值或 ref 或 getter」的映射 */
|
|
76
|
-
type Store<S extends {}> = {
|
|
83
|
+
type Store<S extends {}> = {
|
|
84
|
+
[K in keyof S]?: MaybeRefOrGetter<S[K]>;
|
|
85
|
+
};
|
|
77
86
|
|
|
78
87
|
/**
|
|
79
88
|
* 把 "a.b.c" 形式的扁平键名归并为 { a: { b: { c } } } 的嵌套结构,
|
|
@@ -103,9 +112,14 @@ export interface ActiveMenu {
|
|
|
103
112
|
* 触发时把附加值写入作用域,并在鼠标位置弹出自定义菜单(由 global 插槽渲染)。
|
|
104
113
|
* @param id 菜单标识,menu() 注册的条目将展示在该菜单中
|
|
105
114
|
*/
|
|
106
|
-
export function useMenu<K extends keyof ActionContext>(
|
|
115
|
+
export function useMenu<K extends keyof ActionContext>(
|
|
116
|
+
id: K,
|
|
117
|
+
) {
|
|
107
118
|
const ctx = useContext();
|
|
108
|
-
return (
|
|
119
|
+
return (
|
|
120
|
+
event: MouseEvent,
|
|
121
|
+
value: MaybeRefOrGetter<ActionContext[K]>,
|
|
122
|
+
) => {
|
|
109
123
|
ctx.define(id, value);
|
|
110
124
|
event.preventDefault();
|
|
111
125
|
const { clientX, clientY } = event;
|
|
@@ -133,7 +147,9 @@ export default class ActionService extends Service {
|
|
|
133
147
|
super(ctx, "$action", true);
|
|
134
148
|
ctx.mixin("$action", ["action", "menu", "define"]);
|
|
135
149
|
|
|
136
|
-
ctx.internal.scope = shallowReactive(
|
|
150
|
+
ctx.internal.scope = shallowReactive(
|
|
151
|
+
{} as Store<ActionContext>,
|
|
152
|
+
);
|
|
137
153
|
ctx.internal.menus = reactive({});
|
|
138
154
|
ctx.internal.actions = reactive({});
|
|
139
155
|
ctx.internal.activeMenus = reactive([]);
|
|
@@ -142,7 +158,9 @@ export default class ActionService extends Service {
|
|
|
142
158
|
// 与键盘事件逐项比对(ctrl 在 macOS 平台映射为 meta 键)
|
|
143
159
|
ctx.addEventListener("keydown", (event) => {
|
|
144
160
|
const scope = this.createScope();
|
|
145
|
-
for (const action of Object.values(
|
|
161
|
+
for (const action of Object.values(
|
|
162
|
+
ctx.internal.actions,
|
|
163
|
+
)) {
|
|
146
164
|
if (!action.shortcut) continue;
|
|
147
165
|
const keys = action.shortcut
|
|
148
166
|
.split("+")
|
|
@@ -158,7 +176,11 @@ export default class ActionService extends Service {
|
|
|
158
176
|
continue;
|
|
159
177
|
case "ctrl":
|
|
160
178
|
// macOS 没有 ctrl+字母 的快捷键惯例,改用 Cmd(meta)
|
|
161
|
-
if (
|
|
179
|
+
if (
|
|
180
|
+
navigator.platform
|
|
181
|
+
.toLowerCase()
|
|
182
|
+
.includes("mac")
|
|
183
|
+
) {
|
|
162
184
|
metaKey = true;
|
|
163
185
|
} else {
|
|
164
186
|
ctrlKey = true;
|
|
@@ -195,7 +217,8 @@ export default class ActionService extends Service {
|
|
|
195
217
|
items.forEach((item) => insert(list, item));
|
|
196
218
|
return () => {
|
|
197
219
|
items.forEach((item) => remove(list, item));
|
|
198
|
-
if (!list.length)
|
|
220
|
+
if (!list.length)
|
|
221
|
+
delete this.ctx.internal.menus[id];
|
|
199
222
|
};
|
|
200
223
|
});
|
|
201
224
|
}
|
|
@@ -222,7 +245,10 @@ export default class ActionService extends Service {
|
|
|
222
245
|
* 可用 override 临时覆盖若干键值。
|
|
223
246
|
*/
|
|
224
247
|
createScope(override = {}) {
|
|
225
|
-
const scope = {
|
|
248
|
+
const scope = {
|
|
249
|
+
...this.ctx.internal.scope,
|
|
250
|
+
...override,
|
|
251
|
+
};
|
|
226
252
|
return createScope(scope);
|
|
227
253
|
}
|
|
228
254
|
}
|
|
@@ -241,11 +267,18 @@ function createScope(
|
|
|
241
267
|
if (typeof key === "symbol")
|
|
242
268
|
return (target as Record<symbol, unknown>)[key];
|
|
243
269
|
key = prefix + key;
|
|
244
|
-
const source = scope as Record<
|
|
270
|
+
const source = scope as Record<
|
|
271
|
+
string,
|
|
272
|
+
MaybeRefOrGetter<unknown>
|
|
273
|
+
>;
|
|
245
274
|
if (key in scope) return toValue(source[key]);
|
|
246
275
|
// 键本身不存在,但存在以其为前缀的子键:返回子级作用域代理
|
|
247
276
|
const _prefix = `${key}.`;
|
|
248
|
-
if (
|
|
277
|
+
if (
|
|
278
|
+
Object.keys(scope).some((k) =>
|
|
279
|
+
k.startsWith(_prefix),
|
|
280
|
+
)
|
|
281
|
+
) {
|
|
249
282
|
return createScope(scope, `${key}.`);
|
|
250
283
|
}
|
|
251
284
|
// 完全未命中:显式返回 undefined(noImplicitReturns)
|
package/client/plugins/i18n.ts
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
// Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
3
|
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
4
|
|
|
5
|
+
import { type Dict, valueMap } from "cosmokit";
|
|
5
6
|
import { watchEffect } from "vue";
|
|
6
7
|
import { createI18n } from "vue-i18n";
|
|
7
8
|
import type { Context } from "../context";
|
|
9
|
+
import deDE from "../locales/de-DE.yml";
|
|
10
|
+
import enUS from "../locales/en-US.yml";
|
|
11
|
+
import frFR from "../locales/fr-FR.yml";
|
|
12
|
+
import jaJP from "../locales/ja-JP.yml";
|
|
13
|
+
import ruRU from "../locales/ru-RU.yml";
|
|
14
|
+
import zhCN from "../locales/zh-CN.yml";
|
|
15
|
+
import zhTW from "../locales/zh-TW.yml";
|
|
8
16
|
import { Service } from "../utils";
|
|
9
17
|
import { useConfig } from "./setting";
|
|
10
18
|
|
|
@@ -14,22 +22,69 @@ declare module "../context" {
|
|
|
14
22
|
}
|
|
15
23
|
}
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
const
|
|
25
|
+
/** 宿主自身的全局词典:随服务启动注入,键结构见 client/locales/zh-CN.yml */
|
|
26
|
+
export const localeMessages: Dict<Dict> = {
|
|
27
|
+
"de-DE": deDE,
|
|
28
|
+
"en-US": enUS,
|
|
29
|
+
"fr-FR": frFR,
|
|
30
|
+
"ja-JP": jaJP,
|
|
31
|
+
"ru-RU": ruRU,
|
|
32
|
+
"zh-CN": zhCN,
|
|
33
|
+
"zh-TW": zhTW,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 从一组 `{ locale: 词典 }` 映射中按路径摘取各语种的同一子树,
|
|
38
|
+
* 构造 Schemastery `.i18n()` 的入参形态(`{ locale: 子树 }`)。
|
|
39
|
+
* 宿主与扩展的 schema 词典均可经此摘取;yaml 为动态数据,
|
|
40
|
+
* 收窄集中在这一处。
|
|
41
|
+
*/
|
|
42
|
+
export function pickFrom(
|
|
43
|
+
messages: Dict<unknown>,
|
|
44
|
+
...path: string[]
|
|
45
|
+
): Dict {
|
|
46
|
+
return valueMap(messages, (data) => {
|
|
47
|
+
let node: unknown = data;
|
|
48
|
+
for (const key of path) {
|
|
49
|
+
node = (node as Dict<unknown> | undefined)?.[key];
|
|
50
|
+
}
|
|
51
|
+
return node;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 从宿主词典中按路径摘取各语种的同一子树(`pickFrom` 的宿主词典
|
|
57
|
+
* 专用便捷形式),叶节点上亦可取得 `Dict<string>`(const 选项的
|
|
58
|
+
* 显示名直接传 description)。
|
|
59
|
+
*/
|
|
60
|
+
export function pickMessages<T = unknown>(
|
|
61
|
+
...path: string[]
|
|
62
|
+
): Dict<T> {
|
|
63
|
+
return pickFrom(localeMessages, ...path) as Dict<T>;
|
|
64
|
+
}
|
|
19
65
|
|
|
20
66
|
/**
|
|
21
67
|
* 国际化服务:持有 vue-i18n 实例(默认回退语言 zh-CN),
|
|
22
68
|
* 并监听本地配置中的 locale 变化实时切换界面语言。
|
|
23
|
-
*
|
|
69
|
+
*
|
|
70
|
+
* 宿主词典在实例创建时整体传入;各扩展自身的语言包由其 client
|
|
71
|
+
* 入口调用 `ctx.$i18n.extend()` 深合并注入(vue-i18n 已被构建别名
|
|
72
|
+
* 钉到宿主单实例,扩展与宿主共享同一 composer)。
|
|
24
73
|
*/
|
|
25
74
|
export default class I18nService extends Service {
|
|
26
75
|
public i18n = createI18n({
|
|
27
76
|
legacy: false,
|
|
28
77
|
fallbackLocale: "zh-CN",
|
|
78
|
+
messages: localeMessages,
|
|
29
79
|
});
|
|
30
80
|
|
|
31
81
|
constructor(ctx: Context) {
|
|
32
82
|
super(ctx, "$i18n", true);
|
|
83
|
+
instance = this;
|
|
84
|
+
|
|
85
|
+
// 客户端本地配置(在构造期获取而非模块顶层,避免与
|
|
86
|
+
// setting.ts 的循环依赖在模块求值顺序上踩 TDZ)
|
|
87
|
+
const config = useConfig();
|
|
33
88
|
|
|
34
89
|
// flush: "post" 确保在语言切换引起重渲染前完成 locale 写入
|
|
35
90
|
ctx.effect(() =>
|
|
@@ -44,4 +99,64 @@ export default class I18nService extends Service {
|
|
|
44
99
|
),
|
|
45
100
|
);
|
|
46
101
|
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 注入扩展语言包:按 locale 深合并进全局词典。
|
|
105
|
+
* 扩展应把自身的键收纳在独立命名空间(如 `explorer.*`)下,
|
|
106
|
+
* 避免与其他扩展或宿主词典冲突。
|
|
107
|
+
*/
|
|
108
|
+
extend(locale: string, messages: Dict) {
|
|
109
|
+
this.i18n.global.mergeLocaleMessage(locale, messages);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 全局翻译函数(非组件上下文使用,如 activity 名的 getter 与
|
|
114
|
+
* echarts 等纯 ts 模块)。可携带位置插值参数。
|
|
115
|
+
*/
|
|
116
|
+
t(key: string, args?: unknown[]) {
|
|
117
|
+
return this.i18n.global.t(key, args ?? []);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 服务实例的模块级引用:仅用于开发期 HMR 回调中定位热替换目标
|
|
122
|
+
let instance: I18nService | undefined;
|
|
123
|
+
|
|
124
|
+
// 开发模式下宿主词典 yml 改动即时热替换(accept 的路径须为静态字面量);
|
|
125
|
+
// 用 merge 而非 set,以免覆盖扩展此前经 extend() 注入的同语种词条
|
|
126
|
+
if (import.meta.hot) {
|
|
127
|
+
const replace = (locale: string) => (module: unknown) => {
|
|
128
|
+
// 热替换模块的 default 导出即该语种的完整词典
|
|
129
|
+
const messages = (
|
|
130
|
+
module as { default?: Dict } | undefined
|
|
131
|
+
)?.default;
|
|
132
|
+
instance?.extend(locale, messages ?? {});
|
|
133
|
+
};
|
|
134
|
+
import.meta.hot.accept(
|
|
135
|
+
"../locales/de-DE.yml",
|
|
136
|
+
replace("de-DE"),
|
|
137
|
+
);
|
|
138
|
+
import.meta.hot.accept(
|
|
139
|
+
"../locales/en-US.yml",
|
|
140
|
+
replace("en-US"),
|
|
141
|
+
);
|
|
142
|
+
import.meta.hot.accept(
|
|
143
|
+
"../locales/fr-FR.yml",
|
|
144
|
+
replace("fr-FR"),
|
|
145
|
+
);
|
|
146
|
+
import.meta.hot.accept(
|
|
147
|
+
"../locales/ja-JP.yml",
|
|
148
|
+
replace("ja-JP"),
|
|
149
|
+
);
|
|
150
|
+
import.meta.hot.accept(
|
|
151
|
+
"../locales/ru-RU.yml",
|
|
152
|
+
replace("ru-RU"),
|
|
153
|
+
);
|
|
154
|
+
import.meta.hot.accept(
|
|
155
|
+
"../locales/zh-CN.yml",
|
|
156
|
+
replace("zh-CN"),
|
|
157
|
+
);
|
|
158
|
+
import.meta.hot.accept(
|
|
159
|
+
"../locales/zh-TW.yml",
|
|
160
|
+
replace("zh-TW"),
|
|
161
|
+
);
|
|
47
162
|
}
|
package/client/plugins/loader.ts
CHANGED
|
@@ -29,12 +29,17 @@ export function defineExtension(callback: Extension) {
|
|
|
29
29
|
|
|
30
30
|
/** 兼容 rollup 与 vite 两种产物:优先取 default 导出 */
|
|
31
31
|
export function unwrapExports(module: unknown): Plugin {
|
|
32
|
-
const record = module as
|
|
32
|
+
const record = module as
|
|
33
|
+
| { default?: unknown }
|
|
34
|
+
| null
|
|
35
|
+
| undefined;
|
|
33
36
|
return (record?.default || module) as Plugin;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/** 按文件后缀分发的加载器:样式表插入 <link>,其余按动态模块导入 */
|
|
37
|
-
const loaders: Dict<
|
|
40
|
+
const loaders: Dict<
|
|
41
|
+
(ctx: Context, url: string) => Promise<void>
|
|
42
|
+
> = {
|
|
38
43
|
async [`.css`](ctx, url) {
|
|
39
44
|
const link = document.createElement("link");
|
|
40
45
|
link.rel = "stylesheet";
|
|
@@ -53,7 +58,10 @@ const loaders: Dict<(ctx: Context, url: string) => Promise<void>> = {
|
|
|
53
58
|
async [``](ctx, url) {
|
|
54
59
|
const exports = await import(/* @vite-ignore */ url);
|
|
55
60
|
// 扩展入口均为函数式插件;断言到 Function 形状以走 config 可选的重载
|
|
56
|
-
ctx.plugin(
|
|
61
|
+
ctx.plugin(
|
|
62
|
+
unwrapExports(exports) as Plugin.Function,
|
|
63
|
+
ctx.extension?.data,
|
|
64
|
+
);
|
|
57
65
|
},
|
|
58
66
|
};
|
|
59
67
|
|
|
@@ -77,20 +85,25 @@ export default class LoaderService extends Service {
|
|
|
77
85
|
private backendId: string | undefined;
|
|
78
86
|
|
|
79
87
|
/** 当前已加载扩展的 id → LoadResult 映射 */
|
|
80
|
-
public extensions: Dict<LoadResult> = shallowReactive(
|
|
88
|
+
public extensions: Dict<LoadResult> = shallowReactive(
|
|
89
|
+
{} as Dict<LoadResult>,
|
|
90
|
+
);
|
|
81
91
|
|
|
82
92
|
constructor(ctx: Context) {
|
|
83
93
|
super(ctx, "$loader", true);
|
|
84
94
|
|
|
85
95
|
// 服务端推送扩展入口的随附数据(entry-data):
|
|
86
96
|
// 同步到 store.entry 并刷新已加载扩展的 data ref
|
|
87
|
-
receive<{ id: string; data: unknown }>(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
receive<{ id: string; data: unknown }>(
|
|
98
|
+
"entry-data",
|
|
99
|
+
({ id, data }) => {
|
|
100
|
+
const entry = store.entry?.[id];
|
|
101
|
+
if (!entry) return;
|
|
102
|
+
entry.data = data;
|
|
103
|
+
const extension = this.extensions[id];
|
|
104
|
+
if (extension) extension.data.value = data;
|
|
105
|
+
},
|
|
106
|
+
);
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
/**
|
|
@@ -102,10 +115,15 @@ export default class LoaderService extends Service {
|
|
|
102
115
|
() => store.entry,
|
|
103
116
|
async (newValue, oldValue) => {
|
|
104
117
|
// _id 标识后端实例:变化说明服务端已重启,页面状态不再可信
|
|
105
|
-
const { _id, ...rest } = (newValue ||
|
|
118
|
+
const { _id, ...rest } = (newValue ||
|
|
119
|
+
{}) as Dict<EntryData> & {
|
|
106
120
|
_id?: string | undefined;
|
|
107
121
|
};
|
|
108
|
-
if (
|
|
122
|
+
if (
|
|
123
|
+
this.backendId &&
|
|
124
|
+
_id &&
|
|
125
|
+
this.backendId !== _id
|
|
126
|
+
) {
|
|
109
127
|
window.location.reload();
|
|
110
128
|
return;
|
|
111
129
|
}
|
|
@@ -119,38 +137,42 @@ export default class LoaderService extends Service {
|
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
await Promise.all(
|
|
122
|
-
Object.entries(rest).map(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
140
|
+
Object.entries(rest).map(
|
|
141
|
+
([key, { files, paths, data }]) => {
|
|
142
|
+
if (this.extensions[key]) return;
|
|
143
|
+
// 每个扩展在隔离的 "extension" 作用域中运行,
|
|
144
|
+
// 并通过 ctx.extension 拿到自己的加载结果
|
|
145
|
+
const scope = this.ctx
|
|
146
|
+
.isolate("extension")
|
|
147
|
+
.plugin(() => {});
|
|
148
|
+
scope.ctx.extension = this.extensions[key] = {
|
|
149
|
+
done: ref(false),
|
|
150
|
+
scope,
|
|
151
|
+
paths,
|
|
152
|
+
data: ref(data),
|
|
153
|
+
};
|
|
154
|
+
// 依次加载入口声明的全部文件,按后缀分发给对应 loader;
|
|
155
|
+
// task 返回给外层 Promise.all,使 initTask 真正等到全部
|
|
156
|
+
// 入口文件 settle 后才放行(否则 router install 早于扩展
|
|
157
|
+
// 注册路由,初始导航会对当前路径报 no match)。
|
|
158
|
+
// 扩展加载失败不阻塞界面可用性
|
|
159
|
+
const task = Promise.all(
|
|
160
|
+
files.map((url) => {
|
|
161
|
+
for (const ext in loaders) {
|
|
162
|
+
if (!url.endsWith(ext)) continue;
|
|
163
|
+
return loaders[ext]?.(scope.ctx, url);
|
|
164
|
+
}
|
|
165
|
+
// 无匹配后缀的入口文件:显式跳过(noImplicitReturns)
|
|
166
|
+
return undefined;
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
void task.finally(() => {
|
|
170
|
+
const extension = this.extensions[key];
|
|
171
|
+
if (extension) extension.done.value = true;
|
|
172
|
+
});
|
|
173
|
+
return task.catch(() => {});
|
|
174
|
+
},
|
|
175
|
+
),
|
|
154
176
|
);
|
|
155
177
|
|
|
156
178
|
// 首次加载完成(oldValue 为空)时放行 initTask
|