weifuwu 0.33.1 → 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/README.md +160 -3
- package/dist/client/app.d.ts +24 -0
- package/dist/client/components/Chat.d.ts +17 -0
- package/dist/client/components/LoginForm.d.ts +14 -0
- package/dist/client/index.d.ts +28 -0
- package/dist/client/index.js +694 -0
- package/dist/client/jsx-runtime.d.ts +60 -0
- package/dist/client/jsx-runtime.js +694 -0
- 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/router.d.ts +51 -0
- package/dist/client/signal.d.ts +19 -0
- package/dist/client/types.d.ts +75 -0
- package/dist/index.d.ts +2 -4
- package/dist/index.js +57 -475
- package/dist/ui/index.d.ts +37 -0
- package/package.json +11 -24
- package/dist/middleware/esbuild-dev.d.ts +0 -55
- package/dist/middleware/esbuild-dev.js +0 -266
- package/dist/middleware/tailwind-dev.d.ts +0 -43
- package/dist/middleware/tailwind-dev.js +0 -199
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
// src/client/signal.ts
|
|
2
|
+
var currentEffect = null;
|
|
3
|
+
var Signal = class {
|
|
4
|
+
#value;
|
|
5
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
6
|
+
constructor(value) {
|
|
7
|
+
this.#value = value;
|
|
8
|
+
}
|
|
9
|
+
get value() {
|
|
10
|
+
if (currentEffect) this.#listeners.add(currentEffect);
|
|
11
|
+
return this.#value;
|
|
12
|
+
}
|
|
13
|
+
set value(v) {
|
|
14
|
+
if (v !== this.#value) {
|
|
15
|
+
this.#value = v;
|
|
16
|
+
const fns = [...this.#listeners];
|
|
17
|
+
for (const fn of fns) fn();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function signal(initial) {
|
|
22
|
+
return new Signal(initial);
|
|
23
|
+
}
|
|
24
|
+
function isSignal(value) {
|
|
25
|
+
return value instanceof Signal;
|
|
26
|
+
}
|
|
27
|
+
function effect(fn) {
|
|
28
|
+
const wrapper = () => {
|
|
29
|
+
const prev = currentEffect;
|
|
30
|
+
currentEffect = wrapper;
|
|
31
|
+
try {
|
|
32
|
+
fn();
|
|
33
|
+
} finally {
|
|
34
|
+
currentEffect = prev;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
wrapper();
|
|
38
|
+
return () => {
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function computed(fn) {
|
|
42
|
+
const s = new Signal(void 0);
|
|
43
|
+
effect(() => {
|
|
44
|
+
s.value = fn();
|
|
45
|
+
});
|
|
46
|
+
return s;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/client/jsx-runtime.ts
|
|
50
|
+
var currentCtx = null;
|
|
51
|
+
function setCtx(ctx) {
|
|
52
|
+
currentCtx = ctx;
|
|
53
|
+
}
|
|
54
|
+
function setProp(el, key, value) {
|
|
55
|
+
if (key === "class" || key === "className") {
|
|
56
|
+
if (isSignal(value)) {
|
|
57
|
+
effect(() => {
|
|
58
|
+
el.className = String(value.value);
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
el.className = String(value ?? "");
|
|
62
|
+
}
|
|
63
|
+
} else if (key === "style" && typeof value === "object" && value !== null) {
|
|
64
|
+
Object.assign(el.style, value);
|
|
65
|
+
} else if (key.startsWith("on") && typeof value === "function") {
|
|
66
|
+
el.addEventListener(key.slice(2).toLowerCase(), value);
|
|
67
|
+
} else if (key === "ref" && typeof value === "function") {
|
|
68
|
+
value(el);
|
|
69
|
+
} else if (isSignal(value)) {
|
|
70
|
+
effect(() => {
|
|
71
|
+
const v = value.value;
|
|
72
|
+
if (v == null || v === false) el.removeAttribute(key);
|
|
73
|
+
else if (v === true) el.setAttribute(key, "");
|
|
74
|
+
else el.setAttribute(key, String(v));
|
|
75
|
+
});
|
|
76
|
+
} else if (value != null && value !== false) {
|
|
77
|
+
if (value === true) el.setAttribute(key, "");
|
|
78
|
+
else el.setAttribute(key, String(value));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function appendChild(parent, child) {
|
|
82
|
+
if (child == null || child === false || child === true) return;
|
|
83
|
+
if (Array.isArray(child)) {
|
|
84
|
+
child.forEach((c) => appendChild(parent, c));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (child instanceof Node) {
|
|
88
|
+
parent.appendChild(child);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (isSignal(child)) {
|
|
92
|
+
const text = document.createTextNode("");
|
|
93
|
+
effect(() => {
|
|
94
|
+
text.textContent = String(child.value);
|
|
95
|
+
});
|
|
96
|
+
parent.appendChild(text);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
parent.appendChild(document.createTextNode(String(child)));
|
|
100
|
+
}
|
|
101
|
+
function jsx(type, props, ...children) {
|
|
102
|
+
if (typeof type === "function") {
|
|
103
|
+
const merged = children.length > 0 ? { ...props, children } : props;
|
|
104
|
+
return type(merged, currentCtx) ?? document.createDocumentFragment();
|
|
105
|
+
}
|
|
106
|
+
const el = document.createElement(type);
|
|
107
|
+
if (props) {
|
|
108
|
+
for (const [k, v] of Object.entries(props)) {
|
|
109
|
+
if (k !== "children") setProp(el, k, v);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const childList = children.length > 0 ? children : props?.children != null ? [props.children] : [];
|
|
113
|
+
for (const child of childList) appendChild(el, child);
|
|
114
|
+
return el;
|
|
115
|
+
}
|
|
116
|
+
function jsxs(type, props, ...children) {
|
|
117
|
+
return jsx(type, props, ...children);
|
|
118
|
+
}
|
|
119
|
+
function jsxDEV(type, props, _key, _isStatic, _source, _self) {
|
|
120
|
+
return jsx(type, props, ...props?.children ? [props.children] : []);
|
|
121
|
+
}
|
|
122
|
+
function Fragment(props, ...children) {
|
|
123
|
+
const frag = document.createDocumentFragment();
|
|
124
|
+
for (const child of children) appendChild(frag, child);
|
|
125
|
+
return frag;
|
|
126
|
+
}
|
|
127
|
+
function domMount(root, app) {
|
|
128
|
+
const container = typeof root === "string" ? document.querySelector(root) : root;
|
|
129
|
+
if (!container) throw new Error(`mount target not found: ${root}`);
|
|
130
|
+
container.innerHTML = "";
|
|
131
|
+
container.appendChild(app);
|
|
132
|
+
}
|
|
133
|
+
function toNode(v) {
|
|
134
|
+
if (v instanceof Node) return v;
|
|
135
|
+
if (typeof v === "function") return toNode(v());
|
|
136
|
+
return document.createTextNode(String(v ?? ""));
|
|
137
|
+
}
|
|
138
|
+
function Show({ when, children, fallback }) {
|
|
139
|
+
const el = document.createElement("div");
|
|
140
|
+
function render(show) {
|
|
141
|
+
el.textContent = "";
|
|
142
|
+
if (show && children != null) {
|
|
143
|
+
el.appendChild(toNode(children));
|
|
144
|
+
} else if (!show && fallback != null) {
|
|
145
|
+
el.appendChild(toNode(fallback));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (isSignal(when)) {
|
|
149
|
+
effect(() => render(Boolean(when.value)));
|
|
150
|
+
} else {
|
|
151
|
+
render(Boolean(when));
|
|
152
|
+
}
|
|
153
|
+
return el;
|
|
154
|
+
}
|
|
155
|
+
function For({ each, children }) {
|
|
156
|
+
const el = document.createElement("div");
|
|
157
|
+
function render(list) {
|
|
158
|
+
el.textContent = "";
|
|
159
|
+
for (let i = 0; i < list.length; i++) {
|
|
160
|
+
el.appendChild(children(list[i], i));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (isSignal(each)) {
|
|
164
|
+
effect(() => render(each.value));
|
|
165
|
+
} else {
|
|
166
|
+
render(each);
|
|
167
|
+
}
|
|
168
|
+
return el;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/client/app.ts
|
|
172
|
+
function createApp() {
|
|
173
|
+
const middlewares = [];
|
|
174
|
+
const provides = /* @__PURE__ */ new Map();
|
|
175
|
+
let ctx = {
|
|
176
|
+
route: {
|
|
177
|
+
path: window.location.pathname,
|
|
178
|
+
params: {},
|
|
179
|
+
query: Object.fromEntries(new URLSearchParams(window.location.search)),
|
|
180
|
+
hash: window.location.hash,
|
|
181
|
+
component: null
|
|
182
|
+
},
|
|
183
|
+
app: {
|
|
184
|
+
navigate(path) {
|
|
185
|
+
window.history.pushState({}, "", path);
|
|
186
|
+
ctx.route.path = path;
|
|
187
|
+
ctx.route.query = Object.fromEntries(new URLSearchParams(window.location.search));
|
|
188
|
+
ctx.route.hash = window.location.hash;
|
|
189
|
+
window.dispatchEvent(new CustomEvent("wefu:navigate", { detail: { path } }));
|
|
190
|
+
}
|
|
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,
|
|
203
|
+
provide(key, value) {
|
|
204
|
+
provides.set(key, value);
|
|
205
|
+
},
|
|
206
|
+
inject(key) {
|
|
207
|
+
return provides.get(key) ?? null;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
return {
|
|
211
|
+
get ctx() {
|
|
212
|
+
return ctx;
|
|
213
|
+
},
|
|
214
|
+
use(mw) {
|
|
215
|
+
middlewares.push(mw);
|
|
216
|
+
return this;
|
|
217
|
+
},
|
|
218
|
+
async mount(rootSelector, RootComponent) {
|
|
219
|
+
for (const mw of middlewares) {
|
|
220
|
+
ctx = await mw(ctx);
|
|
221
|
+
}
|
|
222
|
+
setCtx(ctx);
|
|
223
|
+
const app = jsx(RootComponent, {});
|
|
224
|
+
domMount(rootSelector, app);
|
|
225
|
+
setCtx(null);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/client/router.ts
|
|
231
|
+
function router(opts) {
|
|
232
|
+
const mode = opts.mode ?? "hash";
|
|
233
|
+
const matchers = opts.routes.map((route) => {
|
|
234
|
+
const parts = route.path.split("/").filter(Boolean);
|
|
235
|
+
const keys = [];
|
|
236
|
+
const reStr = "^/" + parts.map((p) => {
|
|
237
|
+
if (p.startsWith(":")) {
|
|
238
|
+
keys.push(p.slice(1));
|
|
239
|
+
return "([^/]+)";
|
|
240
|
+
}
|
|
241
|
+
return p;
|
|
242
|
+
}).join("/") + "$";
|
|
243
|
+
return { re: new RegExp(reStr), keys, route };
|
|
244
|
+
});
|
|
245
|
+
function matchPath(path) {
|
|
246
|
+
for (const { re, keys, route } of matchers) {
|
|
247
|
+
const m = path.match(re);
|
|
248
|
+
if (m) {
|
|
249
|
+
const params = {};
|
|
250
|
+
keys.forEach((k, i) => {
|
|
251
|
+
params[k] = decodeURIComponent(m[i + 1]);
|
|
252
|
+
});
|
|
253
|
+
return { component: route.component, params, title: route.title, auth: route.auth };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
return (ctx) => {
|
|
259
|
+
function handleRoute() {
|
|
260
|
+
const raw = mode === "hash" ? window.location.hash.slice(1) || "/" : window.location.pathname + window.location.search;
|
|
261
|
+
const [path, qs] = raw.split("?");
|
|
262
|
+
ctx.route.query = Object.fromEntries(new URLSearchParams(qs ?? ""));
|
|
263
|
+
const matched = matchPath(path);
|
|
264
|
+
if (matched) {
|
|
265
|
+
ctx.route.path = path;
|
|
266
|
+
ctx.route.params = matched.params;
|
|
267
|
+
ctx.route.component = matched.component;
|
|
268
|
+
ctx.route.title = matched.title;
|
|
269
|
+
ctx.route.auth = matched.auth;
|
|
270
|
+
if (matched.title) document.title = matched.title;
|
|
271
|
+
if (matched.auth && !ctx.user) {
|
|
272
|
+
setTimeout(() => ctx.app.navigate("/login"), 0);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
ctx.route.component = opts.notFound ?? null;
|
|
277
|
+
}
|
|
278
|
+
window.dispatchEvent(new CustomEvent("wefu:route", { detail: { path } }));
|
|
279
|
+
}
|
|
280
|
+
const origNavigate = ctx.app.navigate;
|
|
281
|
+
ctx.app.navigate = (path) => {
|
|
282
|
+
if (mode === "hash") {
|
|
283
|
+
window.location.hash = "#" + path;
|
|
284
|
+
} else {
|
|
285
|
+
window.history.pushState({}, "", path);
|
|
286
|
+
handleRoute();
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
if (mode === "hash") {
|
|
290
|
+
window.addEventListener("hashchange", handleRoute);
|
|
291
|
+
} else {
|
|
292
|
+
window.addEventListener("popstate", handleRoute);
|
|
293
|
+
}
|
|
294
|
+
handleRoute();
|
|
295
|
+
return ctx;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function RouteView(_props, ctx) {
|
|
299
|
+
const el = document.createElement("div");
|
|
300
|
+
function render() {
|
|
301
|
+
const Component = ctx.route.component;
|
|
302
|
+
if (!Component) {
|
|
303
|
+
el.textContent = "";
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
el.textContent = "";
|
|
307
|
+
setCtx(ctx);
|
|
308
|
+
const page = jsx(Component, {});
|
|
309
|
+
el.appendChild(page);
|
|
310
|
+
setCtx(null);
|
|
311
|
+
}
|
|
312
|
+
render();
|
|
313
|
+
window.addEventListener("wefu:route", render);
|
|
314
|
+
return el;
|
|
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
|
+
}
|
|
672
|
+
export {
|
|
673
|
+
ApiClient,
|
|
674
|
+
ApiError,
|
|
675
|
+
Chat,
|
|
676
|
+
For,
|
|
677
|
+
Fragment,
|
|
678
|
+
LoginForm,
|
|
679
|
+
RouteView,
|
|
680
|
+
Show,
|
|
681
|
+
api,
|
|
682
|
+
auth,
|
|
683
|
+
computed,
|
|
684
|
+
createApp,
|
|
685
|
+
domMount,
|
|
686
|
+
effect,
|
|
687
|
+
isSignal,
|
|
688
|
+
jsx,
|
|
689
|
+
jsxDEV,
|
|
690
|
+
jsxs,
|
|
691
|
+
router,
|
|
692
|
+
signal,
|
|
693
|
+
ws
|
|
694
|
+
};
|