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 CHANGED
@@ -10,30 +10,17 @@ npm install weifuwu
10
10
 
11
11
  ---
12
12
 
13
- ## 模块总览
13
+ ## 设计理念
14
14
 
15
- | 导入路径 | 模块 | 用途 | 依赖 |
16
- |---------|------|------|------|
17
- | `weifuwu` | **Router** | Trie 路由 + 中间件链 + WebSocket + GraphQL | — |
18
- | `weifuwu` | **serve** | HTTP 服务器 | Router |
19
- | `weifuwu` | **cors** | CORS 跨域中间件 | Router |
20
- | `weifuwu` | **serveStatic** | 静态文件服务(ETag/304/目录索引) | Router |
21
- | `weifuwu` | **postgres** | PostgreSQL 连接池 `ctx.sql` | Router, DATABASE_URL |
22
- | `weifuwu` | **redis** | Redis 客户端 → `ctx.redis` | Router, REDIS_URL |
23
- | `weifuwu` | **ui** | SSR 渲染 + esbuild JS/CSS 动态编译 `ctx.ui` | Router |
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
- function Home() { return <h1>Hello weifuwu</h1> }
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>` | 读取/编译 CSS(自动 PostCSS + Tailwind) |
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
- - CSS 文件直接返回
451
- - 检测到 `postcss` + `@tailwindcss/postcss` 时自动编译 Tailwind CSS
452
- - mtime 缓存验证
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
- 零外部 npm 运行时依赖。组件模型:纯函数 `(props, ctx) => VNode`。
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
- | 生命周期 | `ctx.ui.onmount / onmounted / onunmount / onupdate` |
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
- ### 闭包变量 + `ctx.ui.render()`(推荐)
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
- ### `ctx.ui.$()` 响应式状态容器
834
+ 适合状态极少的简单组件。每次修改后手动调用 `ctx.ui.render()` 同步刷新 DOM。
835
+
836
+ ### `ctx.ui.$()` — 响应式 Proxy(推荐首选)
771
837
 
772
- `ctx.ui.$()` 返回浅 Proxy,赋值自动触发渲染(微任务批量合并):
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', { value: $.email, onInput: (e: any) => { $.email = e.target.value } })
846
+ h('input', {
847
+ value: $.email,
848
+ onInput: (e: any) => { $.email = e.target.value }
849
+ })
780
850
  }
781
851
  ```
782
852
 
783
- **注意**:mount/render/生命周期回调中 `$.x = val` 不触发渲染。仅事件/timer/Promise.then 中生效。
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
- | API | 说明 |
786
- |-----|------|
787
- | `ctx.ui.$()` | 创建响应式状态容器 |
788
- | `ctx.ui.render()` | 立即同步渲染 |
789
- | `ctx.ui.dirty()` | 标记脏状态,下个微任务批量渲染 |
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
- | API | 触发时机 | 参数 | 返回值 |
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
- ctx.ui.onmounted((el) => {
826
- timer = setInterval(() => console.log('tick'), 1000)
827
- return () => clearInterval(timer)
828
- })
829
-
830
- return (props) => h('div', {}, 'Timer')
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
- **注意**:`onmounted` 在首次渲染后触发,此时 DOM 已创建。生命周期回调中 `$.x = val` 不触发渲染(仅事件/timer 中生效)。
1001
+ `ref` 在元素创建时调用 `ref(el)`,元素移除时调用 `ref(null)`。
1002
+ `ref` 不接受返回值,清理逻辑直接在 `else` 分支处理。
835
1003
 
836
- 对于**内嵌元素**(非根元素),用 `el.querySelector()`:
1004
+ 对于**内嵌元素**(非根元素),直接在目标元素上放 `ref`:
837
1005
 
838
1006
  ```tsx
839
- ctx.ui.onmounted((el) => {
840
- const input = el.querySelector('input[type="text"]') as HTMLElement
841
- input?.focus()
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/dist/layout/weifuwu-layout.css">
1669
+ <link rel="stylesheet" href="/node_modules/weifuwu/layout">
1377
1670
  ```
1378
1671
 
1379
- 或通过 `ctx.ui.css` 服务:
1672
+ 或在 weifuwu 服务端通过 `ctx.ui.css` 直接引用包名(`ctx.ui.css` 自动解析 exports map):
1380
1673
 
1381
1674
  ```ts
1382
- app.get('/layout.css', (req, ctx) => ctx.ui.css('./node_modules/weifuwu/dist/layout/weifuwu-layout.css'))
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
- 前端: createApp()
1500
- .use(api({ baseURL: '/api' }))
1501
- .use(auth())
1502
- .mount('#root', App)
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
- | 注入 | 中间件注入 ctx.field | 中间件注入 ctx.field |
1511
- | 读取 | handler 读取 ctx | 组件读取 ctx |
1512
- | 渲染 | 返回 Response | `ctx.ui.render()` / `ctx.ui.dirty()` 触发 VDOM patch |
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
- ## Closeable 接口
1820
+ ## 组件级覆盖
1515
1821
 
1516
- 所有有状态模块(postgres、redis)实现 `close(): Promise<void>`,serve 关闭时自动调用。
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