weifuwu 0.74.0 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +20 -20
  2. package/dist/ai/types.d.ts +3 -1
  3. package/dist/components/AiChat/AiChat.d.ts +5 -1
  4. package/dist/components/ApprovalCard/ApprovalCard.d.ts +5 -2
  5. package/dist/components/Button/Button.d.ts +2 -0
  6. package/dist/components/CitationCard/CitationCard.d.ts +38 -0
  7. package/dist/components/Command/Command.d.ts +1 -1
  8. package/dist/components/JsonSchemaForm/JsonSchemaForm.d.ts +48 -0
  9. package/dist/components/ReasoningBlock/ReasoningBlock.d.ts +25 -0
  10. package/dist/components/Select/Select.d.ts +11 -1
  11. package/dist/components/SessionList/SessionList.d.ts +42 -0
  12. package/dist/components/TreeSelect/TreeSelect.d.ts +3 -2
  13. package/dist/components/index.d.ts +10 -2
  14. package/dist/components/index.js +14 -14
  15. package/dist/components/style.css +608 -20
  16. package/dist/index.d.ts +0 -2
  17. package/dist/index.js +141 -433
  18. package/dist/layout/weifuwu-layout.css +9 -0
  19. package/dist/test/client/setup.d.ts +12 -0
  20. package/dist/ui/index.d.ts +3 -3
  21. package/dist/ui-dom/Confirm.d.ts +5 -41
  22. package/dist/ui-dom/Notification.d.ts +8 -70
  23. package/dist/ui-dom/Toast.d.ts +8 -43
  24. package/dist/ui-dom/ai.d.ts +1 -1
  25. package/dist/ui-dom/browser.d.ts +1 -1
  26. package/dist/ui-dom/focus-trap.d.ts +1 -1
  27. package/dist/ui-dom/hooks/chat.d.ts +11 -0
  28. package/dist/ui-dom/hooks/events.d.ts +30 -0
  29. package/dist/ui-dom/hooks/external.d.ts +20 -0
  30. package/dist/ui-dom/hooks/index.d.ts +31 -0
  31. package/dist/ui-dom/hooks/input.d.ts +33 -0
  32. package/dist/ui-dom/hooks/media.d.ts +17 -0
  33. package/dist/ui-dom/hooks/popup.d.ts +37 -0
  34. package/dist/ui-dom/hooks/stable.d.ts +39 -0
  35. package/dist/ui-dom/hooks/test/hooks.test.d.ts +1 -0
  36. package/dist/ui-dom/hooks/types.d.ts +78 -0
  37. package/dist/ui-dom/index.d.ts +19 -17
  38. package/dist/ui-dom/index.js +12 -10
  39. package/dist/ui-dom/jsx-runtime.js +1 -1
  40. package/dist/ui-dom/middleware/api.d.ts +3 -3
  41. package/dist/ui-dom/middleware/auth.d.ts +1 -1
  42. package/dist/ui-dom/motion.d.ts +1 -1
  43. package/dist/ui-dom/popup.d.ts +1 -1
  44. package/dist/ui-dom/scroll-lock.d.ts +1 -1
  45. package/dist/ui-dom/store.d.ts +24 -0
  46. package/dist/ui-dom/testing.d.ts +74 -0
  47. package/dist/ui-dom/testing.js +1 -0
  48. package/dist/ui-dom/testing.test.d.ts +1 -0
  49. package/dist/ui-dom/types.d.ts +34 -17
  50. package/dist/ui-dom/use-chat.d.ts +19 -14
  51. package/dist/ui-dom/vdom/build.d.ts +34 -0
  52. package/dist/ui-dom/vdom/diff.d.ts +34 -0
  53. package/dist/ui-dom/vdom/hydration.d.ts +17 -0
  54. package/dist/ui-dom/vdom/index.d.ts +23 -0
  55. package/dist/ui-dom/vdom/mount.d.ts +49 -0
  56. package/dist/ui-dom/vdom/registry.d.ts +27 -0
  57. package/dist/ui-dom/vdom/render.d.ts +13 -0
  58. package/dist/ui-dom/vdom/scheduler.d.ts +13 -0
  59. package/dist/ui-dom/vdom/serve.d.ts +30 -0
  60. package/dist/ui-dom/vdom/ssr.d.ts +44 -0
  61. package/dist/ui-dom/vnode.d.ts +11 -44
  62. package/dist/user/token.d.ts +1 -1
  63. package/docs/compat-three-library.md +1 -1
  64. package/docs/components-map.md +19 -2
  65. package/docs/components.md +9 -4
  66. package/docs/custom-components.md +19 -14
  67. package/docs/data.md +16 -33
  68. package/docs/environment.md +1 -1
  69. package/docs/frontend-middleware.md +65 -79
  70. package/docs/frontend-ui-dom.md +4 -2
  71. package/docs/frontend.md +35 -38
  72. package/docs/layout.md +2 -2
  73. package/docs/realtime.md +23 -28
  74. package/docs/saas.md +8 -6
  75. package/package.json +5 -1
  76. package/dist/ui/ssr-page.d.ts +0 -41
  77. package/dist/ui/ssr.d.ts +0 -35
  78. package/dist/ui-dom/diff.d.ts +0 -28
  79. package/dist/ui-dom/hydration.d.ts +0 -13
  80. package/dist/ui-dom/reactive.d.ts +0 -6
  81. package/dist/ui-dom/registry.d.ts +0 -39
  82. package/dist/ui-dom/render.d.ts +0 -46
  83. package/dist/ui-dom/route-match.d.ts +0 -22
  84. package/dist/ui-dom/serve.d.ts +0 -28
  85. package/dist/ui-dom/ssr.d.ts +0 -56
  86. package/dist/ui-dom/ui.d.ts +0 -78
@@ -2,14 +2,14 @@
2
2
 
3
3
  > 本页为 weifuwu 官方文档拆分页 · [返回 README](../README.md)
4
4
 
5
- 109 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
5
+ 113 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--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 + 57 布局原语 + 136 工具类 + 组件样式,一次性引入
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="同意协议" />
@@ -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
- | ApprovalCard | `ApprovalCard` | `request`, `status?`, `onApprove`, `onReject` | 人工审批卡片:待批(允许/拒绝+备注)/ 已批 / 已拒 / 超时(协议 §4.5) |
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
 
@@ -1,8 +1,8 @@
1
1
  # 自定义组件开发指南
2
2
 
3
3
 
4
- > ⚠️ **weifuwu/client 已删除**——前端运行时唯一入口为 `weifuwu/ui-dom`,见 [frontend-ui-dom.md](frontend-ui-dom.md)。
5
- > 用 weifuwu/client 写自己的组件——与内置组件同权:同渲染引擎、同弹层原语、同类型安全。
4
+ > ⚠️ **weifuwu/client 已并入 `weifuwu/ui-dom`**(`src/client/` 已删除)——前端运行时唯一入口为 `weifuwu/ui-dom`,见 [frontend-ui-dom.md](frontend-ui-dom.md)。
5
+ > 用 weifuwu/ui-dom 写自己的组件——与内置组件同权:同渲染引擎、同弹层原语、同类型安全。
6
6
  > 前置:[前端概念](frontend.md)(两阶段模型/ctx.ui)+ [组件列表](components.md)。
7
7
 
8
8
  ---
@@ -10,7 +10,7 @@
10
10
  ## 0. 最小骨架
11
11
 
12
12
  ```tsx
13
- import { h, type Component } from 'weifuwu/client'
13
+ import { h, type Component } from 'weifuwu/ui-dom'
14
14
 
15
15
  // Component<P, C>:P = props(JSX 自动推断),C = ctx 注入依赖(默认 {})
16
16
  const Badge: Component<{ text: string; color?: string }> = () =>
@@ -76,7 +76,7 @@ const MyPopover: Component<{ content: string }> = (_init, ctx) => {
76
76
  全屏对话框(焦点 trap + 滚动锁 + 退场动画)不在 usePopup 范围——用 **`ctx.ui.useDialog`** 组合器(Modal/Drawer 同款:退场状态机 + 滚动锁 + 焦点 trap + animationend 卸载):
77
77
 
78
78
  ```tsx
79
- import { createPortal } from 'weifuwu/client'
79
+ import { createPortal } from 'weifuwu/ui-dom'
80
80
 
81
81
  const MyDialog: Component<{ open: boolean; onClose: () => void }> = (_init, ctx) => {
82
82
  const dialog = ctx.ui.useDialog({ name: 'MyDialog' }) // mount 创建
@@ -98,7 +98,7 @@ const MyDialog: Component<{ open: boolean; onClose: () => void }> = (_init, ctx)
98
98
  ```
99
99
 
100
100
  > `dialog.rootRef` 挂到 portal 根(lockScroll + animationend 退场监听);`panelRef` 挂到面板(trapFocus)。
101
- > 低层原语 `trapFocus`/`lockScroll`/`animateOut` 仍从 `weifuwu/client` 导出(特殊场景组装用)。
101
+ > 低层原语 `trapFocus`/`lockScroll`/`animateOut` 仍从 `weifuwu/ui-dom` 导出(特殊场景组装用)。
102
102
 
103
103
  ## 4. AI 组件
104
104
 
@@ -135,10 +135,9 @@ const UserCard = async (initProps, ctx) => {
135
135
  }
136
136
  ```
137
137
 
138
- - 渲染器按「返回值是 Promise」判别:客户端未 resolve → 占位(`Placeholder`),resolve 后整树补全;SSR 直接 await(无占位)
138
+ - 渲染器按「返回值是 Promise」判别:主路径 `buildVNode` await 全部(无占位);动态挂载兑底占位 + 局部补全;SSR 直接 await
139
139
  - 工厂按实例执行;**数据必须走 ctx.data**(缓存+并发合并,重复执行零成本);禁止副作用裸写工厂
140
- - 占位显示:无边界 → null;`<Suspense fallback={...}>` → 占位处显示 fallback(可选)
141
- - 代码分割/昂贵一次性资源:`asyncComponent(async (ctx) => { const { default: def } = await import('./view'); return def })`(WeakMap 全局一次,兼容保留)
140
+ - 占位显示:动态挂载的 async 组件占位 = 注释节点,resolve 后局部补全(Suspense 边界已裁剪)
142
141
  - **个性化数据不进 ctx.data**(SSR 会序列化给所有客户端)——留在客户端 `$` + fetch
143
142
 
144
143
  ## 6. 类型纪律(编译期防线)
@@ -168,17 +167,23 @@ const Page: Component<{}, { api: ApiInjected['api'] }> = (_init, ctx) => {
168
167
 
169
168
  ## 7. 测试写法
170
169
 
170
+ **官方测试原语 `weifuwu/ui-dom/testing`**(子路径,随包发布)——自研组件测试用官方工具,不手抄:
171
+
171
172
  ```tsx
172
- function renderVNode(Comp: any, props: any, ctx: any) {
173
- const result = Comp(props, ctx)
174
- return typeof result === 'function' ? result(props) : result
175
- }
173
+ import { renderVNode, mountComponent, findByClass, createTestCtx } from 'weifuwu/ui-dom/testing'
174
+ // 仓库内开发用相对路径:from '../../ui-dom/testing.ts'
176
175
 
177
- const ctx = { ui: { $: () => ({}), render: () => {}, dirty: () => {}, useControlled: (o: any) => ({ value: o.value, setValue: o.onChange ?? (() => {}), controlled: o.value !== undefined }) } }
178
- const vnode = renderVNode(Toggle, {}, ctx)
176
+ const vnode = renderVNode(Toggle, {}, createTestCtx())
179
177
  // 断言 vnode 结构(子组件 VNode.type 是组件函数,不是标签名)
178
+
179
+ // 交互流转(内部 let 状态)用 mountComponent(同实例 re-render):
180
+ const render = mountComponent(Toggle, {}, createTestCtx())
181
+ render() // 初始
182
+ // ... 触发点击/输入 ...
183
+ render() // 重渲染,状态保留
180
184
  ```
181
185
 
186
+ - 弹层组件:`createPopupMock(isOpen)` 注入 `createTestCtx({ ui: { usePopup: () => popup } })`
182
187
  - 类型流测试:`@ts-expect-error` 负例(见 [type-flow.test.ts](../src/components/type-flow.test.ts))
183
188
  - 组件测试跑在 node --test;DOM 事件级测试需 `document.body.appendChild(container)`
184
189
 
package/docs/data.md CHANGED
@@ -27,9 +27,11 @@ app.post('/decks', async (req, ctx) => {
27
27
  // 读回来自动是对象:rows[0].deck_json === { slides: [...] }(不是字符串)
28
28
  })
29
29
 
30
- // ③ 事务(postgres.js 兼容 begin)
30
+ // ③ 事务(中间件实例 pg.transaction——不在 ctx.sql 接口)
31
+ const pg = postgres()
32
+ app.use(pg)
31
33
  app.post('/transfer', async (req, ctx) => {
32
- await ctx.sql.begin(async sql => {
34
+ await pg.transaction(async sql => {
33
35
  await sql`UPDATE accounts SET balance = balance - 100 WHERE id = 1`
34
36
  await sql`UPDATE accounts SET balance = balance + 100 WHERE id = 2`
35
37
  })
@@ -105,20 +107,12 @@ const q = queue({ redis })
105
107
  | timestamp / date / interval | `string`(无时区语义——转 Date 按本地时区解析即时区魔法,诚实裁剪不转) |
106
108
  | NULL | `null` |
107
109
 
108
- ### 类型层(查询泛型 + schema 写前校验)
110
+ ### 类型层(查询泛型)
109
111
 
110
112
  ```ts
111
- // ① 查询结果泛型(编译期类型,无需手写 interface + 断言)
113
+ // 查询结果泛型(编译期类型,无需手写 interface + 断言)
112
114
  interface Deck { id: number; title: string; deck_json: { slides: unknown[] } }
113
- const decks = await ctx.sql.query<Deck>('SELECT id, title, deck_json FROM decks')
114
-
115
- // ② schema 注册 → insert 写前校验(脏数据源头拦截)
116
- ctx.sql.register('decks', {
117
- title: { type: 'text', required: true },
118
- status: { type: 'enum', values: ['outline', 'ready'] },
119
- deck_json: { type: 'jsonb' },
120
- })
121
- await ctx.sql.insert('decks', { title: 'x', status: 'INVALID' }) // → ValidationError
115
+ const decks = await ctx.sql`SELECT id, title, deck_json FROM decks` as Deck[]
122
116
  ```
123
117
 
124
118
  ### 方法面
@@ -126,17 +120,13 @@ await ctx.sql.insert('decks', { title: 'x', status: 'INVALID' }) // → Validati
126
120
  | 方法 | 说明 |
127
121
  |------|------|
128
122
  | `ctx.sql\`...\`` | tagged template → 参数化查询(插值=参数,表名需硬编码) |
129
- | `ctx.sql.query<T>(sql, params?)` | 参数化查询 + 泛型 |
130
- | `ctx.sql.unsafe(sql, params?)` | 原生 SQL(DDL / 动态表名) |
131
- | `ctx.sql.begin(fn)` | 事务(回调收到 tagged template sql) |
132
- | `ctx.sql.transaction(fn)` | 事务(回调收到 `{ query }`) |
133
- | `ctx.sql.register(table, schema)` | 注册表结构(写前校验) |
134
- | `ctx.sql.insert(table, row)` | schema 校验 + 参数化插入 |
135
- | `ctx.sql.insertMany(table, rows[], { batchSize? })` | **批量插入**:多行 VALUES 单次往返(默认 500/批;所有行键必须一致) |
136
- | `ctx.sql.update(table, set, where, { returning? })` | **参数化 UPDATE**:SET/WHERE 全部参数化,返回 `affectedRows` |
137
- | `ctx.sql.delete(table, where)` | **参数化 DELETE**:WHERE 必填(防全表误删),返回 `affectedRows` |
138
- | `ctx.sql\`...\` 内嵌片段` | 条件 SQL 片段(嵌套过滤,参数自动重编号) |
139
- | `ctx.sql.close()` | 关闭连接池 |
123
+ | `ctx.sql.unsafe(sql, params?)` | 原生 SQL(DDL / 动态表名;`$1` 占位符) |
124
+ | `ctx.sql.query` | **Query Language**:`sql.query.from('users').where({...}).run()`(AST 双后端) |
125
+ | `ctx.sql.raw\`...\`` | 逃生舱片段(`NOW() - interval '7 days'`——真库透传/内存裁剪) |
126
+ | `pg.transaction(fn)` | 事务(中间件实例;回调收到 callable sql,postgres.js 兼容 begin 语义) |
127
+ | `pg.migrate()` / `markMigrated` / `isMigrated` | 幂等迁移(`_weifuwu_migrations` 表) |
128
+ | `pg.poolStats()` | 连接池摘要(active/idle/waiting/max) |
129
+ | `ctx.sql.close()` / `pg.close()` | 关闭连接池 |
140
130
 
141
131
  ### 影响行数(affectedRows)
142
132
 
@@ -147,14 +137,6 @@ const r = await ctx.sql`UPDATE messages SET read = true WHERE id = ${id}`
147
137
  if (r.affectedRows === 0) return new Response('not found', { status: 404 })
148
138
  ```
149
139
 
150
- ```ts
151
- // 批量插入:100 行 1 次往返
152
- await ctx.sql.insertMany('agent_logs', logs, { batchSize: 500 })
153
- // 语义化更新/删除:WHERE 全参数化 + 返回影响行数
154
- await ctx.sql.update('users', { role: 'admin' }, { id: userId })
155
- await ctx.sql.delete('messages', { id: msgId })
156
- ```
157
-
158
140
  ### 条件片段(嵌套过滤)
159
141
 
160
142
  ```ts
@@ -281,10 +263,11 @@ await ctx.redis.set('user', 1) // 实际写入 'api:user'
281
263
 
282
264
  | 选项 | 类型 | 默认值 | 说明 |
283
265
  |------|------|--------|------|
284
- | `url` | `string` | `REDIS_URL` 环境变量 | 连接字符串 |
266
+ | `url` | `string` | `REDIS_URL` 环境变量(两者都缺 → 构造抛错,禁止静默回退 localhost) | 连接字符串 |
285
267
  | `poolSize` | `number` | `5` | 连接池大小 |
286
268
  | `keyPrefix` | `string` | `''` | 所有 key 自动加前缀(多应用隔离) |
287
269
  | `commandTimeoutMs` | `number` | `0` | 命令超时(阻塞命令 resolve(null);防挂起。0=禁用) |
270
+ | `onCommand` | `(command, args, durationMs, traceId?) => void` | — | 命令观测钩子;第 4 参数为请求级 traceId(`x-trace-id` 头经 ALS 传播) |
288
271
  | `socketTimeoutMs` | `number` | `0` | socket 响应超时(僵尸连接自愈:pending 有命令且超时无数据 → 主动断开重连。0=禁用) |
289
272
 
290
273
  > **连接健康**:断线自动剔除死连接并重建(池不萎缩);`CLIENT KILL`/网络抖动后服务自愈,命令不命中死连接。
@@ -7,7 +7,7 @@
7
7
  | 变量 | 用途 | 模块 | 默认 |
8
8
  |------|------|------|------|
9
9
  | `DATABASE_URL` | PostgreSQL 连接字符串 | `postgres()` | —(必填) |
10
- | `REDIS_URL` | Redis 连接字符串 | `redis()` | `redis://localhost:6379` |
10
+ | `REDIS_URL` | Redis 连接字符串 | `redis()` | —(必填;缺 env 且未传 `url` 构造抛错,不静默回退 localhost) |
11
11
  | `AUTH_SECRET` | userSystem HMAC 签名密钥(≥16 字符) | `userSystem()` | 可传 `options.secret` |
12
12
  | `DEEPSEEK_API_KEY` | LLM 对话 provider API key | `ai()` | — |
13
13
  | `DEEPSEEK_BASE_URL` | LLM 对话 provider 端点 | `ai()` | `https://api.deepseek.com/v1` |
@@ -1,93 +1,79 @@
1
1
  # 前端中间件与工具(weifuwu/ui-dom)
2
2
 
3
- > ⚠️ **`weifuwu/client` 已删除**——中间件(api/auth/ws/i18n)现位于 `weifuwu/ui-dom`,新 API 见 [frontend-ui-dom.md](frontend-ui-dom.md)。
3
+ > ⚠️ **`weifuwu/client` 已并入 `weifuwu/ui-dom`**——中间件(api/auth/ws/i18n)位于 `weifuwu/ui-dom`,前端运行时唯一入口为 `weifuwu/ui-dom`(UIRouter + uiServe),见 [frontend-ui-dom.md](frontend-ui-dom.md)。
4
4
 
5
5
  > 本页为 weifuwu 官方文档拆分页 · [返回 README](../README.md)
6
6
 
7
- ## router + RouteView — 前端路由
7
+ ## UIRouter — 前端路由
8
8
 
9
9
  ```tsx
10
- import { createApp, router, RouteView } from 'weifuwu/client'
11
- import type { RouteDef, WfuiContext } from 'weifuwu/client'
12
-
13
- const routes: RouteDef[] = [
14
- { path: '/', component: Home },
15
- { path: '/users', component: UserList },
16
- { path: '/users/:id', component: UserDetail },
17
- ]
18
-
19
- createApp()
20
- .use(router({
21
- routes,
22
- mode: 'history', // 或 'hash'
23
- notFound: NotFoundPage,
24
- }))
25
- .mount('#root', () => () => <RouteView />) // 根组件也要两阶段:外层返回 render 函数
10
+ import { UIRouter, uiServe, h } from 'weifuwu/ui-dom'
11
+ import type { UIHandler } from 'weifuwu/ui-dom'
12
+
13
+ const app = new UIRouter()
14
+ app.get('/', () => h(Home, {}))
15
+ app.get('/users', () => h(UserList, {}))
16
+ app.get('/users/:id', (loc, ctx) => h(UserDetail, { id: ctx.params.id }))
17
+ app.notFound(() => h(NotFound, {}))
18
+ app.use(toast()) // 中间件(ctx 注入)——app.use(mw)
19
+
20
+ uiServe(app, { root: '#root' }) // 客户端落地(hydrate: true 收养 SSR HTML)
26
21
  ```
27
22
 
28
- ### 嵌套布局
23
+ ### 嵌套布局(中间件两阶段)
29
24
 
30
25
  ```tsx
31
- const routes = [
32
- {
33
- path: '/dashboard',
34
- layout: DashboardLayout, // 持久布局(包含 RouteView)
35
- children: [
36
- { path: '/overview', component: Overview },
37
- { path: '/settings', component: Settings },
38
- ],
39
- },
40
- ]
41
-
42
- function DashboardLayout(_props: {}, ctx: WfuiContext) {
43
- return (props) => (
44
- <div style="display:flex">
45
- <aside>导航菜单</aside>
46
- <main><RouteView /></main> {/* 渲染子路由 */}
47
- </div>
48
- )
26
+ const DashboardLayout: UIMiddleware = async (_loc, ctx, children) => {
27
+ const $ = ctx.ui.$()
28
+ $.open = true
29
+ return async (loc, c) => {
30
+ const child = await children(loc, c) // 子路由/嵌套路由内容
31
+ return h('div', { class: 'wf-row' },
32
+ h('aside', {}, '导航菜单'),
33
+ h('main', {}, child),
34
+ )
35
+ }
49
36
  }
37
+ app.use(DashboardLayout)
50
38
  ```
51
39
 
52
40
  ### 编程式导航
53
41
 
54
42
  ```tsx
55
- // 在任意组件中
43
+ // 在任意组件/页面中(uiServe 注入)
56
44
  ctx.app?.navigate('/users/123?tab=profile')
57
45
  ```
58
46
 
59
47
  | ctx 注入 | 类型 | 说明 |
60
48
  |----------|------|------|
49
+ | `ctx.params` / `ctx.query` | `Record<string, string>` | 路由参数 / URL query(顶层注入) |
61
50
  | `ctx.route.path` | `string` | 当前路由路径 |
62
51
  | `ctx.route.params` | `Record<string, string>` | URL 参数 |
63
52
  | `ctx.route.query` | `Record<string, string>` | 查询参数 |
64
53
  | `ctx.app.navigate(path)` | `(string) => void` | 编程式导航 |
65
54
 
66
- | RouterOptions | 类型 | 默认值 | 说明 |
67
- |---------------|------|--------|------|
68
- | `routes` | `RouteDef[]` | — | 路由定义 |
69
- | `mode` | `'history' \| 'hash'` | `'history'` | 路由模式 |
70
- | `notFound` | `Component` | — | 404 页面 |
55
+ | UIRouter API | 签名 | 说明 |
56
+ |--------------|------|------|
57
+ | `get(path, handler)` | `(string, UIHandler)` | 页面路由(handler = async 组件:`async (location, ctx) => vnode`) |
58
+ | `use(prefix, sub)` / `use(mw)` | `(string, UIRouter) \| (mw)` | 子路由树挂载 / 中间件(ctx 注入) |
59
+ | `notFound(handler)` | `(UIHandler)` | 404 页面 |
60
+ | `mode` | `'history' \| 'hash'` | 路由模式(默认 history) |
71
61
 
72
- | RouteDef | 类型 | 说明 |
73
- |----------|------|------|
74
- | `path` | `string` | 路径(支持 `:param`) |
75
- | `component` | `Component` | 页面组件 |
76
- | `layout` | `Component` | 布局组件(内含 `<RouteView />`) |
77
- | `children` | `RouteDef[]` | 子路由 |
78
- | `auth` | `boolean` | 是否需要认证(配合 auth 中间件) |
79
- | `title` | `string` | 页面标题(自动设置 `document.title`) |
62
+ | UIHandler | 类型 | 说明 |
63
+ |-----------|------|------|
64
+ | 签名 | `(location, ctx) => Promise<VNode> \| VNode` | 页面 = 异步组件(`ctx.data.get` 三场景;async 组件无需包装) |
65
+ | 返回值 | `VNode \| null` | 数据结构(落地由 uiServe/ssrPage 决定) |
80
66
 
81
67
  ---
82
68
 
83
69
  ## api — HTTP 客户端中间件
84
70
 
85
71
  ```tsx
86
- import { createApp, api } from 'weifuwu/client'
72
+ import { UIRouter, api, uiServe } from 'weifuwu/ui-dom'
87
73
 
88
- createApp()
89
- .use(api({ baseURL: '/api' }))
90
- .mount('#root', App)
74
+ const app = new UIRouter()
75
+ app.use(api({ baseURL: '/api' }))
76
+ uiServe(app, { root: '#root' })
91
77
 
92
78
  // 在组件中使用
93
79
  async function loadUsers(ctx: WfuiContext) {
@@ -139,11 +125,11 @@ try {
139
125
  ## auth — 认证中间件
140
126
 
141
127
  ```tsx
142
- import { createApp, auth } from 'weifuwu/client'
128
+ import { UIRouter, auth, uiServe } from 'weifuwu/ui-dom'
143
129
 
144
- createApp()
145
- .use(auth())
146
- .mount('#root', App)
130
+ const app = new UIRouter()
131
+ app.use(auth())
132
+ uiServe(app, { root: '#root' })
147
133
 
148
134
  // 在组件中
149
135
  function Profile(_props: {}, ctx: WfuiContext) {
@@ -191,11 +177,11 @@ await ctx.auth?.refresh() // → boolean
191
177
  ## ws — WebSocket 客户端中间件
192
178
 
193
179
  ```tsx
194
- import { createApp, ws } from 'weifuwu/client'
180
+ import { UIRouter, ws, uiServe } from 'weifuwu/ui-dom'
195
181
 
196
- createApp()
197
- .use(ws({ url: '/ws' }))
198
- .mount('#root', App)
182
+ const app = new UIRouter()
183
+ app.use(ws({ url: '/ws' }))
184
+ uiServe(app, { root: '#root' })
199
185
 
200
186
  // 发送消息
201
187
  ctx.ws?.send({ type: 'chat', body: 'hello' })
@@ -231,10 +217,10 @@ unsubscribe?.()
231
217
  ## i18n — 国际化中间件
232
218
 
233
219
  ```tsx
234
- import { createApp, i18n } from 'weifuwu/client'
220
+ import { UIRouter, i18n, uiServe } from 'weifuwu/ui-dom'
235
221
 
236
- createApp()
237
- .use(i18n({
222
+ const app = new UIRouter()
223
+ app.use(i18n({
238
224
  locale: 'zh-CN',
239
225
  messages: {
240
226
  'title': '仪表盘',
@@ -268,7 +254,7 @@ ctx.i18n?.setLocale('en-US')
268
254
  内置语言包:
269
255
 
270
256
  ```ts
271
- import { zhCN, enUS } from 'weifuwu/client'
257
+ import { zhCN, enUS } from 'weifuwu/ui-dom'
272
258
  ```
273
259
 
274
260
  - `zh-CN`:默认中文
@@ -283,7 +269,7 @@ import { zhCN, enUS } from 'weifuwu/client'
283
269
  ## ErrorBoundary — 错误边界
284
270
 
285
271
  ```tsx
286
- import { ErrorBoundary } from 'weifuwu/client'
272
+ import { ErrorBoundary } from 'weifuwu/ui-dom'
287
273
 
288
274
  <ErrorBoundary fallback={<p>出错了,请刷新页面</p>}>
289
275
  <UserProfile />
@@ -316,12 +302,12 @@ import { ErrorBoundary } from 'weifuwu/client'
316
302
  **① 命令式 `ctx.confirm()`(推荐,操作前询问)**
317
303
 
318
304
  ```tsx
319
- import { createApp } from 'weifuwu/client'
305
+ import { UIRouter, uiServe } from 'weifuwu/ui-dom'
320
306
  import { confirm } from 'weifuwu/components'
321
307
 
322
- createApp()
323
- .use(confirm())
324
- .mount('#root', App)
308
+ const app = new UIRouter()
309
+ app.use(confirm())
310
+ uiServe(app, { root: '#root' })
325
311
 
326
312
  // 任意代码中(组件事件、async 逻辑)
327
313
  async function handleDelete(ctx: WfuiContext) {
@@ -372,12 +358,12 @@ import { Confirm } from 'weifuwu/components'
372
358
  `ctx.toast()` 是 `<Toast>` 组件的全局命令式封装:任意代码中一行调用,自动消失、自动清理,无需宿主状态。
373
359
 
374
360
  ```tsx
375
- import { createApp } from 'weifuwu/client'
361
+ import { UIRouter, uiServe } from 'weifuwu/ui-dom'
376
362
  import { toast } from 'weifuwu/components'
377
363
 
378
- createApp()
379
- .use(toast({ position: 'top-right', duration: 3000, max: 3 }))
380
- .mount('#root', App)
364
+ const app = new UIRouter()
365
+ app.use(toast({ position: 'top-right', duration: 3000, max: 3 }))
366
+ uiServe(app, { root: '#root' })
381
367
 
382
368
  // 任意代码中(组件事件、api 拦截器、WS 回调、定时器)
383
369
  ctx.toast?.('保存成功', 'success')
@@ -400,8 +386,8 @@ ctx.toast?.('普通消息') // 默认 type = 'info'
400
386
  ## ScrollLock / FocusTrap
401
387
 
402
388
  ```tsx
403
- import { lockScroll, unlockScroll } from 'weifuwu/client'
404
- import { trapFocus } from 'weifuwu/client'
389
+ import { lockScroll, unlockScroll } from 'weifuwu/ui-dom'
390
+ import { trapFocus } from 'weifuwu/ui-dom'
405
391
 
406
392
  // 锁定/解锁滚动(支持嵌套计数)
407
393
  lockScroll()
@@ -423,7 +409,7 @@ cleanup() // 恢复之前的焦点
423
409
  ## extendCtx — 上下文扩展
424
410
 
425
411
  ```tsx
426
- import { extendCtx } from 'weifuwu/client'
412
+ import { extendCtx } from 'weifuwu/ui-dom'
427
413
 
428
414
  // 在 AppMiddleware 中创建新 ctx,原 ctx getter 通过原型链继承
429
415
  function myMw(ctx: WfuiContext): WfuiContext {
@@ -3,7 +3,7 @@
3
3
  > **weifuwu 前端唯一运行时**(`weifuwu/ui-dom`,随 npm 包发布):
4
4
  > **UIRouter(纯路由 + ctx 注入链)+ uiServe(渲染运行时)+ ssrPage/hydration**,
5
5
  > 复用 `weifuwu/components`(VNode 契约唯一来源,组件零修改)。
6
- > 已取代 `weifuwu/client`(createApp/router 已删除)。
6
+ > 已取代 `weifuwu/client`——前端运行时唯一入口(createApp/router 旧 API 已删除)。
7
7
  >
8
8
  > 概念对齐:**req = window.location**,**res = VNode**(数据结构),
9
9
  > **uiServe = VDOM**(落地机制),**params/query 在 ctx**(对齐后端 `ctx.params`)。
@@ -161,5 +161,7 @@ uiServe(app, { root: '#root', hydrate: true })
161
161
  ## demo
162
162
 
163
163
  `apps/ui-router-demo`:UIRouter + uiServe + components(Button/Input/Tag/Dropdown)
164
- + toast 注入 + 嵌套路由 + SSR/hydrate 端到端。
164
+ + toast 注入 + 嵌套路由 + SSR/hydrate 端到端(原生 async 组件页 `/async` 验证
165
+ 占位补全 + `__DATA__` 三场景)。server 用 weifuwu serve + `ui()` 中间件
166
+ (`ctx.ui.js` 编译前端 / `ctx.ui.css` 组件样式 / `/*` → ssrPage)。
165
167
  启动:`node apps/ui-router-demo/server.ts` → http://localhost:3100