weifuwu 0.40.1 → 0.51.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 +545 -94
- package/dist/client/app.d.ts +9 -3
- package/dist/client/index.js +180 -116
- package/dist/client/render.d.ts +2 -1
- package/dist/client/types.d.ts +11 -13
- package/dist/client/vnode.d.ts +6 -3
- package/dist/components/Form/Form.d.ts +17 -1
- package/dist/components/Modal/Modal.d.ts +4 -0
- package/dist/components/Select/Select.d.ts +5 -1
- package/dist/components/Skeleton/Skeleton.d.ts +1 -1
- package/dist/components/Table/Table.d.ts +13 -0
- package/dist/components/Toast/Toast.d.ts +9 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +311 -100
- package/dist/components/style.css +744 -2
- package/dist/index.js +39 -13
- package/dist/layout/weifuwu-layout.css +0 -1
- package/dist/ui/index.d.ts +3 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -10,30 +10,17 @@ npm install weifuwu
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## 设计理念
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
| `weifuwu` | **graphql** | GraphQL 端点(支持 GraphiQL) | Router |
|
|
25
|
-
| `weifuwu` | **createMiddleware** | 类型安全中间件工厂 | — |
|
|
26
|
-
| `weifuwu` | **response** | HTTP 响应辅助函数(ok/badRequest/...) | — |
|
|
27
|
-
| `weifuwu` | **parseBody** | JSON 请求体安全解析 | — |
|
|
28
|
-
| `weifuwu/client` | **createApp** | 应用引导 + VDOM 渲染引擎 | — |
|
|
29
|
-
| `weifuwu/client` | **router / RouteView** | 前端路由(history/hash 模式) | createApp |
|
|
30
|
-
| `weifuwu/client` | **api / auth / ws** | HTTP 客户端 / 认证 / WebSocket 中间件 | createApp |
|
|
31
|
-
| `weifuwu/client` | **i18n** | 国际化中间件(运行时切换语言) | createApp |
|
|
32
|
-
| `weifuwu/client` | **ErrorBoundary** | 错误边界组件 | createApp |
|
|
33
|
-
| `weifuwu/client` | **confirm** | Promise 化确认对话框 | createApp |
|
|
34
|
-
| `weifuwu/client** | **lockScroll/trapFocus** | 滚动锁定 / 焦点陷阱工具 | — |
|
|
35
|
-
| `weifuwu/components` | **41 个组件** | Button/Table/Modal/Toast/... | weifuwu/client |
|
|
36
|
-
| `weifuwu/layout` | **CSS 布局** | 35 个布局原语 + 72 个主题 Token | — |
|
|
15
|
+
**零运行时依赖** — 前端无 npm 运行时依赖,不引入 Virtual DOM 库、rxjs、immer 等重型依赖。esbuild 编译 TSX 的结果即可直接运行。
|
|
16
|
+
|
|
17
|
+
**两阶段组件模型** — 组件 = `(initProps, ctx) => (props) => VNode`。外层函数只执行一次(mount),内层函数每次状态/props 变化时执行(render)。无 class、无 `this`、无 Hook。
|
|
18
|
+
|
|
19
|
+
**Proxy 驱动渲染** — `ctx.ui.$()` 返回深度 Proxy,`$.x = val` 自动触发当前组件的 VDOM patch。也支持手动 `ctx.ui.render()` 精确控制渲染时机。无需手动调用 `useState`/`useEffect`。
|
|
20
|
+
|
|
21
|
+
**中间件注入一切** — 后端和前端共用同一理念:中间件向 `ctx` 注入能力(`ctx.sql` / `ctx.redis` / `ctx.api` / `ctx.auth` / `ctx.i18n` 等),Handler/组件从 `ctx` 读取。
|
|
22
|
+
|
|
23
|
+
**SSR + 动态编译** — 后端 `ctx.ui.js()` 用 esbuild 实时编译 TSX,开发时改代码即刷即用,零构建步骤。
|
|
37
24
|
|
|
38
25
|
---
|
|
39
26
|
|
|
@@ -68,8 +55,9 @@ serve(app, { port: 3000 })
|
|
|
68
55
|
```tsx
|
|
69
56
|
// src/main.tsx
|
|
70
57
|
import { createApp, router, RouteView } from 'weifuwu/client'
|
|
58
|
+
import type { Component } from 'weifuwu/client'
|
|
71
59
|
|
|
72
|
-
|
|
60
|
+
const Home: Component = () => () => <h1>Hello weifuwu</h1>
|
|
73
61
|
|
|
74
62
|
createApp()
|
|
75
63
|
.use(router({ routes: [{ path: '/', component: Home }] }))
|
|
@@ -78,8 +66,68 @@ createApp()
|
|
|
78
66
|
|
|
79
67
|
---
|
|
80
68
|
|
|
69
|
+
## 模块总览
|
|
70
|
+
|
|
71
|
+
| 导入路径 | 模块 | 用途 | 依赖 |
|
|
72
|
+
|---------|------|------|------|
|
|
73
|
+
| `weifuwu` | **Router** | Trie 路由 + 中间件链 + WebSocket + GraphQL | — |
|
|
74
|
+
| `weifuwu` | **serve** | HTTP 服务器 | Router |
|
|
75
|
+
| `weifuwu` | **cors** | CORS 跨域中间件 | Router |
|
|
76
|
+
| `weifuwu` | **serveStatic** | 静态文件服务(ETag/304/目录索引) | Router |
|
|
77
|
+
| `weifuwu` | **postgres** | PostgreSQL 连接池 → `ctx.sql` | Router, DATABASE_URL |
|
|
78
|
+
| `weifuwu` | **redis** | Redis 客户端 → `ctx.redis` | Router, REDIS_URL |
|
|
79
|
+
| `weifuwu` | **ui** | SSR 渲染 + esbuild JS/CSS 动态编译 → `ctx.ui` | Router |
|
|
80
|
+
| `weifuwu` | **graphql** | GraphQL 端点(支持 GraphiQL) | Router |
|
|
81
|
+
| `weifuwu` | **createMiddleware** | 类型安全中间件工厂 | — |
|
|
82
|
+
| `weifuwu` | **response** | HTTP 响应辅助函数(ok/badRequest/...) | — |
|
|
83
|
+
| `weifuwu` | **parseBody** | JSON 请求体安全解析 | — |
|
|
84
|
+
| `weifuwu/client` | **createApp** | 应用引导 + VDOM 渲染引擎 | — |
|
|
85
|
+
| `weifuwu/client` | **router / RouteView** | 前端路由(history/hash 模式) | createApp |
|
|
86
|
+
| `weifuwu/client` | **api / auth / ws** | HTTP 客户端 / 认证 / WebSocket 中间件 | createApp |
|
|
87
|
+
| `weifuwu/client` | **i18n** | 国际化中间件(运行时切换语言) | createApp |
|
|
88
|
+
| `weifuwu/client` | **ErrorBoundary** | 错误边界组件 | createApp |
|
|
89
|
+
| `weifuwu/client` | **confirm** | Promise 化确认对话框 | createApp |
|
|
90
|
+
| `weifuwu/client` | **lockScroll/trapFocus** | 滚动锁定 / 焦点陷阱工具 | — |
|
|
91
|
+
| `weifuwu/components` | **41 个组件** | Button/Table/Modal/Toast/... | weifuwu/client |
|
|
92
|
+
| `weifuwu/layout` | **CSS 布局** | 35 个布局原语 + 72 个主题 Token(也支持 `weifuwu/layout/style.css`) | — |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 核心概念
|
|
97
|
+
|
|
98
|
+
### 中间件模式(前后端一致)
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
后端: app.use(cors())
|
|
102
|
+
app.use(postgres())
|
|
103
|
+
app.get('/users', (req, ctx) => { ctx.sql`SELECT *` })
|
|
104
|
+
// ctx 已注入 ctx.sql
|
|
105
|
+
|
|
106
|
+
前端: createApp()
|
|
107
|
+
.use(api({ baseURL: '/api' }))
|
|
108
|
+
.use(auth())
|
|
109
|
+
.mount('#root', App)
|
|
110
|
+
// ctx 已注入 ctx.api, ctx.auth
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 状态管理
|
|
114
|
+
|
|
115
|
+
| 模式 | 后端 | 前端 |
|
|
116
|
+
|------|------|------|
|
|
117
|
+
| 注入 | 中间件注入 ctx.field | 中间件注入 ctx.field |
|
|
118
|
+
| 读取 | handler 读取 ctx | 组件读取 ctx |
|
|
119
|
+
| 渲染 | 返回 Response | `ctx.ui.render()` / `ctx.ui.dirty()` / `$.x = val` 触发局部 VDOM patch |
|
|
120
|
+
|
|
121
|
+
### Closeable 接口
|
|
122
|
+
|
|
123
|
+
所有有状态模块(postgres、redis)实现 `close(): Promise<void>`,serve 关闭时自动调用。
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
81
127
|
# 后端 API (`weifuwu`)
|
|
82
128
|
|
|
129
|
+
> 以下为完整 API 参考,按需查阅。新手建议先阅读上文的「核心概念」和「快速开始」。
|
|
130
|
+
|
|
83
131
|
## Router
|
|
84
132
|
|
|
85
133
|
Trie 路由,支持 URL 参数、通配符、中间件链、WebSocket、GraphQL。
|
|
@@ -417,7 +465,7 @@ app.use(ui())
|
|
|
417
465
|
| `ctx.ui.html` | `` (strings, ...values) => Response `` | HTML 模板 (转义防 XSS) |
|
|
418
466
|
| `ctx.ui.html.unsafe(str)` | `(string) => string` | 插入原始 HTML |
|
|
419
467
|
| `ctx.ui.js(entryPath)` | `(string) => Promise<Response>` | esbuild 编译 TSX → JS bundle |
|
|
420
|
-
| `ctx.ui.css(entryPath)` | `(string) => Promise<Response>` |
|
|
468
|
+
| `ctx.ui.css(entryPath)` | `(string) => Promise<Response>` | 读取 CSS 文件 → CSS Response(如安装 postcss + @tailwindcss/postcss 则自动编译) |
|
|
421
469
|
|
|
422
470
|
### ctx.ui.html — HTML 模板
|
|
423
471
|
|
|
@@ -433,7 +481,8 @@ app.get('/page', (req, ctx) => ctx.ui.html`
|
|
|
433
481
|
### ctx.ui.js — 编译 TSX → JS
|
|
434
482
|
|
|
435
483
|
```ts
|
|
436
|
-
app.get('/app.js', (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
484
|
+
app.get('/app.js', (req, ctx) => ctx.ui.js('./src/main.tsx')) // 相对路径
|
|
485
|
+
app.get('/app.js', (req, ctx) => ctx.ui.js('weifuwu/client')) // 或包名
|
|
437
486
|
```
|
|
438
487
|
|
|
439
488
|
使用 esbuild 编译:
|
|
@@ -444,12 +493,14 @@ app.get('/app.js', (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
|
444
493
|
### ctx.ui.css — CSS 编译
|
|
445
494
|
|
|
446
495
|
```ts
|
|
447
|
-
app.get('/style.css', (req, ctx) => ctx.ui.css('./src/style.css'))
|
|
496
|
+
app.get('/style.css', (req, ctx) => ctx.ui.css('./src/style.css')) // 相对路径
|
|
497
|
+
app.get('/style.css', (req, ctx) => ctx.ui.css('weifuwu/components/style.css')) // 或包名
|
|
448
498
|
```
|
|
449
499
|
|
|
450
|
-
-
|
|
451
|
-
-
|
|
452
|
-
-
|
|
500
|
+
- 无编译工具时直接返回原始 CSS
|
|
501
|
+
- 检测到已安装 `postcss` + `@tailwindcss/postcss` 时自动编译 Tailwind CSS
|
|
502
|
+
- 支持包名(`weifuwu/layout/style.css`, `weifuwu/components/style.css`)或文件路径
|
|
503
|
+
- 带 mtime 缓存验证(开发时编辑文件后自动失效)
|
|
453
504
|
|
|
454
505
|
---
|
|
455
506
|
|
|
@@ -659,7 +710,9 @@ import type { GraphQLOptions, GraphQLHandler } from 'weifuwu'
|
|
|
659
710
|
|
|
660
711
|
# 前端 API (`weifuwu/client`)
|
|
661
712
|
|
|
662
|
-
|
|
713
|
+
> 以下为完整 API 参考,按需查阅。新手建议先阅读上文的「组件模型」和「状态管理」。
|
|
714
|
+
|
|
715
|
+
零外部 npm 运行时依赖。组件签名:`(initProps, ctx) => (props) => VNode`(两阶段模型,外层 mount 只一次,内层 render 每次变化时执行)。无状态组件可简写为 `() => () => VNode`。
|
|
663
716
|
|
|
664
717
|
构建配置(esbuild):
|
|
665
718
|
|
|
@@ -714,25 +767,25 @@ const Counter: Component = (_init, ctx) => {
|
|
|
714
767
|
// ── mount ──
|
|
715
768
|
let count = 0
|
|
716
769
|
|
|
717
|
-
ctx.ui.onmounted((el) => {
|
|
718
|
-
console.log('DOM 已创建', el)
|
|
719
|
-
})
|
|
720
|
-
|
|
721
770
|
// ── render ──
|
|
722
771
|
return (props) =>
|
|
723
772
|
h('button', { onClick: () => { count++; ctx.ui.render() } }, count)
|
|
724
773
|
}
|
|
774
|
+
|
|
775
|
+
// 无状态组件:只有 render
|
|
776
|
+
const Badge: Component = () =>
|
|
777
|
+
(props) => h('span', { class: `badge-${props.variant}` }, props.children)
|
|
725
778
|
```
|
|
726
779
|
|
|
727
780
|
| 规则 | 说明 |
|
|
728
781
|
|------|------|
|
|
729
782
|
| 组件签名 | `(initProps: P, ctx: WfuiContext) => (props: P) => VNode \| null` |
|
|
730
|
-
| mount 阶段 |
|
|
783
|
+
| mount 阶段 | 外层函数只执行一次,初始化状态 |
|
|
731
784
|
| render 阶段 | 内层函数每次 dirty/props 变化时执行,返回 VNode |
|
|
732
785
|
| 无 class | 无 `this`,无实例方法 |
|
|
733
786
|
| 无 hook | 无 `useState` / `useEffect` / `useMemo` |
|
|
734
787
|
| 状态 | 闭包变量 + `ctx.ui.render()` 手动触发,或 `ctx.ui.$()` 响应式容器 |
|
|
735
|
-
|
|
|
788
|
+
| ref 引用 | `ref={el => { if (el) init; else cleanup }}` 获取 DOM |
|
|
736
789
|
|
|
737
790
|
### JSX 工厂
|
|
738
791
|
|
|
@@ -757,7 +810,18 @@ h('div', { class: 'x' }, child1, child2)
|
|
|
757
810
|
|
|
758
811
|
## 状态管理
|
|
759
812
|
|
|
760
|
-
###
|
|
813
|
+
### Render 机制总览
|
|
814
|
+
|
|
815
|
+
| API | 触发时机 | 渲染方式 | 作用域 | 使用场景 |
|
|
816
|
+
|------|---------|---------|--------|---------|
|
|
817
|
+
| `$.x = val` | 赋值后自动 | 微任务批量(异步) | 当前组件 | **日常 UI 状态** — 表单输入、切换开关、异步数据加载等 |
|
|
818
|
+
| `ctx.ui.dirty()` | 主动调用 | 微任务批量(异步) | 当前/指定 | **绕过 Proxy 后手动标记** |
|
|
819
|
+
| `ctx.ui.render()` | 主动调用 | 立即同步 | 当前/指定 | **需要立即拿到最新 DOM** — DOM 测量、动画触发 |
|
|
820
|
+
| `ctx.ui.render(['id'])` | 主动调用 | 立即同步 | 指定组件 | **跨组件精准刷新** — 全局事件、Portal 远程控制 |
|
|
821
|
+
|
|
822
|
+
`render()` 和 `dirty()` 无参 = 当前组件,传参 = 指定组件列表。三套 API 同一 scope 机制。
|
|
823
|
+
|
|
824
|
+
### 闭包变量 + `ctx.ui.render()`(简单场景)
|
|
761
825
|
|
|
762
826
|
```tsx
|
|
763
827
|
const Counter: Component = (_init, ctx) => {
|
|
@@ -767,26 +831,132 @@ const Counter: Component = (_init, ctx) => {
|
|
|
767
831
|
}
|
|
768
832
|
```
|
|
769
833
|
|
|
770
|
-
|
|
834
|
+
适合状态极少的简单组件。每次修改后手动调用 `ctx.ui.render()` 同步刷新 DOM。
|
|
835
|
+
|
|
836
|
+
### `ctx.ui.$()` — 响应式 Proxy(推荐首选)
|
|
771
837
|
|
|
772
|
-
`ctx.ui.$()`
|
|
838
|
+
`ctx.ui.$()` 返回**深度 Proxy** 容器。任意层级赋值操作自动触发渲染(微任务批量合并):
|
|
773
839
|
|
|
774
840
|
```tsx
|
|
775
841
|
const FormPage: Component = (_init, ctx) => {
|
|
776
842
|
const $ = ctx.ui.$()
|
|
777
843
|
$.email = ''
|
|
844
|
+
$.loading = false
|
|
778
845
|
return (props) =>
|
|
779
|
-
h('input', {
|
|
846
|
+
h('input', {
|
|
847
|
+
value: $.email,
|
|
848
|
+
onInput: (e: any) => { $.email = e.target.value }
|
|
849
|
+
})
|
|
780
850
|
}
|
|
781
851
|
```
|
|
782
852
|
|
|
783
|
-
|
|
853
|
+
**深度 Proxy 拦截**:
|
|
854
|
+
- `$.x = val` → 自动排队重渲染
|
|
855
|
+
- `$.obj.a = 1` → 自动 dirty(嵌套对象递归包装)
|
|
856
|
+
- `$.arr.push(val)` / `$.arr[0].x = y` → 自动 dirty(数组变异 + 嵌套属性拦截)
|
|
857
|
+
- `delete $.x` → 自动 dirty
|
|
858
|
+
- 每个组件实例独立 Proxy,同名变量不冲突
|
|
784
859
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
860
|
+
**注意**:mount/render 中 `$.x = val` **不触发渲染**,仅事件/timer/Promise.then 中生效。这是有意设计——初始化和 mount 阶段设置状态不应触发额外渲染。
|
|
861
|
+
|
|
862
|
+
**何时用 `$`**:所有需要触发 UI 重新渲染的状态。90% 以上的场景用 `$` 就够。
|
|
863
|
+
|
|
864
|
+
**何时不用**:
|
|
865
|
+
- 不需要触发渲染的内部缓存(用闭包变量 `let`)
|
|
866
|
+
- 简单组件只有一两个状态变量(闭包变量 + `render()` 更轻量)
|
|
867
|
+
|
|
868
|
+
### `ctx.ui.dirty()` — 异步标记脏
|
|
869
|
+
|
|
870
|
+
异步版本,无参 = 当前组件,传参 = 指定组件列表。多次调用合并为一次微任务渲染。`$` 内部就是调 `dirty()`。
|
|
871
|
+
|
|
872
|
+
### `ctx.ui.render()` — 同步强制渲染
|
|
873
|
+
|
|
874
|
+
与 `dirty()` 的微任务批量不同,`render()` 是**同步执行**的。调用后立即执行 VDOM diff + patch,DOM 立刻更新。无参时只刷新当前组件,传参时可精准刷新指定组件。
|
|
875
|
+
|
|
876
|
+
**何时必须用 `render()`**:
|
|
877
|
+
|
|
878
|
+
```tsx
|
|
879
|
+
// 1. DOM 测量(读取 offsetHeight/scrollWidth 等)
|
|
880
|
+
// 用 ref 在 DOM 创建后操作
|
|
881
|
+
ref: (el) => {
|
|
882
|
+
if (!el) return
|
|
883
|
+
el.style.height = 'auto'
|
|
884
|
+
ctx.ui.render()
|
|
885
|
+
const h = el.offsetHeight
|
|
886
|
+
el.style.height = h + 'px'
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// 2. 动画触发(需要确保上一帧 DOM 已提交)
|
|
890
|
+
function startAnimation() {
|
|
891
|
+
$.animating = true
|
|
892
|
+
ctx.ui.render() // 同步刷新 DOM
|
|
893
|
+
el.startViewTransition(...) // 拿到最新 DOM 启动动画
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// 3. 第三方库需要在事件回调中读取最新 DOM
|
|
897
|
+
onClick: () => {
|
|
898
|
+
$.selected = !$.selected
|
|
899
|
+
ctx.ui.render() // 确保 DOM 已更新
|
|
900
|
+
thirdPartyLib.measure(el) // 读取最新状态
|
|
901
|
+
}
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
**规则**:能用 `$` 就用 `$`。只有当你**必须同步拿到最新 DOM 状态**时才用 `render()`。
|
|
905
|
+
|
|
906
|
+
### 三种方式速查
|
|
907
|
+
|
|
908
|
+
```tsx
|
|
909
|
+
// 自动:$.x = val — 微任务批量,绑定当前组件
|
|
910
|
+
const $ = ctx.ui.$()
|
|
911
|
+
$.count++
|
|
912
|
+
$.name = 'hello' // 多次赋值合并为一次渲染
|
|
913
|
+
|
|
914
|
+
// 手动:ctx.ui.render() — 同步,无参=当前,传参=指定
|
|
915
|
+
let count = 0
|
|
916
|
+
count++
|
|
917
|
+
ctx.ui.render() // DOM 立刻更新
|
|
918
|
+
ctx.ui.render(['stats']) // 精准刷新指定组件
|
|
919
|
+
|
|
920
|
+
// 异步:ctx.ui.dirty() — 微任务批量,同 render() 作用域
|
|
921
|
+
ctx.ui.dirty()
|
|
922
|
+
ctx.ui.dirty(['stats']) // 批处理合并
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
**性能说明**:
|
|
926
|
+
- `$.x = val` 和 `dirty()` 都是微任务批量合并
|
|
927
|
+
- `render()` 每次调用都触发一次完整 diff/patch
|
|
928
|
+
- 三个入口同一套 scope 机制,不想要的渲染不触发
|
|
929
|
+
|
|
930
|
+
### 实践建议
|
|
931
|
+
|
|
932
|
+
**组件库**(可分享组件)推荐手动模式:
|
|
933
|
+
|
|
934
|
+
```tsx
|
|
935
|
+
const DatePicker = (_init, ctx) => {
|
|
936
|
+
let show = false // let 不触发渲染
|
|
937
|
+
return (props) =>
|
|
938
|
+
h('input', {
|
|
939
|
+
onClick: () => { show = true; ctx.ui.render() }
|
|
940
|
+
})
|
|
941
|
+
}
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
行为只由 `render()` 显式控制,不依赖 `$`,测试中 `render()` 直接 mock 为空函数。
|
|
945
|
+
|
|
946
|
+
**业务层**推荐自动模式:
|
|
947
|
+
|
|
948
|
+
```tsx
|
|
949
|
+
const OrderPage = (_init, ctx) => {
|
|
950
|
+
const $ = ctx.ui.$
|
|
951
|
+
$.orders = [] // $ 赋值自动触发渲染
|
|
952
|
+
$.loading = false
|
|
953
|
+
return (props) => h('div', {}, $.loading ? h(Spinner) : h(OrderList, { orders: $.orders }))
|
|
954
|
+
}
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
省事、安全、`$` 绑定所属组件不波及兄弟。
|
|
958
|
+
|
|
959
|
+
同一个组件内可以按变量混用两种模式:需要渲染的用 `$`,不需要的用 `let`。
|
|
790
960
|
|
|
791
961
|
---
|
|
792
962
|
|
|
@@ -807,43 +977,41 @@ const FormPage: Component = (_init, ctx) => {
|
|
|
807
977
|
|
|
808
978
|
---
|
|
809
979
|
|
|
810
|
-
##
|
|
811
|
-
|
|
812
|
-
框架不提供 `ref` prop。使用 `ctx.ui.onmount / onmounted / onunmount / onupdate`:
|
|
980
|
+
## ref 管理 DOM
|
|
813
981
|
|
|
814
|
-
|
|
815
|
-
|-----|---------|------|--------|
|
|
816
|
-
| `ctx.ui.onmount(fn)` | render 前(DOM 未创建) | `() => void` | — |
|
|
817
|
-
| `ctx.ui.onmounted(fn)` | 首次 render 后(DOM 已创建) | `(el: Element) => cleanup` | `() => void`(可选 cleanup)|
|
|
818
|
-
| `ctx.ui.onunmount(fn)` | 组件移除前 | `() => void` | — |
|
|
819
|
-
| `ctx.ui.onupdate(fn)` | props 变化时 | `(prevProps) => void` | — |
|
|
982
|
+
使用 `ref` prop 获取元素引用,适合管理第三方库或读取 DOM:
|
|
820
983
|
|
|
821
984
|
```tsx
|
|
822
985
|
const Timer: Component = (_init, ctx) => {
|
|
823
986
|
let timer: ReturnType<typeof setInterval> | undefined
|
|
824
987
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
988
|
+
return (props) =>
|
|
989
|
+
h('div', {
|
|
990
|
+
ref: (el) => {
|
|
991
|
+
if (el) {
|
|
992
|
+
timer = setInterval(() => console.log('tick'), 1000)
|
|
993
|
+
} else {
|
|
994
|
+
clearInterval(timer)
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
}, 'Timer')
|
|
831
998
|
}
|
|
832
999
|
```
|
|
833
1000
|
|
|
834
|
-
|
|
1001
|
+
`ref` 在元素创建时调用 `ref(el)`,元素移除时调用 `ref(null)`。
|
|
1002
|
+
`ref` 不接受返回值,清理逻辑直接在 `else` 分支处理。
|
|
835
1003
|
|
|
836
|
-
|
|
1004
|
+
对于**内嵌元素**(非根元素),直接在目标元素上放 `ref`:
|
|
837
1005
|
|
|
838
1006
|
```tsx
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1007
|
+
return h('div', {},
|
|
1008
|
+
h('input', {
|
|
1009
|
+
type: 'text',
|
|
1010
|
+
ref: (el) => el?.focus(),
|
|
1011
|
+
})
|
|
1012
|
+
)
|
|
843
1013
|
```
|
|
844
1014
|
|
|
845
|
-
所有钩子遵循替换模式:多次注册只保留最后一次。
|
|
846
|
-
|
|
847
1015
|
### 异步组件
|
|
848
1016
|
|
|
849
1017
|
在 mount 阶段发起请求,数据通过 `$.x = val` 自动触发渲染:
|
|
@@ -1109,7 +1277,7 @@ createApp()
|
|
|
1109
1277
|
|
|
1110
1278
|
// 运行时切换语言
|
|
1111
1279
|
ctx.i18n?.setLocale('en-US')
|
|
1112
|
-
// →
|
|
1280
|
+
// → 自动触发根组件重渲染(所有组件使用新语言文案)
|
|
1113
1281
|
```
|
|
1114
1282
|
|
|
1115
1283
|
| I18nOptions | 类型 | 默认值 | 说明 |
|
|
@@ -1280,9 +1448,131 @@ import type { RouterOptions } from 'weifuwu/client'
|
|
|
1280
1448
|
|
|
1281
1449
|
```ts
|
|
1282
1450
|
import { Button, Input, Table, Modal, Toast } from 'weifuwu/components'
|
|
1283
|
-
import 'weifuwu/components/style.css'
|
|
1451
|
+
import 'weifuwu/components/style.css' // 包含 Token + 35 布局原语 + 组件样式,一次性引入
|
|
1452
|
+
```
|
|
1453
|
+
|
|
1454
|
+
### 使用示例
|
|
1455
|
+
|
|
1456
|
+
```tsx
|
|
1457
|
+
// ├─ 按钮
|
|
1458
|
+
<Button variant="primary" onClick={() => alert('提交')}>提交</Button>
|
|
1459
|
+
<Button variant="ghost" loading>加载中</Button>
|
|
1460
|
+
<Button variant="danger" size="lg" block>删除</Button>
|
|
1461
|
+
|
|
1462
|
+
// ├─ 输入框
|
|
1463
|
+
<Input placeholder="请输入邮箱" />
|
|
1464
|
+
<Input label="用户名" required error="必填" />
|
|
1465
|
+
<Input type="password" hint="至少6位" prefix="🔒" />
|
|
1466
|
+
|
|
1467
|
+
// ├─ 选择器
|
|
1468
|
+
<Select options={[{ value: 'a', label: '选项A' }]} placeholder="请选择" />
|
|
1469
|
+
<Select searchable options={options} onChange={v => setVal(v)} />
|
|
1470
|
+
|
|
1471
|
+
// ├─ 复选框 / 开关 / 单选
|
|
1472
|
+
<Checkbox checked={agree} onChange={setAgree} label="同意协议" />
|
|
1473
|
+
<Switch checked={enabled} onChange={setEnabled} />
|
|
1474
|
+
<RadioGroup options={[{ value: '1', label: '男' }, { value: '2', label: '女' }]} value={gender} />
|
|
1475
|
+
|
|
1476
|
+
// ├─ 表格
|
|
1477
|
+
<Table columns={[{ key: 'id', label: 'ID', sortable: true }, { key: 'name', label: '名称' }]}
|
|
1478
|
+
data={rows} sortKey="id" sortOrder="asc" onSort={(k, o) => setSort(k, o)} />
|
|
1479
|
+
|
|
1480
|
+
// ├─ 模态框 / 抽屉
|
|
1481
|
+
<Modal open={show} title="提示" onClose={() => setShow(false)} width="500px" closable>
|
|
1482
|
+
<p>确认删除?</p>
|
|
1483
|
+
</Modal>
|
|
1484
|
+
<Drawer open={open} title="详情" onClose={() => setOpen(false)} position="right">内容</Drawer>
|
|
1485
|
+
|
|
1486
|
+
// ├─ 消息提示
|
|
1487
|
+
<Toast toasts={items} position="top-right" max={5} onRemove={id => remove(id)} />
|
|
1488
|
+
<Alert variant="warning" closable>注意:磁盘空间不足</Alert>
|
|
1489
|
+
|
|
1490
|
+
// ├─ 标签 / 徽标 / 头像
|
|
1491
|
+
<Badge count={5}>消息</Badge>
|
|
1492
|
+
<Badge variant="success">通过</Badge>
|
|
1493
|
+
<Tag variant="blue" closable onClose={() => {}}>标签</Tag>
|
|
1494
|
+
<Avatar name="张三" size="lg" />
|
|
1495
|
+
|
|
1496
|
+
// ├─ 卡片 / 统计卡片
|
|
1497
|
+
<Card title="卡片标题" extra={<a href="#">更多</a>}>卡片内容</Card>
|
|
1498
|
+
<StatCard title="总用户" value="1,234" trend={12.5} variant="primary" />
|
|
1499
|
+
|
|
1500
|
+
// ├─ 标签页 / 下拉菜单
|
|
1501
|
+
<Tabs items={[{ key: 'a', label: '标签A' }, { key: 'b', label: '标签B' }]} activeKey="a" onChange={setTab} />
|
|
1502
|
+
<Dropdown items={[{ label: '编辑', onClick: () => {} }, { label: '删除', danger: true }]}>操作</Dropdown>
|
|
1503
|
+
|
|
1504
|
+
// ├─ 分页 / 步骤条
|
|
1505
|
+
<Pagination total={100} page={1} pageSize={10} onChange={setPage} />
|
|
1506
|
+
<Steps items={[{ title: '第一步' }, { title: '第二步' }]} current={1} />
|
|
1507
|
+
|
|
1508
|
+
// ├─ 滑块 / 进度条
|
|
1509
|
+
<Slider min={0} max={100} value={50} onChange={setValue} />
|
|
1510
|
+
<ProgressBar value={75} variant="success" label="75%" />
|
|
1511
|
+
|
|
1512
|
+
// ├─ 面包屑 / 分割线
|
|
1513
|
+
<Breadcrumb items={[{ label: '首页' }, { label: '用户管理' }]} />
|
|
1514
|
+
<Divider />
|
|
1515
|
+
<Divider orientation="left">分割文字</Divider>
|
|
1516
|
+
|
|
1517
|
+
// ├─ 加载 / 空状态 / 骨架屏
|
|
1518
|
+
<Loading text="加载中..." />
|
|
1519
|
+
<EmptyState title="暂无数据" description="请先创建一条记录" action={<Button>新建</Button>} />
|
|
1520
|
+
<Skeleton variant="text" lines={3} />
|
|
1521
|
+
<Skeleton variant="table" lines={5} cols={4} />
|
|
1522
|
+
<Skeleton variant="avatar" />
|
|
1523
|
+
<Skeleton variant="image" />
|
|
1524
|
+
|
|
1525
|
+
// ├─ 表单验证
|
|
1526
|
+
<Form validation={{ email: [{ required: true, message: '请输入邮箱' }] }}
|
|
1527
|
+
onSubmit={values => api.post('/login', values)}
|
|
1528
|
+
onError={errors => setErrors(errors)}>
|
|
1529
|
+
<Field label="邮箱" error={errors.email}>
|
|
1530
|
+
<Input name="email" />
|
|
1531
|
+
</Field>
|
|
1532
|
+
<Button type="submit">登录</Button>
|
|
1533
|
+
</Form>
|
|
1534
|
+
```
|
|
1535
|
+
|
|
1536
|
+
> 所有组件引用 `--wf-*` CSS 变量做主题,详见下文的「样式定制指南」。
|
|
1537
|
+
|
|
1538
|
+
### 生命周期映射
|
|
1539
|
+
|
|
1540
|
+
组件没有生命周期函数。每个阶段对应到代码的明确位置:
|
|
1541
|
+
|
|
1542
|
+
```
|
|
1543
|
+
mount ──────────────────────────────────────────
|
|
1544
|
+
const Counter = (_init, ctx) => { ← mount(只一次)
|
|
1545
|
+
let count = 0 ← 初始化状态
|
|
1546
|
+
return (props) => { ← render 函数
|
|
1547
|
+
// ... ← 每次 dirty/props 变化执行
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
ref ────────────────────────────────────────────
|
|
1552
|
+
h('div', {
|
|
1553
|
+
ref: (el) => {
|
|
1554
|
+
if (el) { /* 元素已创建 */ } ← 相当于 onmounted
|
|
1555
|
+
else { /* 元素已移除 */ } ← 相当于 onunmount
|
|
1556
|
+
}
|
|
1557
|
+
})
|
|
1558
|
+
|
|
1559
|
+
props 变化 ─────────────────────────────────────
|
|
1560
|
+
return (props) => {
|
|
1561
|
+
// 每次 render 都收到最新 props ← 相当于 onupdate
|
|
1562
|
+
if (props.value !== prevValue) { ... }
|
|
1563
|
+
}
|
|
1284
1564
|
```
|
|
1285
1565
|
|
|
1566
|
+
| 旧概念 | 新写法 |
|
|
1567
|
+
|--------|--------|
|
|
1568
|
+
| `onmount` | mount 外层函数直接写 |
|
|
1569
|
+
| `onmounted` | `ref` 的 `if (el)` 分支 |
|
|
1570
|
+
| `onunmount` | `ref` 的 `else` 分支 |
|
|
1571
|
+
| `onupdate` | render 内层函数收新 props 自行比较 |
|
|
1572
|
+
| `全局刷新` | `ctx.ui.render(['_wf_root'])` |
|
|
1573
|
+
| `局部刷新` | `ctx.ui.render()` 或 `$.x = val` |
|
|
1574
|
+
| `跨组件刷新` | `ctx.ui.selfId('name')` + `render(['name'])` |
|
|
1575
|
+
|
|
1286
1576
|
## 组件列表
|
|
1287
1577
|
|
|
1288
1578
|
### 表单核心
|
|
@@ -1372,14 +1662,24 @@ import 'weifuwu/components/style.css'
|
|
|
1372
1662
|
|
|
1373
1663
|
纯 CSS 布局原语 + 72 个主题 Token。不绑定任何 JS 框架。
|
|
1374
1664
|
|
|
1665
|
+
> **全栈 weifuwu 项目**:`weifuwu/components/style.css` 已包含布局系统,一条 import 就够了,无需单独引用本页。
|
|
1666
|
+
> 本页仅适用于**非 weifuwu 项目**或**只需 CSS 布局**的场景。
|
|
1667
|
+
|
|
1375
1668
|
```html
|
|
1376
|
-
<link rel="stylesheet" href="/node_modules/weifuwu/
|
|
1669
|
+
<link rel="stylesheet" href="/node_modules/weifuwu/layout">
|
|
1377
1670
|
```
|
|
1378
1671
|
|
|
1379
|
-
|
|
1672
|
+
或在 weifuwu 服务端通过 `ctx.ui.css` 直接引用包名(`ctx.ui.css` 自动解析 exports map):
|
|
1380
1673
|
|
|
1381
1674
|
```ts
|
|
1382
|
-
|
|
1675
|
+
// 方案 A:组件 + 布局全部搞定(推荐)
|
|
1676
|
+
app.get('/style.css', (req, ctx) => ctx.ui.css('weifuwu/components/style.css'))
|
|
1677
|
+
|
|
1678
|
+
// 方案 B:只用布局
|
|
1679
|
+
app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
|
|
1680
|
+
```
|
|
1681
|
+
|
|
1682
|
+
也支持相对路径:`ctx.ui.css('./src/style.css')`。
|
|
1383
1683
|
```
|
|
1384
1684
|
|
|
1385
1685
|
## 35 个布局原语
|
|
@@ -1486,34 +1786,185 @@ document.documentElement.setAttribute('data-theme', 'dark')
|
|
|
1486
1786
|
|
|
1487
1787
|
---
|
|
1488
1788
|
|
|
1489
|
-
#
|
|
1789
|
+
# 样式定制指南
|
|
1490
1790
|
|
|
1491
|
-
##
|
|
1791
|
+
## 全局主题变量
|
|
1492
1792
|
|
|
1793
|
+
所有组件引用 `--wf-*` CSS 变量。在根元素覆盖即可定制主题:
|
|
1794
|
+
|
|
1795
|
+
```css
|
|
1796
|
+
:root {
|
|
1797
|
+
--wf-color-primary: #6366f1;
|
|
1798
|
+
--wf-color-primary-hover: #4f46e5;
|
|
1799
|
+
--wf-radius: 8px;
|
|
1800
|
+
--wf-font-sans: 'Inter', system-ui, sans-serif;
|
|
1801
|
+
}
|
|
1493
1802
|
```
|
|
1494
|
-
后端: app.use(cors())
|
|
1495
|
-
app.use(postgres())
|
|
1496
|
-
app.get('/users', (req, ctx) => { ctx.sql`SELECT *` })
|
|
1497
|
-
// ctx 已注入 ctx.sql
|
|
1498
1803
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
// ctx 已注入 ctx.api, ctx.auth
|
|
1804
|
+
## 暗色模式
|
|
1805
|
+
|
|
1806
|
+
```ts
|
|
1807
|
+
document.documentElement.setAttribute('data-theme', 'dark')
|
|
1504
1808
|
```
|
|
1505
1809
|
|
|
1506
|
-
|
|
1810
|
+
所有 `--wf-*` 变量在 `[data-theme="dark"]` 下自动切换。可自定义暗色变量:
|
|
1507
1811
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1812
|
+
```css
|
|
1813
|
+
[data-theme="dark"] {
|
|
1814
|
+
--wf-color-bg: #1a1a2e;
|
|
1815
|
+
--wf-color-text: #e0e0e0;
|
|
1816
|
+
--wf-color-border: #2a2a4a;
|
|
1817
|
+
}
|
|
1818
|
+
```
|
|
1513
1819
|
|
|
1514
|
-
##
|
|
1820
|
+
## 组件级覆盖
|
|
1515
1821
|
|
|
1516
|
-
|
|
1822
|
+
```css
|
|
1823
|
+
/* 覆盖 Button 主色 */
|
|
1824
|
+
.wf-btn--primary {
|
|
1825
|
+
background: #06b6d4;
|
|
1826
|
+
border-color: #06b6d4;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/* 覆盖 Modal 圆角 */
|
|
1830
|
+
.wf-modal-content {
|
|
1831
|
+
border-radius: 16px;
|
|
1832
|
+
}
|
|
1833
|
+
```
|
|
1834
|
+
|
|
1835
|
+
## 作用域主题
|
|
1836
|
+
|
|
1837
|
+
```html
|
|
1838
|
+
<div style="--wf-color-primary: #f59e0b;">
|
|
1839
|
+
<!-- 此区域内组件使用金色主题,外部不受影响 -->
|
|
1840
|
+
<button class="wf-btn wf-btn--primary">金色按钮</button>
|
|
1841
|
+
</div>
|
|
1842
|
+
```
|
|
1843
|
+
|
|
1844
|
+
CSS 变量会沿 DOM 树继承,利用这一点可实现多主题共存。
|
|
1845
|
+
|
|
1846
|
+
---
|
|
1847
|
+
|
|
1848
|
+
# 组合场景示例
|
|
1849
|
+
|
|
1850
|
+
## 登录表单
|
|
1851
|
+
|
|
1852
|
+
```tsx
|
|
1853
|
+
const LoginPage = (_init, ctx) => {
|
|
1854
|
+
const $ = ctx.ui.$()
|
|
1855
|
+
$.errors = {}
|
|
1856
|
+
$.submitting = false
|
|
1857
|
+
|
|
1858
|
+
return (props) =>
|
|
1859
|
+
h('div', { class: 'wf-stack', style: { maxWidth: 400, margin: '40px auto' } },
|
|
1860
|
+
h(Card, { shadow: 'md' },
|
|
1861
|
+
h('div', { class: 'wf-stack', style: { gap: 'var(--wf-space-md)' } },
|
|
1862
|
+
h('h2', {}, '登录'),
|
|
1863
|
+
h(Form, {
|
|
1864
|
+
validation: {
|
|
1865
|
+
email: [{ required: true, pattern: /@/, message: '请输入有效邮箱' }],
|
|
1866
|
+
password: [{ required: true, minLength: 6, message: '密码至少6位' }],
|
|
1867
|
+
},
|
|
1868
|
+
onSubmit: async (values) => {
|
|
1869
|
+
$.submitting = true
|
|
1870
|
+
await api.post('/login', values)
|
|
1871
|
+
$.submitting = false
|
|
1872
|
+
},
|
|
1873
|
+
onError: (errors) => { $.errors = errors },
|
|
1874
|
+
}, [
|
|
1875
|
+
h(Field, { label: '邮箱', error: $.errors.email },
|
|
1876
|
+
h(Input, { name: 'email', type: 'email', placeholder: 'name@example.com' })),
|
|
1877
|
+
h(Field, { label: '密码', error: $.errors.password },
|
|
1878
|
+
h(Input, { name: 'password', type: 'password' })),
|
|
1879
|
+
h(Button, { type: 'submit', loading: $.submitting, block: true }, '登录'),
|
|
1880
|
+
])
|
|
1881
|
+
)
|
|
1882
|
+
)
|
|
1883
|
+
)
|
|
1884
|
+
}
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
## 数据列表 + 搜索
|
|
1888
|
+
|
|
1889
|
+
```tsx
|
|
1890
|
+
const UserList = (_init, ctx) => {
|
|
1891
|
+
const $ = ctx.ui.$()
|
|
1892
|
+
$.keyword = ''
|
|
1893
|
+
$.sortKey = 'name'
|
|
1894
|
+
$.sortOrder = 'asc'
|
|
1895
|
+
const users = [
|
|
1896
|
+
{ id: 1, name: '张三', email: 'zhang@example.com', role: '管理员' },
|
|
1897
|
+
{ id: 2, name: '李四', email: 'li@example.com', role: '编辑' },
|
|
1898
|
+
]
|
|
1899
|
+
|
|
1900
|
+
const filtered = users.filter(u =>
|
|
1901
|
+
!$.keyword || u.name.includes($.keyword) || u.email.includes($.keyword)
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
return (props) =>
|
|
1905
|
+
h('div', { class: 'wf-stack', style: { gap: 'var(--wf-space-md)' } },
|
|
1906
|
+
h('div', { class: 'wf-row', style: { justifyContent: 'space-between', alignItems: 'center' } },
|
|
1907
|
+
h(SearchInput, { placeholder: '搜索用户...', value: $.keyword, onSearch: (v: string) => { $.keyword = v } }),
|
|
1908
|
+
h(Button, { variant: 'primary' }, '新建用户'),
|
|
1909
|
+
),
|
|
1910
|
+
h(Table, {
|
|
1911
|
+
columns: [
|
|
1912
|
+
{ key: 'id', label: 'ID', width: 60 },
|
|
1913
|
+
{ key: 'name', label: '姓名', sortable: true },
|
|
1914
|
+
{ key: 'email', label: '邮箱', sortable: true },
|
|
1915
|
+
{ key: 'role', label: '角色' },
|
|
1916
|
+
],
|
|
1917
|
+
data: filtered,
|
|
1918
|
+
sortKey: $.sortKey,
|
|
1919
|
+
sortOrder: $.sortOrder,
|
|
1920
|
+
onSort: (key, order) => { $.sortKey = key; $.sortOrder = order },
|
|
1921
|
+
emptyText: '无匹配用户',
|
|
1922
|
+
}),
|
|
1923
|
+
h(Pagination, { total: filtered.length, page: 1, pageSize: 10, onChange: (p: number) => {} }),
|
|
1924
|
+
)
|
|
1925
|
+
}
|
|
1926
|
+
```
|
|
1927
|
+
|
|
1928
|
+
## 消息提示
|
|
1929
|
+
|
|
1930
|
+
```tsx
|
|
1931
|
+
// 在任意组件中调用
|
|
1932
|
+
let toastId = 0
|
|
1933
|
+
|
|
1934
|
+
function showToast(ctx: WfuiContext, type: ToastType, message: string) {
|
|
1935
|
+
// 通过 ctx 管理 Toast 列表
|
|
1936
|
+
const $ = ctx.ui.$()
|
|
1937
|
+
$.toasts = $.toasts ?? []
|
|
1938
|
+
const id = String(++toastId)
|
|
1939
|
+
$.toasts = [...$.toasts, { id, type, message }]
|
|
1940
|
+
|
|
1941
|
+
// 自动消失
|
|
1942
|
+
if (type !== 'error') {
|
|
1943
|
+
setTimeout(() => {
|
|
1944
|
+
$.toasts = $.toasts.filter((t: any) => t.id !== id)
|
|
1945
|
+
}, 3000)
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
// 页面中使用
|
|
1950
|
+
const App = (_init, ctx) => {
|
|
1951
|
+
const $ = ctx.ui.$()
|
|
1952
|
+
$.toasts = []
|
|
1953
|
+
|
|
1954
|
+
return (props) =>
|
|
1955
|
+
h('div', {}, [
|
|
1956
|
+
h(Button, {
|
|
1957
|
+
onClick: () => showToast(ctx, 'success', '操作成功'),
|
|
1958
|
+
}, '显示提示'),
|
|
1959
|
+
h(Toast, {
|
|
1960
|
+
toasts: $.toasts,
|
|
1961
|
+
position: 'top-right',
|
|
1962
|
+
max: 3,
|
|
1963
|
+
onRemove: (id) => { $.toasts = $.toasts.filter((t: any) => t.id !== id) },
|
|
1964
|
+
}),
|
|
1965
|
+
])
|
|
1966
|
+
}
|
|
1967
|
+
```
|
|
1517
1968
|
|
|
1518
1969
|
---
|
|
1519
1970
|
|