weifuwu 0.36.1 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +113 -12
  2. package/dist/client/app.d.ts +4 -7
  3. package/dist/client/i18n.d.ts +27 -0
  4. package/dist/client/index.d.ts +4 -0
  5. package/dist/client/index.js +192 -66
  6. package/dist/client/locale/en_US.d.ts +38 -0
  7. package/dist/client/locale/zh_CN.d.ts +38 -0
  8. package/dist/client/vnode.d.ts +2 -0
  9. package/dist/components/Accordion/Accordion.d.ts +11 -0
  10. package/dist/components/Alert/Alert.d.ts +9 -0
  11. package/dist/components/Avatar/Avatar.d.ts +7 -0
  12. package/dist/components/Badge/Badge.d.ts +8 -0
  13. package/dist/components/Breadcrumb/Breadcrumb.d.ts +9 -0
  14. package/dist/components/Button/Button.d.ts +12 -0
  15. package/dist/components/Card/Card.d.ts +9 -0
  16. package/dist/components/Checkbox/Checkbox.d.ts +8 -0
  17. package/dist/components/Divider/Divider.d.ts +6 -0
  18. package/dist/components/Drawer/Drawer.d.ts +11 -0
  19. package/dist/components/Dropdown/Dropdown.d.ts +14 -0
  20. package/dist/components/EmptyState/EmptyState.d.ts +8 -0
  21. package/dist/components/Field/Field.d.ts +9 -0
  22. package/dist/components/FileUpload/FileUpload.d.ts +13 -0
  23. package/dist/components/Form/Form.d.ts +6 -0
  24. package/dist/components/Input/Input.d.ts +15 -0
  25. package/dist/components/Loading/Loading.d.ts +5 -0
  26. package/dist/components/Modal/Modal.d.ts +9 -0
  27. package/dist/components/PageHeader/PageHeader.d.ts +7 -0
  28. package/dist/components/Pagination/Pagination.d.ts +8 -0
  29. package/dist/components/ProgressBar/ProgressBar.d.ts +8 -0
  30. package/dist/components/RadioGroup/RadioGroup.d.ts +14 -0
  31. package/dist/components/SearchInput/SearchInput.d.ts +8 -0
  32. package/dist/components/Select/Select.d.ts +18 -0
  33. package/dist/components/Slider/Slider.d.ts +10 -0
  34. package/dist/components/StatCard/StatCard.d.ts +9 -0
  35. package/dist/components/Steps/Steps.d.ts +12 -0
  36. package/dist/components/Switch/Switch.d.ts +8 -0
  37. package/dist/components/Table/Table.d.ts +13 -0
  38. package/dist/components/Tabs/Tabs.d.ts +12 -0
  39. package/dist/components/Tag/Tag.d.ts +8 -0
  40. package/dist/components/Textarea/Textarea.d.ts +13 -0
  41. package/dist/components/Toast/Toast.d.ts +12 -0
  42. package/dist/components/Tooltip/Tooltip.d.ts +9 -0
  43. package/dist/components/index.d.ts +75 -0
  44. package/dist/components/index.js +887 -0
  45. package/dist/components/style.css +1779 -0
  46. package/package.json +7 -2
package/README.md CHANGED
@@ -22,7 +22,8 @@ npm install weifuwu
22
22
  | **redis** | `weifuwu` | Redis 客户端 → `ctx.redis` | `Router` |
23
23
  | **ui** | `weifuwu` | SSR 渲染 + 动态 JS/CSS 编译 → `ctx.ui` | `Router` |
24
24
  | **graphql** | `weifuwu` | GraphQL 端点 | `Router` |
25
- | **client** | `weifuwu/client` | 前端 VDOM + Proxy 框架 | — |
25
+ | **client** | `weifuwu/client` | 前端 VDOM + Proxy 框架 + i18n + ErrorBoundary | — |
26
+ | **components** | `weifuwu/components` | 34 个 HTML 原语组件(Button/Table/Modal/...) | `client` |
26
27
  | **layout** | `weifuwu/layout` | 纯 CSS 布局原语 + 主题 Token | — |
27
28
 
28
29
  ---
@@ -39,6 +40,7 @@ npm install weifuwu
39
40
  ctx.sql ctx.ws
40
41
  ctx.redis ctx.route
41
42
  ctx.ui ctx.api / ctx.auth
43
+ ctx.i18n
42
44
  ```
43
45
 
44
46
  ---
@@ -71,7 +73,7 @@ serve(app, { port: 3000 })
71
73
 
72
74
  ```tsx
73
75
  // src/main.tsx —— 前端
74
- import { createApp, router, RouteView } from 'weifuwu/client'
76
+ import { createApp, router, RouteView, i18n } from 'weifuwu/client'
75
77
  import type { WfuiContext, RouteDef } from 'weifuwu/client'
76
78
 
77
79
  function Home(_props: {}, ctx: WfuiContext) {
@@ -345,21 +347,28 @@ function Counter(_props: {}, ctx: WfuiContext) {
345
347
 
346
348
  ### 生命周期 —— ref 回调
347
349
 
350
+ `ref` 回调在 mount 时触发,接收 DOM 元素。返回的清理函数在 unmount 时由框架保证调用。
351
+
348
352
  ```tsx
349
353
  function MyComponent(_props: {}, ctx: WfuiContext) {
350
- function onRef(el: HTMLElement | null) {
351
- if (el) {
352
- // mount: 加事件监听等
354
+ return (
355
+ <div ref={el => {
356
+ // mount: el 是 DOM 元素
353
357
  el.addEventListener('scroll', handler)
354
- } else {
355
- // unmount: 清理
356
- }
357
- }
358
-
359
- return <div ref={onRef} />
358
+ // 返回清理函数,unmount 时框架保证调用
359
+ return () => el.removeEventListener('scroll', handler)
360
+ }} />
361
+ )
360
362
  }
361
363
  ```
362
364
 
365
+ | 场景 | 写法 |
366
+ |------|------|
367
+ | 事件监听 | `ref={el => { el.addEventListener(...); return () => el.removeEventListener(...) }}` |
368
+ | 定时器 | `ref={el => { const t = setInterval(f, 1000); return () => clearInterval(t) }}` |
369
+ | 第三方库 | `ref={el => { const c = new Chart(el); return () => c.destroy() }}` |
370
+ | 仅 mount | `ref={el => { init(el) }}` |
371
+
363
372
  ### 应用 —— createApp
364
373
 
365
374
  ```tsx
@@ -490,9 +499,36 @@ import { ErrorBoundary } from 'weifuwu/client'
490
499
  |------|------|
491
500
  | `extendCtx(ctx, fields)` | 创建新 ctx,继承原 ctx 的 getter |
492
501
 
502
+ ### 国际化 — `ctx.i18n`
503
+
504
+ `i18n()` 中间件注入 `ctx.i18n`,支持运行时语言切换:
505
+
506
+ ```ts
507
+ import { createApp, i18n } from 'weifuwu/client'
508
+
509
+ createApp()
510
+ .use(i18n({
511
+ locale: 'zh-CN',
512
+ messages: { 'users.title': '用户管理' },
513
+ }))
514
+ .mount('#root', () => <App />)
515
+
516
+ // 页面中使用
517
+ ctx.i18n?.t('users.title') // → '用户管理'
518
+
519
+ // 运行时切换
520
+ ctx.i18n?.setLocale('en-US') // → 自动触发重渲染
521
+ ```
522
+
523
+ 内置语言包:`zh-CN`(默认)、`en-US`。组件文案(Button 的 `加载中...`、FileUpload 的 `点击或拖拽上传文件`)随语言自动切换。组件通过 `ctx.i18n?.components?.ComponentName.field` 读取,支持 `props.locale` 局部覆盖。
524
+
525
+ ```ts
526
+ import { i18n, zhCN, enUS } from 'weifuwu/client'
527
+ ```
528
+
493
529
  ### 前端类型
494
530
 
495
- `VNode`, `VNodeType`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`, `ApiClient`, `ApiOptions`, `ApiRequestOptions`, `ApiError`, `AuthClient`, `AuthOptions`, `ErrorBoundaryProps`
531
+ `VNode`, `VNodeType`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`, `ApiClient`, `ApiOptions`, `ApiRequestOptions`, `ApiError`, `AuthClient`, `AuthOptions`, `ErrorBoundaryProps`, `I18nOptions`, `I18nState`, `LocalePackage`
496
532
 
497
533
  ---
498
534
 
@@ -607,6 +643,65 @@ document.documentElement.setAttribute('data-theme', 'dark')
607
643
 
608
644
  ---
609
645
 
646
+ ## 组件库 — `weifuwu/components`
647
+
648
+ 29 个 **HTML 原语**,覆盖 90% 的 SaaS 页面 HTML 需求。每个组件是 `(props, ctx) => VNode` 纯函数,引用 `weifuwu/layout` 的 CSS 变量做主题。
649
+
650
+ ```ts
651
+ import { Button, Input, Table, Modal, Toast } from 'weifuwu/components'
652
+ import 'weifuwu/components/style.css'
653
+ ```
654
+
655
+ ### 模块总览
656
+
657
+ | 类别 | 组件 | 用途 |
658
+ |------|------|------|
659
+ | **表单核心** | `Button` `Input` `Textarea` `Select` | 4 个最常用的表单元素 |
660
+ | **表单核心** | `InputNumber` | 数字输入,带自定义步进按钮 (`showStepper`) |
661
+ | **表单选择** | `Checkbox` `Switch` `RadioGroup` `Slider` | 选择类输入 |
662
+ | **表单增强** | `Form` `Field` `FileUpload` `SearchInput` `ProgressBar` | 文件上传、搜索、进度 |
663
+ | **数据展示** | `Table` `Card` `Badge` `Tag` `Avatar` `StatCard` `PageHeader` | 数据展示与页面标题 |
664
+ | **数据反馈** | `Modal` `Drawer` `Tooltip` `Toast` `Alert` `Loading` `EmptyState` | 弹窗、抽屉、提示 |
665
+ | **导航组件** | `Breadcrumb` `Tabs` `Dropdown` `Pagination` `Steps` `Accordion` | 面包屑、标签页、分页 |
666
+ | **布局** | `Divider` | 分割线 (水平/垂直/带文字) |
667
+
668
+ ### 状态管理说明
669
+
670
+ `ctx.ui.$` 是**组件级**状态——每个组件实例有独立的 Proxy(基于 `vnode._$`),同名变量不会冲突:
671
+
672
+ ```tsx
673
+ // 组件 A
674
+ const $ = ctx.ui.$
675
+ $.open = true // 只影响组件 A
676
+
677
+ // 组件 B(在同一页面)
678
+ const $ = ctx.ui.$
679
+ $.open = false // 只影响组件 B,不影响 A
680
+ ```
681
+
682
+ 跨组件共享状态请使用 `ctx` 直接挂载属性(延续中间件模式):
683
+
684
+ ```ts
685
+ ctx.theme = 'dark' // 所有组件可读
686
+ ctx.toast?.success('成功') // 如已注入 toast 中间件
687
+ ```
688
+
689
+ ### 页面模板 — `docs/pages/`
690
+
691
+ | 模板 | 文件 | 用途 |
692
+ |------|------|------|
693
+ | **列表页** | `docs/pages/list-page.md` | 搜索 + 表格 + 分页 + 加载/空/错误状态 |
694
+ | **表单页** | `docs/pages/form-page.md` | 表单 + 字段 + 校验 + 提交 |
695
+ | **详情页** | `docs/pages/detail-page.md` | 信息展示 + Tabs + 操作 |
696
+ | **设置页** | `docs/pages/settings-page.md` | 分组设置 + 独立保存 |
697
+ | **仪表盘** | `docs/pages/dashboard-page.md` | KPI 卡片 + 图表 + 列表 |
698
+ | **认证页** | `docs/pages/auth-page.md` | 居中卡片 + 表单 + 错误提示 |
699
+ | **应用壳** | `docs/pages/app-layout.md` | 侧边栏 + 导航菜单 + 认证守卫 |
700
+
701
+ 每个模板标注了「改这里」——复制代码后改 API 路径、字段定义、操作按钮、导航项即可使用。
702
+
703
+ ---
704
+
610
705
  ## 环境变量
611
706
 
612
707
  | 变量 | 用途 | 默认值 |
@@ -647,6 +742,12 @@ src/
647
742
  │ ├── api.ts
648
743
  │ ├── auth.ts
649
744
  │ └── ws.ts
745
+ ├── components/ # 29 个 HTML 原语组件
746
+ │ ├── index.ts
747
+ │ ├── Button/ # Button.ts + .css + .test.ts
748
+ │ ├── Input/
749
+ │ ├── ...
750
+ │ └── PageHeader/
650
751
  └── layout/ # 纯 CSS 布局 + 主题
651
752
  ├── weifuwu-layout.css
652
753
  ├── _tokens.css
@@ -5,15 +5,12 @@
5
5
  *
6
6
  * ctx.ui 在 mount 时注入:
7
7
  * ctx.ui.render() 触发组件重渲染
8
- * ctx.ui.$ 持久化组件状态(由渲染器管理)
8
+ * ctx.ui.$ 组件级状态 Proxy(由 renderComponent 创建)
9
9
  * ctx.ui.ready 首次执行标记(由渲染器管理)
10
10
  *
11
- * ctx.ui.$ 是深度 Proxy
12
- * $.x = val → 自动渲染(顶层属性赋值)
13
- * $.items.push(val) 自动渲染(数组突变方法)
14
- * $.items[0].x = val → 自动渲染(对象属性突变)
15
- * $.items.push({x: 1}) → 新对象自动深度包装
16
- * ctx.ui.dirty() → 极少需要(仅当绕过 Proxy 直接操作底层对象)
11
+ * ctx.ui.$ 是组件级 Proxy(组件间隔离):
12
+ * $.x = val → 设置当前组件状态,自动触发渲染
13
+ * ctx.ui.dirty() 标记脏状态(仅深层突变时需要)
17
14
  */
18
15
  import type { WfuiContext, AppMiddleware } from './types.ts';
19
16
  import type { Component } from './vnode.ts';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * weifuwu i18n — 中间件
3
+ *
4
+ * 注入 ctx.i18n 支持运行时语言切换。
5
+ *
6
+ * 使用:
7
+ * app.use(i18n({ locale: 'en', messages: { 'title': 'Dashboard' } }))
8
+ * ctx.i18n?.t('title') // → 'Dashboard'
9
+ * ctx.i18n?.setLocale('zh-CN') // → 切换语言,自动重渲染
10
+ */
11
+ import type { AppMiddleware } from './types.ts';
12
+ export interface I18nOptions {
13
+ locale?: string;
14
+ messages?: Record<string, string>;
15
+ components?: Record<string, Record<string, string>>;
16
+ }
17
+ export interface LocalePackage {
18
+ messages?: Record<string, string>;
19
+ components?: Record<string, Record<string, string>>;
20
+ }
21
+ export interface I18nState {
22
+ locale: string;
23
+ t: (key: string, fallback?: string) => string;
24
+ setLocale: (lang: string) => void;
25
+ components: Record<string, Record<string, string>>;
26
+ }
27
+ export declare function i18n(opts?: I18nOptions): AppMiddleware;
@@ -29,3 +29,7 @@ export { extendCtx } from './types.ts';
29
29
  export type { WfuiContext, AppMiddleware, RouteDef } from './types.ts';
30
30
  export { ErrorBoundary } from './error-boundary.ts';
31
31
  export type { ErrorBoundaryProps } from './error-boundary.ts';
32
+ export { i18n } from './i18n.ts';
33
+ export type { I18nOptions, I18nState } from './i18n.ts';
34
+ export { zhCN } from './locale/zh_CN.ts';
35
+ export { enUS } from './locale/en_US.ts';
@@ -29,6 +29,57 @@ function normalizeProps(props) {
29
29
  }
30
30
 
31
31
  // src/client/render.ts
32
+ var mutationMethods = ["push", "pop", "splice", "shift", "unshift", "sort", "reverse"];
33
+ var wrappedCache = /* @__PURE__ */ new WeakMap();
34
+ function wrapDeep(val, dirty) {
35
+ if (val === null || typeof val !== "object") return val;
36
+ if (val instanceof Node) return val;
37
+ if (typeof Blob !== "undefined" && val instanceof Blob) return val;
38
+ if (wrappedCache.has(val)) return wrappedCache.get(val);
39
+ const handler = Array.isArray(val) ? {
40
+ get(target, key, receiver) {
41
+ const v = Reflect.get(target, key, receiver);
42
+ if (typeof key === "string" && mutationMethods.includes(key) && typeof v === "function") {
43
+ return function(...args) {
44
+ const r = v.apply(target, args);
45
+ dirty();
46
+ return r;
47
+ };
48
+ }
49
+ return wrapDeep(v, dirty);
50
+ },
51
+ set(target, key, v) {
52
+ Reflect.set(target, key, wrapDeep(v, dirty));
53
+ dirty();
54
+ return true;
55
+ }
56
+ } : {
57
+ get(_target, key, receiver) {
58
+ const v = Reflect.get(_target, key, receiver);
59
+ return wrapDeep(v, dirty);
60
+ },
61
+ set(target, key, v) {
62
+ Reflect.set(target, key, wrapDeep(v, dirty));
63
+ dirty();
64
+ return true;
65
+ }
66
+ };
67
+ const proxy = new Proxy(val, handler);
68
+ wrappedCache.set(val, proxy);
69
+ return proxy;
70
+ }
71
+ function createComponentProxy(target, dirty) {
72
+ return new Proxy(target, {
73
+ set(t, k, v) {
74
+ t[k] = wrapDeep(v, dirty);
75
+ dirty();
76
+ return true;
77
+ },
78
+ get(t, k) {
79
+ return wrapDeep(t[k], dirty);
80
+ }
81
+ });
82
+ }
32
83
  function render(input, ctx) {
33
84
  return renderValue(input, ctx);
34
85
  }
@@ -56,7 +107,8 @@ function renderValue(v, ctx) {
56
107
  el.appendChild(renderValue(child, ctx));
57
108
  }
58
109
  if (vnode.props?.ref) {
59
- queueMicrotask(() => vnode.props.ref(el));
110
+ const result = vnode.props.ref(el);
111
+ if (typeof result === "function") vnode._cleanup = result;
60
112
  }
61
113
  return el;
62
114
  }
@@ -65,6 +117,8 @@ function renderComponent(Comp, props, vnode, ctx) {
65
117
  if (!prev$) vnode._$ = {};
66
118
  ctx.ui = ctx.ui ?? {};
67
119
  ctx.ui.ready = !!prev$;
120
+ const _target = vnode._$;
121
+ ctx.ui.$ = createComponentProxy(_target, () => ctx.ui?.dirty?.());
68
122
  let childVNode;
69
123
  try {
70
124
  childVNode = Comp(props, ctx);
@@ -140,7 +194,7 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
140
194
  }
141
195
  if (newInput == null) {
142
196
  if (oldNode) {
143
- callRef(oldInput, null);
197
+ callRefCleanup(oldInput);
144
198
  oldNode.remove();
145
199
  }
146
200
  return null;
@@ -148,7 +202,7 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
148
202
  const oldType = typeOf(oldInput);
149
203
  const newType = typeOf(newInput);
150
204
  if (oldType !== newType) {
151
- callRef(oldInput, null);
205
+ callRefCleanup(oldInput);
152
206
  const node = renderValue(newInput, ctx);
153
207
  if (oldNode?.parentNode) {
154
208
  oldNode.parentNode.replaceChild(node, oldNode);
@@ -168,6 +222,8 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
168
222
  if (oldV._$) newV._$ = oldV._$;
169
223
  ctx.ui = ctx.ui ?? {};
170
224
  ctx.ui.ready = !!newV._$;
225
+ const _tgt = newV._$;
226
+ ctx.ui.$ = createComponentProxy(_tgt, () => ctx.ui?.dirty?.());
171
227
  const childNew = comp(newV.props, ctx);
172
228
  newV._child = childNew;
173
229
  return patchValue(parent, oldNode, oldV._child, childNew, ctx);
@@ -181,7 +237,7 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
181
237
  patchProps(oldNode, oldV.props, newV.props);
182
238
  patchChildren(oldNode, oldV, newV, ctx);
183
239
  } else if (oldNode) {
184
- callRef(oldInput, null);
240
+ callRefCleanup(oldInput);
185
241
  const node = renderValue(newInput, ctx);
186
242
  oldNode.parentNode?.replaceChild(node, oldNode);
187
243
  return node;
@@ -210,7 +266,9 @@ function patchProps(el, oldProps, newProps) {
210
266
  const newKeys = newProps ? Object.keys(newProps).filter((k) => k !== "children" && k !== "key" && k !== "ref") : [];
211
267
  for (const key of oldKeys) {
212
268
  if (!newKeys.includes(key)) {
213
- if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
269
+ if (key.startsWith("on") && typeof oldProps[key] === "function") {
270
+ el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
271
+ } else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
214
272
  ;
215
273
  el.value = "";
216
274
  } else {
@@ -286,7 +344,7 @@ function patchSimpleChildren(parent, oldChildren, newChildren, ctx) {
286
344
  parent.appendChild(node);
287
345
  } else if (newChild === void 0) {
288
346
  if (existingNode) {
289
- callRef(oldChild, null);
347
+ callRefCleanup(oldChild);
290
348
  existingNode.remove();
291
349
  }
292
350
  } else {
@@ -306,7 +364,7 @@ function patchKeyedChildren(parent, oldChildren, newChildren, ctx) {
306
364
  for (const key of oldKeyMap.keys()) {
307
365
  if (!newKeys.includes(key)) {
308
366
  const entry = oldKeyMap.get(key);
309
- callRef(entry.vnode, null);
367
+ callRefCleanup(entry.vnode);
310
368
  entry.node?.remove();
311
369
  oldKeyMap.delete(key);
312
370
  }
@@ -316,7 +374,7 @@ function patchKeyedChildren(parent, oldChildren, newChildren, ctx) {
316
374
  if (key === void 0) {
317
375
  const node = parent.childNodes[i];
318
376
  if (node) {
319
- callRef(oldChildren[i], null);
377
+ callRefCleanup(oldChildren[i]);
320
378
  node.remove();
321
379
  }
322
380
  }
@@ -337,13 +395,21 @@ function patchKeyedChildren(parent, oldChildren, newChildren, ctx) {
337
395
  }
338
396
  }
339
397
  }
340
- function callRef(input, el) {
398
+ function runRefCleanup(vnode) {
399
+ if (vnode._cleanup) {
400
+ vnode._cleanup();
401
+ vnode._cleanup = void 0;
402
+ }
403
+ forEach(vnode.props?.children, (child) => {
404
+ if (child && typeof child === "object" && child._cleanup) {
405
+ runRefCleanup(child);
406
+ }
407
+ });
408
+ }
409
+ function callRefCleanup(input) {
341
410
  if (input == null || typeof input !== "object") return;
342
411
  const vnode = input;
343
- if (typeof vnode.props?.ref === "function") {
344
- vnode.props.ref(el);
345
- }
346
- forEach(vnode.props?.children, (child) => callRef(child, el));
412
+ if (vnode._cleanup) runRefCleanup(vnode);
347
413
  }
348
414
 
349
415
  // src/client/router.ts
@@ -404,18 +470,21 @@ function router(opts) {
404
470
  params[match.keys[i]] = decodeURIComponent(m[i + 1]);
405
471
  }
406
472
  }
473
+ const last = match.chain[match.chain.length - 1];
407
474
  return {
408
475
  path: match.def.path,
409
476
  params,
410
477
  query: Object.fromEntries(new URLSearchParams(window.location.search)),
411
- chain: match.chain
478
+ chain: match.chain,
479
+ title: last?.title ?? ""
412
480
  };
413
481
  }
414
- return { path, params: {}, query: {}, chain: [] };
482
+ return { path, params: {}, query: {}, chain: [], title: "" };
415
483
  }
416
484
  return (ctx) => {
417
485
  const resolved = resolve(getPath());
418
486
  ctx.route = resolved;
487
+ if (resolved.title) document.title = resolved.title;
419
488
  if (!ctx.app) ctx.app = {};
420
489
  ctx.app.navigate = (path) => {
421
490
  if (mode === "hash") {
@@ -427,11 +496,13 @@ function router(opts) {
427
496
  const resolved2 = resolve(path);
428
497
  ctx.route = resolved2;
429
498
  }
499
+ if (ctx.route?.title) document.title = ctx.route.title;
430
500
  ctx.ui?.render();
431
501
  };
432
502
  const onPop = () => {
433
503
  const resolved2 = resolve(getPath());
434
504
  ctx.route = resolved2;
505
+ if (ctx.route?.title) document.title = ctx.route.title;
435
506
  ctx.ui?.render();
436
507
  };
437
508
  window.addEventListener("popstate", onPop);
@@ -457,48 +528,6 @@ function RouteView(_props, ctx) {
457
528
  }
458
529
 
459
530
  // src/client/app.ts
460
- var mutationMethods = ["push", "pop", "splice", "shift", "unshift", "sort", "reverse"];
461
- var wrappedCache = /* @__PURE__ */ new WeakMap();
462
- function wrapDeep(val, dirty) {
463
- if (val === null || typeof val !== "object") return val;
464
- if (val instanceof Node) return val;
465
- if (wrappedCache.has(val)) return wrappedCache.get(val);
466
- let proxy;
467
- if (Array.isArray(val)) {
468
- proxy = new Proxy(val, {
469
- get(target, key, receiver) {
470
- const v = Reflect.get(target, key, receiver);
471
- if (typeof key === "string" && mutationMethods.includes(key) && typeof v === "function") {
472
- return function(...args) {
473
- const r = v.apply(target, args);
474
- dirty();
475
- return r;
476
- };
477
- }
478
- return wrapDeep(v, dirty);
479
- },
480
- set(target, key, val2) {
481
- target[key] = wrapDeep(val2, dirty);
482
- dirty();
483
- return true;
484
- }
485
- });
486
- } else {
487
- proxy = new Proxy(val, {
488
- get(_target, key, receiver) {
489
- const v = Reflect.get(_target, key, receiver);
490
- return wrapDeep(v, dirty);
491
- },
492
- set(target, key, val2) {
493
- target[key] = wrapDeep(val2, dirty);
494
- dirty();
495
- return true;
496
- }
497
- });
498
- }
499
- wrappedCache.set(val, proxy);
500
- return proxy;
501
- }
502
531
  function createApp() {
503
532
  const middlewares = [];
504
533
  let ctx = {};
@@ -533,7 +562,7 @@ function createApp() {
533
562
  ctx.ui.render();
534
563
  });
535
564
  }
536
- const $target = {};
565
+ ;
537
566
  ctx.ui = {
538
567
  render: () => {
539
568
  if (!container || !rootComponent || !oldVNode) return;
@@ -550,13 +579,11 @@ function createApp() {
550
579
  dirty: () => {
551
580
  scheduleRender();
552
581
  },
553
- $: new Proxy($target, {
554
- set(_target, key, val) {
555
- _target[key] = wrapDeep(val, scheduleRender);
556
- scheduleRender();
557
- return true;
558
- }
559
- }),
582
+ /**
583
+ * 组件级 $ — 在 renderComponent 中被覆盖为每个组件独立的 Proxy。
584
+ * 这里保留占位,组件实际使用 ctx.ui.$ 时拿到的是 vnode._$ 上的 Proxy。
585
+ */
586
+ $: {},
560
587
  ready: false
561
588
  };
562
589
  oldVNode = wrapComponent(RootComponent, ctx);
@@ -866,6 +893,102 @@ function ErrorBoundary(props, ctx) {
866
893
  return fb ?? null;
867
894
  }
868
895
  }
896
+
897
+ // src/client/locale/zh_CN.ts
898
+ var zhCN = {
899
+ components: {
900
+ Button: { loading: "\u52A0\u8F7D\u4E2D..." },
901
+ FileUpload: {
902
+ placeholder: "\u70B9\u51FB\u6216\u62D6\u62FD\u4E0A\u4F20\u6587\u4EF6",
903
+ supportedFormats: "\u652F\u6301\u683C\u5F0F: ",
904
+ maxSize: "\u6700\u5927 ",
905
+ remove: "\u5220\u9664"
906
+ },
907
+ Pagination: { ariaLabel: "\u5206\u9875" },
908
+ ProgressBar: { ariaLabel: "\u8FDB\u5EA6" },
909
+ Switch: { ariaLabel: "\u5207\u6362" },
910
+ Breadcrumb: { ariaLabel: "\u9762\u5305\u5C51" },
911
+ Modal: { ariaLabel: "\u5F39\u7A97" },
912
+ Drawer: { ariaLabel: "\u4FA7\u8FB9\u9762\u677F" },
913
+ InputNumber: { increase: "\u589E\u52A0", decrease: "\u51CF\u5C11" }
914
+ }
915
+ };
916
+
917
+ // src/client/locale/en_US.ts
918
+ var enUS = {
919
+ components: {
920
+ Button: { loading: "Loading..." },
921
+ FileUpload: {
922
+ placeholder: "Click or drag to upload",
923
+ supportedFormats: "Supported formats: ",
924
+ maxSize: "Max size: ",
925
+ remove: "Remove"
926
+ },
927
+ Pagination: { ariaLabel: "Pagination" },
928
+ ProgressBar: { ariaLabel: "Progress" },
929
+ Switch: { ariaLabel: "Toggle" },
930
+ Breadcrumb: { ariaLabel: "Breadcrumb" },
931
+ Modal: { ariaLabel: "Dialog" },
932
+ Drawer: { ariaLabel: "Panel" },
933
+ InputNumber: { increase: "Increase", decrease: "Decrease" }
934
+ }
935
+ };
936
+
937
+ // src/client/i18n.ts
938
+ var LOCALE_PACKAGES = {
939
+ "zh-CN": zhCN,
940
+ "en-US": enUS
941
+ };
942
+ function resolveLang(locale) {
943
+ if (LOCALE_PACKAGES[locale]) return locale;
944
+ const prefix = locale.split("-")[0];
945
+ const match = Object.keys(LOCALE_PACKAGES).find((k) => k.startsWith(prefix));
946
+ return match ?? "zh-CN";
947
+ }
948
+ function i18n(opts = {}) {
949
+ const { locale: raw = "zh-CN", messages = {}, components = {} } = opts;
950
+ const lang = resolveLang(raw);
951
+ const pkg = LOCALE_PACKAGES[lang];
952
+ let merged = mergeLocales(pkg, messages, components);
953
+ const state = {
954
+ locale: lang,
955
+ t: (key, fallback) => merged.messages?.[key] ?? fallback ?? key,
956
+ setLocale: () => {
957
+ },
958
+ components: merged.components
959
+ };
960
+ return (ctx) => {
961
+ ;
962
+ ctx.i18n = state;
963
+ state.setLocale = (raw2) => {
964
+ const lang2 = resolveLang(raw2);
965
+ const pkg2 = LOCALE_PACKAGES[lang2];
966
+ if (!pkg2) return;
967
+ merged = mergeLocales(pkg2, messages, components);
968
+ state.locale = lang2;
969
+ state.components = merged.components;
970
+ ctx?.ui?.render();
971
+ };
972
+ return ctx;
973
+ };
974
+ }
975
+ function mergeLocales(pkg, userMessages, userComponents) {
976
+ return {
977
+ messages: { ...pkg?.messages, ...userMessages },
978
+ components: deepMerge(pkg?.components ?? {}, userComponents)
979
+ };
980
+ }
981
+ function deepMerge(a, b) {
982
+ const result = { ...a };
983
+ for (const key of Object.keys(b)) {
984
+ if (typeof b[key] === "object" && b[key] !== null && typeof result[key] === "object") {
985
+ result[key] = { ...result[key], ...b[key] };
986
+ } else {
987
+ result[key] = b[key];
988
+ }
989
+ }
990
+ return result;
991
+ }
869
992
  export {
870
993
  ApiError,
871
994
  ErrorBoundary,
@@ -874,11 +997,14 @@ export {
874
997
  api,
875
998
  auth,
876
999
  createApp,
1000
+ enUS,
877
1001
  extendCtx,
878
1002
  h,
1003
+ i18n,
879
1004
  jsx,
880
1005
  jsxDEV,
881
1006
  jsxs,
882
1007
  router,
883
- ws
1008
+ ws,
1009
+ zhCN
884
1010
  };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * weifuwu i18n — 英文组件文案
3
+ */
4
+ export declare const enUS: {
5
+ components: {
6
+ Button: {
7
+ loading: string;
8
+ };
9
+ FileUpload: {
10
+ placeholder: string;
11
+ supportedFormats: string;
12
+ maxSize: string;
13
+ remove: string;
14
+ };
15
+ Pagination: {
16
+ ariaLabel: string;
17
+ };
18
+ ProgressBar: {
19
+ ariaLabel: string;
20
+ };
21
+ Switch: {
22
+ ariaLabel: string;
23
+ };
24
+ Breadcrumb: {
25
+ ariaLabel: string;
26
+ };
27
+ Modal: {
28
+ ariaLabel: string;
29
+ };
30
+ Drawer: {
31
+ ariaLabel: string;
32
+ };
33
+ InputNumber: {
34
+ increase: string;
35
+ decrease: string;
36
+ };
37
+ };
38
+ };