weifuwu 0.52.0 → 0.54.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 CHANGED
@@ -8,6 +8,11 @@ npm install weifuwu
8
8
 
9
9
  一个包 = 后端 (`weifuwu`) + 前端 (`weifuwu/client`) + 组件库 (`weifuwu/components`) + 布局系统 (`weifuwu/layout`)。
10
10
 
11
+ > ⚠️ **注意:前后端都有 `ctx.ui`,但用途完全不同**
12
+ > - **后端** `ctx.ui`(SSR/编译):`ctx.ui.html`(HTML 模板)、`ctx.ui.js`(TSX→JS 动态编译)、`ctx.ui.css`(CSS 编译)
13
+ > - **前端** `ctx.ui`(渲染引擎):`ctx.ui.$()`(响应式状态)、`ctx.ui.render()` / `dirty()`(渲染控制)、`useMedia()` / `useBreakpoint()` / `usePopupPosition()`(浏览器事件监听)
14
+ > 后端的是「把页面和代码交给浏览器」,前端的是「在浏览器里驱动 UI」。
15
+
11
16
  ---
12
17
 
13
18
  ## 设计理念
@@ -61,9 +66,13 @@ const Home: Component = () => () => <h1>Hello weifuwu</h1>
61
66
 
62
67
  createApp()
63
68
  .use(router({ routes: [{ path: '/', component: Home }] }))
64
- .mount('#root', () => <RouteView />)
69
+ .mount('#root', () => () => <RouteView />) // 根组件也要两阶段:外层返回 render 函数
65
70
  ```
66
71
 
72
+ 运行 `node server.ts`,访问 `http://localhost:3000` 即可看到页面。后端 `ctx.ui.js()` 会实时编译 `src/main.tsx`,改代码刷新即生效,无需任何构建步骤。
73
+
74
+ > 想**零后端、零构建**最快跑起来?直接跳到下面的「CDN 快速原型」。
75
+
67
76
  ---
68
77
 
69
78
  ## CDN 快速原型(零构建、纯 HTML)
@@ -82,7 +91,7 @@ createApp()
82
91
  <!-- 组件样式(可选,如只用 weifuwu/client 则不需要) -->
83
92
  <link
84
93
  rel="stylesheet"
85
- href="https://unpkg.com/weifuwu@0.51.0/dist/components/style.css"
94
+ href="https://unpkg.com/weifuwu@latest/dist/components/style.css"
86
95
  />
87
96
  </head>
88
97
  <body>
@@ -92,8 +101,8 @@ createApp()
92
101
  <script type="importmap">
93
102
  {
94
103
  "imports": {
95
- "weifuwu/client": "https://unpkg.com/weifuwu@0.51.0/dist/client/index.js",
96
- "weifuwu/components": "https://unpkg.com/weifuwu@0.51.0/dist/components/index.js"
104
+ "weifuwu/client": "https://unpkg.com/weifuwu@latest/dist/client/index.js",
105
+ "weifuwu/components": "https://unpkg.com/weifuwu@latest/dist/components/index.js"
97
106
  }
98
107
  }
99
108
  </script>
@@ -137,12 +146,11 @@ createApp()
137
146
 
138
147
  | 资源 | CDN 地址 | 说明 |
139
148
  |------|---------|------|
140
- | `weifuwu/client` | `https://unpkg.com/weifuwu@0.51.0/dist/client/index.js` | 客户端核心(createApp, h, 路由, 状态管理等) |
141
- | `weifuwu/components` | `https://unpkg.com/weifuwu@0.51.0/dist/components/index.js` | 41 个 UI 组件(Button, Card, Table, Modal 等) |
142
- | 组件样式 | `https://unpkg.com/weifuwu@0.51.0/dist/components/style.css` | 组件 CSS + 72 个主题 Token + 35 个布局原语 |
143
- | 独立布局系统 | `https://unpkg.com/weifuwu@0.51.0/dist/layout/weifuwu-layout.css` | 仅 CSS 布局,不依赖 JS |
149
+ | `weifuwu/client` | `https://unpkg.com/weifuwu@latest/dist/client/index.js` | 客户端核心(createApp, h, 路由, 状态管理等) |
150
+ | `weifuwu/components` | `https://unpkg.com/weifuwu@latest/dist/components/index.js` | 41 个 UI 组件(Button, Card, Table, Modal 等) |
151
+ | 组件样式 | `https://unpkg.com/weifuwu@latest/dist/components/style.css` | 组件 CSS + 72 个主题 Token + 35 个布局原语 |
152
+ | 独立布局系统 | `https://unpkg.com/weifuwu@latest/dist/layout/weifuwu-layout.css` | 仅 CSS 布局,不依赖 JS |
144
153
 
145
- > 提示:将 `@0.51.0` 替换为 `@latest` 始终使用最新版,或固定版本避免意外变更。
146
154
 
147
155
  ---
148
156
 
@@ -166,9 +174,9 @@ createApp()
166
174
  | `weifuwu/client` | **api / auth / ws** | HTTP 客户端 / 认证 / WebSocket 中间件 | createApp |
167
175
  | `weifuwu/client` | **i18n** | 国际化中间件(运行时切换语言) | createApp |
168
176
  | `weifuwu/client` | **ErrorBoundary** | 错误边界组件 | createApp |
169
- | `weifuwu/client` | **confirm** | Promise 化确认对话框 | createApp |
170
177
  | `weifuwu/client` | **lockScroll/trapFocus** | 滚动锁定 / 焦点陷阱工具 | — |
171
- | `weifuwu/components` | **41 个组件** | Button/Table/Modal/Toast/... | weifuwu/client |
178
+ | `weifuwu/client` | **popup** | 弹层 fixed 定位工具(`computeFixedPos` / `computeFixedPosRect`) | |
179
+ | `weifuwu/components` | **42 个组件** | Button/Table/Modal/Confirm/Toast/... + `confirm()` / `toast()` 命令式中间件 | weifuwu/client |
172
180
  | `weifuwu/layout` | **CSS 布局** | 35 个布局原语 + 72 个主题 Token(也支持 `weifuwu/layout/style.css`) | — |
173
181
 
174
182
  ---
@@ -890,6 +898,20 @@ h('div', { class: 'x' }, child1, child2)
890
898
 
891
899
  ## 状态管理
892
900
 
901
+ ### ctx.ui 方法速查
902
+
903
+ | 方法 | 签名 | 一句话说明 |
904
+ |------|------|-----------|
905
+ | `$()` | `$(): Record<string, any>` | 深度 Proxy 响应式状态容器,赋值自动触发渲染(**推荐首选**) |
906
+ | `render()` | `render(ids?: string[])` | 同步强制渲染;无参 = 当前组件,传参 = 指定组件列表 |
907
+ | `dirty()` | `dirty(ids?: string[])` | 异步渲染(微任务批处理合并);`$` 内部就是调它 |
908
+ | `selfId()` | `selfId(name: string)` | 注册组件自定义 ID,配合 `render(['id'])` 跨组件精准刷新 |
909
+ | `useMedia()` | `useMedia(query, cb)` | 响应式媒体查询,断点变化时自动回调 |
910
+ | `useBreakpoint()` | `useBreakpoint(cb \| bps, cb?)` | 命名断点 mobile/tablet/desktop |
911
+ | `usePopupPosition()` | `usePopupPosition(opts)` | 弹层坐标跟随:scroll/resize 时自动重算 fixed 坐标 |
912
+
913
+ > 每个方法的完整说明见下文对应章节。
914
+
893
915
  ### Render 机制总览
894
916
 
895
917
  | API | 触发时机 | 渲染方式 | 作用域 | 使用场景 |
@@ -898,6 +920,9 @@ h('div', { class: 'x' }, child1, child2)
898
920
  | `ctx.ui.dirty()` | 主动调用 | 微任务批量(异步) | 当前/指定 | **绕过 Proxy 后手动标记** |
899
921
  | `ctx.ui.render()` | 主动调用 | 立即同步 | 当前/指定 | **需要立即拿到最新 DOM** — DOM 测量、动画触发 |
900
922
  | `ctx.ui.render(['id'])` | 主动调用 | 立即同步 | 指定组件 | **跨组件精准刷新** — 全局事件、Portal 远程控制 |
923
+ | `ctx.ui.useMedia()` | 注册监听 | 浏览器事件驱动 | 当前组件 | **响应式媒体查询** — 断点变化时自动 dirty |
924
+ | `ctx.ui.useBreakpoint()` | 注册监听 | 浏览器事件驱动 | 当前组件 | **命名断点** — mobile/tablet/desktop 自动 dirty |
925
+ | `ctx.ui.usePopupPosition()` | 注册监听 | 浏览器事件驱动 | 当前组件 | **弹层坐标跟随** — scroll/resize 时自动重算 fixed 坐标 |
901
926
 
902
927
  `render()` 和 `dirty()` 无参 = 当前组件,传参 = 指定组件列表。三套 API 同一 scope 机制。
903
928
 
@@ -945,10 +970,152 @@ const FormPage: Component = (_init, ctx) => {
945
970
  - 不需要触发渲染的内部缓存(用闭包变量 `let`)
946
971
  - 简单组件只有一两个状态变量(闭包变量 + `render()` 更轻量)
947
972
 
973
+ ### 响应式自适应组件
974
+
975
+ #### `ctx.ui.useMedia(query, callback)` — 响应式媒体查询
976
+
977
+ 注册媒体查询监听,值变化时自动调用 callback(callback 内赋值 `$` 触发 dirty):
978
+
979
+ ```tsx
980
+ const Card = (_init, ctx) => {
981
+ const $ = ctx.ui.$()
982
+ $.isMobile = false
983
+ // 立即回调一次(取当前值),之后变化时自动重新回调
984
+ ctx.ui.useMedia('(max-width: 640px)', (v) => { $.isMobile = v })
985
+
986
+ return (props) => (
987
+ <div class={$.isMobile ? 'wf-stack' : 'wf-row'}>
988
+ {!$.isMobile && <Sidebar />}
989
+ <Content />
990
+ </div>
991
+ )
992
+ }
993
+ ```
994
+
995
+ `callback` 在 mount 时立即执行一次,之后断点变化时再次执行。赋值给 `$` 的属性自动触发渲染。
996
+
997
+ #### `ctx.ui.useBreakpoint(callback)` — 命名断点
998
+
999
+ 预设三个断点名称:`mobile`(<640px)、`tablet`(640-1023px)、`desktop`(≥1024px):
1000
+
1001
+ ```tsx
1002
+ const Layout = (_init, ctx) => {
1003
+ const $ = ctx.ui.$()
1004
+ ctx.ui.useBreakpoint((vp) => { $.vp = vp })
1005
+
1006
+ return (props) =>
1007
+ <div class={`sidebar-${$.vp}`}>
1008
+ {$.vp === 'mobile' ? <BottomNav /> : <SideNav />}
1009
+ {$.vp === 'mobile' ? <MobileContent /> : <Content />}
1010
+ </div>
1011
+ }
1012
+ ```
1013
+
1014
+ 也支持自定义断点:
1015
+
1016
+ ```tsx
1017
+ ctx.ui.useBreakpoint(
1018
+ { narrow: '(max-width: 480px)', wide: '(min-width: 1200px)' },
1019
+ (vp) => { $.size = vp },
1020
+ )
1021
+ ```
1022
+
1023
+ #### `ctx.ui.usePopupPosition(options)` — 弹层坐标跟随
1024
+
1025
+ 解决弹出层(Popover / Tooltip / Dropdown / DatePicker 等)在 **页面滚动 / 窗口缩放后不跟随触发元素** 的问题。基于 `position: fixed` + `getBoundingClientRect()`(视口坐标)的弹层,滚动后坐标需要重算——本 API 用全局 scroll/resize 监听(rAF 节流)自动重算并精准刷新当前组件。
1026
+
1027
+ ```tsx
1028
+ const DatePicker = (_init, ctx) => {
1029
+ let show = false
1030
+ let inputEl: HTMLElement | null = null
1031
+ let prevOpen = false
1032
+
1033
+ // mount 阶段注册:scroll/resize 时自动重算 pos
1034
+ const pos = ctx.ui.usePopupPosition({
1035
+ el: () => inputEl, // 锚定元素(ref 保存)
1036
+ isOpen: () => show, // 弹层是否显示
1037
+ compute: (r) => ({ top: r.bottom + 4, left: r.left }), // rect → 坐标
1038
+ })
1039
+
1040
+ return (props) => {
1041
+ const isOpen = show
1042
+ // 打开瞬间算一次初始坐标(受控/非受控统一覆盖)
1043
+ if (isOpen && !prevOpen) pos.refresh()
1044
+ prevOpen = isOpen
1045
+
1046
+ return h('div', {}, [
1047
+ h('input', {
1048
+ ref: (el) => { inputEl = el as HTMLElement },
1049
+ onClick: () => { show = !show; ctx.ui.render() },
1050
+ }),
1051
+ isOpen ? h('div', { style: { top: pos.top, left: pos.left } }) : null,
1052
+ ].filter(Boolean))
1053
+ }
1054
+ }
1055
+ ```
1056
+
1057
+ 要点:
1058
+
1059
+ - `pos` 是稳定对象,render 闭包直接读取 `top/left/width`,滚动重算原地更新,无需重新绑定
1060
+ - `pos.refresh()` 只重算不渲染——配合打开路径上已有的 `render()`,避免重复渲染
1061
+ - 监听是**全局单例**(capture 捕获所有嵌套滚动容器 + rAF 节流),按组件 selfId 注册,组件多时开销 O(1)
1062
+ - `compute` 是纯函数(rect → 坐标),可单独单测
1063
+
1064
+ 已内置接入的组件:**Popover / Tooltip / Dropdown / DatePicker / Chart**(tooltip)——它们的弹出层在页面滚动、嵌套容器滚动、窗口缩放时都会自动跟随触发元素,无需额外配置。
1065
+
1066
+ #### `ctx.ui.selfId(name)` — 跨组件精准刷新
1067
+
1068
+ 用于全局事件通知、Portal 远程控制、兄弟组件协调等场景——绕过多层 props 传递,直接按 ID 刷新目标组件:
1069
+
1070
+ ```tsx
1071
+ // 组件 A:mount 阶段注册自定义 ID
1072
+ const StatsPanel = (_init, ctx) => {
1073
+ ctx.ui.selfId('stats')
1074
+ const $ = ctx.ui.$()
1075
+ $.data = []
1076
+ return (props) => h('div', {}, String($.data.length))
1077
+ }
1078
+
1079
+ // 组件 B(或其他任何地方)用 ID 精准刷新
1080
+ ctx.ui.render(['stats']) // 同步刷新
1081
+ // 或:ctx.ui.dirty(['stats']) // 异步批处理版本
1082
+ ```
1083
+
1084
+ **语义**:
1085
+
1086
+ - 必须在 **mount 阶段**调用(组件初始化时),注册后组件即可被 `render(['id'])` / `dirty(['id'])` 精准定位
1087
+ - **同名冲突直接抛错**,每个自定义 ID 必须全局唯一
1088
+ - 配合 `selfId` 注册的组件在跨组件场景下无需把刷新逻辑层层传 props
1089
+
1090
+ #### CSS 层响应式(不碰 JS)
1091
+
1092
+ 配合 `weifuwu/layout` 的断点变体,纯 CSS 实现布局方向切换:
1093
+
1094
+ ```html
1095
+ <!-- 小屏堆叠,桌面并排 -->
1096
+ <div class="wf-stack wf-stack@md"></div>
1097
+
1098
+ <!-- 小屏隐藏侧栏 -->
1099
+ <aside class="wf-hidden wf-block@md"></aside>
1100
+ ```
1101
+
1102
+ 可用断点变体:
1103
+
1104
+ | 原语 | 变体 | 效果 |
1105
+ |------|------|------|
1106
+ | `wf-stack` | `@sm` `@md` `@lg` | 断点以上改为横向排列 |
1107
+ | `wf-row` | `@sm` `@md` `@lg` | 断点以上保持横向 |
1108
+ | `wf-hidden` | `@sm` `@md` `@lg` | 断点以上隐藏 |
1109
+ | `wf-block` | `@sm` `@md` `@lg` | 断点以上显示 |
1110
+
1111
+ 断点尺寸:`--wf-bp-sm: 640px` / `--wf-bp-md: 768px` / `--wf-bp-lg: 1024px` / `--wf-bp-xl: 1280px`
1112
+
948
1113
  ### `ctx.ui.dirty()` — 异步标记脏
949
1114
 
950
1115
  异步版本,无参 = 当前组件,传参 = 指定组件列表。多次调用合并为一次微任务渲染。`$` 内部就是调 `dirty()`。
951
1116
 
1117
+ 与 `render()` 的区别:`dirty()` 是**异步**(微任务批量合并,同帧多次调用只渲染一次),`render()` 是**同步**(立即执行 VDOM diff + patch)。日常 UI 状态用 `$` 或 `dirty()`,需要立即拿到最新 DOM(测量/动画/第三方库)时用 `render()`。
1118
+
952
1119
  ### `ctx.ui.render()` — 同步强制渲染
953
1120
 
954
1121
  与 `dirty()` 的微任务批量不同,`render()` 是**同步执行**的。调用后立即执行 VDOM diff + patch,DOM 立刻更新。无参时只刷新当前组件,传参时可精准刷新指定组件。
@@ -1004,8 +1171,14 @@ ctx.ui.dirty(['stats']) // 批处理合并
1004
1171
 
1005
1172
  **性能说明**:
1006
1173
  - `$.x = val` 和 `dirty()` 都是微任务批量合并
1007
- - `render()` 每次调用都触发一次完整 diff/patch
1008
- - 三个入口同一套 scope 机制,不想要的渲染不触发
1174
+ - `render()` dirty 组件**向下**遍历(scope render),兄弟组件不遍历
1175
+ - **三态 skip 自动优化**:组件重新渲染时,框架自动检查三个维度:
1176
+ - **props**(含 children 元素级比较)——值没变则不渲染
1177
+ - **`$` 状态**——没被 dirty 标记则不渲染
1178
+ - **ctx 版本**——ctx 没变化则不渲染
1179
+ 三个条件全部满足时跳过整个子树(零 `_render` 调用、零 `patchValue` 遍历)
1180
+ - **lastIndex keyed diff**:列表 diff 采用正向 lastIndex 算法(React 同款),顺序不变时零 `insertBefore`。对比传统的逆序循环全量移动,DOM 修改从 O(N) 降到 O(0)。
1181
+ - 示例:DemoButton 点击一次,DOM 修改从 34 次降到 **1 次**(仅变更文本节点的 `textContent`)
1009
1182
 
1010
1183
  ### 实践建议
1011
1184
 
@@ -1027,7 +1200,7 @@ const DatePicker = (_init, ctx) => {
1027
1200
 
1028
1201
  ```tsx
1029
1202
  const OrderPage = (_init, ctx) => {
1030
- const $ = ctx.ui.$
1203
+ const $ = ctx.ui.$()
1031
1204
  $.orders = [] // $ 赋值自动触发渲染
1032
1205
  $.loading = false
1033
1206
  return (props) => h('div', {}, $.loading ? h(Spinner) : h(OrderList, { orders: $.orders }))
@@ -1038,6 +1211,17 @@ const OrderPage = (_init, ctx) => {
1038
1211
 
1039
1212
  同一个组件内可以按变量混用两种模式:需要渲染的用 `$`,不需要的用 `let`。
1040
1213
 
1214
+ ### VDOM diff 优化机制
1215
+
1216
+ weifuwu 的 VDOM 在每次 render 时自动执行**三态 skip 判定**,减少不必要的组件渲染和 DOM 操作:
1217
+
1218
+ ```
1219
+ canSkip = (props 没变) AND ($ 没脏) AND (ctx 版本一致)
1220
+ ↑ 值级浅比较 ↑ VNode dirty 标记 ↑ 全局版本号
1221
+ ```
1222
+
1223
+ 三个维度各自独立判断,AND 合并。任何一个维度说
1224
+
1041
1225
  ---
1042
1226
 
1043
1227
  ## 条件与列表
@@ -1132,7 +1316,7 @@ createApp()
1132
1316
  mode: 'history', // 或 'hash'
1133
1317
  notFound: NotFoundPage,
1134
1318
  }))
1135
- .mount('#root', () => <RouteView />)
1319
+ .mount('#root', () => () => <RouteView />) // 根组件也要两阶段:外层返回 render 函数
1136
1320
  ```
1137
1321
 
1138
1322
  ### 嵌套布局
@@ -1150,7 +1334,7 @@ const routes = [
1150
1334
  ]
1151
1335
 
1152
1336
  function DashboardLayout(_props: {}, ctx: WfuiContext) {
1153
- return (
1337
+ return (props) => (
1154
1338
  <div style="display:flex">
1155
1339
  <aside>导航菜单</aside>
1156
1340
  <main><RouteView /></main> {/* 渲染子路由 */}
@@ -1257,8 +1441,10 @@ createApp()
1257
1441
 
1258
1442
  // 在组件中
1259
1443
  function Profile(_props: {}, ctx: WfuiContext) {
1260
- if (!ctx.auth?.isLoggedIn) return <p>请登录</p>
1261
- return <p>欢迎, {ctx.auth?.user?.name}</p>
1444
+ return (props) => {
1445
+ if (!ctx.auth?.isLoggedIn) return <p>请登录</p>
1446
+ return <p>欢迎, {ctx.auth?.user?.name}</p>
1447
+ }
1262
1448
  }
1263
1449
 
1264
1450
  // 登录
@@ -1419,14 +1605,19 @@ import { ErrorBoundary } from 'weifuwu/client'
1419
1605
 
1420
1606
  ## confirm — 确认对话框
1421
1607
 
1608
+ 两种用法,共享同一视觉与行为(基于 Modal 封装):
1609
+
1610
+ **① 命令式 `ctx.confirm()`(推荐,操作前询问)**
1611
+
1422
1612
  ```tsx
1423
- import { createApp, confirm } from 'weifuwu/client'
1613
+ import { createApp } from 'weifuwu/client'
1614
+ import { confirm } from 'weifuwu/components'
1424
1615
 
1425
1616
  createApp()
1426
1617
  .use(confirm())
1427
1618
  .mount('#root', App)
1428
1619
 
1429
- // 在组件中使用
1620
+ // 任意代码中(组件事件、async 逻辑)
1430
1621
  async function handleDelete(ctx: WfuiContext) {
1431
1622
  const ok = await ctx.confirm?.('确定删除这条记录?', {
1432
1623
  title: '确认删除',
@@ -1440,17 +1631,63 @@ async function handleDelete(ctx: WfuiContext) {
1440
1631
  }
1441
1632
  ```
1442
1633
 
1634
+ **② 声明式 `<Confirm>`(需要受控状态时)**
1635
+
1636
+ ```tsx
1637
+ import { Confirm } from 'weifuwu/components'
1638
+
1639
+ <Confirm
1640
+ open={confirming}
1641
+ title="确认删除"
1642
+ message="确定删除这条记录?"
1643
+ confirmText="删除"
1644
+ variant="danger"
1645
+ onConfirm={() => doDelete()}
1646
+ onCancel={() => setConfirming(false)}
1647
+ />
1648
+ ```
1649
+
1443
1650
  | ConfirmOptions | 类型 | 默认值 | 说明 |
1444
1651
  |----------------|------|--------|------|
1445
1652
  | `title` | `string` | `'确认操作'` | 对话框标题 |
1446
1653
  | `confirmText` | `string` | `'确定'` | 确认按钮文字 |
1447
1654
  | `cancelText` | `string` | `'取消'` | 取消按钮文字 |
1448
1655
  | `variant` | `'primary' \| 'danger'` | `'primary'` | 按钮样式变体 |
1656
+ | `width` | `string` | Modal 默认 | 对话框宽度 |
1657
+
1658
+ - `ctx.confirm()` 返回 `Promise<boolean>`,ESC / 点击遮罩 / 取消 → resolve(false)
1659
+ - 组件化渲染(Modal + portal),自动锁定滚动 + 焦点陷阱,i18n 文案可配置
1660
+ - 多次调用各自独立渲染(叠放语义),互不干扰
1661
+
1662
+ ---
1663
+
1664
+ ## toast — 命令式消息提示
1665
+
1666
+ `ctx.toast()` 是 `<Toast>` 组件的全局命令式封装:任意代码中一行调用,自动消失、自动清理,无需宿主状态。
1667
+
1668
+ ```tsx
1669
+ import { createApp } from 'weifuwu/client'
1670
+ import { toast } from 'weifuwu/components'
1449
1671
 
1450
- - 直接 DOM 渲染(不经过 VDOM)
1451
- - 返回 `Promise<boolean>`
1452
- - ESC / 点击遮罩 → resolve(false)
1453
- - 自动锁定背景滚动
1672
+ createApp()
1673
+ .use(toast({ position: 'top-right', duration: 3000, max: 3 }))
1674
+ .mount('#root', App)
1675
+
1676
+ // 任意代码中(组件事件、api 拦截器、WS 回调、定时器)
1677
+ ctx.toast?.('保存成功', 'success')
1678
+ ctx.toast?.('请求失败', 'error')
1679
+ ctx.toast?.('普通消息') // 默认 type = 'info'
1680
+ ```
1681
+
1682
+ | ToastOptions | 类型 | 默认值 | 说明 |
1683
+ |-------------|------|--------|------|
1684
+ | `position` | `ToastPosition` | `'top-right'` | 容器位置 |
1685
+ | `duration` | `number` | `3000` | 默认自动消失时间(ms),0 = 不消失 |
1686
+ | `max` | `number` | `3` | 最大显示条数,超出移除最早 |
1687
+
1688
+ 单条可覆盖自动消失时间:`ctx.toast('慢一点消失', 'info', 5000)`。
1689
+
1690
+ 与声明式 `<Toast toasts={...}/>` 共存:声明式用于局部列表(合并消息、自定义布局),命令式用于全局一次性反馈。
1454
1691
 
1455
1692
  ---
1456
1693
 
@@ -1500,7 +1737,9 @@ import type { ApiClient, ApiOptions, ApiRequestOptions, ApiError } from 'weifuwu
1500
1737
  import type { AuthClient, AuthOptions } from 'weifuwu/client'
1501
1738
  import type { ErrorBoundaryProps } from 'weifuwu/client'
1502
1739
  import type { I18nOptions, I18nState, LocalePackage } from 'weifuwu/client'
1503
- import type { ConfirmOptions, ConfirmState } from 'weifuwu/client'
1740
+ import type { PopupPositionOptions, PopupPosition } from 'weifuwu/client'
1741
+ import type { ConfirmProps, ConfirmOptions } from 'weifuwu/components'
1742
+ import type { ToastOptions, ToastPosition } from 'weifuwu/components'
1504
1743
  import type { RouterOptions } from 'weifuwu/client'
1505
1744
  ```
1506
1745
 
@@ -1509,7 +1748,7 @@ import type { RouterOptions } from 'weifuwu/client'
1509
1748
  | `VNode` | `{ type, props, key? }` |
1510
1749
  | `VNodeType` | `string \| Component \| typeof Fragment` |
1511
1750
  | `Component<P>` | `(initProps: P, ctx: WfuiContext) => (props: P) => VNode \| null` |
1512
- | `WfuiContext` | `{ ui, route?, app?, ws?, api?, auth?, i18n?, confirm?, [key]: unknown }` |
1751
+ | `WfuiContext` | `{ ui, route?, app?, ws?, api?, auth?, i18n?, confirm?, toast?, [key]: unknown }` |
1513
1752
  | `AppMiddleware` | `(ctx: WfuiContext) => WfuiContext` |
1514
1753
  | `RouteDef` | `{ path, component?, layout?, children?, auth?, title? }` |
1515
1754
  | `ApiClient` | `{ get, post, put, patch, delete }` |
@@ -1518,13 +1757,17 @@ import type { RouterOptions } from 'weifuwu/client'
1518
1757
  | `I18nOptions` | `{ locale?, messages?, components? }` |
1519
1758
  | `I18nState` | `{ locale, t, setLocale, components }` |
1520
1759
  | `ErrorBoundaryProps` | `{ fallback?, children? }` |
1521
- | `ConfirmOptions` | `{ title?, confirmText?, cancelText?, variant? }` |
1760
+ | `ConfirmProps` | `{ open?, title?, message?, confirmText?, cancelText?, variant?, width?, onConfirm?, onCancel? }` |
1761
+ | `ConfirmOptions` | `{ title?, confirmText?, cancelText?, variant?, width? }` — 命令式 ctx.confirm 选项 |
1762
+ | `ToastOptions` | `{ position?, duration?, max? }` — 命令式 ctx.toast 配置 |
1763
+ | `PopupPositionOptions` | `{ el, isOpen, compute }` — 弹层位置跟踪配置(见 usePopupPosition) |
1764
+ | `PopupPosition` | `{ top, left, width?, refresh }` — 弹层位置跟踪器 |
1522
1765
 
1523
1766
  ---
1524
1767
 
1525
1768
  # 组件库 (`weifuwu/components`)
1526
1769
 
1527
- 41 个 HTML 原语组件。每个是 `(props, ctx) => VNode` 纯函数,引用 `--wf-*` CSS 变量做主题。
1770
+ 42 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
1528
1771
 
1529
1772
  ```ts
1530
1773
  import { Button, Input, Table, Modal, Toast } from 'weifuwu/components'
@@ -1541,8 +1784,9 @@ import 'weifuwu/components/style.css' // 包含 Token + 35 布局原语 + 组
1541
1784
 
1542
1785
  // ├─ 输入框
1543
1786
  <Input placeholder="请输入邮箱" />
1544
- <Input label="用户名" required error="必填" />
1545
- <Input type="password" hint="至少6位" prefix="🔒" />
1787
+ <Input label="用户名" name="username" required error="必填" />
1788
+ <Input type="password" hint="至少6位" />
1789
+ <Input name="email" type="email" disabled placeholder="name@example.com" />
1546
1790
 
1547
1791
  // ├─ 选择器
1548
1792
  <Select options={[{ value: 'a', label: '选项A' }]} placeholder="请选择" />
@@ -1557,10 +1801,12 @@ import 'weifuwu/components/style.css' // 包含 Token + 35 布局原语 + 组
1557
1801
  <Table columns={[{ key: 'id', label: 'ID', sortable: true }, { key: 'name', label: '名称' }]}
1558
1802
  data={rows} sortKey="id" sortOrder="asc" onSort={(k, o) => setSort(k, o)} />
1559
1803
 
1560
- // ├─ 模态框 / 抽屉
1804
+ // ├─ 模态框 / 确认框 / 抽屉
1561
1805
  <Modal open={show} title="提示" onClose={() => setShow(false)} width="500px" closable>
1562
1806
  <p>确认删除?</p>
1563
1807
  </Modal>
1808
+ <Confirm open={confirming} message="确定删除?" variant="danger" onConfirm={doDelete} onCancel={() => setConfirming(false)} />
1809
+ // 命令式:await ctx.confirm?.('确定删除?') —— 组件里直接调用
1564
1810
  <Drawer open={open} title="详情" onClose={() => setOpen(false)} position="right">内容</Drawer>
1565
1811
 
1566
1812
  // ├─ 消息提示
@@ -1604,7 +1850,7 @@ import 'weifuwu/components/style.css' // 包含 Token + 35 布局原语 + 组
1604
1850
 
1605
1851
  // ├─ 表单验证
1606
1852
  <Form validation={{ email: [{ required: true, message: '请输入邮箱' }] }}
1607
- onSubmit={values => api.post('/login', values)}
1853
+ onSubmit={values => ctx.api?.post('/login', values)} // ctx.api 由中间件注入
1608
1854
  onError={errors => setErrors(errors)}>
1609
1855
  <Field label="邮箱" error={errors.email}>
1610
1856
  <Input name="email" />
@@ -1660,7 +1906,7 @@ props 变化 ──────────────────────
1660
1906
  | 组件 | 导入名 | 关键 Props | 说明 |
1661
1907
  |-----|--------|-----------|------|
1662
1908
  | Button | `Button` | `variant`, `size`, `loading`, `disabled`, `block`, `type` | 按钮 |
1663
- | Input | `Input` | `variant`, `size`, `placeholder`, `disabled`, `error`, `prefix`, `suffix` | 输入框 |
1909
+ | Input | `Input` | `label`, `name`, `type`, `value`, `placeholder`, `required`, `disabled`, `error`, `hint`, `onInput`, `onChange` | 输入框 |
1664
1910
  | Textarea | `Textarea` | `rows`, `resize`, `maxLength`, `error` | 文本域 |
1665
1911
  | Select | `Select` | `options: SelectOption[]`, `placeholder`, `searchable` | 下拉选择 |
1666
1912
 
@@ -1702,14 +1948,15 @@ props 变化 ──────────────────────
1702
1948
  | 组件 | 导入名 | 关键 Props | 说明 |
1703
1949
  |-----|--------|-----------|------|
1704
1950
  | Modal | `Modal` | `open`, `title`, `onClose`, `width`, `footer`, `closable` | 模态框 |
1951
+ | Confirm | `Confirm` | `open`, `message`, `confirmText`, `cancelText`, `variant`, `onConfirm`, `onCancel` | 确认对话框(同 `ctx.confirm()` 命令式) |
1705
1952
  | Drawer | `Drawer` | `open`, `title`, `onClose`, `position: DrawerPosition`, `width` | 抽屉 |
1706
- | Tooltip | `Tooltip` | `content`, `position: TooltipPosition`, `trigger` | 工具提示 |
1707
- | Popover | `Popover` | `content`, `position: PopoverPosition`, `trigger` | 弹出层 |
1953
+ | Tooltip | `Tooltip` | `content`, `position: TooltipPosition`, `disabled` | 工具提示(hover/focus 触发) |
1954
+ | Popover | `Popover` | `content`, `position: PopoverPosition`, `trigger`, `open`, `onOpenChange`, `disabled` | 弹出层 |
1708
1955
  | Toast | `Toast` | `items: ToastItem[]`, `position`, `max` | 消息提示 |
1709
1956
  | Alert | `Alert` | `variant: AlertVariant`, `title`, `closable`, `icon` | 警告提示 |
1710
1957
  | Loading | `Loading` | `size`, `text`, `fullscreen` | 加载中 |
1711
1958
  | EmptyState | `EmptyState` | `title`, `description`, `action`, `icon` | 空状态 |
1712
- | Skeleton | `Skeleton` | `variant: SkeletonVariant`, `rows`, `width`, `height` | 骨架屏 |
1959
+ | Skeleton | `Skeleton` | `variant: SkeletonVariant`, `lines`, `cols`, `width`, `height` | 骨架屏 |
1713
1960
 
1714
1961
  ### 导航组件
1715
1962
 
@@ -1717,7 +1964,7 @@ props 变化 ──────────────────────
1717
1964
  |-----|--------|-----------|------|
1718
1965
  | Breadcrumb | `Breadcrumb` | `items: BreadcrumbItem[]` | 面包屑 |
1719
1966
  | Tabs | `Tabs` | `items: TabItem[]`, `activeKey`, `onChange`, `type` | 标签页 |
1720
- | Dropdown | `Dropdown` | `items: DropdownItem[]`, `trigger`, `placement` | 下拉菜单 |
1967
+ | Dropdown | `Dropdown` | `trigger`, `items: DropdownItem[]`, `open` | 下拉菜单 |
1721
1968
  | Pagination | `Pagination` | `total`, `page`, `pageSize`, `onChange` | 分页 |
1722
1969
  | Steps | `Steps` | `items: StepItem[]`, `current`, `direction`, `size` | 步骤条 |
1723
1970
  | Accordion | `Accordion` | `items: AccordionItem[]`, `multiple`, `defaultActive` | 手风琴 |
@@ -1727,7 +1974,7 @@ props 变化 ──────────────────────
1727
1974
  | 组件 | 导入名 | 关键 Props | 说明 |
1728
1975
  |-----|--------|-----------|------|
1729
1976
  | Chart | `Chart` | `type: ChartType`, `data`, `options`, `title`, `area` | SVG 图表(line/bar/pie)|
1730
- | DatePicker | `DatePicker` | `mode: DatePickerMode`, `value`, `onChange`, `placeholder` | 日期选择器(date/datetime/time/range)|
1977
+ | DatePicker | `DatePicker` | `mode: DatePickerMode`, `value`, `onChange`, `placeholder`, `disabled` | 日期选择器(date/datetime/time/range)|
1731
1978
  | Editor | `Editor` | `value`, `onChange`, `toolbar`, `placeholder`, `disabled` | 富文本编辑器,零依赖 |
1732
1979
 
1733
1980
  ### 布局
@@ -1766,10 +2013,10 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
1766
2013
 
1767
2014
  | 类别 | 原语 | 效果 |
1768
2015
  |------|------|------|
1769
- | **排列** | `wf-stack` | 纵向 flex + gap |
1770
- | | `wf-stack-reverse` | 纵向反向 |
1771
- | | `wf-row` | 横向 flex + wrap + gap |
1772
- | | `wf-row-reverse` | 横向反向 |
2016
+ | **排列** | `wf-stack` `wf-stack@sm/md/lg` | 纵向 flex + gap(断点变体→横向) |
2017
+ | | `wf-stack-reverse` `@sm/md/lg` | 纵向反向 |
2018
+ | | `wf-row` `wf-row@sm/md/lg` | 横向 flex + wrap + gap |
2019
+ | | `wf-row-reverse` `@sm/md/lg` | 横向反向 |
1773
2020
  | | `wf-nowrap` | flex-wrap: nowrap |
1774
2021
  | | `wf-cluster` | 换行居中簇 |
1775
2022
  | **分布** | `wf-split` | justify-content: space-between |
@@ -1794,8 +2041,8 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
1794
2041
  | | `wf-container` | max-width + margin: auto |
1795
2042
  | | `wf-scroll` | overflow: auto |
1796
2043
  | | `wf-clip` | overflow: hidden |
1797
- | **显隐** | `wf-hidden` | display: none |
1798
- | | `wf-block` | display: block |
2044
+ | **显隐** | `wf-hidden` `wf-hidden@sm/md/lg` | display: none |
2045
+ | | `wf-block` `wf-block@sm/md/lg` | display: block |
1799
2046
  | | `wf-inline` | display: inline |
1800
2047
  | | `wf-inline-block` | display: inline-block |
1801
2048
  | | `wf-contents` | display: contents |
@@ -1947,7 +2194,7 @@ const LoginPage = (_init, ctx) => {
1947
2194
  },
1948
2195
  onSubmit: async (values) => {
1949
2196
  $.submitting = true
1950
- await api.post('/login', values)
2197
+ await ctx.api?.post('/login', values) // api 客户端由中间件注入 ctx.api
1951
2198
  $.submitting = false
1952
2199
  },
1953
2200
  onError: (errors) => { $.errors = errors },
@@ -1977,12 +2224,13 @@ const UserList = (_init, ctx) => {
1977
2224
  { id: 2, name: '李四', email: 'li@example.com', role: '编辑' },
1978
2225
  ]
1979
2226
 
1980
- const filtered = users.filter(u =>
1981
- !$.keyword || u.name.includes($.keyword) || u.email.includes($.keyword)
1982
- )
2227
+ return (props) => {
2228
+ // 派生数据必须在 render 内计算(每次 render 读最新 $.keyword
2229
+ const filtered = users.filter(u =>
2230
+ !$.keyword || u.name.includes($.keyword) || u.email.includes($.keyword)
2231
+ )
1983
2232
 
1984
- return (props) =>
1985
- h('div', { class: 'wf-stack', style: { gap: 'var(--wf-space-md)' } },
2233
+ return h('div', { class: 'wf-stack', style: { gap: 'var(--wf-space-md)' } },
1986
2234
  h('div', { class: 'wf-row', style: { justifyContent: 'space-between', alignItems: 'center' } },
1987
2235
  h(SearchInput, { placeholder: '搜索用户...', value: $.keyword, onSearch: (v: string) => { $.keyword = v } }),
1988
2236
  h(Button, { variant: 'primary' }, '新建用户'),
@@ -2002,6 +2250,7 @@ const UserList = (_init, ctx) => {
2002
2250
  }),
2003
2251
  h(Pagination, { total: filtered.length, page: 1, pageSize: 10, onChange: (p: number) => {} }),
2004
2252
  )
2253
+ }
2005
2254
  }
2006
2255
  ```
2007
2256
 
@@ -35,7 +35,5 @@ export { lockScroll, unlockScroll } from './scroll-lock.ts';
35
35
  export { trapFocus } from './focus-trap.ts';
36
36
  export { computeFixedPos } from './popup.ts';
37
37
  export type { FixedPos, Placement } from './popup.ts';
38
- export { confirm } from './confirm.ts';
39
- export type { ConfirmOptions, ConfirmState } from './confirm.ts';
40
38
  export { zhCN } from './locale/zh_CN.ts';
41
39
  export { enUS } from './locale/en_US.ts';