weifuwu 0.36.2 → 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 +97 -3
- 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 +171 -54
- package/dist/client/locale/en_US.d.ts +38 -0
- package/dist/client/locale/zh_CN.d.ts +38 -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) {
|
|
@@ -497,9 +499,36 @@ import { ErrorBoundary } from 'weifuwu/client'
|
|
|
497
499
|
|------|------|
|
|
498
500
|
| `extendCtx(ctx, fields)` | 创建新 ctx,继承原 ctx 的 getter |
|
|
499
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
|
+
|
|
500
529
|
### 前端类型
|
|
501
530
|
|
|
502
|
-
`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`
|
|
503
532
|
|
|
504
533
|
---
|
|
505
534
|
|
|
@@ -614,6 +643,65 @@ document.documentElement.setAttribute('data-theme', 'dark')
|
|
|
614
643
|
|
|
615
644
|
---
|
|
616
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
|
+
|
|
617
705
|
## 环境变量
|
|
618
706
|
|
|
619
707
|
| 变量 | 用途 | 默认值 |
|
|
@@ -654,6 +742,12 @@ src/
|
|
|
654
742
|
│ ├── api.ts
|
|
655
743
|
│ ├── auth.ts
|
|
656
744
|
│ └── ws.ts
|
|
745
|
+
├── components/ # 29 个 HTML 原语组件
|
|
746
|
+
│ ├── index.ts
|
|
747
|
+
│ ├── Button/ # Button.ts + .css + .test.ts
|
|
748
|
+
│ ├── Input/
|
|
749
|
+
│ ├── ...
|
|
750
|
+
│ └── PageHeader/
|
|
657
751
|
└── layout/ # 纯 CSS 布局 + 主题
|
|
658
752
|
├── weifuwu-layout.css
|
|
659
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
|
}
|
|
@@ -66,6 +117,8 @@ function renderComponent(Comp, props, vnode, ctx) {
|
|
|
66
117
|
if (!prev$) vnode._$ = {};
|
|
67
118
|
ctx.ui = ctx.ui ?? {};
|
|
68
119
|
ctx.ui.ready = !!prev$;
|
|
120
|
+
const _target = vnode._$;
|
|
121
|
+
ctx.ui.$ = createComponentProxy(_target, () => ctx.ui?.dirty?.());
|
|
69
122
|
let childVNode;
|
|
70
123
|
try {
|
|
71
124
|
childVNode = Comp(props, ctx);
|
|
@@ -169,6 +222,8 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
|
|
|
169
222
|
if (oldV._$) newV._$ = oldV._$;
|
|
170
223
|
ctx.ui = ctx.ui ?? {};
|
|
171
224
|
ctx.ui.ready = !!newV._$;
|
|
225
|
+
const _tgt = newV._$;
|
|
226
|
+
ctx.ui.$ = createComponentProxy(_tgt, () => ctx.ui?.dirty?.());
|
|
172
227
|
const childNew = comp(newV.props, ctx);
|
|
173
228
|
newV._child = childNew;
|
|
174
229
|
return patchValue(parent, oldNode, oldV._child, childNew, ctx);
|
|
@@ -211,7 +266,9 @@ function patchProps(el, oldProps, newProps) {
|
|
|
211
266
|
const newKeys = newProps ? Object.keys(newProps).filter((k) => k !== "children" && k !== "key" && k !== "ref") : [];
|
|
212
267
|
for (const key of oldKeys) {
|
|
213
268
|
if (!newKeys.includes(key)) {
|
|
214
|
-
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)) {
|
|
215
272
|
;
|
|
216
273
|
el.value = "";
|
|
217
274
|
} else {
|
|
@@ -413,18 +470,21 @@ function router(opts) {
|
|
|
413
470
|
params[match.keys[i]] = decodeURIComponent(m[i + 1]);
|
|
414
471
|
}
|
|
415
472
|
}
|
|
473
|
+
const last = match.chain[match.chain.length - 1];
|
|
416
474
|
return {
|
|
417
475
|
path: match.def.path,
|
|
418
476
|
params,
|
|
419
477
|
query: Object.fromEntries(new URLSearchParams(window.location.search)),
|
|
420
|
-
chain: match.chain
|
|
478
|
+
chain: match.chain,
|
|
479
|
+
title: last?.title ?? ""
|
|
421
480
|
};
|
|
422
481
|
}
|
|
423
|
-
return { path, params: {}, query: {}, chain: [] };
|
|
482
|
+
return { path, params: {}, query: {}, chain: [], title: "" };
|
|
424
483
|
}
|
|
425
484
|
return (ctx) => {
|
|
426
485
|
const resolved = resolve(getPath());
|
|
427
486
|
ctx.route = resolved;
|
|
487
|
+
if (resolved.title) document.title = resolved.title;
|
|
428
488
|
if (!ctx.app) ctx.app = {};
|
|
429
489
|
ctx.app.navigate = (path) => {
|
|
430
490
|
if (mode === "hash") {
|
|
@@ -436,11 +496,13 @@ function router(opts) {
|
|
|
436
496
|
const resolved2 = resolve(path);
|
|
437
497
|
ctx.route = resolved2;
|
|
438
498
|
}
|
|
499
|
+
if (ctx.route?.title) document.title = ctx.route.title;
|
|
439
500
|
ctx.ui?.render();
|
|
440
501
|
};
|
|
441
502
|
const onPop = () => {
|
|
442
503
|
const resolved2 = resolve(getPath());
|
|
443
504
|
ctx.route = resolved2;
|
|
505
|
+
if (ctx.route?.title) document.title = ctx.route.title;
|
|
444
506
|
ctx.ui?.render();
|
|
445
507
|
};
|
|
446
508
|
window.addEventListener("popstate", onPop);
|
|
@@ -466,48 +528,6 @@ function RouteView(_props, ctx) {
|
|
|
466
528
|
}
|
|
467
529
|
|
|
468
530
|
// src/client/app.ts
|
|
469
|
-
var mutationMethods = ["push", "pop", "splice", "shift", "unshift", "sort", "reverse"];
|
|
470
|
-
var wrappedCache = /* @__PURE__ */ new WeakMap();
|
|
471
|
-
function wrapDeep(val, dirty) {
|
|
472
|
-
if (val === null || typeof val !== "object") return val;
|
|
473
|
-
if (val instanceof Node) return val;
|
|
474
|
-
if (wrappedCache.has(val)) return wrappedCache.get(val);
|
|
475
|
-
let proxy;
|
|
476
|
-
if (Array.isArray(val)) {
|
|
477
|
-
proxy = new Proxy(val, {
|
|
478
|
-
get(target, key, receiver) {
|
|
479
|
-
const v = Reflect.get(target, key, receiver);
|
|
480
|
-
if (typeof key === "string" && mutationMethods.includes(key) && typeof v === "function") {
|
|
481
|
-
return function(...args) {
|
|
482
|
-
const r = v.apply(target, args);
|
|
483
|
-
dirty();
|
|
484
|
-
return r;
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
return wrapDeep(v, dirty);
|
|
488
|
-
},
|
|
489
|
-
set(target, key, val2) {
|
|
490
|
-
target[key] = wrapDeep(val2, dirty);
|
|
491
|
-
dirty();
|
|
492
|
-
return true;
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
} else {
|
|
496
|
-
proxy = new Proxy(val, {
|
|
497
|
-
get(_target, key, receiver) {
|
|
498
|
-
const v = Reflect.get(_target, key, receiver);
|
|
499
|
-
return wrapDeep(v, dirty);
|
|
500
|
-
},
|
|
501
|
-
set(target, key, val2) {
|
|
502
|
-
target[key] = wrapDeep(val2, dirty);
|
|
503
|
-
dirty();
|
|
504
|
-
return true;
|
|
505
|
-
}
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
wrappedCache.set(val, proxy);
|
|
509
|
-
return proxy;
|
|
510
|
-
}
|
|
511
531
|
function createApp() {
|
|
512
532
|
const middlewares = [];
|
|
513
533
|
let ctx = {};
|
|
@@ -542,7 +562,7 @@ function createApp() {
|
|
|
542
562
|
ctx.ui.render();
|
|
543
563
|
});
|
|
544
564
|
}
|
|
545
|
-
|
|
565
|
+
;
|
|
546
566
|
ctx.ui = {
|
|
547
567
|
render: () => {
|
|
548
568
|
if (!container || !rootComponent || !oldVNode) return;
|
|
@@ -559,13 +579,11 @@ function createApp() {
|
|
|
559
579
|
dirty: () => {
|
|
560
580
|
scheduleRender();
|
|
561
581
|
},
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
}),
|
|
582
|
+
/**
|
|
583
|
+
* 组件级 $ — 在 renderComponent 中被覆盖为每个组件独立的 Proxy。
|
|
584
|
+
* 这里保留占位,组件实际使用 ctx.ui.$ 时拿到的是 vnode._$ 上的 Proxy。
|
|
585
|
+
*/
|
|
586
|
+
$: {},
|
|
569
587
|
ready: false
|
|
570
588
|
};
|
|
571
589
|
oldVNode = wrapComponent(RootComponent, ctx);
|
|
@@ -875,6 +893,102 @@ function ErrorBoundary(props, ctx) {
|
|
|
875
893
|
return fb ?? null;
|
|
876
894
|
}
|
|
877
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
|
+
}
|
|
878
992
|
export {
|
|
879
993
|
ApiError,
|
|
880
994
|
ErrorBoundary,
|
|
@@ -883,11 +997,14 @@ export {
|
|
|
883
997
|
api,
|
|
884
998
|
auth,
|
|
885
999
|
createApp,
|
|
1000
|
+
enUS,
|
|
886
1001
|
extendCtx,
|
|
887
1002
|
h,
|
|
1003
|
+
i18n,
|
|
888
1004
|
jsx,
|
|
889
1005
|
jsxDEV,
|
|
890
1006
|
jsxs,
|
|
891
1007
|
router,
|
|
892
|
-
ws
|
|
1008
|
+
ws,
|
|
1009
|
+
zhCN
|
|
893
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
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu i18n — 中文组件文案(缺省值)
|
|
3
|
+
*/
|
|
4
|
+
export declare const zhCN: {
|
|
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
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Component } from '../../client/vnode.ts';
|
|
2
|
+
export interface AccordionItem {
|
|
3
|
+
key: string;
|
|
4
|
+
title: string;
|
|
5
|
+
content?: any;
|
|
6
|
+
}
|
|
7
|
+
export interface AccordionProps {
|
|
8
|
+
items?: AccordionItem[];
|
|
9
|
+
multiple?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const Accordion: Component<AccordionProps>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Component } from '../../client/vnode.ts';
|
|
2
|
+
export type AlertVariant = 'info' | 'success' | 'warning' | 'error';
|
|
3
|
+
export interface AlertProps {
|
|
4
|
+
variant?: AlertVariant;
|
|
5
|
+
closable?: boolean;
|
|
6
|
+
onClose?: () => void;
|
|
7
|
+
children?: any;
|
|
8
|
+
}
|
|
9
|
+
export declare const Alert: Component<AlertProps>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Component } from '../../client/vnode.ts';
|
|
2
|
+
export type BadgeVariant = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
|
3
|
+
export interface BadgeProps {
|
|
4
|
+
variant?: BadgeVariant;
|
|
5
|
+
dot?: boolean;
|
|
6
|
+
children?: any;
|
|
7
|
+
}
|
|
8
|
+
export declare const Badge: Component<BadgeProps>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Component } from '../../client/vnode.ts';
|
|
2
|
+
export interface ButtonProps {
|
|
3
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
4
|
+
size?: 'sm' | 'md' | 'lg';
|
|
5
|
+
block?: boolean;
|
|
6
|
+
loading?: boolean;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
type?: 'button' | 'submit';
|
|
9
|
+
onClick?: (e: MouseEvent) => void;
|
|
10
|
+
children?: any;
|
|
11
|
+
}
|
|
12
|
+
export declare const Button: Component<ButtonProps>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Component } from '../../client/vnode.ts';
|
|
2
|
+
export interface CardProps {
|
|
3
|
+
variant?: 'default' | 'outlined';
|
|
4
|
+
padding?: 'sm' | 'md' | 'lg';
|
|
5
|
+
clickable?: boolean;
|
|
6
|
+
onClick?: () => void;
|
|
7
|
+
children?: any;
|
|
8
|
+
}
|
|
9
|
+
export declare const Card: Component<CardProps>;
|