weifuwu 0.33.2 → 0.33.3
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/dist/client/app.d.ts +6 -3
- package/dist/client/components/Chat.d.ts +17 -0
- package/dist/client/components/LoginForm.d.ts +14 -0
- package/dist/client/index.d.ts +11 -4
- package/dist/client/index.js +377 -3
- package/dist/client/jsx-runtime.js +377 -3
- package/dist/client/middleware/api.d.ts +32 -0
- package/dist/client/middleware/auth.d.ts +32 -0
- package/dist/client/middleware/ws.d.ts +21 -0
- package/dist/client/types.d.ts +36 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +46 -0
- package/dist/ui/index.d.ts +37 -0
- package/package.json +1 -1
package/dist/client/app.d.ts
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
* weifuwu/client 应用 — 创建 ctx + 中间件链 + 挂载组件
|
|
3
3
|
*
|
|
4
4
|
* ```tsx
|
|
5
|
-
* import { createApp, router } from 'weifuwu/client'
|
|
5
|
+
* import { createApp, api, auth, ws, router } from 'weifuwu/client'
|
|
6
6
|
*
|
|
7
7
|
* const app = createApp()
|
|
8
|
-
* app.use(
|
|
8
|
+
* app.use(api())
|
|
9
|
+
* app.use(auth())
|
|
10
|
+
* app.use(ws())
|
|
11
|
+
* app.use(router({ routes }))
|
|
9
12
|
* app.mount('#root', AppShell)
|
|
10
13
|
* ```
|
|
11
14
|
*/
|
|
@@ -17,5 +20,5 @@ import type { Component } from './jsx-runtime.ts';
|
|
|
17
20
|
export declare function createApp(): {
|
|
18
21
|
ctx: WfuiContext;
|
|
19
22
|
use: (mw: AppMiddleware) => any;
|
|
20
|
-
mount: (rootSelector: string, RootComponent: Component) => void
|
|
23
|
+
mount: (rootSelector: string, RootComponent: Component) => Promise<void>;
|
|
21
24
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat — 消息聊天组件
|
|
3
|
+
*
|
|
4
|
+
* 与 weifuwu 后端 messager + agent 模块对接。
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { Chat } from 'weifuwu/client'
|
|
8
|
+
*
|
|
9
|
+
* function ChatPage(_, ctx) {
|
|
10
|
+
* return Chat({ conversationId: '123' }, ctx)
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import type { WfuiContext } from '../types.ts';
|
|
15
|
+
export declare function Chat({ conversationId }: {
|
|
16
|
+
conversationId: string;
|
|
17
|
+
}, ctx: WfuiContext): Node;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoginForm — 登录/注册组件
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { LoginForm } from 'weifuwu/client'
|
|
6
|
+
*
|
|
7
|
+
* function LoginPage(_, ctx) {
|
|
8
|
+
* if (ctx.isAuthenticated) return ctx.app.navigate('/')
|
|
9
|
+
* return LoginForm({}, ctx)
|
|
10
|
+
* }
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import type { WfuiContext } from '../types.ts';
|
|
14
|
+
export declare function LoginForm(_props: {}, ctx: WfuiContext): Node;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* weifuwu/client — 响应式前端框架,TSX + Signal
|
|
3
3
|
*
|
|
4
4
|
* ```tsx
|
|
5
|
-
* import { signal,
|
|
5
|
+
* import { signal, createdApp, api, auth, ws, router, RouteView, Show, For } from 'weifuwu/client'
|
|
6
6
|
* import type { WfuiContext } from 'weifuwu/client'
|
|
7
7
|
*
|
|
8
8
|
* const app = createApp()
|
|
9
|
-
* app.use(
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* app.use(api())
|
|
10
|
+
* app.use(auth())
|
|
11
|
+
* app.use(ws())
|
|
12
|
+
* app.use(router({ routes }))
|
|
12
13
|
* app.mount('#root', AppShell)
|
|
13
14
|
* ```
|
|
14
15
|
*/
|
|
@@ -19,3 +20,9 @@ export type { Component } from './jsx-runtime.ts';
|
|
|
19
20
|
export type { WfuiContext, AppMiddleware, RouteDef } from './types.ts';
|
|
20
21
|
export { createApp } from './app.ts';
|
|
21
22
|
export { router, RouteView } from './router.ts';
|
|
23
|
+
export { api, ApiClient, ApiError } from './middleware/api.ts';
|
|
24
|
+
export { auth } from './middleware/auth.ts';
|
|
25
|
+
export type { UserRecord } from './middleware/auth.ts';
|
|
26
|
+
export { ws } from './middleware/ws.ts';
|
|
27
|
+
export { LoginForm } from './components/LoginForm.ts';
|
|
28
|
+
export { Chat } from './components/Chat.ts';
|
package/dist/client/index.js
CHANGED
|
@@ -189,6 +189,17 @@ function createApp() {
|
|
|
189
189
|
window.dispatchEvent(new CustomEvent("wefu:navigate", { detail: { path } }));
|
|
190
190
|
}
|
|
191
191
|
},
|
|
192
|
+
user: null,
|
|
193
|
+
token: null,
|
|
194
|
+
isAuthenticated: false,
|
|
195
|
+
login: async () => {
|
|
196
|
+
},
|
|
197
|
+
logout: () => {
|
|
198
|
+
},
|
|
199
|
+
register: async () => {
|
|
200
|
+
},
|
|
201
|
+
api: null,
|
|
202
|
+
ws: null,
|
|
192
203
|
provide(key, value) {
|
|
193
204
|
provides.set(key, value);
|
|
194
205
|
},
|
|
@@ -204,9 +215,9 @@ function createApp() {
|
|
|
204
215
|
middlewares.push(mw);
|
|
205
216
|
return this;
|
|
206
217
|
},
|
|
207
|
-
mount(rootSelector, RootComponent) {
|
|
218
|
+
async mount(rootSelector, RootComponent) {
|
|
208
219
|
for (const mw of middlewares) {
|
|
209
|
-
ctx = mw(ctx);
|
|
220
|
+
ctx = await mw(ctx);
|
|
210
221
|
}
|
|
211
222
|
setCtx(ctx);
|
|
212
223
|
const app = jsx(RootComponent, {});
|
|
@@ -302,11 +313,373 @@ function RouteView(_props, ctx) {
|
|
|
302
313
|
window.addEventListener("wefu:route", render);
|
|
303
314
|
return el;
|
|
304
315
|
}
|
|
316
|
+
|
|
317
|
+
// src/client/middleware/api.ts
|
|
318
|
+
var _getToken = () => null;
|
|
319
|
+
function setTokenGetter(fn) {
|
|
320
|
+
_getToken = fn;
|
|
321
|
+
}
|
|
322
|
+
function getToken() {
|
|
323
|
+
return _getToken();
|
|
324
|
+
}
|
|
325
|
+
var ApiError = class extends Error {
|
|
326
|
+
status;
|
|
327
|
+
constructor(status, message) {
|
|
328
|
+
super(`[${status}] ${message}`);
|
|
329
|
+
this.name = "ApiError";
|
|
330
|
+
this.status = status;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
var ApiClient = class {
|
|
334
|
+
#baseUrl;
|
|
335
|
+
constructor(baseUrl = "/api") {
|
|
336
|
+
this.#baseUrl = baseUrl;
|
|
337
|
+
}
|
|
338
|
+
async request(method, path, body) {
|
|
339
|
+
const headers = {};
|
|
340
|
+
const token = getToken();
|
|
341
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
342
|
+
if (body != null) headers["Content-Type"] = "application/json";
|
|
343
|
+
const res = await fetch(`${this.#baseUrl}${path}`, {
|
|
344
|
+
method,
|
|
345
|
+
headers,
|
|
346
|
+
body: body != null ? JSON.stringify(body) : void 0
|
|
347
|
+
});
|
|
348
|
+
if (!res.ok) {
|
|
349
|
+
throw new ApiError(res.status, await res.text().catch(() => res.statusText));
|
|
350
|
+
}
|
|
351
|
+
if (res.status === 204) return void 0;
|
|
352
|
+
return res.json();
|
|
353
|
+
}
|
|
354
|
+
get(path) {
|
|
355
|
+
return this.request("GET", path);
|
|
356
|
+
}
|
|
357
|
+
post(path, body) {
|
|
358
|
+
return this.request("POST", path, body);
|
|
359
|
+
}
|
|
360
|
+
put(path, body) {
|
|
361
|
+
return this.request("PUT", path, body);
|
|
362
|
+
}
|
|
363
|
+
patch(path, body) {
|
|
364
|
+
return this.request("PATCH", path, body);
|
|
365
|
+
}
|
|
366
|
+
delete(path) {
|
|
367
|
+
return this.request("DELETE", path);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
function api(opts = {}) {
|
|
371
|
+
return (ctx) => {
|
|
372
|
+
ctx.api = new ApiClient(opts.baseUrl ?? "/api");
|
|
373
|
+
return ctx;
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/client/middleware/auth.ts
|
|
378
|
+
function auth(opts = {}) {
|
|
379
|
+
const storageKey = opts.storageKey ?? "wefu:auth";
|
|
380
|
+
const loginPath = opts.loginPath ?? "/api/login";
|
|
381
|
+
const registerPath = opts.registerPath ?? "/api/register";
|
|
382
|
+
const mePath = opts.mePath ?? "/api/me";
|
|
383
|
+
return async (ctx) => {
|
|
384
|
+
const userSignal = signal(null);
|
|
385
|
+
const tokenSignal = signal(null);
|
|
386
|
+
setTokenGetter(() => tokenSignal.value);
|
|
387
|
+
function persist(user, token) {
|
|
388
|
+
userSignal.value = user;
|
|
389
|
+
tokenSignal.value = token;
|
|
390
|
+
if (token && user) {
|
|
391
|
+
localStorage.setItem(storageKey, JSON.stringify({ user, token }));
|
|
392
|
+
} else {
|
|
393
|
+
localStorage.removeItem(storageKey);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const saved = localStorage.getItem(storageKey);
|
|
398
|
+
if (saved) {
|
|
399
|
+
const { user, token } = JSON.parse(saved);
|
|
400
|
+
userSignal.value = user;
|
|
401
|
+
tokenSignal.value = token;
|
|
402
|
+
setTokenGetter(() => token);
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
if (tokenSignal.value) {
|
|
407
|
+
try {
|
|
408
|
+
const api2 = new ApiClient();
|
|
409
|
+
const user = await api2.get(mePath);
|
|
410
|
+
persist(user, tokenSignal.value);
|
|
411
|
+
} catch {
|
|
412
|
+
persist(null, null);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
...ctx,
|
|
417
|
+
get user() {
|
|
418
|
+
return userSignal.value;
|
|
419
|
+
},
|
|
420
|
+
get token() {
|
|
421
|
+
return tokenSignal.value;
|
|
422
|
+
},
|
|
423
|
+
get isAuthenticated() {
|
|
424
|
+
return !!tokenSignal.value && !!userSignal.value;
|
|
425
|
+
},
|
|
426
|
+
async login(email, password) {
|
|
427
|
+
const api2 = new ApiClient();
|
|
428
|
+
const res = await api2.post(loginPath, { email, password });
|
|
429
|
+
persist(res.user, res.token);
|
|
430
|
+
},
|
|
431
|
+
logout() {
|
|
432
|
+
persist(null, null);
|
|
433
|
+
},
|
|
434
|
+
async register(input) {
|
|
435
|
+
const api2 = new ApiClient();
|
|
436
|
+
const res = await api2.post(registerPath, input);
|
|
437
|
+
persist(res.user, res.token);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/client/middleware/ws.ts
|
|
444
|
+
function ws(opts = {}) {
|
|
445
|
+
const wsUrl = opts.url ?? "/ws";
|
|
446
|
+
const reconnectInterval = opts.reconnectInterval ?? 3e3;
|
|
447
|
+
const maxReconnect = opts.maxReconnect ?? 10;
|
|
448
|
+
return (ctx) => {
|
|
449
|
+
const isConnected = signal(false);
|
|
450
|
+
const messageHandlers = /* @__PURE__ */ new Set();
|
|
451
|
+
let socket = null;
|
|
452
|
+
let reconnectCount = 0;
|
|
453
|
+
let reconnectTimer = null;
|
|
454
|
+
function connect() {
|
|
455
|
+
if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) return;
|
|
456
|
+
const token = getToken();
|
|
457
|
+
const url = token ? `${wsUrl}?token=${token}` : wsUrl;
|
|
458
|
+
try {
|
|
459
|
+
socket = new WebSocket(url);
|
|
460
|
+
} catch {
|
|
461
|
+
scheduleReconnect();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
socket.onopen = () => {
|
|
465
|
+
isConnected.value = true;
|
|
466
|
+
reconnectCount = 0;
|
|
467
|
+
};
|
|
468
|
+
socket.onmessage = (event) => {
|
|
469
|
+
try {
|
|
470
|
+
const data = JSON.parse(event.data);
|
|
471
|
+
for (const h3 of messageHandlers) h3(data);
|
|
472
|
+
} catch {
|
|
473
|
+
for (const h3 of messageHandlers) h3(event.data);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
socket.onclose = () => {
|
|
477
|
+
isConnected.value = false;
|
|
478
|
+
socket = null;
|
|
479
|
+
scheduleReconnect();
|
|
480
|
+
};
|
|
481
|
+
socket.onerror = () => {
|
|
482
|
+
socket?.close();
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function scheduleReconnect() {
|
|
486
|
+
if (reconnectCount >= maxReconnect) return;
|
|
487
|
+
reconnectCount++;
|
|
488
|
+
reconnectTimer = setTimeout(connect, reconnectInterval * reconnectCount);
|
|
489
|
+
}
|
|
490
|
+
function send(data) {
|
|
491
|
+
if (socket?.readyState === WebSocket.OPEN) {
|
|
492
|
+
socket.send(typeof data === "string" ? data : JSON.stringify(data));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function join(room) {
|
|
496
|
+
send({ type: "join", room });
|
|
497
|
+
}
|
|
498
|
+
function leave(room) {
|
|
499
|
+
send({ type: "leave", room });
|
|
500
|
+
}
|
|
501
|
+
connect();
|
|
502
|
+
return {
|
|
503
|
+
...ctx,
|
|
504
|
+
ws: {
|
|
505
|
+
send,
|
|
506
|
+
onMessage: (handler) => {
|
|
507
|
+
messageHandlers.add(handler);
|
|
508
|
+
return () => messageHandlers.delete(handler);
|
|
509
|
+
},
|
|
510
|
+
join,
|
|
511
|
+
leave,
|
|
512
|
+
get isConnected() {
|
|
513
|
+
return isConnected;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/client/components/LoginForm.ts
|
|
521
|
+
var h = (tag, props, ...children) => jsx(tag, props ?? {}, ...children);
|
|
522
|
+
function LoginForm(_props, ctx) {
|
|
523
|
+
const mode = signal("login");
|
|
524
|
+
const email = signal("");
|
|
525
|
+
const name = signal("");
|
|
526
|
+
const password = signal("");
|
|
527
|
+
const error = signal(null);
|
|
528
|
+
const loading = signal(false);
|
|
529
|
+
const isRegister = computed(() => mode.value === "register");
|
|
530
|
+
const hasError = computed(() => error.value != null);
|
|
531
|
+
const submit = async () => {
|
|
532
|
+
error.value = null;
|
|
533
|
+
loading.value = true;
|
|
534
|
+
try {
|
|
535
|
+
if (mode.value === "login") {
|
|
536
|
+
await ctx.login(email.value, password.value);
|
|
537
|
+
} else {
|
|
538
|
+
await ctx.register({ email: email.value, name: name.value, password: password.value });
|
|
539
|
+
}
|
|
540
|
+
} catch (e) {
|
|
541
|
+
error.value = e.message ?? "\u64CD\u4F5C\u5931\u8D25";
|
|
542
|
+
} finally {
|
|
543
|
+
loading.value = false;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
return h(
|
|
547
|
+
"div",
|
|
548
|
+
{ class: "wefu-login" },
|
|
549
|
+
h(
|
|
550
|
+
"div",
|
|
551
|
+
{ class: "wefu-login-card" },
|
|
552
|
+
h("h2", null, mode.value === "login" ? "\u767B\u5F55" : "\u6CE8\u518C"),
|
|
553
|
+
h(
|
|
554
|
+
"div",
|
|
555
|
+
{ class: "wefu-login-tabs" },
|
|
556
|
+
h("button", { class: mode.value === "login" ? "active" : "", onClick: () => mode.value = "login" }, "\u767B\u5F55"),
|
|
557
|
+
h("button", { class: mode.value === "register" ? "active" : "", onClick: () => mode.value = "register" }, "\u6CE8\u518C")
|
|
558
|
+
),
|
|
559
|
+
h(
|
|
560
|
+
"form",
|
|
561
|
+
{ onSubmit: (e) => {
|
|
562
|
+
e.preventDefault();
|
|
563
|
+
submit();
|
|
564
|
+
} },
|
|
565
|
+
Show({
|
|
566
|
+
when: isRegister,
|
|
567
|
+
children: h(
|
|
568
|
+
"div",
|
|
569
|
+
{ class: "wefu-field" },
|
|
570
|
+
h("label", null, "\u6635\u79F0"),
|
|
571
|
+
h("input", { value: name, onInput: (e) => name.value = e.target.value, placeholder: "\u4F60\u7684\u6635\u79F0" })
|
|
572
|
+
)
|
|
573
|
+
}),
|
|
574
|
+
h(
|
|
575
|
+
"div",
|
|
576
|
+
{ class: "wefu-field" },
|
|
577
|
+
h("label", null, "\u90AE\u7BB1"),
|
|
578
|
+
h("input", { type: "email", value: email, onInput: (e) => email.value = e.target.value, placeholder: "\u90AE\u7BB1" })
|
|
579
|
+
),
|
|
580
|
+
h(
|
|
581
|
+
"div",
|
|
582
|
+
{ class: "wefu-field" },
|
|
583
|
+
h("label", null, "\u5BC6\u7801"),
|
|
584
|
+
h("input", { type: "password", value: password, onInput: (e) => password.value = e.target.value, placeholder: "\u5BC6\u7801" })
|
|
585
|
+
),
|
|
586
|
+
Show({
|
|
587
|
+
when: hasError,
|
|
588
|
+
children: h("div", { class: "wefu-error" }, error)
|
|
589
|
+
}),
|
|
590
|
+
h(
|
|
591
|
+
"button",
|
|
592
|
+
{ type: "submit", class: "wefu-btn wefu-btn-primary", disabled: loading },
|
|
593
|
+
loading ? "\u5904\u7406\u4E2D..." : mode.value === "login" ? "\u767B\u5F55" : "\u6CE8\u518C"
|
|
594
|
+
)
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/client/components/Chat.ts
|
|
601
|
+
var h2 = (tag, props, ...children) => jsx(tag, props ?? {}, ...children);
|
|
602
|
+
function Chat({ conversationId }, ctx) {
|
|
603
|
+
const messages = signal([]);
|
|
604
|
+
const input = signal("");
|
|
605
|
+
const loading = signal(true);
|
|
606
|
+
const isEmpty = signal(false);
|
|
607
|
+
ctx.api.get(`/api/conversations/${conversationId}/messages`).then((msgs) => {
|
|
608
|
+
messages.value = Array.isArray(msgs) ? msgs.reverse() : [];
|
|
609
|
+
loading.value = false;
|
|
610
|
+
isEmpty.value = messages.value.length === 0;
|
|
611
|
+
}).catch(() => {
|
|
612
|
+
loading.value = false;
|
|
613
|
+
isEmpty.value = true;
|
|
614
|
+
});
|
|
615
|
+
const unsub = ctx.ws.onMessage((data) => {
|
|
616
|
+
if (data.conversation_id === conversationId) {
|
|
617
|
+
messages.value = [...messages.value, data];
|
|
618
|
+
isEmpty.value = false;
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
const send = () => {
|
|
622
|
+
const text = input.value.trim();
|
|
623
|
+
if (!text) return;
|
|
624
|
+
input.value = "";
|
|
625
|
+
ctx.api.post("/api/messages", { conversationId, body: text }).catch(() => {
|
|
626
|
+
});
|
|
627
|
+
};
|
|
628
|
+
return h2(
|
|
629
|
+
"div",
|
|
630
|
+
{ class: "wefu-chat" },
|
|
631
|
+
// 消息列表
|
|
632
|
+
h2(
|
|
633
|
+
"div",
|
|
634
|
+
{ class: "wefu-chat-messages" },
|
|
635
|
+
Show({ when: loading, children: h2("div", { class: "wefu-chat-loading" }, "\u52A0\u8F7D\u4E2D...") }),
|
|
636
|
+
Show({ when: isEmpty, children: h2("div", { class: "wefu-chat-empty" }, "\u6682\u65E0\u6D88\u606F") }),
|
|
637
|
+
h2(
|
|
638
|
+
"div",
|
|
639
|
+
{ class: "wefu-chat-list" },
|
|
640
|
+
For({
|
|
641
|
+
each: messages.value,
|
|
642
|
+
children: (msg) => h2(
|
|
643
|
+
"div",
|
|
644
|
+
{ class: `wefu-chat-msg ${msg.sender_id === ctx.user?.id ? "mine" : ""}` },
|
|
645
|
+
h2(
|
|
646
|
+
"div",
|
|
647
|
+
{ class: "wefu-chat-msg-header" },
|
|
648
|
+
h2("span", { class: "wefu-chat-msg-sender" }, msg.sender_name ?? ""),
|
|
649
|
+
h2("span", { class: "wefu-chat-msg-time" }, new Date(msg.created_at).toLocaleTimeString())
|
|
650
|
+
),
|
|
651
|
+
h2("div", { class: "wefu-chat-msg-body" }, msg.body)
|
|
652
|
+
)
|
|
653
|
+
})
|
|
654
|
+
)
|
|
655
|
+
),
|
|
656
|
+
// 输入区
|
|
657
|
+
h2(
|
|
658
|
+
"div",
|
|
659
|
+
{ class: "wefu-chat-input" },
|
|
660
|
+
h2("input", {
|
|
661
|
+
value: input,
|
|
662
|
+
onInput: (e) => input.value = e.target.value,
|
|
663
|
+
onKeyDown: (e) => {
|
|
664
|
+
if (e.key === "Enter") send();
|
|
665
|
+
},
|
|
666
|
+
placeholder: "\u8F93\u5165\u6D88\u606F..."
|
|
667
|
+
}),
|
|
668
|
+
h2("button", { class: "wefu-btn", onClick: send }, "\u53D1\u9001")
|
|
669
|
+
)
|
|
670
|
+
);
|
|
671
|
+
}
|
|
305
672
|
export {
|
|
673
|
+
ApiClient,
|
|
674
|
+
ApiError,
|
|
675
|
+
Chat,
|
|
306
676
|
For,
|
|
307
677
|
Fragment,
|
|
678
|
+
LoginForm,
|
|
308
679
|
RouteView,
|
|
309
680
|
Show,
|
|
681
|
+
api,
|
|
682
|
+
auth,
|
|
310
683
|
computed,
|
|
311
684
|
createApp,
|
|
312
685
|
domMount,
|
|
@@ -316,5 +689,6 @@ export {
|
|
|
316
689
|
jsxDEV,
|
|
317
690
|
jsxs,
|
|
318
691
|
router,
|
|
319
|
-
signal
|
|
692
|
+
signal,
|
|
693
|
+
ws
|
|
320
694
|
};
|
|
@@ -189,6 +189,17 @@ function createApp() {
|
|
|
189
189
|
window.dispatchEvent(new CustomEvent("wefu:navigate", { detail: { path } }));
|
|
190
190
|
}
|
|
191
191
|
},
|
|
192
|
+
user: null,
|
|
193
|
+
token: null,
|
|
194
|
+
isAuthenticated: false,
|
|
195
|
+
login: async () => {
|
|
196
|
+
},
|
|
197
|
+
logout: () => {
|
|
198
|
+
},
|
|
199
|
+
register: async () => {
|
|
200
|
+
},
|
|
201
|
+
api: null,
|
|
202
|
+
ws: null,
|
|
192
203
|
provide(key, value) {
|
|
193
204
|
provides.set(key, value);
|
|
194
205
|
},
|
|
@@ -204,9 +215,9 @@ function createApp() {
|
|
|
204
215
|
middlewares.push(mw);
|
|
205
216
|
return this;
|
|
206
217
|
},
|
|
207
|
-
mount(rootSelector, RootComponent) {
|
|
218
|
+
async mount(rootSelector, RootComponent) {
|
|
208
219
|
for (const mw of middlewares) {
|
|
209
|
-
ctx = mw(ctx);
|
|
220
|
+
ctx = await mw(ctx);
|
|
210
221
|
}
|
|
211
222
|
setCtx(ctx);
|
|
212
223
|
const app = jsx(RootComponent, {});
|
|
@@ -302,11 +313,373 @@ function RouteView(_props, ctx) {
|
|
|
302
313
|
window.addEventListener("wefu:route", render);
|
|
303
314
|
return el;
|
|
304
315
|
}
|
|
316
|
+
|
|
317
|
+
// src/client/middleware/api.ts
|
|
318
|
+
var _getToken = () => null;
|
|
319
|
+
function setTokenGetter(fn) {
|
|
320
|
+
_getToken = fn;
|
|
321
|
+
}
|
|
322
|
+
function getToken() {
|
|
323
|
+
return _getToken();
|
|
324
|
+
}
|
|
325
|
+
var ApiError = class extends Error {
|
|
326
|
+
status;
|
|
327
|
+
constructor(status, message) {
|
|
328
|
+
super(`[${status}] ${message}`);
|
|
329
|
+
this.name = "ApiError";
|
|
330
|
+
this.status = status;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
var ApiClient = class {
|
|
334
|
+
#baseUrl;
|
|
335
|
+
constructor(baseUrl = "/api") {
|
|
336
|
+
this.#baseUrl = baseUrl;
|
|
337
|
+
}
|
|
338
|
+
async request(method, path, body) {
|
|
339
|
+
const headers = {};
|
|
340
|
+
const token = getToken();
|
|
341
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
342
|
+
if (body != null) headers["Content-Type"] = "application/json";
|
|
343
|
+
const res = await fetch(`${this.#baseUrl}${path}`, {
|
|
344
|
+
method,
|
|
345
|
+
headers,
|
|
346
|
+
body: body != null ? JSON.stringify(body) : void 0
|
|
347
|
+
});
|
|
348
|
+
if (!res.ok) {
|
|
349
|
+
throw new ApiError(res.status, await res.text().catch(() => res.statusText));
|
|
350
|
+
}
|
|
351
|
+
if (res.status === 204) return void 0;
|
|
352
|
+
return res.json();
|
|
353
|
+
}
|
|
354
|
+
get(path) {
|
|
355
|
+
return this.request("GET", path);
|
|
356
|
+
}
|
|
357
|
+
post(path, body) {
|
|
358
|
+
return this.request("POST", path, body);
|
|
359
|
+
}
|
|
360
|
+
put(path, body) {
|
|
361
|
+
return this.request("PUT", path, body);
|
|
362
|
+
}
|
|
363
|
+
patch(path, body) {
|
|
364
|
+
return this.request("PATCH", path, body);
|
|
365
|
+
}
|
|
366
|
+
delete(path) {
|
|
367
|
+
return this.request("DELETE", path);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
function api(opts = {}) {
|
|
371
|
+
return (ctx) => {
|
|
372
|
+
ctx.api = new ApiClient(opts.baseUrl ?? "/api");
|
|
373
|
+
return ctx;
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/client/middleware/auth.ts
|
|
378
|
+
function auth(opts = {}) {
|
|
379
|
+
const storageKey = opts.storageKey ?? "wefu:auth";
|
|
380
|
+
const loginPath = opts.loginPath ?? "/api/login";
|
|
381
|
+
const registerPath = opts.registerPath ?? "/api/register";
|
|
382
|
+
const mePath = opts.mePath ?? "/api/me";
|
|
383
|
+
return async (ctx) => {
|
|
384
|
+
const userSignal = signal(null);
|
|
385
|
+
const tokenSignal = signal(null);
|
|
386
|
+
setTokenGetter(() => tokenSignal.value);
|
|
387
|
+
function persist(user, token) {
|
|
388
|
+
userSignal.value = user;
|
|
389
|
+
tokenSignal.value = token;
|
|
390
|
+
if (token && user) {
|
|
391
|
+
localStorage.setItem(storageKey, JSON.stringify({ user, token }));
|
|
392
|
+
} else {
|
|
393
|
+
localStorage.removeItem(storageKey);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const saved = localStorage.getItem(storageKey);
|
|
398
|
+
if (saved) {
|
|
399
|
+
const { user, token } = JSON.parse(saved);
|
|
400
|
+
userSignal.value = user;
|
|
401
|
+
tokenSignal.value = token;
|
|
402
|
+
setTokenGetter(() => token);
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
if (tokenSignal.value) {
|
|
407
|
+
try {
|
|
408
|
+
const api2 = new ApiClient();
|
|
409
|
+
const user = await api2.get(mePath);
|
|
410
|
+
persist(user, tokenSignal.value);
|
|
411
|
+
} catch {
|
|
412
|
+
persist(null, null);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
...ctx,
|
|
417
|
+
get user() {
|
|
418
|
+
return userSignal.value;
|
|
419
|
+
},
|
|
420
|
+
get token() {
|
|
421
|
+
return tokenSignal.value;
|
|
422
|
+
},
|
|
423
|
+
get isAuthenticated() {
|
|
424
|
+
return !!tokenSignal.value && !!userSignal.value;
|
|
425
|
+
},
|
|
426
|
+
async login(email, password) {
|
|
427
|
+
const api2 = new ApiClient();
|
|
428
|
+
const res = await api2.post(loginPath, { email, password });
|
|
429
|
+
persist(res.user, res.token);
|
|
430
|
+
},
|
|
431
|
+
logout() {
|
|
432
|
+
persist(null, null);
|
|
433
|
+
},
|
|
434
|
+
async register(input) {
|
|
435
|
+
const api2 = new ApiClient();
|
|
436
|
+
const res = await api2.post(registerPath, input);
|
|
437
|
+
persist(res.user, res.token);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/client/middleware/ws.ts
|
|
444
|
+
function ws(opts = {}) {
|
|
445
|
+
const wsUrl = opts.url ?? "/ws";
|
|
446
|
+
const reconnectInterval = opts.reconnectInterval ?? 3e3;
|
|
447
|
+
const maxReconnect = opts.maxReconnect ?? 10;
|
|
448
|
+
return (ctx) => {
|
|
449
|
+
const isConnected = signal(false);
|
|
450
|
+
const messageHandlers = /* @__PURE__ */ new Set();
|
|
451
|
+
let socket = null;
|
|
452
|
+
let reconnectCount = 0;
|
|
453
|
+
let reconnectTimer = null;
|
|
454
|
+
function connect() {
|
|
455
|
+
if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) return;
|
|
456
|
+
const token = getToken();
|
|
457
|
+
const url = token ? `${wsUrl}?token=${token}` : wsUrl;
|
|
458
|
+
try {
|
|
459
|
+
socket = new WebSocket(url);
|
|
460
|
+
} catch {
|
|
461
|
+
scheduleReconnect();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
socket.onopen = () => {
|
|
465
|
+
isConnected.value = true;
|
|
466
|
+
reconnectCount = 0;
|
|
467
|
+
};
|
|
468
|
+
socket.onmessage = (event) => {
|
|
469
|
+
try {
|
|
470
|
+
const data = JSON.parse(event.data);
|
|
471
|
+
for (const h3 of messageHandlers) h3(data);
|
|
472
|
+
} catch {
|
|
473
|
+
for (const h3 of messageHandlers) h3(event.data);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
socket.onclose = () => {
|
|
477
|
+
isConnected.value = false;
|
|
478
|
+
socket = null;
|
|
479
|
+
scheduleReconnect();
|
|
480
|
+
};
|
|
481
|
+
socket.onerror = () => {
|
|
482
|
+
socket?.close();
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function scheduleReconnect() {
|
|
486
|
+
if (reconnectCount >= maxReconnect) return;
|
|
487
|
+
reconnectCount++;
|
|
488
|
+
reconnectTimer = setTimeout(connect, reconnectInterval * reconnectCount);
|
|
489
|
+
}
|
|
490
|
+
function send(data) {
|
|
491
|
+
if (socket?.readyState === WebSocket.OPEN) {
|
|
492
|
+
socket.send(typeof data === "string" ? data : JSON.stringify(data));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function join(room) {
|
|
496
|
+
send({ type: "join", room });
|
|
497
|
+
}
|
|
498
|
+
function leave(room) {
|
|
499
|
+
send({ type: "leave", room });
|
|
500
|
+
}
|
|
501
|
+
connect();
|
|
502
|
+
return {
|
|
503
|
+
...ctx,
|
|
504
|
+
ws: {
|
|
505
|
+
send,
|
|
506
|
+
onMessage: (handler) => {
|
|
507
|
+
messageHandlers.add(handler);
|
|
508
|
+
return () => messageHandlers.delete(handler);
|
|
509
|
+
},
|
|
510
|
+
join,
|
|
511
|
+
leave,
|
|
512
|
+
get isConnected() {
|
|
513
|
+
return isConnected;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/client/components/LoginForm.ts
|
|
521
|
+
var h = (tag, props, ...children) => jsx(tag, props ?? {}, ...children);
|
|
522
|
+
function LoginForm(_props, ctx) {
|
|
523
|
+
const mode = signal("login");
|
|
524
|
+
const email = signal("");
|
|
525
|
+
const name = signal("");
|
|
526
|
+
const password = signal("");
|
|
527
|
+
const error = signal(null);
|
|
528
|
+
const loading = signal(false);
|
|
529
|
+
const isRegister = computed(() => mode.value === "register");
|
|
530
|
+
const hasError = computed(() => error.value != null);
|
|
531
|
+
const submit = async () => {
|
|
532
|
+
error.value = null;
|
|
533
|
+
loading.value = true;
|
|
534
|
+
try {
|
|
535
|
+
if (mode.value === "login") {
|
|
536
|
+
await ctx.login(email.value, password.value);
|
|
537
|
+
} else {
|
|
538
|
+
await ctx.register({ email: email.value, name: name.value, password: password.value });
|
|
539
|
+
}
|
|
540
|
+
} catch (e) {
|
|
541
|
+
error.value = e.message ?? "\u64CD\u4F5C\u5931\u8D25";
|
|
542
|
+
} finally {
|
|
543
|
+
loading.value = false;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
return h(
|
|
547
|
+
"div",
|
|
548
|
+
{ class: "wefu-login" },
|
|
549
|
+
h(
|
|
550
|
+
"div",
|
|
551
|
+
{ class: "wefu-login-card" },
|
|
552
|
+
h("h2", null, mode.value === "login" ? "\u767B\u5F55" : "\u6CE8\u518C"),
|
|
553
|
+
h(
|
|
554
|
+
"div",
|
|
555
|
+
{ class: "wefu-login-tabs" },
|
|
556
|
+
h("button", { class: mode.value === "login" ? "active" : "", onClick: () => mode.value = "login" }, "\u767B\u5F55"),
|
|
557
|
+
h("button", { class: mode.value === "register" ? "active" : "", onClick: () => mode.value = "register" }, "\u6CE8\u518C")
|
|
558
|
+
),
|
|
559
|
+
h(
|
|
560
|
+
"form",
|
|
561
|
+
{ onSubmit: (e) => {
|
|
562
|
+
e.preventDefault();
|
|
563
|
+
submit();
|
|
564
|
+
} },
|
|
565
|
+
Show({
|
|
566
|
+
when: isRegister,
|
|
567
|
+
children: h(
|
|
568
|
+
"div",
|
|
569
|
+
{ class: "wefu-field" },
|
|
570
|
+
h("label", null, "\u6635\u79F0"),
|
|
571
|
+
h("input", { value: name, onInput: (e) => name.value = e.target.value, placeholder: "\u4F60\u7684\u6635\u79F0" })
|
|
572
|
+
)
|
|
573
|
+
}),
|
|
574
|
+
h(
|
|
575
|
+
"div",
|
|
576
|
+
{ class: "wefu-field" },
|
|
577
|
+
h("label", null, "\u90AE\u7BB1"),
|
|
578
|
+
h("input", { type: "email", value: email, onInput: (e) => email.value = e.target.value, placeholder: "\u90AE\u7BB1" })
|
|
579
|
+
),
|
|
580
|
+
h(
|
|
581
|
+
"div",
|
|
582
|
+
{ class: "wefu-field" },
|
|
583
|
+
h("label", null, "\u5BC6\u7801"),
|
|
584
|
+
h("input", { type: "password", value: password, onInput: (e) => password.value = e.target.value, placeholder: "\u5BC6\u7801" })
|
|
585
|
+
),
|
|
586
|
+
Show({
|
|
587
|
+
when: hasError,
|
|
588
|
+
children: h("div", { class: "wefu-error" }, error)
|
|
589
|
+
}),
|
|
590
|
+
h(
|
|
591
|
+
"button",
|
|
592
|
+
{ type: "submit", class: "wefu-btn wefu-btn-primary", disabled: loading },
|
|
593
|
+
loading ? "\u5904\u7406\u4E2D..." : mode.value === "login" ? "\u767B\u5F55" : "\u6CE8\u518C"
|
|
594
|
+
)
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/client/components/Chat.ts
|
|
601
|
+
var h2 = (tag, props, ...children) => jsx(tag, props ?? {}, ...children);
|
|
602
|
+
function Chat({ conversationId }, ctx) {
|
|
603
|
+
const messages = signal([]);
|
|
604
|
+
const input = signal("");
|
|
605
|
+
const loading = signal(true);
|
|
606
|
+
const isEmpty = signal(false);
|
|
607
|
+
ctx.api.get(`/api/conversations/${conversationId}/messages`).then((msgs) => {
|
|
608
|
+
messages.value = Array.isArray(msgs) ? msgs.reverse() : [];
|
|
609
|
+
loading.value = false;
|
|
610
|
+
isEmpty.value = messages.value.length === 0;
|
|
611
|
+
}).catch(() => {
|
|
612
|
+
loading.value = false;
|
|
613
|
+
isEmpty.value = true;
|
|
614
|
+
});
|
|
615
|
+
const unsub = ctx.ws.onMessage((data) => {
|
|
616
|
+
if (data.conversation_id === conversationId) {
|
|
617
|
+
messages.value = [...messages.value, data];
|
|
618
|
+
isEmpty.value = false;
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
const send = () => {
|
|
622
|
+
const text = input.value.trim();
|
|
623
|
+
if (!text) return;
|
|
624
|
+
input.value = "";
|
|
625
|
+
ctx.api.post("/api/messages", { conversationId, body: text }).catch(() => {
|
|
626
|
+
});
|
|
627
|
+
};
|
|
628
|
+
return h2(
|
|
629
|
+
"div",
|
|
630
|
+
{ class: "wefu-chat" },
|
|
631
|
+
// 消息列表
|
|
632
|
+
h2(
|
|
633
|
+
"div",
|
|
634
|
+
{ class: "wefu-chat-messages" },
|
|
635
|
+
Show({ when: loading, children: h2("div", { class: "wefu-chat-loading" }, "\u52A0\u8F7D\u4E2D...") }),
|
|
636
|
+
Show({ when: isEmpty, children: h2("div", { class: "wefu-chat-empty" }, "\u6682\u65E0\u6D88\u606F") }),
|
|
637
|
+
h2(
|
|
638
|
+
"div",
|
|
639
|
+
{ class: "wefu-chat-list" },
|
|
640
|
+
For({
|
|
641
|
+
each: messages.value,
|
|
642
|
+
children: (msg) => h2(
|
|
643
|
+
"div",
|
|
644
|
+
{ class: `wefu-chat-msg ${msg.sender_id === ctx.user?.id ? "mine" : ""}` },
|
|
645
|
+
h2(
|
|
646
|
+
"div",
|
|
647
|
+
{ class: "wefu-chat-msg-header" },
|
|
648
|
+
h2("span", { class: "wefu-chat-msg-sender" }, msg.sender_name ?? ""),
|
|
649
|
+
h2("span", { class: "wefu-chat-msg-time" }, new Date(msg.created_at).toLocaleTimeString())
|
|
650
|
+
),
|
|
651
|
+
h2("div", { class: "wefu-chat-msg-body" }, msg.body)
|
|
652
|
+
)
|
|
653
|
+
})
|
|
654
|
+
)
|
|
655
|
+
),
|
|
656
|
+
// 输入区
|
|
657
|
+
h2(
|
|
658
|
+
"div",
|
|
659
|
+
{ class: "wefu-chat-input" },
|
|
660
|
+
h2("input", {
|
|
661
|
+
value: input,
|
|
662
|
+
onInput: (e) => input.value = e.target.value,
|
|
663
|
+
onKeyDown: (e) => {
|
|
664
|
+
if (e.key === "Enter") send();
|
|
665
|
+
},
|
|
666
|
+
placeholder: "\u8F93\u5165\u6D88\u606F..."
|
|
667
|
+
}),
|
|
668
|
+
h2("button", { class: "wefu-btn", onClick: send }, "\u53D1\u9001")
|
|
669
|
+
)
|
|
670
|
+
);
|
|
671
|
+
}
|
|
305
672
|
export {
|
|
673
|
+
ApiClient,
|
|
674
|
+
ApiError,
|
|
675
|
+
Chat,
|
|
306
676
|
For,
|
|
307
677
|
Fragment,
|
|
678
|
+
LoginForm,
|
|
308
679
|
RouteView,
|
|
309
680
|
Show,
|
|
681
|
+
api,
|
|
682
|
+
auth,
|
|
310
683
|
computed,
|
|
311
684
|
createApp,
|
|
312
685
|
domMount,
|
|
@@ -316,5 +689,6 @@ export {
|
|
|
316
689
|
jsxDEV,
|
|
317
690
|
jsxs,
|
|
318
691
|
router,
|
|
319
|
-
signal
|
|
692
|
+
signal,
|
|
693
|
+
ws
|
|
320
694
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api middleware — 注入 ctx.api
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* app.use(api({ baseUrl: '/api' }))
|
|
6
|
+
* app.use(auth({ api: ctx.api }))
|
|
7
|
+
*
|
|
8
|
+
* // In component:
|
|
9
|
+
* const users = await ctx.api.get('/api/users')
|
|
10
|
+
* const msg = await ctx.api.post('/api/messages', { body: 'hello' })
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import type { AppMiddleware } from '../types.ts';
|
|
14
|
+
export declare function setTokenGetter(fn: () => string | null): void;
|
|
15
|
+
export declare function getToken(): string | null;
|
|
16
|
+
export declare class ApiError extends Error {
|
|
17
|
+
status: number;
|
|
18
|
+
constructor(status: number, message: string);
|
|
19
|
+
}
|
|
20
|
+
export declare class ApiClient {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(baseUrl?: string);
|
|
23
|
+
request<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
|
|
24
|
+
get<T = unknown>(path: string): Promise<T>;
|
|
25
|
+
post<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
26
|
+
put<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
27
|
+
patch<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
28
|
+
delete<T = unknown>(path: string): Promise<T>;
|
|
29
|
+
}
|
|
30
|
+
export declare function api(opts?: {
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
}): AppMiddleware;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth middleware — 注入 ctx.user / ctx.login / ctx.logout / ctx.register
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* app.use(api())
|
|
6
|
+
* app.use(auth())
|
|
7
|
+
*
|
|
8
|
+
* // In component:
|
|
9
|
+
* if (!ctx.user) return <LoginPage />
|
|
10
|
+
* ctx.login(email, password)
|
|
11
|
+
* ctx.logout()
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import type { AppMiddleware } from '../types.ts';
|
|
15
|
+
export interface UserRecord {
|
|
16
|
+
id: string;
|
|
17
|
+
email: string;
|
|
18
|
+
name: string;
|
|
19
|
+
role: string;
|
|
20
|
+
avatar?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AuthOptions {
|
|
23
|
+
/** localStorage key,默认 'wefu:auth' */
|
|
24
|
+
storageKey?: string;
|
|
25
|
+
/** 登录 API 路径,默认 '/api/login' */
|
|
26
|
+
loginPath?: string;
|
|
27
|
+
/** 注册 API 路径,默认 '/api/register' */
|
|
28
|
+
registerPath?: string;
|
|
29
|
+
/** 获取当前用户 API 路径,默认 '/api/me' */
|
|
30
|
+
mePath?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function auth(opts?: AuthOptions): AppMiddleware;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ws middleware — 注入 ctx.ws
|
|
3
|
+
*
|
|
4
|
+
* WebSocket 客户端,自动重连,支持房间。
|
|
5
|
+
*
|
|
6
|
+
* ```tsx
|
|
7
|
+
* app.use(ws({ url: '/ws' }))
|
|
8
|
+
*
|
|
9
|
+
* // In component:
|
|
10
|
+
* ctx.ws.onMessage((data) => ...)
|
|
11
|
+
* ctx.ws.join('conversation:123')
|
|
12
|
+
* ctx.ws.send({ type: 'ping' })
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import type { AppMiddleware } from '../types.ts';
|
|
16
|
+
export interface WsOptions {
|
|
17
|
+
url?: string;
|
|
18
|
+
reconnectInterval?: number;
|
|
19
|
+
maxReconnect?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function ws(opts?: WsOptions): AppMiddleware;
|
package/dist/client/types.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* wefu 类型定义
|
|
3
3
|
*/
|
|
4
|
+
import type { Signal } from './signal.ts';
|
|
4
5
|
import type { Component } from './jsx-runtime.ts';
|
|
5
6
|
/**
|
|
6
7
|
* wefu 上下文 — 组件通过第二个参数访问
|
|
7
8
|
*
|
|
8
9
|
* ```tsx
|
|
9
10
|
* function MyPage(props, ctx: WfuiContext) {
|
|
10
|
-
* ctx.
|
|
11
|
-
* ctx.
|
|
12
|
-
*
|
|
11
|
+
* ctx.user // 当前登录用户
|
|
12
|
+
* ctx.login(email, pw) // 登录
|
|
13
|
+
* ctx.api.get('/users') // API 请求
|
|
14
|
+
* ctx.ws.send(data) // WebSocket
|
|
13
15
|
* }
|
|
14
16
|
* ```
|
|
15
17
|
*/
|
|
@@ -19,16 +21,44 @@ export interface WfuiContext {
|
|
|
19
21
|
params: Record<string, string>;
|
|
20
22
|
query: Record<string, string>;
|
|
21
23
|
hash: string;
|
|
22
|
-
/** 当前匹配的路由组件(由 router 中间件注入) */
|
|
23
24
|
component: Component | null;
|
|
24
|
-
/** 当前匹配的路由配置 */
|
|
25
25
|
title?: string;
|
|
26
26
|
auth?: boolean;
|
|
27
27
|
};
|
|
28
28
|
app: {
|
|
29
29
|
navigate: (path: string) => void;
|
|
30
30
|
};
|
|
31
|
-
|
|
31
|
+
user: {
|
|
32
|
+
id: string;
|
|
33
|
+
email: string;
|
|
34
|
+
name: string;
|
|
35
|
+
role: string;
|
|
36
|
+
avatar?: string;
|
|
37
|
+
} | null;
|
|
38
|
+
token: string | null;
|
|
39
|
+
isAuthenticated: boolean;
|
|
40
|
+
login: (email: string, password: string) => Promise<void>;
|
|
41
|
+
logout: () => void;
|
|
42
|
+
register: (input: {
|
|
43
|
+
email: string;
|
|
44
|
+
name: string;
|
|
45
|
+
password: string;
|
|
46
|
+
}) => Promise<void>;
|
|
47
|
+
api: {
|
|
48
|
+
get<T>(path: string): Promise<T>;
|
|
49
|
+
post<T>(path: string, body?: unknown): Promise<T>;
|
|
50
|
+
put<T>(path: string, body?: unknown): Promise<T>;
|
|
51
|
+
patch<T>(path: string, body?: unknown): Promise<T>;
|
|
52
|
+
delete<T>(path: string): Promise<T>;
|
|
53
|
+
};
|
|
54
|
+
ws: {
|
|
55
|
+
send: (data: unknown) => void;
|
|
56
|
+
onMessage: (handler: (data: unknown) => void) => () => void;
|
|
57
|
+
join: (room: string) => void;
|
|
58
|
+
leave: (room: string) => void;
|
|
59
|
+
isConnected: Signal<boolean>;
|
|
60
|
+
};
|
|
61
|
+
/** 跨组件共享数据 */
|
|
32
62
|
provide: <T>(key: string, value: T) => void;
|
|
33
63
|
inject: <T>(key: string) => T | null;
|
|
34
64
|
/** 中间件注入扩展 */
|
package/dist/index.d.ts
CHANGED
|
@@ -47,4 +47,6 @@ export type { CMSAPI, CMSOptions, Content, ContentStatus, ContentType, Tag, TagW
|
|
|
47
47
|
export { kb, KB } from './kb/index.ts';
|
|
48
48
|
export type { KBAPI, KBOptions, Document, Chunk, SearchResult, ImportOptions, SearchOptions, } from './kb/types.ts';
|
|
49
49
|
export type { BaseAPI, BaseDef, BaseOptions, TableSchema, FieldSchema, FieldType, ColumnMap, CreateBaseInput, UpdateBaseInput, QueryOptions, } from './base/types.ts';
|
|
50
|
+
export { ui } from './ui/index.ts';
|
|
51
|
+
export type { UiOptions, UiRenderOptions } from './ui/index.ts';
|
|
50
52
|
export { requireRole } from './user/types.ts';
|
package/dist/index.js
CHANGED
|
@@ -4316,6 +4316,51 @@ function kb(opts) {
|
|
|
4316
4316
|
mw.__meta = { injects: ["kb"], depends: ["sql"] };
|
|
4317
4317
|
return mw;
|
|
4318
4318
|
}
|
|
4319
|
+
|
|
4320
|
+
// src/ui/index.ts
|
|
4321
|
+
function defaultTemplate(title, script, propsJson) {
|
|
4322
|
+
return `<!DOCTYPE html>
|
|
4323
|
+
<html lang="zh-CN">
|
|
4324
|
+
<head>
|
|
4325
|
+
<meta charset="UTF-8">
|
|
4326
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4327
|
+
<title>${escapeHtml(title)}</title>
|
|
4328
|
+
</head>
|
|
4329
|
+
<body>
|
|
4330
|
+
<div id="root"></div>
|
|
4331
|
+
${propsJson ? `<script>window.__WFUI_PROPS__=${propsJson}</script>` : ""}
|
|
4332
|
+
<script src="${escapeHtml(script)}"></script>
|
|
4333
|
+
</body>
|
|
4334
|
+
</html>`;
|
|
4335
|
+
}
|
|
4336
|
+
function escapeHtml(s) {
|
|
4337
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4338
|
+
}
|
|
4339
|
+
function ui(opts = {}) {
|
|
4340
|
+
const defaultTitle = opts.title ?? "weifuwu";
|
|
4341
|
+
const defaultScript = opts.script ?? "/static/app.js";
|
|
4342
|
+
const template = opts.template;
|
|
4343
|
+
return async (req, ctx, next) => {
|
|
4344
|
+
ctx.ui = {
|
|
4345
|
+
html(renderOpts = {}) {
|
|
4346
|
+
const title = renderOpts.title ?? defaultTitle;
|
|
4347
|
+
const script = renderOpts.script ?? defaultScript;
|
|
4348
|
+
let propsJson = "";
|
|
4349
|
+
if (renderOpts.props) {
|
|
4350
|
+
try {
|
|
4351
|
+
propsJson = JSON.stringify(renderOpts.props);
|
|
4352
|
+
} catch {
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
const body = template ? template.replace(/\{\{title\}\}/g, escapeHtml(title)).replace(/\{\{script\}\}/g, escapeHtml(script)).replace(/\{\{props\}\}/g, propsJson) : defaultTemplate(title, script, propsJson);
|
|
4356
|
+
return new Response(body, {
|
|
4357
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
4358
|
+
});
|
|
4359
|
+
}
|
|
4360
|
+
};
|
|
4361
|
+
return next(req, ctx);
|
|
4362
|
+
};
|
|
4363
|
+
}
|
|
4319
4364
|
export {
|
|
4320
4365
|
Base,
|
|
4321
4366
|
CMS,
|
|
@@ -4351,6 +4396,7 @@ export {
|
|
|
4351
4396
|
serveStatic,
|
|
4352
4397
|
trace,
|
|
4353
4398
|
traceElapsed,
|
|
4399
|
+
ui,
|
|
4354
4400
|
upload,
|
|
4355
4401
|
user
|
|
4356
4402
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui 中间件 — 注入 ctx.ui.html()
|
|
3
|
+
*
|
|
4
|
+
* 返回 SPA HTML shell,加载前端 client bundle。
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { ui, serveStatic } from 'weifuwu'
|
|
8
|
+
*
|
|
9
|
+
* app.use(ui({ title: 'My App', script: '/static/app.js' }))
|
|
10
|
+
* app.get('/static/*', serveStatic('./dist/client'))
|
|
11
|
+
* app.get('*', async (req, ctx) => ctx.ui.html())
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import type { Middleware } from '../types.ts';
|
|
15
|
+
declare module '../types.ts' {
|
|
16
|
+
interface Context {
|
|
17
|
+
ui: {
|
|
18
|
+
/** 返回 SPA HTML 页面 */
|
|
19
|
+
html: (opts?: UiRenderOptions) => Response;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export interface UiOptions {
|
|
24
|
+
/** 页面标题,默认 'weifuwu' */
|
|
25
|
+
title?: string;
|
|
26
|
+
/** Client bundle JS 路径,默认 '/static/app.js' */
|
|
27
|
+
script?: string;
|
|
28
|
+
/** 自定义 HTML 模板,覆盖默认 */
|
|
29
|
+
template?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface UiRenderOptions {
|
|
32
|
+
title?: string;
|
|
33
|
+
script?: string;
|
|
34
|
+
/** 内嵌到页面的初始数据(通过 window.__WFUI_PROPS__ 访问) */
|
|
35
|
+
props?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
export declare function ui(opts?: UiOptions): Middleware;
|