weifuwu 0.75.0 → 0.77.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 +48 -53
- package/dist/ai/types.d.ts +2 -0
- package/dist/components/AiChat/AiChat.d.ts +5 -1
- package/dist/components/ApprovalCard/ApprovalCard.d.ts +5 -2
- package/dist/components/CitationCard/CitationCard.d.ts +38 -0
- package/dist/components/Command/Command.d.ts +1 -1
- package/dist/components/DatePicker/DatePicker.d.ts +1 -1
- package/dist/components/JsonSchemaForm/JsonSchemaForm.d.ts +48 -0
- package/dist/components/ReasoningBlock/ReasoningBlock.d.ts +25 -0
- package/dist/components/Select/Select.d.ts +11 -1
- package/dist/components/SessionList/SessionList.d.ts +42 -0
- package/dist/components/Tour/Tour.d.ts +1 -1
- package/dist/components/TreeSelect/TreeSelect.d.ts +3 -2
- package/dist/components/index.d.ts +10 -2
- package/dist/components/index.js +14 -14
- package/dist/components/style.css +625 -20
- package/dist/index.js +1349 -1529
- package/dist/layout/weifuwu-layout.css +9 -0
- package/dist/scheduler/index.d.ts +7 -2
- package/dist/test/client/setup.d.ts +12 -0
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui-dom/Confirm.d.ts +5 -41
- package/dist/ui-dom/Notification.d.ts +8 -70
- package/dist/ui-dom/Toast.d.ts +8 -43
- package/dist/ui-dom/hooks/chat.d.ts +11 -0
- package/dist/ui-dom/hooks/events.d.ts +30 -0
- package/dist/ui-dom/hooks/external.d.ts +20 -0
- package/dist/ui-dom/hooks/index.d.ts +30 -0
- package/dist/ui-dom/hooks/input.d.ts +33 -0
- package/dist/ui-dom/hooks/media.d.ts +17 -0
- package/dist/ui-dom/hooks/popup.d.ts +28 -0
- package/dist/ui-dom/hooks/stable.d.ts +39 -0
- package/dist/ui-dom/hooks/test/hooks.test.d.ts +1 -0
- package/dist/ui-dom/hooks/types.d.ts +77 -0
- package/dist/ui-dom/index.d.ts +19 -17
- package/dist/ui-dom/index.js +11 -11
- package/dist/ui-dom/jsx-runtime.js +1 -1
- package/dist/ui-dom/store.d.ts +24 -0
- package/dist/ui-dom/testing.d.ts +74 -0
- package/dist/ui-dom/testing.js +1 -0
- package/dist/ui-dom/testing.test.d.ts +1 -0
- package/dist/ui-dom/types.d.ts +55 -50
- package/dist/ui-dom/use-chat.d.ts +19 -14
- package/dist/ui-dom/vdom/build.d.ts +41 -0
- package/dist/ui-dom/vdom/diff.d.ts +33 -0
- package/dist/ui-dom/vdom/hydration.d.ts +17 -0
- package/dist/ui-dom/vdom/index.d.ts +23 -0
- package/dist/ui-dom/vdom/mount.d.ts +58 -0
- package/dist/ui-dom/vdom/registry.d.ts +27 -0
- package/dist/ui-dom/vdom/render.d.ts +15 -0
- package/dist/ui-dom/vdom/serve.d.ts +30 -0
- package/dist/ui-dom/vdom/ssr.d.ts +44 -0
- package/dist/ui-dom/vnode.d.ts +19 -31
- package/docs/components-map.md +18 -1
- package/docs/components.md +12 -7
- package/docs/custom-components.md +90 -60
- package/docs/examples.md +35 -41
- package/docs/frontend-middleware.md +3 -4
- package/docs/frontend-ui-dom.md +21 -18
- package/docs/frontend.md +243 -168
- package/docs/layout.md +2 -2
- package/docs/mobile.md +2 -2
- package/docs/realtime.md +8 -3
- package/package.json +5 -1
- package/dist/ui/ssr.d.ts +0 -35
- package/dist/ui-dom/diff.d.ts +0 -28
- package/dist/ui-dom/focus-trap.d.ts +0 -4
- package/dist/ui-dom/hydration.d.ts +0 -13
- package/dist/ui-dom/reactive.d.ts +0 -6
- package/dist/ui-dom/registry.d.ts +0 -27
- package/dist/ui-dom/render.d.ts +0 -46
- package/dist/ui-dom/scroll-lock.d.ts +0 -5
- package/dist/ui-dom/serve.d.ts +0 -28
- package/dist/ui-dom/ssr.d.ts +0 -57
- package/dist/ui-dom/ui.d.ts +0 -78
package/dist/ui-dom/vnode.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* --jsxImportSource=weifuwu/ui-dom
|
|
8
8
|
*/
|
|
9
9
|
import type { WfuiContext } from './types.ts';
|
|
10
|
-
export type VNodeType = string | Component<any, any> |
|
|
10
|
+
export type VNodeType = string | Component<any, any> | typeof Fragment | typeof Portal;
|
|
11
11
|
/**
|
|
12
12
|
* VNode 子节点合法值——组件可返回/渲染的多态内容。
|
|
13
13
|
* 递归联合:string/number/VNode/array/null/boolean 任意组合。
|
|
@@ -24,55 +24,40 @@ export interface VNode {
|
|
|
24
24
|
_remoteEl?: HTMLElement | undefined;
|
|
25
25
|
/** VNode 的 DOM 归属:'local' 在父 DOM 树下,'remote' 在别处 */
|
|
26
26
|
_placement?: 'local' | 'remote';
|
|
27
|
-
/** 两阶段组件的 render 函数(mount
|
|
28
|
-
_render?: (props: Record<string, unknown>) => VNode | null
|
|
27
|
+
/** 两阶段组件的 render 函数(mount 返回的函数——强制异步:props 变化时可 await 数据) */
|
|
28
|
+
_render?: (props: Record<string, unknown>) => Promise<VNode | null>;
|
|
29
29
|
/** 组件实例 ID(如 '_wf_0') */
|
|
30
30
|
_id?: string;
|
|
31
31
|
/** 自定义组件 ID(ctx.ui.selfId() 注册,跨组件精准刷新) */
|
|
32
32
|
_customId?: string;
|
|
33
33
|
/** 组件输出的 DOM 父节点 */
|
|
34
34
|
_parentNode?: Node;
|
|
35
|
+
/** 父 vnode 引用(动态挂载补全向上找持有组件) */
|
|
36
|
+
_parentVNode?: VNode;
|
|
35
37
|
/** 组件输出的第一个 DOM 节点 */
|
|
36
38
|
_refNode?: Node | null;
|
|
37
|
-
/** 原生 async 组件缓存(vnode 级按实例):in-flight Promise → resolved renderFn(diff 传递继承) */
|
|
38
|
-
_asyncDef?: ((props: Record<string, unknown>) => VNode | null) | Promise<((props: Record<string, unknown>) => VNode | null)> | null;
|
|
39
39
|
/** Fragment 展开后的多个直属 DOM 节点范围(diff 对齐用,见 diff.ts) */
|
|
40
40
|
_childNodes?: Node[];
|
|
41
|
-
/** 组件
|
|
41
|
+
/** 组件 renderFn 上次执行时的 ctx 版本号(buildVNode 剪枝 + diff 三态 skip 的版本比较——
|
|
42
|
+
* bumpCtxVersion 递增后版本不同 → 强制重跑 renderFn,如 i18n 切换语言) */
|
|
42
43
|
_ctxVersion?: number;
|
|
43
44
|
}
|
|
44
45
|
/**
|
|
45
|
-
*
|
|
46
|
+
* 两阶段异步组件(weifuwu 唯一组件形态):
|
|
47
|
+
* async (initProps, ctx) => Promise<renderFn>
|
|
48
|
+
* 外层 = mount(一次,可 await 数据),内层 = renderFn(每次 dirty/props 变化——**强制异步**,
|
|
49
|
+
* 可 await 数据;统一异步心智:两阶段都可 await,无「同步组件 vs 异步组件」二元形态)。
|
|
46
50
|
* P = props 类型(JSX 自动推断),C = 组件依赖的 ctx 注入(如 ApiInjected & RouteInjected)
|
|
47
|
-
*/
|
|
48
|
-
export type Component<P = {}, C extends object = {}> = (initProps: P, ctx: WfuiContext & C) => ((props: P) => VNode | null) | null;
|
|
49
|
-
/**
|
|
50
|
-
* 异步组件(统一签名——与 Component 同参,唯一差别是 async):
|
|
51
|
-
* async (initProps, ctx) => Promise<renderFn | null>
|
|
52
51
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
52
|
+
* renderFn 签名:async (props) => Promise<VNode | null>——同步 renderFn 是类型错误
|
|
53
|
+
* (diff 永不执行 renderFn——渲染器在 buildVNode 阶段 await,同步上下文拿不到 vnode)。
|
|
54
|
+
* 渲染器按「返回值是 Promise」判别(主路径 buildVNode await 全部工厂 + renderFn)。
|
|
56
55
|
*/
|
|
57
|
-
export type
|
|
56
|
+
export type RenderFn<P> = (props: P) => Promise<VNode | null>;
|
|
57
|
+
export type Component<P = {}, C extends object = {}> = (initProps: P, ctx: WfuiContext & C) => Promise<RenderFn<P> | null>;
|
|
58
58
|
export declare const Fragment: unique symbol;
|
|
59
59
|
/** Portal — 将子 VNode 渲染到 document.body 下的独立容器 */
|
|
60
60
|
export declare const Portal: unique symbol;
|
|
61
|
-
/**
|
|
62
|
-
* 占位显示策略组件(原生 async 组件未 resolve 时的占位 vnode)。
|
|
63
|
-
* 渲染时查 ctx 原型链上最近的 Suspense 边界:有 → fallback;无 → null(向后兼容)。
|
|
64
|
-
*/
|
|
65
|
-
export declare const Placeholder: Component;
|
|
66
|
-
/**
|
|
67
|
-
* Suspense 边界(可选):子树内 async 组件占位时显示 fallback。
|
|
68
|
-
* 渲染器对 type === Suspense 的组件在 childCtx 挂 _suspense(Object.create 继承 → 子树可见)。
|
|
69
|
-
* 语义:占位处局部显示 fallback(非 React 整树回滚);无边界时占位为 null。
|
|
70
|
-
*
|
|
71
|
-
* ```tsx
|
|
72
|
-
* h(Suspense, { fallback: h(Spinner) }, h(UserProfile, {}))
|
|
73
|
-
* ```
|
|
74
|
-
*/
|
|
75
|
-
export declare const Suspense: Component;
|
|
76
61
|
/** JSX 类型声明 — 使 TypeScript 理解自定义 JSX 运行时 */
|
|
77
62
|
export declare function jsx(type: VNodeType, props: Record<string, any> | null, key?: string | null): VNode;
|
|
78
63
|
export declare const jsxs: typeof jsx;
|
|
@@ -80,6 +65,9 @@ export declare function jsxDEV(type: VNodeType, props: Record<string, any> | nul
|
|
|
80
65
|
/** `h`(hyperscript)支持 variadic children: `h('div', {class:'x'}, child1, child2)` */
|
|
81
66
|
export declare function h(type: VNodeType, props: Record<string, any> | null, ...children: VNodeChild[]): VNode;
|
|
82
67
|
export declare function isNative(vnode: VNode): boolean;
|
|
68
|
+
/** 递归文本/数组归一化(children 数组展开——嵌套数组扁平化,DOM 范围对齐)。
|
|
69
|
+
* 栈展开(索引遍历替代 shift/unshift 头部操作——长数组 O(n) 而非 O(n²));逆序入栈 + pop 保持原顺序 */
|
|
70
|
+
export declare function normalizeChildren(c: VNodeChild | undefined | null): VNodeChild[];
|
|
83
71
|
export declare function isComponent(vnode: VNode): boolean;
|
|
84
72
|
export declare function isFragment(vnode: VNode): boolean;
|
|
85
73
|
export declare function isPortal(vnode: VNode): boolean;
|
package/docs/components-map.md
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|------|---------|------|-------------|-----------|---------|
|
|
39
39
|
| 输入框 | `<Input label error hint variant>` | Input | Input | Input | weifuwu 自带 label/error/hint 包装(antd 需 Form.Item);`variant="borderless"` 内联编辑 |
|
|
40
40
|
| 文本域 | `<Textarea rows showCount maxLength>` | Input.TextArea | Input textarea | Textarea | weifuwu 独立组件 + 字数统计 |
|
|
41
|
-
| 选择器 | `<Select options searchable multiple>` | Select | Select | Select / Combobox | **searchable + 键盘 ↑↓ + multiple
|
|
41
|
+
| 选择器 | `<Select options searchable multiple>` | Select | Select | Select / Combobox | **searchable + 键盘 ↑↓ + multiple + optgroup 分组已实现**(近 Combobox) |
|
|
42
42
|
| 复选框 | `<Checkbox checked label>` | Checkbox | Checkbox | Checkbox | 等价 |
|
|
43
43
|
| 复选组 | `<CheckboxGroup options value>` | Checkbox.Group | Checkbox 组 | — | 数组受控 + 栅格列数 |
|
|
44
44
|
| 单选 | `<RadioGroup options value>` | Radio.Group | Radio | RadioGroup | 等价 |
|
|
@@ -115,6 +115,10 @@
|
|
|
115
115
|
| `<CodeBlock code lang>` | 代码块 + 语言标签 + 复制按钮 |
|
|
116
116
|
| `<MessageBubble role status>` | 聊天气泡(独立复用,AiChat 抽取) |
|
|
117
117
|
| `<ToolCallCard>` | AI 工具调用状态机(running/ok/error) |
|
|
118
|
+
| `<JsonSchemaForm schema value onSubmit>` | JSON Schema → 参数输入表单:类型映射 + 必填/范围校验 + 嵌套/数组(工具参数输入面;不支持项告警降级) |
|
|
119
|
+
| `<ReasoningBlock content>` | CoT 推理折叠展示(thinking 模式 reasoning_content;流式脉冲) |
|
|
120
|
+
| `<CitationCard items>` | RAG 引用来源列表:折叠 + 序号/标题/来源/片段/链接 + 溢出 +N |
|
|
121
|
+
| `<SessionList sessions>` | 会话管理:分组(今天/昨天/更早)+ 搜索 + 重命名/删除/新建 + 键盘导航 |
|
|
118
122
|
| `<ApprovalCard>` | HITL 人工审批卡片(pending/approved/rejected/timeout) |
|
|
119
123
|
| `<Editor>` | 富文本编辑器(contentEditable + 工具栏/表格/图片,零依赖) |
|
|
120
124
|
| `<ThemeSwitch>` | 明暗主题切换(CSS 变量驱动,配 `data-theme`) |
|
|
@@ -122,6 +126,19 @@
|
|
|
122
126
|
| `<AspectRatio>` | 宽高比容器(内容填满) |
|
|
123
127
|
| `<CopyButton>` | 复制(clipboard + execCommand 降级 + 成功状态机) |
|
|
124
128
|
|
|
129
|
+
## 第九批:AI 差异化(109 → 111 组件,2026-08)
|
|
130
|
+
|
|
131
|
+
> 三库业务组件覆盖 ~100% 后的差异化期——补 AI **输入层 + 推理展示层**。
|
|
132
|
+
> 完整设计/裁剪见 `design/ai-differentiation-plan.md`。
|
|
133
|
+
|
|
134
|
+
| 组件 | 类型 | 三库等价 | 要点 |
|
|
135
|
+
|------|------|---------|------|
|
|
136
|
+
| **JsonSchemaForm** | 新增 | @rjsf(依赖重,weifuwu 零依赖子集) | JSON Schema(对象子集)→ 参数输入表单:string/number/integer/boolean/enum/object/array 类型映射 + required/范围校验 + 嵌套/数组增删;不支持项($ref/组合 schema)告警降级 |
|
|
137
|
+
| **ReasoningBlock** | 新增 | 三库无 | CoT 推理折叠展示:aria-expanded + 键盘可达 + 流式脉冲 |
|
|
138
|
+
| **AiChat 集成** | 增强 | — | `WfDone.reasoning`(additive,收尾一次性下发)+ use-chat 聚合 + toChatMessages 回传 `reasoning_content`(thinking 模式闭环) |
|
|
139
|
+
|
|
140
|
+
> 协议变更:`wf:done` 新增可选 `reasoning` 字段(向后兼容),见 `design/ai-contract.md` §3.4。
|
|
141
|
+
|
|
125
142
|
## 快速迁移路径(三库 → weifuwu)
|
|
126
143
|
|
|
127
144
|
```tsx
|
package/docs/components.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
> 本页为 weifuwu 官方文档拆分页 · [返回 README](../README.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
113 个 HTML 原语组件。每个是 `async (initProps, ctx) => (props) => Promise<VNode>`(两阶段组件,与前端框架同一模型——外层工厂 + 内层 renderFn 强制异步),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
|
|
6
6
|
|
|
7
7
|
> **组件速查(weifuwu 组件 ↔ antd / Element Plus / shadcn-ui 对应 + 迁移示例)**:见 [`docs/components-map.md`](components-map.md)——从其他组件库迁来的开发者按功能直接找对应组件。
|
|
8
8
|
> **自定义组件开发**:见 [docs/custom-components.md](custom-components.md)——usePopup/useControlled/对话框/AI 组件/类型纪律逐步指南。
|
|
9
9
|
|
|
10
10
|
```ts
|
|
11
11
|
import { Button, Input, Table, Modal, Toast } from 'weifuwu/components'
|
|
12
|
-
import 'weifuwu/components/style.css' // 包含 Token +
|
|
12
|
+
import 'weifuwu/components/style.css' // 包含 Token + 58 布局原语 + 136 工具类 + 组件样式,一次性引入
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
### 使用示例
|
|
@@ -26,9 +26,10 @@ import 'weifuwu/components/style.css' // 包含 Token + 57 布局原语 + 136
|
|
|
26
26
|
<Input type="password" hint="至少6位" />
|
|
27
27
|
<Input name="email" type="email" disabled placeholder="name@example.com" />
|
|
28
28
|
|
|
29
|
-
// ├─
|
|
29
|
+
// ├─ 选择器(options 支持平铺项与分组混用:{ label, options } → optgroup)
|
|
30
30
|
<Select options={[{ value: 'a', label: '选项A' }]} placeholder="请选择" />
|
|
31
31
|
<Select searchable options={options} onChange={v => setVal(v)} />
|
|
32
|
+
<Select options={[{ label: '一线', options: [{ value: 'bj', label: '北京' }] }, { value: 'other', label: '其他' }]} />
|
|
32
33
|
|
|
33
34
|
// ├─ 复选框 / 开关 / 单选
|
|
34
35
|
<Checkbox checked={agree} onChange={setAgree} label="同意协议" />
|
|
@@ -132,9 +133,9 @@ import 'weifuwu/components/style.css' // 包含 Token + 57 布局原语 + 136
|
|
|
132
133
|
|
|
133
134
|
```
|
|
134
135
|
mount ──────────────────────────────────────────
|
|
135
|
-
const Counter = (_init, ctx) => {
|
|
136
|
+
const Counter = async (_init, ctx) => { ← mount(只一次,可 await)
|
|
136
137
|
let count = 0 ← 初始化状态
|
|
137
|
-
return (props) => {
|
|
138
|
+
return async (props) => { ← renderFn(强制异步)
|
|
138
139
|
// ... ← 每次 dirty/props 变化执行
|
|
139
140
|
}
|
|
140
141
|
}
|
|
@@ -148,7 +149,7 @@ ref ─────────────────────────
|
|
|
148
149
|
})
|
|
149
150
|
|
|
150
151
|
props 变化 ─────────────────────────────────────
|
|
151
|
-
return (props) => {
|
|
152
|
+
return async (props) => {
|
|
152
153
|
// 每次 render 都收到最新 props ← 相当于 onupdate
|
|
153
154
|
if (props.value !== prevValue) { ... }
|
|
154
155
|
}
|
|
@@ -647,7 +648,11 @@ props 变化 ──────────────────────
|
|
|
647
648
|
| AiChat | `AiChat` | `chat`, `maxHeight?`, `labels?`, `renderMessage?`, `renderToolArgs?` | 标准 AI 对话界面:气泡 + 工具卡 + 审批卡 + 自动滚动 + 错误重试(接收 `ctx.ui.useChat()` handle) |
|
|
648
649
|
| MessageBubble | `MessageBubble` | `content`, `role`, `status`, `actions` | 独立消息气泡(业务聊天页复用) |
|
|
649
650
|
| ToolCallCard | `ToolCallCard` | `call`, `progress?`, `result?`, `renderArgs?` | 工具调用卡片:running(进度条)/ ok / error 三态(协议 §4) |
|
|
650
|
-
|
|
|
651
|
+
| JsonSchemaForm | `JsonSchemaForm` | `schema`, `value?`, `onChange?`, `onSubmit?`, `submitLabel?` | JSON Schema(对象子集)→ 参数输入表单:类型映射 + required/范围校验 + 嵌套/数组(工具参数输入面;不支持项告警降级) |
|
|
652
|
+
| ReasoningBlock | `ReasoningBlock` | `content`, `label?`, `defaultExpanded?`, `streaming?` | CoT 推理折叠展示(thinking 模式 `reasoning_content`;aria-expanded + 键盘可达 + 流式脉冲) |
|
|
653
|
+
| CitationCard | `CitationCard` | `items`, `label?`, `maxVisible?`, `defaultExpanded?`, `onOpen?` | RAG 引用来源展示:折叠「引用 N 条」+ 条目(序号/标题/来源/片段/链接)+ 溢出 +N;onOpen 时全部可点(调用方处理跳转) |
|
|
654
|
+
| SessionList | `SessionList` | `sessions`, `activeId?`, `onSelect?`, `onNew?`, `onRename?`, `onDelete?`, `searchable?` | 会话管理列表:分组(今天/昨天/更早)+ 选中高亮 + 搜索 + 行内重命名(Enter 确认/Escape 取消)/删除 + 键盘方向键导航 |
|
|
655
|
+
| ApprovalCard | `ApprovalCard` | `request`, `status?`, `onApprove(modifiedArgs?)`, `onReject`, `argsSchema?` | 人工审批卡片:待批(允许/拒绝+备注)/ 已批 / 已拒 / 超时;`argsSchema` 提供时渲染「修改参数」(JsonSchemaForm 预填 args,提交带修改后参数 → `onApprove(modifiedArgs)` → 父层选 modified 决策) |
|
|
651
656
|
|
|
652
657
|
### 全局工具
|
|
653
658
|
|
|
@@ -13,31 +13,51 @@
|
|
|
13
13
|
import { h, type Component } from 'weifuwu/ui-dom'
|
|
14
14
|
|
|
15
15
|
// Component<P, C>:P = props(JSX 自动推断),C = ctx 注入依赖(默认 {})
|
|
16
|
-
const Badge: Component<{ text: string; color?: string }> = () =>
|
|
17
|
-
(props) => h('span', { class: 'my-badge', style: { color: props.color } }, props.text)
|
|
16
|
+
const Badge: Component<{ text: string; color?: string }> = async () =>
|
|
17
|
+
async (props) => h('span', { class: 'my-badge', style: { color: props.color } }, props.text)
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
- 两阶段:外层 `(initProps, ctx) => …` 只执行一次(mount),内层 `(props) => VNode
|
|
21
|
-
-
|
|
20
|
+
- 两阶段:外层 `(initProps, ctx) => …` 只执行一次(mount),内层 `(props) => Promise<VNode>` 每次渲染执行(renderFn 强制异步——可 await 数据)
|
|
21
|
+
- 有状态组件用闭包 `let` + 事件里 `ctx.ui.render()`(render-only);不需要渲染的状态不调 `render()`
|
|
22
|
+
|
|
23
|
+
### mount 与 render 的职责(事件函数写在哪层)
|
|
24
|
+
|
|
25
|
+
| | mount(外层工厂,一次) | render(内层 renderFn,每次) |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| 职责 | 初始化状态 / 订阅 / 定时器 / **定义依赖稳定引用的回调** | 读最新 props / 派生数据 / **定义依赖它们的回调** / 输出视图 |
|
|
28
|
+
| 可访问 | `initProps`(首次)、`ctx`、mount `let`、稳定 handle | 最新 `props`、mount 闭包、`ctx` |
|
|
29
|
+
| 事件函数 | **只依赖稳定引用**(ctx / mount let / 稳定 handle 如 useChat 的 `chat`)→ mount 定义,天然引用恒等 | 依赖最新 props / 派生状态(如 Table 的 `rowSelection`、Menu 的 `openSet`)→ render 内定义(闭包捕获最新值) |
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
const AiChat = async (initProps, ctx) => {
|
|
33
|
+
const chat = initProps.chat // 稳定 handle(useChat 返回,引用不变)
|
|
34
|
+
const onSend = () => chat.send() // ✅ mount 定义:只依赖稳定引用——引用恒等,零重绑
|
|
35
|
+
return async (props) => {
|
|
36
|
+
const onSelect = (k: string) => props.onSelect?.(k) // ✅ render 定义:依赖最新 props——闭包捕获当前值
|
|
37
|
+
return h('button', { onClick: onSelect }, '选')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**规则**:回调只依赖 ctx / mount `let` / 稳定 handle → **mount 定义**(天然稳定,不重绑);依赖最新 props / 派生数据 → **render 内定义**(闭包捕获最新值;引用变化导致事件重绑是**正确性要求**——必须读最新状态,框架不做稳定引用魔法)。
|
|
22
43
|
|
|
23
44
|
## 1. 有状态组件
|
|
24
45
|
|
|
25
46
|
```tsx
|
|
26
|
-
const Toggle: Component = (_init, ctx) => {
|
|
27
|
-
|
|
28
|
-
$.on = false
|
|
47
|
+
const Toggle: Component = async (_init, ctx) => {
|
|
48
|
+
let on = false // 普通对象状态(render-only:无 $ Proxy)
|
|
29
49
|
|
|
30
|
-
return (props) => h('button', {
|
|
50
|
+
return async (props) => h('button', {
|
|
31
51
|
class: 'my-toggle',
|
|
32
|
-
onClick: () =>
|
|
33
|
-
},
|
|
52
|
+
onClick: () => { on = !on; ctx.ui.render() }, // 改状态后显式 render()
|
|
53
|
+
}, on ? '开' : '关')
|
|
34
54
|
}
|
|
35
55
|
```
|
|
36
56
|
|
|
37
57
|
| 状态类型 | 存放位置 | 触发渲染 |
|
|
38
58
|
|---------|---------|---------|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
59
|
+
| 组件内部状态 | 闭包 `let` | 改后调 `ctx.ui.render()` |
|
|
60
|
+
| 共享状态 | `createStore` + `ctx.ui.useExternal()` | store 变更自动 |
|
|
41
61
|
| 内部缓存 | 闭包 `let` | 不触发 |
|
|
42
62
|
|
|
43
63
|
## 2. 带弹层的组件(最高频的自定义场景)
|
|
@@ -45,23 +65,22 @@ const Toggle: Component = (_init, ctx) => {
|
|
|
45
65
|
用 `ctx.ui.usePopup`——一个组合器收敛 open 状态 + 触发(hover→tap 降级/longpress)+ Escape + 外部点击 + 定位/视口 clamp + portal:
|
|
46
66
|
|
|
47
67
|
```tsx
|
|
48
|
-
const MyPopover: Component<{ content: string }> = (_init, ctx) => {
|
|
49
|
-
|
|
50
|
-
$.open = false
|
|
68
|
+
const MyPopover: Component<{ content: string }> = async (_init, ctx) => {
|
|
69
|
+
let open = false
|
|
51
70
|
let wrapEl: HTMLElement | null = null
|
|
52
71
|
const wrapRef = (el: HTMLElement | null) => { wrapEl = el }
|
|
53
72
|
|
|
54
73
|
const popup = ctx.ui.usePopup({
|
|
55
74
|
trigger: 'hover', // 触屏自动降级 tap(useHoverCapable 内部判定)
|
|
56
75
|
el: () => wrapEl, // 锚点
|
|
57
|
-
isOpen: () =>
|
|
58
|
-
setOpen: (v) => {
|
|
76
|
+
isOpen: () => open,
|
|
77
|
+
setOpen: (v) => { open = v; ctx.ui.render() }, // 改状态 + render
|
|
59
78
|
width: 320, // 自动 clamp 视口
|
|
60
79
|
closeOnOutside: true, // 外部点击关闭(默认)
|
|
61
80
|
closeOnEscape: true, // Escape 关闭(默认,document 级——portal 焦点也生效)
|
|
62
81
|
})
|
|
63
82
|
|
|
64
|
-
return (props) =>
|
|
83
|
+
return async (props) =>
|
|
65
84
|
h('span', { class: 'anchor', ref: wrapRef, ...popup.wrapProps },
|
|
66
85
|
props.children,
|
|
67
86
|
popup.portal(h('div', { class: 'wf-panel' }, props.content)),
|
|
@@ -73,54 +92,62 @@ const MyPopover: Component<{ content: string }> = (_init, ctx) => {
|
|
|
73
92
|
|
|
74
93
|
## 3. 对话框类组件(Modal 系)
|
|
75
94
|
|
|
76
|
-
全屏对话框(焦点 trap + 滚动锁 +
|
|
95
|
+
全屏对话框(焦点 trap + 滚动锁 + 退场动画)是 usePopup 的**会话级模态模式**(`presence/trapFocus/lockScroll/positioning: 'none'`——Modal/Drawer 同款):
|
|
77
96
|
|
|
78
97
|
```tsx
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
98
|
+
const MyDialog: Component<{ open: boolean; onClose: () => void }> = async (_init, ctx) => {
|
|
99
|
+
let latestOpen = false
|
|
100
|
+
const popup = ctx.ui.usePopup({
|
|
101
|
+
presence: true, // 退场状态机(open → exit → closed + animationend)
|
|
102
|
+
trapFocus: true, // 焦点 trap(面板挂载锁定/卸载归还)
|
|
103
|
+
lockScroll: true, // 滚动锁(打开锁 / 面板卸载释放)
|
|
104
|
+
positioning: 'none', // 组件自定义定位(.wf-modal inset:0 居中)
|
|
105
|
+
closeOnOutside: false, closeOnEscape: false, // 关闭语义组件自控
|
|
106
|
+
isOpen: () => latestOpen,
|
|
107
|
+
setOpen: () => {},
|
|
108
|
+
}) // mount 创建
|
|
109
|
+
|
|
110
|
+
return async (props) => {
|
|
111
|
+
latestOpen = !!props.open
|
|
112
|
+
const phase = popup.sync!(latestOpen) // render 同步 open
|
|
86
113
|
if (phase === 'closed') return null
|
|
87
114
|
|
|
88
|
-
return
|
|
115
|
+
return popup.portal(h('div', {
|
|
89
116
|
class: 'wf-overlay',
|
|
90
117
|
onClick: (e: any) => { if (e.target === e.currentTarget) props.onClose() },
|
|
91
118
|
}, h('div', {
|
|
92
119
|
class: `wf-modal ${phase === 'exit' ? 'wf-modal--exit' : 'wf-modal--enter'}`,
|
|
93
|
-
ref: dialog.panelRef, // 焦点 trap 目标
|
|
94
120
|
onKeyDown: (e: any) => { if (e.key === 'Escape') props.onClose() }, // Escape 语义组件层
|
|
95
|
-
}, props.children)),
|
|
121
|
+
}, props.children)), 'modal') // portalKey 语义化(#__wf_portal 容器标记)
|
|
96
122
|
}
|
|
97
123
|
}
|
|
98
124
|
```
|
|
99
125
|
|
|
100
|
-
>
|
|
101
|
-
>
|
|
126
|
+
> **ref 接线**:`trapFocus`/`lockScroll`/presence 退场监听全部由 usePopup 内部接线到 portal 面板
|
|
127
|
+
> (`portalPanelRef`——content 的 `ref` prop 会被转发调用)——组件层无需手挂 `rootRef`/`panelRef`。
|
|
128
|
+
> 低层原语 `trapFocus`/`lockScroll` 已收编为 usePopup 内部实现(**不对外导出**——AGENTS.md §5.4);
|
|
129
|
+
> `animateOut` 仍从 `weifuwu/ui-dom` 导出(非弹窗动画场景用)。
|
|
102
130
|
|
|
103
131
|
## 4. AI 组件
|
|
104
132
|
|
|
105
133
|
会话语义由 `ctx.ui.useChat` 提供(消息/流式/工具/审批/stop/retry 全封装),返回的 handle 与 `$` 同一容器:
|
|
106
134
|
|
|
107
135
|
```tsx
|
|
108
|
-
const ChatPanel: Component = (_init, ctx) => {
|
|
109
|
-
const
|
|
136
|
+
const ChatPanel: Component = async (_init, ctx) => {
|
|
137
|
+
const chat = ctx.ui.useChat({
|
|
110
138
|
url: '/api/chat',
|
|
111
139
|
approveUrl: '/api/approve', // HITL 审批上行(缺省 approve() 只清卡片)
|
|
112
140
|
body: (messages) => ({ messages, mode: 'agent' }),
|
|
113
141
|
})
|
|
114
142
|
|
|
115
143
|
return () => h('div', { class: 'chat' },
|
|
116
|
-
|
|
117
|
-
h('
|
|
118
|
-
h('button', { onClick: () => $.send() }, $.streaming ? '…' : '发送'),
|
|
144
|
+
h(AiChat, { chat }), // 标准界面:输入/消息/流式/工具卡全内置
|
|
145
|
+
h('button', { onClick: () => chat.send() }, chat.streaming ? '…' : '发送'),
|
|
119
146
|
)
|
|
120
147
|
}
|
|
121
148
|
```
|
|
122
149
|
|
|
123
|
-
**共享
|
|
150
|
+
**共享 handle 给子组件**(如 `<AiChat chat={chat} />`):会话 handle 带 `subscribe(cb)`——子组件 mount 期 `ctx.ui.useExternal(initProps.chat)` 自订阅(AiChat 已内置),会话状态变化自动重渲染订阅组件。
|
|
124
151
|
|
|
125
152
|
## 5. 异步组件(数据声明在工厂层)
|
|
126
153
|
|
|
@@ -129,29 +156,27 @@ const ChatPanel: Component = (_init, ctx) => {
|
|
|
129
156
|
```tsx
|
|
130
157
|
const UserCard = async (initProps, ctx) => {
|
|
131
158
|
const user = await ctx.data.get(`/api/user/${initProps.userId}`) // 三场景:SSR→__DATA__ / hydration 种子 / SPA fetch
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return (props) => h('div', {}, user.name, h('button', { onClick: () => $.liked = !$.liked }))
|
|
159
|
+
let liked = false
|
|
160
|
+
return async (props) => h('div', {}, user.name, h('button', { onClick: () => { liked = !liked; ctx.ui.render() } }))
|
|
135
161
|
}
|
|
136
162
|
```
|
|
137
163
|
|
|
138
|
-
- 渲染器按「返回值是 Promise
|
|
164
|
+
- 渲染器按「返回值是 Promise」判别:主路径 `buildVNode` await 全部(无占位);运行时首次挂载的 async 组件在 buildVNode 阶段 await(无占位/补全回调);SSR 直接 await
|
|
139
165
|
- 工厂按实例执行;**数据必须走 ctx.data**(缓存+并发合并,重复执行零成本);禁止副作用裸写工厂
|
|
140
|
-
-
|
|
141
|
-
- **个性化数据不进 ctx.data**(SSR 会序列化给所有客户端)——留在客户端 `$` + fetch
|
|
166
|
+
- **个性化数据不进 ctx.data**(SSR 会序列化给所有客户端)——留在客户端 `let` + fetch + `render()`
|
|
142
167
|
|
|
143
168
|
## 6. 类型纪律(编译期防线)
|
|
144
169
|
|
|
145
170
|
```tsx
|
|
146
|
-
const Badge: Component<{ variant: 'primary' | 'muted' }> = () =>
|
|
147
|
-
(props) => h('span', { class: `badge-${props.variant}` }, props.children)
|
|
171
|
+
const Badge: Component<{ variant: 'primary' | 'muted' }> = async () =>
|
|
172
|
+
async (props) => h('span', { class: `badge-${props.variant}` }, props.children)
|
|
148
173
|
|
|
149
174
|
// 负例:variant 传错 → tsc 报错(@ts-expect-error 是类型流测试的写法)
|
|
150
175
|
// @ts-expect-error variant 不允许 'bogus'
|
|
151
176
|
const bad: { variant: 'primary' | 'muted' } = { variant: 'bogus' }
|
|
152
177
|
|
|
153
178
|
// ctx 注入声明(C 泛型):声明了才能用,未声明编译期报错
|
|
154
|
-
const Page: Component<{}, { api: ApiInjected['api'] }> = (_init, ctx) => {
|
|
179
|
+
const Page: Component<{}, { api: ApiInjected['api'] }> = async (_init, ctx) => {
|
|
155
180
|
ctx.api.get('/x')
|
|
156
181
|
return () => null
|
|
157
182
|
}
|
|
@@ -167,17 +192,23 @@ const Page: Component<{}, { api: ApiInjected['api'] }> = (_init, ctx) => {
|
|
|
167
192
|
|
|
168
193
|
## 7. 测试写法
|
|
169
194
|
|
|
195
|
+
**官方测试原语 `weifuwu/ui-dom/testing`**(子路径,随包发布)——自研组件测试用官方工具,不手抄:
|
|
196
|
+
|
|
170
197
|
```tsx
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return typeof result === 'function' ? result(props) : result
|
|
174
|
-
}
|
|
198
|
+
import { renderVNode, mountComponent, findByClass, createTestCtx } from 'weifuwu/ui-dom/testing'
|
|
199
|
+
// 仓库内开发用相对路径:from '../../ui-dom/testing.ts'
|
|
175
200
|
|
|
176
|
-
const
|
|
177
|
-
const vnode = renderVNode(Toggle, {}, ctx)
|
|
201
|
+
const vnode = renderVNode(Toggle, {}, createTestCtx())
|
|
178
202
|
// 断言 vnode 结构(子组件 VNode.type 是组件函数,不是标签名)
|
|
203
|
+
|
|
204
|
+
// 交互流转(内部 let 状态)用 mountComponent(同实例 re-render):
|
|
205
|
+
const render = mountComponent(Toggle, {}, createTestCtx())
|
|
206
|
+
render() // 初始
|
|
207
|
+
// ... 触发点击/输入 ...
|
|
208
|
+
render() // 重渲染,状态保留
|
|
179
209
|
```
|
|
180
210
|
|
|
211
|
+
- 弹层组件:`createPopupMock(isOpen)` 注入 `createTestCtx({ ui: { usePopup: () => popup } })`
|
|
181
212
|
- 类型流测试:`@ts-expect-error` 负例(见 [type-flow.test.ts](../src/components/type-flow.test.ts))
|
|
182
213
|
- 组件测试跑在 node --test;DOM 事件级测试需 `document.body.appendChild(container)`
|
|
183
214
|
|
|
@@ -186,8 +217,8 @@ const vnode = renderVNode(Toggle, {}, ctx)
|
|
|
186
217
|
新受控组件**必须**用 `ctx.ui.useControlled`(受控判定 + 缺回调 warn 一次 + 非受控内部状态跨渲染保持):
|
|
187
218
|
|
|
188
219
|
```tsx
|
|
189
|
-
const CollapseItem: Component<{ active?: boolean; onChange?: (v: boolean) => void }> = (_init, ctx) => {
|
|
190
|
-
return (props) => {
|
|
220
|
+
const CollapseItem: Component<{ active?: boolean; onChange?: (v: boolean) => void }> = async (_init, ctx) => {
|
|
221
|
+
return async (props) => {
|
|
191
222
|
const ctrl = ctx.ui.useControlled<boolean>({ value: props.active, onChange: props.onChange, name: 'CollapseItem' })
|
|
192
223
|
return h('button', {
|
|
193
224
|
onClick: () => ctrl.setValue(!(ctrl.value ?? false)), // 受控走 onChange;非受控内部状态
|
|
@@ -215,10 +246,10 @@ const CollapseItem: Component<{ active?: boolean; onChange?: (v: boolean) => voi
|
|
|
215
246
|
> SSR 安全(shim 安全默认)+ 测试 mock 单点 + 环境差异隔离。
|
|
216
247
|
|
|
217
248
|
```tsx
|
|
218
|
-
const MyComp: Component = (_init, ctx) => {
|
|
249
|
+
const MyComp: Component = async (_init, ctx) => {
|
|
219
250
|
// mount 层取 browser(ctx.browser 优先,测试/无注入环境 fallback jsdom)
|
|
220
251
|
const browser = ctx.browser ?? createClientBrowser()
|
|
221
|
-
return (props) =>
|
|
252
|
+
return async (props) =>
|
|
222
253
|
h('button', {
|
|
223
254
|
onClick: () => {
|
|
224
255
|
// 复制/查询/存储/滚动——全部经 browser
|
|
@@ -260,9 +291,8 @@ const MyComp: Component = (_init, ctx) => {
|
|
|
260
291
|
|
|
261
292
|
## 已知边界(诚实裁剪)
|
|
262
293
|
|
|
263
|
-
- `usePopup`
|
|
294
|
+
- `usePopup` 是**统一弹窗能力层**:锚定浮层(Tooltip/Popover/Dropdown/Select/AutoComplete/Mentions/Cascader/ContextMenu/NavMenu/Popconfirm/TreeSelect)+ 会话级模态(Modal/Drawer/Confirm——presence/trapFocus/lockScroll/positioning 'none',Escape 语义留组件层)+ mask 模式(Command/Img preview/Tour——mask/maskCentered/自定义 mask VNode)+ focus 触发(DatePicker)+ positioning 'none' 常驻容器(Toast/Notification)——**全部弹窗单一入口**
|
|
264
295
|
- **事件监听纪律**:组件库内部浏览器事件监听**统一走 `ctx.ui.useXXX`**——滚动/观察/弹层/对话框/快捷键/拖拽/DnD 全覆盖:
|
|
265
|
-
`useInView`(InfiniteScroll)、`useScrollPosition`(AiChat/Affix/BackTop/VirtualList)、`usePopupPosition`(Affix 阈值重算)、`usePopup
|
|
266
|
-
-
|
|
267
|
-
- **Select/DatePicker** 是 inline/absolute 菜单(自适宽),不迁移 usePopup——菜单直接挂在锚点下
|
|
296
|
+
`useInView`(InfiniteScroll)、`useScrollPosition`(AiChat/Affix/BackTop/VirtualList)、`usePopupPosition`(Affix 阈值重算)、`usePopup`(弹窗统一——ContextMenu 自由定位 + Modal/Drawer 模态模式 + mask 遮罩)、`useGlobalKey`(Command 快捷键/Img preview Escape)、`useDrag`(Resizable)、`useDragDrop`(FileUpload)、`useControlled`/`useStableRef`(状态/ref)
|
|
297
|
+
- **唯一保留 usePopupPosition 独立使用**:Affix / Chart(坐标工具——非弹窗组合器,滚动跟随自动)
|
|
268
298
|
- `createReactiveState` 已导出:组件外建全局 store(`createReactiveState(() => {})` + `$.__watch(cb)` 订阅)
|
package/docs/examples.md
CHANGED
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
## 登录表单
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
|
-
const LoginPage = (_init, ctx) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
$.submitting = false
|
|
8
|
+
const LoginPage = async (_init, ctx) => {
|
|
9
|
+
let errors: Record<string, string> = {}
|
|
10
|
+
let submitting = false
|
|
12
11
|
|
|
13
|
-
return (props) =>
|
|
12
|
+
return async (props) =>
|
|
14
13
|
h('div', { class: 'wf-stack', style: { maxWidth: 400, margin: '40px auto' } },
|
|
15
14
|
h(Card, { padding: 'lg' },
|
|
16
15
|
h('div', { class: 'wf-stack', style: { gap: 'var(--wf-space-md)' } },
|
|
@@ -21,17 +20,19 @@ const LoginPage = (_init, ctx) => {
|
|
|
21
20
|
password: [{ required: true, minLength: 6, message: '密码至少6位' }],
|
|
22
21
|
},
|
|
23
22
|
onSubmit: async (values) => {
|
|
24
|
-
|
|
23
|
+
submitting = true
|
|
24
|
+
ctx.ui.render()
|
|
25
25
|
await ctx.api?.post('/login', values) // api 客户端由中间件注入 ctx.api
|
|
26
|
-
|
|
26
|
+
submitting = false
|
|
27
|
+
ctx.ui.render()
|
|
27
28
|
},
|
|
28
|
-
onError: (
|
|
29
|
+
onError: (errs) => { errors = errs; ctx.ui.render() },
|
|
29
30
|
}, [
|
|
30
|
-
h(Field, { label: '邮箱', error:
|
|
31
|
+
h(Field, { label: '邮箱', error: errors.email },
|
|
31
32
|
h(Input, { name: 'email', type: 'email', placeholder: 'name@example.com' })),
|
|
32
|
-
h(Field, { label: '密码', error:
|
|
33
|
+
h(Field, { label: '密码', error: errors.password },
|
|
33
34
|
h(Input, { name: 'password', type: 'password' })),
|
|
34
|
-
h(Button, { type: 'submit', loading:
|
|
35
|
+
h(Button, { type: 'submit', loading: submitting, block: true }, '登录'),
|
|
35
36
|
])
|
|
36
37
|
)
|
|
37
38
|
)
|
|
@@ -42,20 +43,19 @@ const LoginPage = (_init, ctx) => {
|
|
|
42
43
|
## 数据列表 + 搜索
|
|
43
44
|
|
|
44
45
|
```tsx
|
|
45
|
-
const UserList = (_init, ctx) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
$.sortOrder = 'asc'
|
|
46
|
+
const UserList = async (_init, ctx) => {
|
|
47
|
+
let keyword = ''
|
|
48
|
+
let sortKey = 'name'
|
|
49
|
+
let sortOrder = 'asc'
|
|
50
50
|
const users = [
|
|
51
51
|
{ id: 1, name: '张三', email: 'zhang@example.com', role: '管理员' },
|
|
52
52
|
{ id: 2, name: '李四', email: 'li@example.com', role: '编辑' },
|
|
53
53
|
]
|
|
54
54
|
|
|
55
|
-
return (props) => {
|
|
56
|
-
// 派生数据必须在 render 内计算(每次 render 读最新
|
|
55
|
+
return async (props) => {
|
|
56
|
+
// 派生数据必须在 render 内计算(每次 render 读最新 keyword)
|
|
57
57
|
const filtered = users.filter(u =>
|
|
58
|
-
|
|
58
|
+
!keyword || u.name.includes(keyword) || u.email.includes(keyword)
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
return h('div', { class: 'wf-stack', style: { gap: 'var(--wf-space-md)' } },
|
|
@@ -85,39 +85,33 @@ const UserList = (_init, ctx) => {
|
|
|
85
85
|
## 消息提示
|
|
86
86
|
|
|
87
87
|
```tsx
|
|
88
|
-
//
|
|
89
|
-
|
|
88
|
+
// 官方推荐:命令式中间件(app.use(toast()) → ctx.toast('消息', 'success'))
|
|
89
|
+
app.use(toast())
|
|
90
|
+
// 任意组件:ctx.toast?.('操作成功', 'success')
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
$.toasts = $.toasts ?? []
|
|
95
|
-
const id = String(++toastId)
|
|
96
|
-
$.toasts = [...$.toasts, { id, type, message }]
|
|
92
|
+
// 自管理列表(render-only:let + render())
|
|
93
|
+
let toasts: { id: string; type: string; message: string }[] = []
|
|
94
|
+
let toastId = 0
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
function showToast(ctx: WfuiContext, type: string, message: string) {
|
|
97
|
+
toasts = [...toasts, { id: String(++toastId), type, message }]
|
|
98
|
+
ctx.ui.render()
|
|
99
99
|
if (type !== 'error') {
|
|
100
|
-
setTimeout(() => {
|
|
101
|
-
$.toasts = $.toasts.filter((t: any) => t.id !== id)
|
|
102
|
-
}, 3000)
|
|
100
|
+
setTimeout(() => { toasts = toasts.filter((t: any) => t.id !== String(toastId)); ctx.ui.render() }, 3000)
|
|
103
101
|
}
|
|
104
102
|
}
|
|
105
103
|
|
|
106
104
|
// 页面中使用
|
|
107
|
-
const App = (_init, ctx) => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return (props) =>
|
|
105
|
+
const App = async (_init, ctx) => {
|
|
106
|
+
toasts = []
|
|
107
|
+
return async (props) =>
|
|
112
108
|
h('div', {}, [
|
|
113
|
-
h(Button, {
|
|
114
|
-
onClick: () => showToast(ctx, 'success', '操作成功'),
|
|
115
|
-
}, '显示提示'),
|
|
109
|
+
h(Button, { onClick: () => showToast(ctx, 'success', '操作成功') }, '显示提示'),
|
|
116
110
|
h(Toast, {
|
|
117
|
-
toasts
|
|
111
|
+
toasts,
|
|
118
112
|
position: 'top-right',
|
|
119
113
|
max: 3,
|
|
120
|
-
onRemove: (id) => {
|
|
114
|
+
onRemove: (id) => { toasts = toasts.filter((t: any) => t.id !== id); ctx.ui.render() },
|
|
121
115
|
}),
|
|
122
116
|
])
|
|
123
117
|
}
|
|
@@ -24,8 +24,7 @@ uiServe(app, { root: '#root' }) // 客户端落地(hydrate: true 收养 SS
|
|
|
24
24
|
|
|
25
25
|
```tsx
|
|
26
26
|
const DashboardLayout: UIMiddleware = async (_loc, ctx, children) => {
|
|
27
|
-
|
|
28
|
-
$.open = true
|
|
27
|
+
let open = true
|
|
29
28
|
return async (loc, c) => {
|
|
30
29
|
const child = await children(loc, c) // 子路由/嵌套路由内容
|
|
31
30
|
return h('div', { class: 'wf-row' },
|
|
@@ -132,8 +131,8 @@ app.use(auth())
|
|
|
132
131
|
uiServe(app, { root: '#root' })
|
|
133
132
|
|
|
134
133
|
// 在组件中
|
|
135
|
-
function Profile(_props: {}, ctx: WfuiContext) {
|
|
136
|
-
return (props) => {
|
|
134
|
+
async function Profile(_props: {}, ctx: WfuiContext) {
|
|
135
|
+
return async (props) => {
|
|
137
136
|
if (!ctx.auth?.isLoggedIn) return <p>请登录</p>
|
|
138
137
|
return <p>欢迎, {ctx.auth?.user?.name}</p>
|
|
139
138
|
}
|