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.
- package/README.md +113 -12
- package/dist/client/app.d.ts +4 -7
- package/dist/client/i18n.d.ts +27 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +192 -66
- package/dist/client/locale/en_US.d.ts +38 -0
- package/dist/client/locale/zh_CN.d.ts +38 -0
- package/dist/client/vnode.d.ts +2 -0
- package/dist/components/Accordion/Accordion.d.ts +11 -0
- package/dist/components/Alert/Alert.d.ts +9 -0
- package/dist/components/Avatar/Avatar.d.ts +7 -0
- package/dist/components/Badge/Badge.d.ts +8 -0
- package/dist/components/Breadcrumb/Breadcrumb.d.ts +9 -0
- package/dist/components/Button/Button.d.ts +12 -0
- package/dist/components/Card/Card.d.ts +9 -0
- package/dist/components/Checkbox/Checkbox.d.ts +8 -0
- package/dist/components/Divider/Divider.d.ts +6 -0
- package/dist/components/Drawer/Drawer.d.ts +11 -0
- package/dist/components/Dropdown/Dropdown.d.ts +14 -0
- package/dist/components/EmptyState/EmptyState.d.ts +8 -0
- package/dist/components/Field/Field.d.ts +9 -0
- package/dist/components/FileUpload/FileUpload.d.ts +13 -0
- package/dist/components/Form/Form.d.ts +6 -0
- package/dist/components/Input/Input.d.ts +15 -0
- package/dist/components/Loading/Loading.d.ts +5 -0
- package/dist/components/Modal/Modal.d.ts +9 -0
- package/dist/components/PageHeader/PageHeader.d.ts +7 -0
- package/dist/components/Pagination/Pagination.d.ts +8 -0
- package/dist/components/ProgressBar/ProgressBar.d.ts +8 -0
- package/dist/components/RadioGroup/RadioGroup.d.ts +14 -0
- package/dist/components/SearchInput/SearchInput.d.ts +8 -0
- package/dist/components/Select/Select.d.ts +18 -0
- package/dist/components/Slider/Slider.d.ts +10 -0
- package/dist/components/StatCard/StatCard.d.ts +9 -0
- package/dist/components/Steps/Steps.d.ts +12 -0
- package/dist/components/Switch/Switch.d.ts +8 -0
- package/dist/components/Table/Table.d.ts +13 -0
- package/dist/components/Tabs/Tabs.d.ts +12 -0
- package/dist/components/Tag/Tag.d.ts +8 -0
- package/dist/components/Textarea/Textarea.d.ts +13 -0
- package/dist/components/Toast/Toast.d.ts +12 -0
- package/dist/components/Tooltip/Tooltip.d.ts +9 -0
- package/dist/components/index.d.ts +75 -0
- package/dist/components/index.js +887 -0
- package/dist/components/style.css +1779 -0
- 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
|
-
|
|
351
|
-
|
|
352
|
-
// mount:
|
|
354
|
+
return (
|
|
355
|
+
<div ref={el => {
|
|
356
|
+
// mount: el 是 DOM 元素
|
|
353
357
|
el.addEventListener('scroll', handler)
|
|
354
|
-
|
|
355
|
-
|
|
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
|
package/dist/client/app.d.ts
CHANGED
|
@@ -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.$
|
|
12
|
-
* $.x = val →
|
|
13
|
-
*
|
|
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;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
+
};
|