@koishi-ce/client 1.0.3 → 1.0.4
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/app/home/welcome.vue +12 -6
- 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/settings.vue +6 -4
- package/app/settings/theme.vue +4 -1
- package/app/status/loading.vue +4 -1
- 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/index.ts +5 -1
- package/app/theme/index.vue +3 -1
- package/app/theme/menu/menu-item.vue +10 -4
- package/app/theme/menu/menu.vue +9 -4
- package/client/components/chat/overlay.vue +30 -8
- 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/plugins/action.ts +46 -13
- package/client/plugins/loader.ts +67 -45
- package/client/plugins/router.ts +34 -11
- package/client/plugins/setting.ts +45 -12
- package/client/plugins/theme.ts +21 -6
- 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.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
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
// Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
3
|
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type App,
|
|
7
|
+
type Component,
|
|
8
|
+
defineComponent,
|
|
9
|
+
h,
|
|
10
|
+
} from "vue";
|
|
6
11
|
import { useContext } from "../context";
|
|
7
12
|
|
|
8
13
|
/** 插槽条目的基础形状:参与排序的组件 */
|
|
@@ -47,10 +52,15 @@ export const KSlot = defineComponent({
|
|
|
47
52
|
? []
|
|
48
53
|
: [...(slots["default"]?.() || [])]
|
|
49
54
|
.filter((node) => node.type === KSlotItem)
|
|
50
|
-
.map((node) => ({
|
|
55
|
+
.map((node) => ({
|
|
56
|
+
node,
|
|
57
|
+
order: node.props?.["order"] || 0,
|
|
58
|
+
}));
|
|
51
59
|
// external:ctx.slot() 注册的视图,disabled 的不渲染
|
|
52
60
|
const external = [
|
|
53
|
-
...(props.name
|
|
61
|
+
...(props.name
|
|
62
|
+
? ctx.$router.views[props.name] || []
|
|
63
|
+
: []),
|
|
54
64
|
]
|
|
55
65
|
.filter((item) => !item.disabled?.())
|
|
56
66
|
.map((item) => ({
|
|
@@ -85,7 +95,12 @@ function defineSlotComponent(name: string) {
|
|
|
85
95
|
return defineComponent({
|
|
86
96
|
inheritAttrs: false,
|
|
87
97
|
setup(_, { slots, attrs }) {
|
|
88
|
-
return () =>
|
|
98
|
+
return () =>
|
|
99
|
+
h(
|
|
100
|
+
KSlot,
|
|
101
|
+
{ name, data: attrs, single: true },
|
|
102
|
+
slots,
|
|
103
|
+
);
|
|
89
104
|
},
|
|
90
105
|
});
|
|
91
106
|
}
|
package/client/context.ts
CHANGED
|
@@ -26,7 +26,8 @@ import ThemeService from "./plugins/theme";
|
|
|
26
26
|
// —— 布局相关的 Context 类型扩展 ——
|
|
27
27
|
|
|
28
28
|
/** 前端侧事件表:在 cordis 事件的基础上扩展本库自定义事件 */
|
|
29
|
-
export interface Events<C extends Context = Context>
|
|
29
|
+
export interface Events<C extends Context = Context>
|
|
30
|
+
extends cordis.Events<C> {}
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* 前端 Context 接口:各服务插件通过 `declare module "../context"`
|
|
@@ -84,7 +85,10 @@ export class Context extends cordis.Context {
|
|
|
84
85
|
this.app = createApp(
|
|
85
86
|
defineComponent({
|
|
86
87
|
setup: () => () => [
|
|
87
|
-
h(resolveComponent("k-slot"), {
|
|
88
|
+
h(resolveComponent("k-slot"), {
|
|
89
|
+
name: "root",
|
|
90
|
+
single: true,
|
|
91
|
+
}),
|
|
88
92
|
h(resolveComponent("k-slot"), { name: "global" }),
|
|
89
93
|
],
|
|
90
94
|
}),
|
|
@@ -115,12 +119,16 @@ export class Context extends cordis.Context {
|
|
|
115
119
|
*/
|
|
116
120
|
addEventListener<K extends keyof WindowEventMap>(
|
|
117
121
|
type: K,
|
|
118
|
-
listener: (
|
|
122
|
+
listener: (
|
|
123
|
+
this: Window,
|
|
124
|
+
ev: WindowEventMap[K],
|
|
125
|
+
) => unknown,
|
|
119
126
|
options?: boolean | AddEventListenerOptions,
|
|
120
127
|
) {
|
|
121
128
|
return this.effect(() => {
|
|
122
129
|
window.addEventListener(type, listener, options);
|
|
123
|
-
return () =>
|
|
130
|
+
return () =>
|
|
131
|
+
window.removeEventListener(type, listener, options);
|
|
124
132
|
});
|
|
125
133
|
}
|
|
126
134
|
|
package/client/data.ts
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
4
|
|
|
5
5
|
/// <reference path="./shims.d.ts" />
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
Promisify,
|
|
8
|
+
Universal,
|
|
9
|
+
} from "@koishi-ce/koishi";
|
|
7
10
|
import type {
|
|
8
11
|
ClientConfig,
|
|
9
12
|
Console,
|
|
@@ -43,7 +46,8 @@ export function withProxy(url: string) {
|
|
|
43
46
|
/** 当前与服务端的 WebSocket 连接(未连接时为 null) */
|
|
44
47
|
export const socket = ref<Universal.WebSocket | null>(null);
|
|
45
48
|
/** 按事件名登记的推送监听器(receive 注册) */
|
|
46
|
-
const listeners: Record<string, (data: unknown) => void> =
|
|
49
|
+
const listeners: Record<string, (data: unknown) => void> =
|
|
50
|
+
{};
|
|
47
51
|
/** 按报文 id 暂存的 RPC 应答钩子 [resolve, reject](send 写入) */
|
|
48
52
|
const responseHooks: Record<
|
|
49
53
|
string,
|
|
@@ -93,19 +97,26 @@ type StoreValue = Store[keyof Store];
|
|
|
93
97
|
|
|
94
98
|
// 服务端整表推送:直接覆盖 store 中对应键
|
|
95
99
|
// (key 为联合类型时直写 store[key] 会被要求交叉类型,故经 Record 视图写入)
|
|
96
|
-
receive<{ key: keyof Store; value: unknown }>(
|
|
97
|
-
|
|
98
|
-
})
|
|
100
|
+
receive<{ key: keyof Store; value: unknown }>(
|
|
101
|
+
"data",
|
|
102
|
+
({ key, value }) => {
|
|
103
|
+
(store as Record<keyof Store, StoreValue>)[key] =
|
|
104
|
+
value as StoreValue;
|
|
105
|
+
},
|
|
106
|
+
);
|
|
99
107
|
|
|
100
108
|
// 服务端增量推送:数组做追加,对象做浅合并
|
|
101
|
-
receive<{ key: keyof Store; value: unknown }>(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
receive<{ key: keyof Store; value: unknown }>(
|
|
110
|
+
"patch",
|
|
111
|
+
({ key, value }) => {
|
|
112
|
+
const current = store[key];
|
|
113
|
+
if (Array.isArray(current)) {
|
|
114
|
+
(current as unknown[]).push(...(value as unknown[]));
|
|
115
|
+
} else if (current) {
|
|
116
|
+
Object.assign(current, value);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
);
|
|
109
120
|
|
|
110
121
|
// RPC 应答分发:按报文 id 找到 send() 留下的 resolve/reject 并结算
|
|
111
122
|
receive<{ id: string; value?: unknown; error?: unknown }>(
|
|
@@ -131,7 +142,10 @@ receive<{ id: string; value?: unknown; error?: unknown }>(
|
|
|
131
142
|
* @param ctx 根 Context,用于把消息转发为 cordis 事件
|
|
132
143
|
* @param callback 创建 WebSocket 实例的工厂函数
|
|
133
144
|
*/
|
|
134
|
-
export function connect(
|
|
145
|
+
export function connect(
|
|
146
|
+
ctx: Context,
|
|
147
|
+
callback: () => Universal.WebSocket,
|
|
148
|
+
) {
|
|
135
149
|
const value = callback();
|
|
136
150
|
|
|
137
151
|
let sendTimer: number;
|
|
@@ -142,8 +156,14 @@ export function connect(ctx: Context, callback: () => Universal.WebSocket) {
|
|
|
142
156
|
if (!global.heartbeat) return;
|
|
143
157
|
clearTimeout(sendTimer);
|
|
144
158
|
clearTimeout(closeTimer);
|
|
145
|
-
sendTimer = +setTimeout(
|
|
146
|
-
|
|
159
|
+
sendTimer = +setTimeout(
|
|
160
|
+
() => send("ping"),
|
|
161
|
+
global.heartbeat.interval,
|
|
162
|
+
);
|
|
163
|
+
closeTimer = +setTimeout(
|
|
164
|
+
() => value?.close(),
|
|
165
|
+
global.heartbeat.timeout,
|
|
166
|
+
);
|
|
147
167
|
};
|
|
148
168
|
|
|
149
169
|
const reconnect = () => {
|
|
@@ -152,10 +172,14 @@ export function connect(ctx: Context, callback: () => Universal.WebSocket) {
|
|
|
152
172
|
for (const key in store) {
|
|
153
173
|
(store as Record<string, unknown>)[key] = undefined;
|
|
154
174
|
}
|
|
155
|
-
console.log(
|
|
175
|
+
console.log(
|
|
176
|
+
"[koishi] websocket disconnected, will retry in 1s...",
|
|
177
|
+
);
|
|
156
178
|
setTimeout(() => {
|
|
157
179
|
connect(ctx, callback).then(location.reload, () => {
|
|
158
|
-
console.log(
|
|
180
|
+
console.log(
|
|
181
|
+
"[koishi] websocket disconnected, will retry in 1s...",
|
|
182
|
+
);
|
|
159
183
|
});
|
|
160
184
|
}, 1000);
|
|
161
185
|
};
|
|
@@ -163,7 +187,12 @@ export function connect(ctx: Context, callback: () => Universal.WebSocket) {
|
|
|
163
187
|
value.addEventListener("message", (ev) => {
|
|
164
188
|
refresh();
|
|
165
189
|
const data = JSON.parse(ev.data);
|
|
166
|
-
console.debug(
|
|
190
|
+
console.debug(
|
|
191
|
+
"↓%c",
|
|
192
|
+
"color:purple",
|
|
193
|
+
data.type,
|
|
194
|
+
data.body,
|
|
195
|
+
);
|
|
167
196
|
if (data.type in listeners) {
|
|
168
197
|
listeners[data.type]?.(data.body);
|
|
169
198
|
}
|
|
@@ -172,11 +201,13 @@ export function connect(ctx: Context, callback: () => Universal.WebSocket) {
|
|
|
172
201
|
|
|
173
202
|
value.addEventListener("close", reconnect);
|
|
174
203
|
|
|
175
|
-
return new Promise<Universal.WebSocket.Event>(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
204
|
+
return new Promise<Universal.WebSocket.Event>(
|
|
205
|
+
(resolve, reject) => {
|
|
206
|
+
value.addEventListener("open", (event) => {
|
|
207
|
+
socket.value = markRaw(value);
|
|
208
|
+
resolve(event);
|
|
209
|
+
});
|
|
210
|
+
value.addEventListener("error", reject);
|
|
211
|
+
},
|
|
212
|
+
);
|
|
182
213
|
}
|
package/client/index.ts
CHANGED
|
@@ -37,7 +37,8 @@ export const ScopeStatus = {
|
|
|
37
37
|
DISPOSED: 4,
|
|
38
38
|
} as const;
|
|
39
39
|
|
|
40
|
-
export type ScopeStatus =
|
|
40
|
+
export type ScopeStatus =
|
|
41
|
+
(typeof ScopeStatus)[keyof typeof ScopeStatus];
|
|
41
42
|
export * from "./components";
|
|
42
43
|
export * from "./context";
|
|
43
44
|
export * from "./data";
|
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/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
|
package/client/plugins/router.ts
CHANGED
|
@@ -19,7 +19,11 @@ import {
|
|
|
19
19
|
ref,
|
|
20
20
|
toValue,
|
|
21
21
|
} from "vue";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
createRouter,
|
|
24
|
+
createWebHistory,
|
|
25
|
+
START_LOCATION,
|
|
26
|
+
} from "vue-router";
|
|
23
27
|
import type { SlotOptions } from "../components";
|
|
24
28
|
import Overlay from "../components/chat/overlay.vue";
|
|
25
29
|
import type { Context } from "../context";
|
|
@@ -106,8 +110,15 @@ export class Activity {
|
|
|
106
110
|
this.options = options;
|
|
107
111
|
options.order ??= 0;
|
|
108
112
|
options.position ??= "top";
|
|
109
|
-
Object.assign(
|
|
110
|
-
|
|
113
|
+
Object.assign(
|
|
114
|
+
this,
|
|
115
|
+
omit(options, ["icon", "name", "desc", "disabled"]),
|
|
116
|
+
);
|
|
117
|
+
const {
|
|
118
|
+
path,
|
|
119
|
+
id = getActivityId(path),
|
|
120
|
+
component,
|
|
121
|
+
} = options;
|
|
111
122
|
// 路由挂载时把 activity 实例放进 meta,供导航守卫与标题栏取用
|
|
112
123
|
this._disposables.push(
|
|
113
124
|
ctx.$router.router.addRoute({
|
|
@@ -127,7 +138,9 @@ export class Activity {
|
|
|
127
138
|
/** 若存在待跳转目标且当前路由已能解析,则立即完成该跳转 */
|
|
128
139
|
handleUpdate() {
|
|
129
140
|
if (redirectTo.value) {
|
|
130
|
-
const location = this.ctx.$router.router.resolve(
|
|
141
|
+
const location = this.ctx.$router.router.resolve(
|
|
142
|
+
redirectTo.value,
|
|
143
|
+
);
|
|
131
144
|
if (location.matched.length) {
|
|
132
145
|
redirectTo.value = null;
|
|
133
146
|
this.ctx.$router.router.replace(location);
|
|
@@ -156,7 +169,8 @@ export class Activity {
|
|
|
156
169
|
*/
|
|
157
170
|
disabled() {
|
|
158
171
|
if (this.ctx.bail("activity", this)) return true;
|
|
159
|
-
if (!this.fields?.every((key) => store[key]))
|
|
172
|
+
if (!this.fields?.every((key) => store[key]))
|
|
173
|
+
return true;
|
|
160
174
|
if (this.when && !this.when()) return true;
|
|
161
175
|
if (this.options.disabled?.()) return true;
|
|
162
176
|
return false;
|
|
@@ -165,10 +179,13 @@ export class Activity {
|
|
|
165
179
|
dispose() {
|
|
166
180
|
this._disposables.forEach((dispose) => dispose());
|
|
167
181
|
// 若被卸载的恰是当前展示的页面:暂存目标路由,退回缓存的首页
|
|
168
|
-
const current =
|
|
182
|
+
const current =
|
|
183
|
+
this.ctx.$router.router.currentRoute.value;
|
|
169
184
|
if (current?.meta?.activity === this) {
|
|
170
185
|
redirectTo.value = current.fullPath;
|
|
171
|
-
this.ctx.$router.router.push(
|
|
186
|
+
this.ctx.$router.router.push(
|
|
187
|
+
this.ctx.$router.cache["home"] || "/",
|
|
188
|
+
);
|
|
172
189
|
}
|
|
173
190
|
return delete this.ctx.$router.pages[this.id];
|
|
174
191
|
}
|
|
@@ -203,7 +220,8 @@ export default class RouterService extends Service {
|
|
|
203
220
|
const initialTitle = document.title;
|
|
204
221
|
ctx.effect(() =>
|
|
205
222
|
this.router.afterEach((route) => {
|
|
206
|
-
const { name, fullPath } =
|
|
223
|
+
const { name, fullPath } =
|
|
224
|
+
this.router.currentRoute.value;
|
|
207
225
|
if (name) {
|
|
208
226
|
// 本应用的路由名均为字符串;reactive 收窄了索引签名,不含 symbol
|
|
209
227
|
this.cache[name as string] = fullPath;
|
|
@@ -211,7 +229,8 @@ export default class RouterService extends Service {
|
|
|
211
229
|
// 进入 activity 页面时更新标签页标题为 "页面名 | 原标题"
|
|
212
230
|
if (route.meta.activity) {
|
|
213
231
|
document.title = `${route.meta.activity.name}`;
|
|
214
|
-
if (initialTitle)
|
|
232
|
+
if (initialTitle)
|
|
233
|
+
document.title += ` | ${initialTitle}`;
|
|
215
234
|
}
|
|
216
235
|
}),
|
|
217
236
|
);
|
|
@@ -254,7 +273,9 @@ export default class RouterService extends Service {
|
|
|
254
273
|
*/
|
|
255
274
|
slot(options: SlotOptions) {
|
|
256
275
|
options.order ??= 0;
|
|
257
|
-
const component = this.ctx.wrapComponent(
|
|
276
|
+
const component = this.ctx.wrapComponent(
|
|
277
|
+
options.component,
|
|
278
|
+
);
|
|
258
279
|
if (component) options.component = component;
|
|
259
280
|
if (options.when) {
|
|
260
281
|
const { when } = options;
|
|
@@ -272,7 +293,9 @@ export default class RouterService extends Service {
|
|
|
272
293
|
|
|
273
294
|
/** 注册 activity 页面(自动挂到 vue-router 与侧栏);返回取消注册函数 */
|
|
274
295
|
page(options: Activity.Options) {
|
|
275
|
-
const component = this.ctx.wrapComponent(
|
|
296
|
+
const component = this.ctx.wrapComponent(
|
|
297
|
+
options.component,
|
|
298
|
+
);
|
|
276
299
|
if (component) options.component = component;
|
|
277
300
|
return this.ctx.effect(() => {
|
|
278
301
|
const activity = new Activity(this.ctx, options);
|