# @kne/react-form

> react-form 是一个轻量级且功能强大的 React 表单库，专为现代化应用设计，提供了简洁的 API 和完整的表单管理解决方案。

Latest version **3.1.13** (published 2026-09-14) · ISC license · 0 weekly downloads

## Install

```sh
npm install @kne/react-form
pnpm add @kne/react-form
yarn add @kne/react-form
bun add @kne/react-form
```

## Health

**Score 65/100 (B)** — status: active.

Positive: esm support; no vulnerabilities; has provenance; recently updated; high maintenance score.

Warnings: low downloads; no types.

## Facts

| | |
|---|---|
| Version | 3.1.13 |
| Published | 2026-09-14 |
| First published | 2019-12-31 |
| Weekly downloads | 0 |
| License | ISC |
| TypeScript types | none |
| Module format | ESM + CommonJS |
| Dependencies | 6 |
| Unpacked size | 524.1 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Provenance | attested (GitHub Actions) |
| GitHub stars | 0 |
| Author | linzp |
| Maintainers | lingtong, yangwang123456, juliewang, metacoo |

## Links

- npm: https://www.npmjs.com/package/@kne/react-form
- Repository: https://github.com/kne-union/react-form3
- Homepage: https://kne-union.github.io/react-form3
- Issues: https://github.com/kne-union/react-form3/issues
- npm.io page: https://npm.io/package/@kne/react-form

## Dependencies (6)

- [lodash](https://npm.io/package/lodash.md) ^4.17.21
- [@kne/compose](https://npm.io/package/@kne/compose.md) ^0.1.0
- [use-debounce](https://npm.io/package/use-debounce.md) ^10.0.0
- [@kne/is-empty](https://npm.io/package/@kne/is-empty.md) ^1.1.0
- [@kne/use-event](https://npm.io/package/@kne/use-event.md) ^0.1.5
- [@kne/use-ref-callback](https://npm.io/package/@kne/use-ref-callback.md) ^0.1.2

## Recent versions

- 3.1.13 (latest) — 2026-09-14
- 3.1.12 — 2026-09-14
- 3.1.11 — 2026-09-14
- 3.1.10 — 2026-08-27
- 3.1.9 — 2026-04-17
- 3.1.8 — 2026-03-24
- 3.1.6 — 2026-03-23
- 3.1.5 — 2026-02-02
- 3.1.4 — 2026-02-02
- 3.1.3 — 2026-02-02
- 3.1.2 — 2025-07-10
- 3.1.1 — 2025-04-17
- 3.1.0 — 2024-08-01
- 3.1.0-alpha.5 — 2024-08-01
- 3.1.0-alpha.4 — 2024-08-01
- … 130 more at https://npm.io/package/@kne/react-form/versions

## README

# react-form

### 描述

react-form 是一个轻量级且功能强大的 React 表单库，专为现代化应用设计，提供了简洁的 API 和完整的表单管理解决方案。

### 安装

```shell
npm i --save @kne/react-form
```

### 概述

#### 核心特性

##### 📦 轻量级状态管理
- 基于 React Context + State，零额外依赖
- 支持嵌套分组和复杂数据结构
- 实时状态更新，O(1) 高效数据访问

##### 🎛️ 灵活字段控制
- `useField` Hook，字段级精细化控制
- 支持字段级拦截器与关联联动
- 内置防抖，智能去空值处理

##### 📡 事件驱动架构
- 基于事件发射器的解耦设计
- 支持表单/字段/分组三级事件监听
- 便于扩展和自定义

##### ✅ 强大验证系统
- 内置规则：必填、手机号、邮箱、长度等
- 支持异步远程验证
- 可自定义规则与错误提示

##### 📁 分组管理
- 支持无限嵌套分组结构
- GroupList 动态增删，轻松管理
- 支持分组级数据批量操作

##### 🎮 表单 API
- `useFormApi` Hook，完整操作能力
- 支持获取/设置表单数据
- 支持字段级验证控制

#### 设计理念

Form 组件采用分层架构设计，将表单管理、状态管理、事件处理、验证逻辑等职责分离，通过 Context 进行数据传递，实现了高度解耦和可扩展性。

#### 核心架构

##### Context 层

Form 通过 Provider 将表单的核心能力传递给子组件，包括：

- `emitter` - 事件发射器，负责表单内部的事件通信
- `openApi` - 表单操作接口，提供数据获取、设置、验证等方法
- `task` - 任务管理器，处理异步任务队列
- `formState` - 表单状态，使用 Map 存储所有字段的状态
- `setFormState` - 表单状态更新函数
- `formIsMount` - 表单挂载状态
- `rules` - 验证规则集合，包含内置规则和自定义规则
- `interceptor` - 拦截器配置
- `onSubmit` / `onError` / `onFormDataChange` - 提交、错误与数据变化回调

##### Provider 层次结构

```
Form (根组件)
  ├─ Provider (Context 传递)
  │   ├─ FormApiProvider (封装 openApi)
  │   │   └─ FormEvent (事件监听和处理)
  │   │       └─ children (表单字段)
```

#### 字段系统

##### useField Hook

```javascript
const fieldProps = useField({
  name: 'fieldName',      // 字段名称（必填）
  label: '字段标签',       // 字段标签
  rule: 'REQ LEN-3-10', // 验证规则
  interceptor: 'trim',      // 拦截器
  associations: { ... }     // 字段关联
});
```

##### 字段状态

每个字段都有独立的状态对象，包含：

- `id` - 字段唯一标识
- `name` - 字段名称
- `groupName` - 所属分组名称
- `groupIndex` - 分组索引
- `value` - 字段值
- `label` - 字段标签
- `rule` - 验证规则
- `validate` - 验证状态（INIT/PENDING/PASS/ERROR）
- `errMsg` - 错误信息

##### 表单状态

使用 Map 结构存储所有字段状态，通过 `formStateRef.current` 持有引用，确保状态更新的一致性。

#### 验证规则

##### 规则格式

验证规则支持三种格式：

1. **字符串格式**：`"REQ LEN-3-10 EMAIL"` - 空格分隔多个规则
2. **函数格式**：自定义验证函数
3. **正则表达式**：直接使用正则验证

##### 规则解析

```
规则格式：{RULE_NAME}-{arg1}-{arg2}-{arg3}...

示例：
- "REQ"              → 规则名：REQ，无参数
- "LEN-3-10"         → 规则名：LEN，参数：min=3, max=10
- "REQ LEN-3-10"     → 多个规则组合
```

**解析步骤**：
1. 按空格分割规则字符串 → `["REQ", "LEN-3-10"]`
2. 对每个规则按 `-` 分割 → `["LEN", "3", "10"]`
3. 第一部分为规则名（转为大写匹配） → `LEN`
4. 后续部分为规则参数 → `[3, 10]`

##### 内置规则

| 规则名 | 参数 | 说明 | 验证逻辑 | 错误提示 |
|--------|------|------|---------|---------|
| `REQ` | 无 | 必填验证 | 值非空（null/undefined/''） | - |
| `TEL` | 无 | 手机号验证 | 匹配 `/^1[0-9]{10}$/` | "请输入有效的手机号" |
| `EMAIL` | 无 | 邮箱验证 | 匹配邮箱正则 | "请输入有效的邮箱" |
| `LEN` | min, max | 长度验证 | min ≤ 长度 ≤ max | "%s长度必须大于min/小于max/等于min" |

##### 自定义规则

规则函数接收参数：`(value, ...args, { data, field })`

```javascript
// 同步验证规则
rules: {
  PASSWORD_STRENGTH: (value, level, { data }) => {
    const strength = checkStrength(value);
    return {
      result: strength >= level,
      errMsg: strength >= level ? '' : '密码强度不足',
      data: { strength }
    };
  }
}

// 异步验证规则
rules: {
  CHECK_UNIQUE: async (value, { field }) => {
    const exists = await api.checkUnique(field.name, value);
    return {
      result: !exists,
      errMsg: exists ? '该值已存在' : ''
    };
  }
}

// 使用数据联动
rules: {
  MATCH_PASSWORD: (value, { data }) => {
    return {
      result: value === data.password,
      errMsg: '两次输入的密码不一致'
    };
  }
}
```

##### 验证流程

**单个字段验证流程**：

```
1. 触发验证
   ├─ 用户输入 (onChange + 防抖)
   ├─ 失焦事件 (onBlur)
   ├─ 手动调用 validateField()
   └─ 表单提交 (validateAll())
   ↓
2. 查找字段
   ├─ 通过 id 查找
   ├─ 通过 name 查找
   ├─ 通过 groupName + groupIndex + name 查找
   └─ 未找到则跳过
   ↓
3. 解析验证规则
   ├─ 分割规则字符串 (空格分隔)
   ├─ 匹配内置规则 (REQ, TEL, EMAIL, LEN)
   └─ 匹配自定义规则
   ↓
4. 执行验证
   ├─ 更新字段状态为 PENDING
   ├─ 依次执行每个规则
   ├─ 同步规则立即返回结果
   └─ 异步规则等待 Promise
   ↓
5. 处理验证结果
   ├─ 全部通过 → 状态设为 PASS
   ├─ 任意失败 → 状态设为 ERROR，记录错误信息
   └─ 更新 formState
   ↓
6. 触发事件
   ├─ emit('form-field:validate:{fieldId}')
   ├─ 触发关联字段的 associations 回调
   └─ 计算表单整体 isPass 状态
```

**表单整体验证流程**：

```
1. 触发 validateAll()
   ↓
2. 遍历所有字段
   ├─ 对每个字段执行单字段验证流程
   └─ 收集验证结果
   ↓
3. 判断表单状态
   ├─ 所有字段都 PASS → isPass = true
   └─ 存在任意 ERROR 或 PENDING → isPass = false
   ↓
4. 返回验证结果
   └─ 提交处理
```

**验证规则执行顺序**：

规则按照声明顺序依次执行，遇到第一个失败的规则即停止。

示例：`rule="REQ LEN-3-20 EMAIL"`

1. 检查必填 (REQ)
2. 检查长度 (LEN-3-20)
3. 检查邮箱格式 (EMAIL)

若第 1 步失败，则不会执行后续检查。

#### 字段关联

##### 设计机制

字段关联是 react-form 实现表单字段联动的核心机制，通过监听关联字段的变化，自动更新目标字段的值。

##### 配置结构

```javascript
{
  fields: [{ name: 'field1' }, { name: 'field2' }],
  callback: ({ target, origin, openApi }) => {
    // 当关联字段变化时，更新目标字段
    openApi.setFieldValue(target, computedValue);
  }
}
```

| 属性 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `fields` | `Array<FieldReference>` | 是 | 关联字段列表，当这些字段变化时触发回调 |
| `callback` | `Function` | 是 | 关联回调函数，接收变化信息并更新目标字段 |

##### FieldReference 引用类型

```javascript
// 单字段引用
{ name: 'fieldName' }

// 分组字段引用
{ name: 'fieldName', groupName: 'groupName' }

// 分组特定项引用
{
  name: 'fieldName',
  groupName: 'groupName',
  groupIndex: 0  // 索引从 0 开始
}
```

##### Callback 回调参数

```javascript
callback: ({ target, origin, openApi, data }) => {
  // target: 目标字段的完整对象
  //   - id: 字段唯一标识
  //   - name: 字段名称
  //   - value: 字段当前值
  //   - groupName: 所属分组（如果有）
  //   - groupIndex: 分组索引（如果有）

  // origin: 源字段的值（仅在单字段关联时有值）

  // openApi: 表单 API 对象，提供操作方法
  //   - getFormData(): 获取表单数据
  //   - setFieldValue(target, value): 设置字段值
  //   - setFieldValidate(target, validate): 设置验证状态

  // data: 表单完整数据（包含所有字段）
}
```

##### 触发时机

1. **关联字段值变化** - 用户输入或程序修改关联字段的值
2. **关联字段验证完成** - 字段验证通过后触发
3. **表单数据批量设置** - 通过 `setFormData()` 或 `setFields()` 批量更新数据

##### 执行顺序

关联更新会递归触发，注意避免循环依赖。

##### 避免循环关联

```javascript
// ❌ 错误：循环依赖
<Input name="a" associations={{ fields: [{ name: 'b' }], callback: ({ origin }) => origin }} />
<Input name="b" associations={{ fields: [{ name: 'a' }], callback: ({ origin }) => origin }} />

// ✅ 正确：单向依赖
<Input name="fullName" associations={{
  fields: [{ name: 'firstName' }, { name: 'lastName' }],
  callback: ({ data }) => \`\${data.firstName}\${data.lastName}\`
}} />
```

##### 实用场景

详见 `associations.js` 示例文件，包含以下场景：

1. **单字段值复制** - 将一个字段的值自动复制到另一个字段
2. **多字段值拼接** - 将多个字段的值拼接成一个新值
3. **数值计算** - 基于多个字段的值进行计算
4. **分组字段聚合** - 对分组内的多个字段值进行汇总

条件显示与赋值见 `linkage.js`：用 `useWatch` 订阅 `form:field:set-value` 后条件渲染字段；显示时用 `setField` 赋值（未挂载会写入 pending），显示后继续用 `associations` 跟随源字段。

#### 拦截器

##### 拦截器类型

| 类型 | 执行时机 | 用途 | 方向 |
|------|---------|------|------|
| `input` | 值存入表单前 | 数据清理、转换、规范化 | 外部 → 内部 |
| `output` | 值从表单取出时 | 数据格式化、转换 | 内部 → 外部 |

##### 注册机制

```javascript
// 全局拦截器（所有表单共享）
import { interceptors } from 'react-form';

// 注册输入拦截器
interceptors.input.use('trim', value => value.trim());

// 注册输出拦截器
interceptors.output.use('formatDate', value => {
  return value ? new Date(value).toISOString() : null;
});
```

##### 表单级拦截器（优先级高于全局）

```javascript
<Form
  interceptors={{
    input: [
      {
        name: 'customTrim',
        exec: value => value.trim()
      }
    ],
    output: [
      {
        name: 'uppercase',
        exec: value => value.toUpperCase()
      }
    ]
  }}
>
```

##### 字段级拦截器配置

```javascript
<Input
  name="email"
  rule="EMAIL"
  interceptor="trim"  // 指定使用的拦截器
/>
```

##### 优先级

```
优先级从高到低：
1. 字段指定拦截器
2. 表单级拦截器
3. 全局拦截器

同名拦截器：后注册的覆盖先注册的
```

##### 常用拦截器

```javascript
import { interceptors } from 'react-form';

// 常用输入拦截器
interceptors.input.use('trim', v => v?.trim?.());
interceptors.input.use('toUpperCase', v => v?.toUpperCase?.());
interceptors.input.use('toLowerCase', v => v?.toLowerCase?.());
interceptors.input.use('removeComma', v => v?.replace?.(/,/g, ''));

// 常用输出拦截器
interceptors.output.use('toNumber', v => v ? parseFloat(v) : null);
interceptors.output.use('formatCurrency', v => v ? \`¥\${v.toFixed(2)}\` : '¥0.00');
interceptors.output.use('defaultEmpty', v => v || '');
```

#### 分组管理

##### GroupList 组件

用于管理动态增删的分组字段。

```javascript
<GroupList ref={ref} name="groupName" defaultLength={2}>
  {({ index, onAdd, onRemove, length }) => (
    // 分组项内容
  )}
</GroupList>
```

##### 回调参数

| 参数 | 类型 | 说明 |
|------|------|------|
| `index` | `number` | 当前项的索引 |
| `onAdd` | `Function` | 添加新项的函数 |
| `onRemove` | `Function` | 移除当前项的函数 |
| `length` | `number` | 分组当前项数 |

#### 表单 API

##### useSubmit Hook

```javascript
const { isLoading, isPass, onClick } = useSubmit();
```

| 参数 | 类型 | 说明 |
|------|------|------|
| `isLoading` | `boolean` | 提交中状态 |
| `isPass` | `boolean` | 表单是否通过验证 |
| `onClick` | `Function` | 提交函数 |

##### useReset Hook

```javascript
const { onClick } = useReset();
```

| 参数 | 类型 | 说明 |
|------|------|------|
| `onClick` | `Function` | 重置函数 |

##### useFormApi Hook

```javascript
const { openApi } = useFormApi();
```

##### openApi 方法

| 类别 | 方法 | 说明 |
|------|------|------|
| **数据操作** | `data` / `setData()` | 获取/设置表单数据 |
| | `getFormData()` / `setFormData()` | 获取/设置表单数据 |
| | `getField()` / `getFields()` | 获取单个/多个字段 |
| | `setField()` / `setFields()` | 设置单个/多个字段属性 |
| | `setFieldValue()` | 设置字段值 |
| **验证操作** | `validateField()` | 验证单个字段 |
| | `validateAll()` | 验证所有字段 |
| | `setFieldValidate()` | 设置字段验证状态 |
| | `isPass` | 判断表单是否通过验证 |
| **错误处理** | `errors` | 获取所有错误信息 |
| **表单控制** | `submit()` | 提交表单 |
| | `reset()` | 重置表单 |
| | `onReady()` | 表单就绪回调（已 mount 则立即执行） |
| | `onDestroy()` | 表单销毁回调 |

#### 事件系统

##### 事件类型

| 事件名称 | 触发时机 | 参数 |
|---------|---------|------|
| `form:submit` | 表单提交时 | (args) - 提交参数 |
| `form:submit:complete` | 提交完成时 | - |
| `form:reset` | 表单重置时 | - |
| `form:validate` | 表单验证时 | - |
| `form:set-data` | 设置表单数据时 | ({ data, runValidate }) |
| `form:set-fields` | 设置字段属性时 | ({ data, runValidate }) |
| `form-group:change` | 分组数据变化时 | ({ parentId, name, list }) |
| `form-group:remove` | 分组移除时 | ({ parentId, name }) |
| `form-field:validate:{fieldId}` | 字段验证时 | - |
| `form-field:change:{fieldId}` | 字段值变化时 | - |
| `form:mount` | 表单挂载完成时 | - |
| `form:unmount` | 表单卸载时 | - |

##### 监听方式

```javascript
// 在组件内通过 emitter 监听
const { emitter } = useFormContext();

// 监听表单提交
const submitListener = emitter.addListener('form:submit', (args) => {
  console.log('表单提交', args);
});

// 监听数据变化
const dataListener = emitter.addListener('form:set-data', ({ data }) => {
  console.log('数据变化', data);
});

// 清理监听器
useEffect(() => {
  return () => {
    submitListener.remove();
    dataListener.remove();
  };
}, []);
```

##### 生命周期钩子

```javascript
const formApiRef = useRef();
formApiRef.current.onReady(() => {
  console.log('表单已就绪');
  // 可以在这里执行初始化操作
});

formApiRef.current.onDestroy(() => {
  console.log('表单即将销毁');
  // 可以在这里执行清理操作
});
```

#### 生命周期

##### 表单生命周期

| 阶段 | 触发时机 | 执行操作 |
|------|---------|---------|
| 挂载 | Form 组件首次渲染 | - 初始化 formState (Map 结构)<br>- 创建事件发射器<br>- 初始化 openApi<br>- 设置初始数据<br>- 绑定事件监听器 |
| 更新 | Form 组件属性变化 | - 更新 rules 配置<br>- 更新 interceptors 配置<br>- data 只更新后挂载字段的初始值，不重放已挂载字段 |
| 卸载 | Form 组件销毁 | - 清理所有事件监听器<br>- 清理异步任务<br>- 释放内存资源 |

##### 字段生命周期

| 阶段 | 状态 | 说明 | 可执行操作 |
|------|------|------|-----------|
| PRE_INIT | 预初始化 | 字段刚创建，尚未绑定到表单状态 | - |
| INIT | 已初始化 | 字段已注册到表单，可以交互 | 获取/设置值、验证、触发事件 |

##### 字段状态转换流程

```
字段创建
  ↓
PRE_INIT (useField 初始化)
  ↓
INIT (字段注册到 formState)
  ↓
用户交互
  ↓
PENDING (正在验证)
  ↓
PASS 或 ERROR (验证结果)
  ↓
用户修改或手动重置
  ↓
INIT (回到初始状态)
```

##### 验证状态枚举

| 状态值 | 枚举名 | 说明 | UI 展示建议 |
|--------|--------|------|------------|
| 0 | INIT | 初始状态 | 不显示错误提示 |
| 1 | PASS | 验证通过 | 不显示错误提示 |
| 2 | ERROR | 验证失败 | 显示错误信息 |
| 3 | PENDING | 验证中 | 显示加载状态 |

### 示例

#### 示例代码

- 基本示例
- 展示基本的表单使用方式，包括表单绑定、验证规则、提交和重置
- _ReactForm(@kne/current-lib_react-form),antd(antd)

```jsx
const { default: ReactForm, useField, useSubmit, useReset } = _ReactForm;
const { Input: AntInput, Button, Space, Card, Alert, Typography, message } = antd;
const { Text } = Typography;

// ========================================
// 通用组件
// ========================================

const Input = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;
  const isValidating = fieldProps.errState === 3;

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
      </div>
      <div>
        <AntInput
          ref={fieldProps.fieldRef}
          type={props.type || 'text'}
          value={fieldProps.value || ''}
          onChange={e => {
            fieldProps.onChange(e.target.value);
            fieldProps.triggerValidate();
          }}
          onBlur={fieldProps.triggerValidate}
          placeholder={props.placeholder}
          status={isError ? 'error' : undefined}
          style={{ width: props.width || 200 }}
        />
        {fieldProps.errMsg && (
          <Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
            {fieldProps.errMsg}
          </Text>
        )}
        {isValidating && (
          <Text type="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
            验证中...
          </Text>
        )}
      </div>
    </div>
  );
};

const SubmitButton = ({ children, isPassButton = false }) => {
  const { isLoading, isPass, onClick } = useSubmit();
  return (
    <Button
      type="primary"
      onClick={onClick}
      disabled={isPassButton ? (isLoading || !isPass) : isLoading}
      loading={isLoading}
    >
      {children}
    </Button>
  );
};

const ResetButton = () => {
  const { onClick } = useReset();
  return <Button onClick={onClick}>重置</Button>;
};

// ========================================
// 基本表单示例
// ========================================

const BaseExample = () => {
  return (
    <Card title="基本表单示例" style={{ marginBottom: 24 }}>
      <ReactForm
        debug
        data={{ name: '哈哈哈' }}
        onSubmit={async data => {
          await new Promise(resolve => {
            setTimeout(() => {
              resolve();
            }, 3000);
          });
          console.log('submit:', data);
          message.success('提交成功: ' + JSON.stringify(data, null, 2));
        }}
      >
        <Input name="name" label="名称" rule="REQ LEN-0-10" />
        <Input name="email" label="邮箱" rule="REQ EMAIL" />
        <Input name="phone" label="手机号" rule="REQ TEL" />
        <Space>
          <SubmitButton>提交</SubmitButton>
          <ResetButton />
        </Space>
      </ReactForm>
    </Card>
  );
};

// ========================================
// isPass 测试示例
// ========================================

const IsPassStatusDisplay = () => {
  const { isPass } = useSubmit();
  return (
    <Alert
      message={&#96;表单验证状态：${isPass ? '全部通过' : '存在错误'}&#96;}
      description={isPass ? '所有字段验证通过，可以提交' : '请检查并修正错误信息'}
      type={isPass ? 'success' : 'error'}
      showIcon
      style={{ marginBottom: 20 }}
    />
  );
};

const IsPassExample = () => {
  return (
    <Card title="isPass 测试示例" extra={
      <Text type="secondary" style={{ fontSize: 12 }}>
        所有字段在输入停止后（失焦）触发校验
      </Text>
    }>
      <ReactForm
        debug
        data={{
          username: '',
          email: '',
          password: '',
          confirmPassword: '',
          age: ''
        }}
        onSubmit={async data => {
          await new Promise(resolve => {
            setTimeout(() => {
              resolve();
            }, 1000);
          });
          console.log('submit:', data);
          message.success('提交成功: ' + JSON.stringify(data, null, 2));
        }}
      >
        <IsPassStatusDisplay />

        <div style={{ marginBottom: 16 }}>
          <Text strong>基本信息</Text>
        </div>
        <Input
          name="username"
          label="用户名"
          rule="REQ LEN-3-20"
          placeholder="请输入用户名（3-20字符）"
          width={250}
        />
        <Input
          name="email"
          label="邮箱"
          rule="REQ EMAIL"
          placeholder="请输入邮箱地址"
          width={250}
        />
        <Input
          name="age"
          label="年龄"
          rule="REQ LEN-1-3"
          placeholder="请输入年龄（1-3位数字）"
          width={250}
        />

        <div style={{ marginBottom: 16, marginTop: 16 }}>
          <Text strong>安全信息</Text>
        </div>
        <Input
          name="password"
          label="密码"
          rule="REQ LEN-6-20"
          placeholder="请输入密码（6-20字符）"
          type="password"
          width={250}
        />
        <Input
          name="confirmPassword"
          label="确认密码"
          rule="REQ LEN-6-20"
          placeholder="请再次输入密码"
          type="password"
          width={250}
        />

        <SubmitButton isPassButton>提交注册</SubmitButton>
      </ReactForm>
    </Card>
  );
};

// ========================================
// 主组件
// ========================================

const App = () => {
  return (
    <div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
      <Space direction="vertical" style={{ width: '100%' }} size="large">
        <BaseExample />
        <IsPassExample />
      </Space>
    </div>
  );
};

render(<App />);

```

- defaultValue 初值
- 通过 defaultValue 为表单字段设置初始值，FormInner 组件内部使用 useFormApi 访问表单 API
- _ReactForm(@kne/current-lib_react-form),antd(antd)

```jsx
const { default: ReactForm, useField, useSubmit, useReset, useFormApi } = _ReactForm;
const { useRef } = React;
const { Button, Space, Card, Input: AntInput, Typography, message, Divider, Descriptions, Select, Row, Col } = antd;
const { Text } = Typography;

// ========================================
// 通用输入组件
// ========================================

const Input = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
      </div>
      <div>
        <AntInput
          ref={fieldProps.fieldRef}
          type="text"
          value={fieldProps.value || ''}
          onChange={e => fieldProps.onChange(e.target.value)}
          onBlur={fieldProps.triggerValidate}
          placeholder={props.placeholder}
          status={isError ? 'error' : undefined}
          style={{ width: 200 }}
        />
        {fieldProps.errMsg && (
          <Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
            {fieldProps.errMsg}
          </Text>
        )}
      </div>
    </div>
  );
};

// ========================================
// Object 类型字段组件 (defaultValue 为对象)
// ========================================

const AddressField = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;
  const value = fieldProps.value || {};

  const handleChange = (key, newValue) => {
    fieldProps.onChange({
      ...value,
      [key]: newValue
    });
  };

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
      </div>
      <Row gutter={8}>
        <Col>
          <Select
            value={value.province || undefined}
            onChange={v => handleChange('province', v)}
            placeholder="省份"
            style={{ width: 120 }}
            options={[
              { value: '北京市', label: '北京市' },
              { value: '上海市', label: '上海市' },
              { value: '广东省', label: '广东省' },
              { value: '浙江省', label: '浙江省' }
            ]}
          />
        </Col>
        <Col>
          <Select
            value={value.city || undefined}
            onChange={v => handleChange('city', v)}
            placeholder="城市"
            style={{ width: 120 }}
            options={[
              { value: '朝阳区', label: '朝阳区' },
              { value: '海淀区', label: '海淀区' },
              { value: '浦东新区', label: '浦东新区' },
              { value: '天河区', label: '天河区' }
            ]}
          />
        </Col>
        <Col>
          <AntInput value={value.detail || ''} onChange={e => handleChange('detail', e.target.value)} placeholder="详细地址" style={{ width: 200 }} />
        </Col>
      </Row>
      {fieldProps.errMsg && (
        <Text type="danger" style={{ fontSize: 12 }}>
          {fieldProps.errMsg}
        </Text>
      )}
    </div>
  );
};

const TimeRangeField = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;
  const value = fieldProps.value || { start: '', end: '' };

  const handleChange = (key, newValue) => {
    fieldProps.onChange({
      ...value,
      [key]: newValue
    });
  };

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
      </div>
      <Space>
        <AntInput type="time" value={value.start || ''} onChange={e => handleChange('start', e.target.value)} placeholder="开始时间" style={{ width: 150 }} />
        <Text>至</Text>
        <AntInput type="time" value={value.end || ''} onChange={e => handleChange('end', e.target.value)} placeholder="结束时间" style={{ width: 150 }} />
      </Space>
      {fieldProps.errMsg && (
        <div>
          <Text type="danger" style={{ fontSize: 12 }}>
            {fieldProps.errMsg}
          </Text>
        </div>
      )}
    </div>
  );
};

const SubmitButton = ({ children }) => {
  const { isLoading, onClick } = useSubmit();
  return (
    <Button type="primary" onClick={onClick} loading={isLoading} style={{ marginRight: 8 }}>
      {children}
    </Button>
  );
};

const ResetButton = () => {
  const { onClick } = useReset();
  return <Button onClick={onClick}>重置</Button>;
};

// ========================================
// FormInner 组件：在内部使用 useFormApi
// ========================================

const FormInner = () => {
  const { openApi: formApi } = useFormApi();
  const formData = formApi.getFormData();

  return (
    <Card type="inner" title="FormInner 组件 (使用 useFormApi)" style={{ marginBottom: 16 }}>
      <Descriptions column={1} size="small" bordered>
        <Descriptions.Item label="当前表单数据">
          <Text code>{JSON.stringify(formData)}</Text>
        </Descriptions.Item>
        <Descriptions.Item label="操作">
          <Space wrap>
            <Button
              size="small"
              onClick={() => {
                const name = formData.name || '默认姓名';
                formApi.setField({ name: 'name', value: name + '_modified' });
                message.success('已修改 name 字段');
              }}>
              修改姓名
            </Button>
            <Button
              size="small"
              onClick={() => {
                formApi.setFields([
                  { name: 'email', value: 'default@example.com' },
                  { name: 'phone', value: '13800138000' }
                ]);
                message.success('已批量设置字段');
              }}>
              批量设置
            </Button>
            <Button
              size="small"
              onClick={() => {
                formApi.reset();
                message.info('表单已重置');
              }}>
              重置表单
            </Button>
          </Space>
        </Descriptions.Item>
      </Descriptions>
    </Card>
  );
};

// ========================================
// 主示例
// ========================================

const BaseExample = () => {
  return (
    <div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
      <Card title="defaultValue 初值示例">
        <Space direction="vertical" style={{ width: '100%' }}>
          <Text type="secondary">通过 defaultValue 为表单字段设置初始值，支持基础类型和对象类型</Text>

          <ReactForm
            debug
            onSubmit={async data => {
              await new Promise(resolve => setTimeout(resolve, 1000));
              console.log('submit:', data);
              message.success('提交成功');
            }}>
            <FormInner />

            <Divider>基础类型 defaultValue</Divider>

            <Input name="name" label="姓名" rule="REQ LEN-0-10" defaultValue="张三" placeholder="请输入姓名" />
            <Input name="email" label="邮箱" rule="REQ EMAIL" defaultValue="zhangsan@example.com" placeholder="请输入邮箱" />
            <Input name="phone" label="手机号" rule="REQ TEL" defaultValue="13900139000" placeholder="请输入手机号" />

            <Divider>Object 类型 defaultValue</Divider>

            <AddressField
              name="address"
              label="地址"
              rule="REQ"
              defaultValue={{
                province: '北京市',
                city: '朝阳区',
                detail: '望京街道'
              }}
            />
            <TimeRangeField
              name="workTime"
              label="工作时间"
              defaultValue={{
                start: '09:00',
                end: '18:00'
              }}
            />

            <Divider />

            <Space>
              <SubmitButton>提交</SubmitButton>
              <ResetButton />
            </Space>
          </ReactForm>
        </Space>
      </Card>
    </div>
  );
};

render(<BaseExample />);

```

- ref 操作 API
- 通过 ref 访问表单的 openApi，实现数据获取、字段设置、验证控制等操作
- _ReactForm(@kne/current-lib_react-form),antd(antd)

```jsx
const { default: ReactForm, useField, useSubmit, useReset } = _ReactForm;
const { useRef } = React;
const { Button, Space, Card, Input: AntInput, Divider, Typography, message } = antd;
const { Text } = Typography;

const Input = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;
  const isValidating = fieldProps.errState === 3;

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
      </div>
      <div>
        <AntInput
          ref={fieldProps.fieldRef}
          type="text"
          value={fieldProps.value || ''}
          onChange={e => fieldProps.onChange(e.target.value)}
          onBlur={fieldProps.triggerValidate}
          status={isError ? 'error' : undefined}
          style={{ width: 200 }}
        />
        {fieldProps.errMsg && (
          <Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
            {fieldProps.errMsg}
          </Text>
        )}
        {isValidating && (
          <Text type="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
            验证中...
          </Text>
        )}
      </div>
    </div>
  );
};

const SubmitButton = ({ children }) => {
  const { isLoading, onClick } = useSubmit();
  return (
    <Button type="primary" onClick={onClick} loading={isLoading} style={{ marginRight: 8 }}>
      {children}
    </Button>
  );
};

const ResetButton = () => {
  const { onClick } = useReset();
  return <Button onClick={onClick}>重置</Button>;
};

const BaseExample = () => {
  const formApiRef = useRef();
  return (
    <div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
      <Card title="ref 操作 API 示例" bordered={false}>
        <Card type="inner" title="操作面板" style={{ marginBottom: 16 }}>
          <Space wrap>
            <Button type="primary" onClick={() => {
              const data = formApiRef.current.data;
              console.log('表单数据:', data);
              message.success('请查看控制台');
            }}>
              获取表单值
            </Button>
            <Button onClick={() => {
              formApiRef.current.setField({ name: 'name', value: '设置的新名称' });
            }}>
              设置 name 字段值
            </Button>
            <Button onClick={() => {
              formApiRef.current.setFields([
                { name: 'name', value: '张三' },
                { name: 'email', value: 'zhangsan@example.com' },
                { name: 'phone', value: '13800138000' }
              ]);
            }}>
              批量设置字段值
            </Button>
            <Button danger onClick={() => {
              formApiRef.current.setFieldValidate({
                name: 'email',
                validate: { status: 2, msg: '邮箱格式不正确' }
              });
            }}>
              设置校验错误
            </Button>
            <Button onClick={() => {
              formApiRef.current.reset();
            }}>
              重置表单
            </Button>
          </Space>
        </Card>

        <ReactForm ref={formApiRef}>
          <Input name="name" label="姓名" rule="REQ LEN-0-10" />
          <Input name="email" label="邮箱" rule="REQ EMAIL" />
          <Input name="phone" label="手机号" rule="REQ TEL" />
          <div>
            <Space>
              <SubmitButton>提交</SubmitButton>
              <ResetButton />
            </Space>
          </div>
        </ReactForm>
      </Card>
    </div>
  );
};

render(<BaseExample />);

```

- useFormApi Hook
- 使用 useFormApi Hook 在表单内部访问和控制表单状态
- _ReactForm(@kne/current-lib_react-form),antd(antd)

```jsx
const { default: ReactForm, useField, useSubmit, useReset, useFormApi } = _ReactForm;
const { useRef } = React;
const { Button, Space, Card, Input: AntInput, Divider, Typography, message } = antd;
const { Text } = Typography;

const Input = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;
  const isValidating = fieldProps.errState === 3;

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
      </div>
      <div>
        <AntInput
          ref={fieldProps.fieldRef}
          type="text"
          value={fieldProps.value || ''}
          onChange={e => fieldProps.onChange(e.target.value)}
          onBlur={fieldProps.triggerValidate}
          status={isError ? 'error' : undefined}
          style={{ width: 200 }}
        />
        {fieldProps.errMsg && (
          <Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
            {fieldProps.errMsg}
          </Text>
        )}
        {isValidating && (
          <Text type="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
            验证中...
          </Text>
        )}
      </div>
    </div>
  );
};

const SubmitButton = ({ children }) => {
  const { isLoading, onClick } = useSubmit();
  return (
    <Button type="primary" onClick={onClick} loading={isLoading} style={{ marginRight: 8 }}>
      {children}
    </Button>
  );
};

const ResetButton = () => {
  const { onClick } = useReset();
  return <Button onClick={onClick}>重置</Button>;
};

const Options = () => {
  const { openApi: formApi } = useFormApi();

  return (
    <Card type="inner" title="操作面板 (useFormApi)" style={{ marginBottom: 16 }}>
      <Space wrap>
        <Button type="primary" onClick={() => {
          const data = formApi.getFormData();
          console.log('表单数据:', data);
          message.success('请查看控制台');
        }}>
          获取表单值
        </Button>
        <Button onClick={() => {
          formApi.setField({ name: 'name', value: '修改后的姓名' });
        }}>
          设置 name 字段
        </Button>
        <Button onClick={() => {
          formApi.setFields([
            { name: 'name', value: '李四' },
            { name: 'email', value: 'lisi@example.com' },
            { name: 'phone', value: '13900139000' }
          ]);
        }}>
          批量设置字段
        </Button>
        <Button danger onClick={() => {
          formApi.setFieldValidate({
            name: 'phone',
            validate: { status: 2, msg: '手机号格式错误' }
          });
        }}>
          设置验证错误
        </Button>
        <Button onClick={() => {
          formApi.reset();
        }}>
          重置表单
        </Button>
      </Space>
    </Card>
  );
};

const BaseExample = () => {
  const formApiRef = useRef();
  return (
    <div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
      <Card title="useFormApi Hook 示例" bordered={false}>
        <ReactForm ref={formApiRef}>
          <Options />
          <Input name="name" label="姓名" rule="REQ LEN-0-10" />
          <Input name="email" label="邮箱" rule="REQ EMAIL" />
          <Input name="phone" label="手机号" rule="REQ TEL" />
          <div>
            <Space>
              <SubmitButton>提交</SubmitButton>
              <ResetButton />
            </Space>
          </div>
        </ReactForm>
      </Card>
    </div>
  );
};

render(<BaseExample />);

```

- 动态分组
- 使用 Group 和 GroupList 实现动态增删的表单分组，支持嵌套分组
- _ReactForm(@kne/current-lib_react-form),antd(antd)

```jsx
const { default: ReactForm, useField, useSubmit, useReset, GroupList } = _ReactForm;
const { useRef } = React;
const { Button, Space, Card, Input: AntInput, Tag, Typography, message } = antd;
const { Text } = Typography;

const Input = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;
  const isValidating = fieldProps.errState === 3;

  return (
    <div style={{ marginBottom: 8 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined} style={{ fontSize: 12 }}>
          {fieldProps.label}
        </Text>
      </div>
      <div>
        <AntInput
          ref={fieldProps.fieldRef}
          type="text"
          value={fieldProps.value || ''}
          onChange={e => fieldProps.onChange(e.target.value)}
          onBlur={fieldProps.triggerValidate}
          status={isError ? 'error' : undefined}
          size="small"
          style={{ width: 120 }}
        />
        {fieldProps.errMsg && (
          <Text type="danger" style={{ marginLeft: 4, fontSize: 12 }}>
            {fieldProps.errMsg}
          </Text>
        )}
        {isValidating && (
          <Text type="secondary" style={{ marginLeft: 4, fontSize: 12 }}>
            验证中...
          </Text>
        )}
      </div>
    </div>
  );
};

const SubmitButton = ({ children }) => {
  const { isLoading, onClick } = useSubmit();
  return (
    <Button type="primary" onClick={onClick} loading={isLoading} style={{ marginRight: 8 }}>
      {children}
    </Button>
  );
};

const ResetButton = () => {
  const { onClick } = useReset();
  return <Button onClick={onClick}>重置</Button>;
};

const ChildrenGroup = () => {
  const ref = useRef(null);
  return (
    <div style={{ marginTop: 12 }}>
      <div style={{ fontSize: 12, color: '#666', marginBottom: 8 }}>子分组：</div>
      <GroupList ref={ref} name="inner" defaultLength={0} reverseOrder={false}>
        {({ index: innerIndex, onRemove: innerRemove, length: innerLength }) => {
          return (
            <div
              key={innerIndex}
              style={{
                padding: 12,
                marginBottom: 8,
                background: '#e8e8e8',
                borderRadius: 4
              }}>
              <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
                <span style={{ fontSize: 12 }}>子项 {innerIndex + 1}</span>
                <Input name="detail" label="详情" rule="LEN-0-20" />
                <Button size="small" danger onClick={innerRemove}>
                  删除
                </Button>
              </div>
            </div>
          );
        }}
      </GroupList>
      <div style={{ marginTop: 8 }}>
        <Button size="small" onClick={() => ref.current.onAdd()}>
          添加子项
        </Button>
      </div>
    </div>
  );
};

const BaseExample = () => {
  const ref = useRef();
  const formApiRef = useRef();

  return (
    <div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
      <Card title="动态分组示例" bordered={false}>
        <Space wrap style={{ marginBottom: 20 }}>
          <Button
            type="primary"
            onClick={() => {
              formApiRef.current.setField({
                name: 'name',
                groupName: 'group',
                groupIndex: 0,
                value: '第一项名称'
              });
            }}>
            设置第一项名称
          </Button>
          <Button
            onClick={() => {
              formApiRef.current.setField({
                name: 'name',
                groupName: 'group',
                value: '所有项名称'
              });
            }}>
            设置所有项名称
          </Button>
          <Button
            onClick={() => {
              formApiRef.current.setFormData({
                group: [
                  { name: '张三', des: '描述1' },
                  { name: '李四', des: '描述2' },
                  { name: '王五', des: '描述3' }
                ]
              });
            }}>
            批量设置数据
          </Button>
        </Space>
        <ReactForm
          ref={formApiRef}
          onSubmit={data => {
            console.log('submit:', data);
            message.success('提交成功: ' + JSON.stringify(data, null, 2));
          }}>
          <div style={{ marginBottom: 16 }}>
            <Button type="primary" onClick={() => ref.current.onAdd()}>
              添加
            </Button>
          </div>

          <GroupList ref={ref} name="group" defaultLength={1}>
            {({ index, onRemove, length }) => {
              return (
                <div
                  key={index}
                  style={{
                    padding: 16,
                    marginBottom: 16,
                    border: '1px solid #ddd',
                    borderRadius: 8,
                    background: '#fafafa'
                  }}>
                  <div style={{ fontWeight: 'bold', marginBottom: 12 }}>
                    联系人 {index + 1} (共 {length} 项)
                  </div>
                  <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
                    <Input name="name" label="姓名" rule="REQ LEN-0-10" />
                    <Input name="phone" label="手机号" rule="TEL" />
                    <Input name="email" label="邮箱" rule="EMAIL" />
                  </div>

                  <ChildrenGroup key={index}/>

                  <div style={{ marginTop: 12 }}>
                    <Button danger size="small" onClick={onRemove}>
                      删除联系人 {index + 1}
                    </Button>
                  </div>
                </div>
              );
            }}
          </GroupList>

          <div style={{ marginTop: 20 }}>
            <Space>
              <SubmitButton>提交</SubmitButton>
              <ResetButton />
            </Space>
          </div>
        </ReactForm>
      </Card>
    </div>
  );
};

render(<BaseExample />);

```

- 嵌套 GroupList setFormData
- 验收外层+内层 GroupList 一次 setFormData 后内层数据完整（setFormData 同步更新 initFormData）
- _ReactForm(@kne/current-lib_react-form),antd(antd)

```jsx
/**
 * 验证：外层 GroupList + 内层 GroupList，一次 setFormData 后
 * 每条外层项的内层列表都应有数据（修复：setFormData 同步更新 initFormData）。
 *
 * 操作：点「一次 setFormData」→ 三条技能下依据均应有内容；再「读取 getFormData」对照。
 */
const { default: ReactForm, useField, useSubmit, useReset, useFormApi, GroupList } = _ReactForm;
const { useRef, useState } = React;
const { Button, Space, Card, Input: AntInput, Typography, message, Alert, Divider } = antd;
const { Text, Paragraph } = Typography;

const SAMPLE_DATA = {
  skill: [
    {
      name: '技能 A',
      contentItems: [
        { title: '依据 A1', description: '描述 A1', source: 'JD' },
        { title: '依据 A2', description: '描述 A2', source: '报告' }
      ]
    },
    {
      name: '技能 B',
      contentItems: [
        { title: '依据 B1', description: '描述 B1', source: 'JD' },
        { title: '依据 B2', description: '描述 B2', source: '报告' }
      ]
    },
    {
      name: '技能 C',
      contentItems: [
        { title: '依据 C1', description: '描述 C1', source: 'JD' },
        { title: '依据 C2', description: '描述 C2', source: '报告' }
      ]
    }
  ]
};

const Input = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;
  return (
    <div style={{ marginBottom: 8, minWidth: 140 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined} style={{ fontSize: 12 }}>
          {fieldProps.label}
        </Text>
      </div>
      <AntInput
        ref={fieldProps.fieldRef}
        value={fieldProps.value ?? ''}
        onChange={e => fieldProps.onChange(e.target.value)}
        onBlur={fieldProps.triggerValidate}
        status={isError ? 'error' : undefined}
        size="small"
      />
    </div>
  );
};

const ContentItems = () => {
  const ref = useRef(null);
  return (
    <div style={{ marginTop: 8, padding: 8, background: '#f0f5ff', borderRadius: 4 }}>
      <Text strong style={{ fontSize: 12 }}>
        依据（内层 GroupList / contentItems）
      </Text>
      <GroupList ref={ref} name="contentItems" defaultLength={0} reverseOrder={false}>
        {({ index, onRemove }) => (
          <div
            style={{
              display: 'flex',
              flexWrap: 'wrap',
              gap: 8,
              alignItems: 'flex-end',
              marginTop: 8,
              padding: 8,
              background: '#fff',
              borderRadius: 4,
              border: '1px dashed #91caff'
            }}>
            <Text style={{ fontSize: 12 }}>依据 {index + 1}</Text>
            <Input name="title" label="标题" />
            <Input name="description" label="描述" />
            <Input name="source" label="来源" />
            <Button size="small" danger onClick={onRemove}>
              删
            </Button>
          </div>
        )}
      </GroupList>
      <Button size="small" style={{ marginTop: 8 }} onClick={() => ref.current?.onAdd()}>
        添加依据
      </Button>
    </div>
  );
};

const SnapshotPanel = () => {
  const { openApi } = useFormApi();
  const [snap, setSnap] = useState(null);
  return (
    <div style={{ marginTop: 12 }}>
      <Button
        onClick={() => {
          const data = openApi.getFormData();
          setSnap(data);
          console.log('[nested-set-form-data] getFormData', data);
          const summary = (data.skill || []).map((s, i) => ({
            index: i,
            name: s?.name,
            contentItemsLen: Array.isArray(s?.contentItems) ? s.contentItems.length : 0,
            contentItems: s?.contentItems
          }));
          console.log('[nested-set-form-data] contentItems summary', summary);
          message.info('已打印 getFormData，见控制台');
        }}>
        读取 getFormData
      </Button>
      {snap ? (
        <pre style={{ marginTop: 8, fontSize: 11, maxHeight: 240, overflow: 'auto', background: '#fafafa', padding: 8 }}>
          {JSON.stringify(snap, null, 2)}
        </pre>
      ) : null}
    </div>
  );
};

const Toolbar = () => {
  const { openApi } = useFormApi();
  return (
    <Space wrap style={{ marginBottom: 16 }}>
      <Button
        type="primary"
        onClick={() => {
          console.log('[nested-set-form-data] setFormData once', SAMPLE_DATA);
          openApi.setFormData(SAMPLE_DATA, false);
        }}>
        一次 setFormData（模拟 AI 填充）
      </Button>
      <Button
        onClick={() => {
          openApi.setFormData({ skill: [{ name: '', contentItems: [] }] }, false);
        }}>
        重置为 1 条空技能
      </Button>
    </Space>
  );
};

const SubmitButton = ({ children }) => {
  const { isLoading, onClick } = useSubmit();
  return (
    <Button type="primary" onClick={onClick} loading={isLoading}>
      {children}
    </Button>
  );
};

const ResetButton = () => {
  const { onClick } = useReset();
  return <Button onClick={onClick}>重置</Button>;
};

const BaseExample = () => {
  const skillRef = useRef();
  return (
    <div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
      <Card title="嵌套 GroupList + setFormData 复现" bordered={false}>
        <Alert
          type="info"
          showIcon
          style={{ marginBottom: 16 }}
          message="验收点"
          description={
            <Paragraph style={{ marginBottom: 0 }}>
              初始 1 条外层技能（defaultLength=1），内层依据 defaultLength=0。一次 setFormData 写入 3 条技能、每条 2 条依据后，<strong>三条技能的依据都应有内容</strong>（依赖 setFormData 同步更新 initFormData）。
            </Paragraph>
          }
        />
        <ReactForm
          onSubmit={data => {
            console.log('submit', data);
            message.success('已提交，见控制台');
          }}>
          <Toolbar />
          <div style={{ marginBottom: 12 }}>
            <Button type="dashed" onClick={() => skillRef.current?.onAdd()}>
              手动添加技能
            </Button>
          </div>
          <GroupList ref={skillRef} name="skill" defaultLength={1} reverseOrder={false}>
            {({ index, onRemove, length }) => (
              <div
                style={{
                  padding: 16,
                  marginBottom: 12,
                  border: '1px solid #d9d9d9',
                  borderRadius: 8,
                  background: '#fff'
                }}>
                <div style={{ fontWeight: 600, marginBottom: 8 }}>
                  技能 {index + 1} / 共 {length}
                </div>
                <Input name="name" label="技能名称" rule="REQ" />
                <ContentItems />
                <Button danger size="small" style={{ marginTop: 8 }} onClick={onRemove}>
                  删除技能
                </Button>
              </div>
            )}
          </GroupList>
          <Divider />
          <SnapshotPanel />
          <Space style={{ marginTop: 16 }}>
            <SubmitButton>提交</SubmitButton>
            <ResetButton />
          </Space>
        </ReactForm>
      </Card>
    </div>
  );
};

render(<BaseExample />);

```

- 字段关联
- 通过 associations 实现字段间的联动关系，支持单字段联动、多字段联动、分组联动和计算联动
- _ReactForm(@kne/current-lib_react-form),antd(antd)

```jsx
const { default: ReactForm, useField, useSubmit, useReset, GroupList } = _ReactForm;
const { useRef } = React;
const { Button, Space, Card, Input: AntInput, Tag, Typography, message } = antd;
const { Text } = Typography;

const Input = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;
  const isValidating = fieldProps.errState === 3;

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
      </div>
      <div>
        <AntInput
          {...fieldProps.associationOptions}
          ref={fieldProps.fieldRef}
          type="text"
          value={fieldProps.value || ''}
          onChange={e => fieldProps.onChange(e.target.value)}
          onBlur={fieldProps.triggerValidate}
          status={isError ? 'error' : undefined}
          style={{ width: 200 }}
        />
        {fieldProps.errMsg && (
          <Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
            {fieldProps.errMsg}
          </Text>
        )}
        {isValidating && (
          <Text type="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
            验证中...
          </Text>
        )}
      </div>
    </div>
  );
};

const SubmitButton = ({ children }) => {
  const { isLoading, onClick } = useSubmit();
  return (
    <Button type="primary" onClick={onClick} loading={isLoading} style={{ marginRight: 8 }}>
      {children}
    </Button>
  );
};

const ResetButton = () => {
  const { onClick } = useReset();
  return <Button onClick={onClick}>重置</Button>;
};

const BaseExample = () => {
  const ref = useRef(null);
  const formApiRef = useRef(null);
  return (
    <div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
      <Card title="字段关联示例" bordered={false}>
        <ReactForm
          ref={formApiRef}
          debug
          onSubmit={data => {
            console.log('submit:', data);
            message.success('提交成功: ' + JSON.stringify(data, null, 2));
          }}>
          <Card
            type="inner"
            title={
              <Space>
                1. 单字段关联<Tag color="blue">描述跟随名称</Tag>
              </Space>
            }
            style={{ marginBottom: 16 }}>
            <Input name="name" label="名称" rule="REQ LEN-0-10" />
            <Input
              name="des"
              label="描述"
              rule="LEN-0-10"
              associations={{
                fields: [{ name: 'name' }],
                callback: ({ target, origin, openApi }) => {
                  openApi.setFieldValue(target, origin.value);
                }
              }}
            />
          </Card>

          <Card
            type="inner"
            title={
              <Space>
                2. 多字段关联<Tag color="green">姓名拼接全名</Tag>
              </Space>
            }
            style={{ marginBottom: 16 }}>
            <Space wrap>
              <Input name="familyName" label="姓" rule="REQ LEN-0-10" />
              <Input name="firstName" label="名" rule="REQ LEN-0-10" />
            </Space>
            <Input
              name="fullName"
              label="全名"
              rule="LEN-0-20"
              associations={{
                fields: [{ name: 'familyName' }, { name: 'firstName' }],
                callback: ({ target, openApi }) => {
                  const { firstName, familyName } = openApi.getFormData();
                  openApi.setFieldValue(target, &#96;${familyName || ''}${firstName || ''}&#96;);
                }
              }}
            />
          </Card>

          <Card
            type="inner"
            title={
              <Space>
                3. 计算关联<Tag color="orange">金额除以比例</Tag>
              </Space>
            }
            style={{ marginBottom: 16 }}>
            <Space wrap>
              <Input name="money" label="总金额" />
              <Input name="ratio" label="比例" />
            </Space>
            <Input
              name="all"
              label="每份金额"
              associations={{
                fields: [{ name: 'money' }, { name: 'ratio' }],
                callback: ({ target, openApi }) => {
                  const { money, ratio } = openApi.getFormData();
                  const numMoney = parseFloat(money) || 0;
                  const numRatio = parseFloat(ratio) || 1;
                  openApi.setFieldValue(target, numRatio > 0 ? (numMoney / numRatio).toFixed(2) : '');
                }
              }}
            />
          </Card>

          <Card
            type="inner"
            title={
              <Space>
                4. 分组关联<Tag color="purple">汇总求和</Tag>
              </Space>
            }
            style={{ marginBottom: 16 }}>
            <Button type="primary" onClick={() => ref.current.onAdd()} style={{ marginBottom: 12 }}>
              添加数量项
            </Button>
            <GroupList ref={ref} name="group" defaultLength={2}>
              {({ index, onRemove }) => (
                <Space key={index} style={{ padding: 12, background: '#f0f0f0', borderRadius: 6, marginBottom: 8, width: '100%' }}>
                  <Tag color="blue">项 {index + 1}</Tag>
                  <Input name="sum" label="数量" />
                  <Button danger size="small" onClick={onRemove}>
                    删除
                  </Button>
                </Space>
              )}
            </GroupList>
            <Input
              name="amount"
              label="总数"
              associations={{
                fields: [{ name: 'sum', groupName: 'group' }],
                callback: ({ target, openApi }) => {
                  const { group } = openApi.getFormData();
                  const total = (group || []).filter(item => item.sum > 0).reduce((a, b) => a + parseInt(b.sum), 0);
                  openApi.setFieldValue(target, total > 0 ? total.toString() : '');
                }
              }}
            />
          </Card>

          <div style={{ marginTop: 16 }}>
            <Space>
              <SubmitButton>提交</SubmitButton>
              <ResetButton />
            </Space>
          </div>
        </ReactForm>
      </Card>
    </div>
  );
};

render(<BaseExample />);

```

- 联动显示与赋值
- 根据表单数据条件显示字段；用按钮一次 setFields 给不同条件的字段整体赋值
- _ReactForm(@kne/current-lib_react-form),antd(antd)

```jsx
const { default: ReactForm, useField, useSubmit, useReset, useFormApi } = _ReactForm;
const { useEffect, useState } = React;
const { Button, Space, Card, Input: AntInput, Select: AntSelect, Radio, Switch, Tag, Typography, message } = antd;
const { Text } = Typography;

const Input = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;
  const isValidating = fieldProps.errState === 3;

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
      </div>
      <div>
        <AntInput
          {...fieldProps.associationOptions}
          ref={fieldProps.fieldRef}
          type="text"
          value={fieldProps.value || ''}
          onChange={e => fieldProps.onChange(e.target.value)}
          onBlur={fieldProps.triggerValidate}
          status={isError ? 'error' : undefined}
          style={{ width: 200 }}
        />
        {fieldProps.errMsg && (
          <Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
            {fieldProps.errMsg}
          </Text>
        )}
        {isValidating && (
          <Text type="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
            验证中...
          </Text>
        )}
      </div>
    </div>
  );
};

const SelectField = props => {
  const { options, ...rest } = props;
  const fieldProps = useField(rest);
  const isError = fieldProps.errState === 2;

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
      </div>
      <AntSelect
        {...fieldProps.associationOptions}
        value={fieldProps.value}
        onChange={value => {
          fieldProps.onChange(value);
          fieldProps.triggerValidate();
        }}
        options={options}
        status={isError ? 'error' : undefined}
        style={{ width: 200 }}
      />
      {fieldProps.errMsg && (
        <Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
          {fieldProps.errMsg}
        </Text>
      )}
    </div>
  );
};

const RadioField = props => {
  const { options, ...rest } = props;
  const fieldProps = useField(rest);

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text>{fieldProps.label}</Text>
      </div>
      <Radio.Group
        value={fieldProps.value}
        onChange={e => {
          fieldProps.onChange(e.target.value);
          fieldProps.triggerValidate();
        }}>
        {options.map(item => (
          <Radio key={item.value} value={item.value}>
            {item.label}
          </Radio>
        ))}
      </Radio.Group>
    </div>
  );
};

const SwitchField = props => {
  const fieldProps = useField(props);

  return (
    <div style={{ marginBottom: 16 }}>
      <Space>
        <Switch
          checked={!!fieldProps.value}
          onChange={checked => {
            fieldProps.onChange(checked);
            fieldProps.triggerValidate();
          }}
        />
        <Text>{fieldProps.label}</Text>
      </Space>
    </div>
  );
};

const SubmitButton = ({ children }) => {
  const { isLoading, onClick } = useSubmit();
  return (
    <Button type="primary" onClick={onClick} loading={isLoading} style={{ marginRight: 8 }}>
      {children}
    </Button>
  );
};

const ResetButton = () => {
  const { onClick } = useReset();
  return <Button onClick={onClick}>重置</Button>;
};

const useWatch = name => {
  const { openApi, emitter } = useFormApi();
  const [value, setValue] = useState(() => (openApi.getFormData() || {})[name]);

  useEffect(() => {
    const syncFromForm = () => {
      setValue((openApi.getFormData() || {})[name]);
    };
    const onSetValue = payload => {
      if (payload.path === name) {
        setValue(payload.value);
      }
    };
    const setToken = emitter.addListener('form:field:set-value', onSetValue);
    const resetToken = emitter.addListener('form:reset', syncFromForm);
    return () => {
      setToken.remove();
      resetToken.remove();
    };
  }, [emitter, name, openApi]);

  return value;
};

const Watch = ({ name, children }) => children(useWatch(name));

const toCompanyEmail = name => {
  const local = String(name || '').replace(/[^a-zA-Z0-9_.-]/g, '');
  return &#96;${local || 'contact'}@example.com&#96;;
};

const FillOnShow = ({ name, from, map }) => {
  const { openApi } = useFormApi();

  useEffect(() => {
    const data = openApi.getFormData() || {};
    if (data[name] !== undefined && data[name] !== '') {
      return;
    }
    const source = data[from];
    const next = map ? map(source, data) : source;
    if (next === undefined || next === '') {
      return;
    }
    openApi.setField({ name, value: next });
  }, []);

  return null;
};

const AssignButtons = ({ items }) => {
  const { openApi } = useFormApi();

  return (
    <Space wrap style={{ marginBottom: 12 }}>
      {items.map(item => (
        <Button
          key={item.label}
          onClick={() => {
            const data = openApi.getFormData() || {};
            const fields = item.getFields ? item.getFields(data) : item.fields;
            openApi.setFields(fields);
            message.success(&#96;已赋值：${item.label}&#96;);
          }}>
          {item.label}
        </Button>
      ))}
    </Space>
  );
};

const BaseExample = () => {
  return (
    <div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
      <Card title="表单数据联动显示与赋值" bordered={false}>
        <ReactForm
          debug
          onSubmit={data => {
            console.log('submit:', data);
            message.success('提交成功: ' + JSON.stringify(data, null, 2));
          }}>
          <Card
            type="inner"
            title={
              <Space>
                1. 按表单数据条件显示字段<Tag color="blue">隐藏字段卸载</Tag>
              </Space>
            }
            style={{ marginBottom: 16 }}>
            <Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
              用 useWatch 读当前表单值，再条件渲染字段。隐藏时卸载，提交不含该字段；再次显示会从 pending 恢复刚才填过的值。
              下方按钮一次 setFields 条件字段和对应输入：当前未挂载的字段会进 pending，切过去就能带出。
            </Text>
            <AssignButtons
              items={[
                {
                  label: '填入邮箱方案',
                  fields: [
                    { name: 'contactType', value: 'email' },
                    { name: 'email', value: 'alice@example.com' }
                  ]
                },
                {
                  label: '填入手机方案',
                  fields: [
                    { name: 'contactType', value: 'phone' },
                    { name: 'phone', value: '13800138000' }
                  ]
                }
              ]}
            />
            <RadioField
              name="contactType"
              label="联系方式"
              defaultValue="email"
              options={[
                { label: '邮箱', value: 'email' },
                { label: '手机', value: 'phone' }
              ]}
            />
            <Watch name="contactType">
              {contactType =>
                contactType === 'email' ? (
                  <Input name="email" label="邮箱" rule="REQ EMAIL" />
                ) : contactType === 'phone' ? (
                  <Input name="phone" label="手机" rule="REQ TEL" />
                ) : null
              }
            </Watch>
          </Card>

          <Card
            type="inner"
            title={
              <Space>
                2. 显示时赋值，并继续跟随<Tag color="green">setField + associations</Tag>
              </Space>
            }
            style={{ marginBottom: 16 }}>
            <Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
              打开开关后挂载昵称：FillOnShow 用 setField 把名称抄过去（字段尚未挂载时会写入 pending）；之后名称变化由 associations 同步。
            </Text>
            <AssignButtons
              items={[
                {
                  label: '打开昵称并抄名称',
                  getFields: data => [
                    { name: 'sameAsName', value: true },
                    { name: 'nickname', value: data.name || '示例昵称' }
                  ]
                },
                {
                  label: '关掉昵称只改名称',
                  fields: [
                    { name: 'sameAsName', value: false },
                    { name: 'name', value: '王五' }
                  ]
                }
              ]}
            />
            <Input name="name" label="名称" rule="REQ LEN-0-10" />
            <SwitchField name="sameAsName" label="使用名称作为昵称" />
            <Watch name="sameAsName">
              {sameAsName =>
                sameAsName ? (
                  <>
                    <FillOnShow name="nickname" from="name" />
                    <Input
                      name="nickname"
                      label="昵称"
                      rule="REQ LEN-0-10"
                      associations={{
                        fields: [{ name: 'name' }],
                        callback: ({ target, origin, openApi }) => {
                          openApi.setFieldValue(target, origin.value);
                        }
                      }}
                    />
                  </>
                ) : null
              }
            </Watch>
          </Card>

          <Card
            type="inner"
            title={
              <Space>
                3. 类型切换后显示并计算赋值<Tag color="orange">setField 计算值</Tag>
              </Space>
            }
            style={{ marginBottom: 16 }}>
            <Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
              选「企业」后显示公司名和联系邮箱：公司名默认用名称，邮箱按名称拼出示例地址。提交时仅包含当前显示的字段。
            </Text>
            <AssignButtons
              items={[
                {
                  label: '切到企业并填公司信息',
                  getFields: data => [
                    { name: 'userType', value: 'company' },
                    { name: 'companyName', value: data.name ? &#96;${data.name}科技&#96; : '示例科技' },
                    { name: 'companyEmail', value: toCompanyEmail(data.name) }
                  ]
                },
                {
                  label: '切回个人',
                  fields: [{ name: 'userType', value: 'person' }]
                }
              ]}
            />
            <SelectField
              name="userType"
              label="用户类型"
              defaultValue="person"
              options={[
                { label: '个人', value: 'person' },
                { label: '企业', value: 'company' }
              ]}
            />
            <Watch name="userType">
              {userType =>
                userType === 'company' ? (
                  <>
                    <FillOnShow name="companyName" from="name" />
                    <FillOnShow
                      name="companyEmail"
                      from="name"
                      map={name => toCompanyEmail(name)}
                    />
                    <Input
                      name="companyName"
                      label="公司名称"
                      rule="REQ"
                      associations={{
                        fields: [{ name: 'name' }],
                        callback: ({ target, origin, openApi }) => {
                          openApi.setFieldValue(target, origin.value);
                        }
                      }}
                    />
                    <Input
                      name="companyEmail"
                      label="企业邮箱"
                      rule="REQ EMAIL"
                      associations={{
                        fields: [{ name: 'name' }],
                        callback: ({ target, origin, openApi }) => {
                          openApi.setFieldValue(target, toCompanyEmail(origin.value));
                        }
                      }}
                    />
                  </>
                ) : null
              }
            </Watch>
          </Card>

          <Card
            type="inner"
            title={
              <Space>
                4. 按条件整套赋值<Tag color="purple">一次 setFields</Tag>
              </Space>
            }
            style={{ marginBottom: 16 }}>
            <Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
              一次写入条件开关和对应字段。隐藏中的字段同样能赋值，切到该条件后从 pending 显示。
            </Text>
            <AssignButtons
              items={[
                {
                  label: '填入完整个人示例',
                  fields: [
                    { name: 'contactType', value: 'email' },
                    { name: 'email', value: 'zhangsan@example.com' },
                    { name: 'name', value: '张三' },
                    { name: 'sameAsName', value: true },
                    { name: 'nickname', value: '张三' },
                    { name: 'userType', value: 'person' }
                  ]
                },
                {
                  label: '填入完整企业示例',
                  fields: [
                    { name: 'contactType', value: 'phone' },
                    { name: 'phone', value: '13900139000' },
                    { name: 'name', value: '李四' },
                    { name: 'sameAsName', value: false },
                    { name: 'userType', value: 'company' },
                    { name: 'companyName', value: '李四科技' },
                    { name: 'companyEmail', value: 'lisi@example.com' }
                  ]
                }
              ]}
            />
          </Card>

          <div style={{ marginTop: 16 }}>
            <Space>
              <SubmitButton>提交</SubmitButton>
              <ResetButton />
            </Space>
          </div>
        </ReactForm>
      </Card>
    </div>
  );
};

render(<BaseExample />);

```

- 修复确认
- 确认 onFormDataChange / onReady、data 引用、defaultLength、增删不改外部 data、getFormData 实时、动态 associations
- _ReactForm(@kne/current-lib_react-form),antd(antd)

```jsx
const { default: ReactForm, useField, useSubmit, useReset, useFormApi, GroupList } = _ReactForm;
const { useEffect, useRef, useState } = React;
const { Button, Space, Card, Input: AntInput, Select: AntSelect, Tag, Typography, message, Alert } = antd;
const { Text } = Typography;

const Input = props => {
  const fieldProps = useField(props);
  return (
    <div style={{ marginBottom: 12 }}>
      <div style={{ marginBottom: 4 }}>
        <Text>{fieldProps.label}</Text>
      </div>
      <AntInput
        {...fieldProps.associationOptions}
        value={fieldProps.value || ''}
        onChange={e => fieldProps.onChange(e.target.value)}
        onBlur={fieldProps.triggerValidate}
        style={{ width: 220 }}
      />
    </div>
  );
};

const SubmitButton = ({ children }) => {
  const { isLoading, onClick } = useSubmit();
  return (
    <Button type="primary" onClick={onClick} loading={isLoading}>
      {children}
    </Button>
  );
};

const ResetButton = () => {
  const { onClick } = useReset();
  return <Button onClick={onClick}>重置</Button>;
};

const FormDataPreview = () => {
  const { openApi, emitter } = useFormApi();
  const [json, setJson] = useState(() => JSON.stringify(openApi.getFormData() || {}, null, 2));
  useEffect(() => {
    const sync = () => setJson(JSON.stringify(openApi.getFormData() || {}, null, 2));
    const token = emitter.addListener('form:field:set-value', sync);
    return () => token.remove();
  }, [emitter, openApi]);
  return (
    <pre style={{ margin: 0, padding: 12, background: '#fafafa', borderRadius: 6, fontSize: 12 }}>{json}</pre>
  );
};

const ReadyProbe = () => {
  const { openApi } = useFormApi();
  const [text, setText] = useState('尚未调用 onReady');
  return (
    <Space>
      <Button
        onClick={() => {
          openApi.onReady(() => setText('已挂载，onReady 立即执行'));
        }}>
        挂载后再点 onReady
      </Button>
      <Text>{text}</Text>
    </Space>
  );
};

const DataRefCard = () => {
  const [, setTick] = useState(0);
  const [showExtra, setShowExtra] = useState(false);
  const formRef = useRef(null);
  const data = { extra: '来自 props' };
  return (
    <Card
      type="inner"
      title={
        <Space>
          4. data 仅引用变化不冲 init<Tag color="blue">晚挂载字段</Tag>
        </Space>
      }
      style={{ marginBottom: 16 }}>
      <Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
        父组件每次点「重渲染」都会传入新的 data 对象，内容仍是「来自 props」。先 setFormData 再 forget pending，重渲染后显示晚挂载字段，应仍是 setFormData 的值。
      </Text>
      <ReactForm ref={formRef} data={data}>
        <Space wrap style={{ marginBottom: 12 }}>
          <Button onClick={() => formRef.current.setFormData({ extra: '来自 setFormData' })}>setFormData</Button>
          <Button onClick={() => formRef.current.forgetField({ name: 'extra' })}>forget pending</Button>
          <Button onClick={() => setTick(x => x + 1)}>父组件重渲染</Button>
          <Button type="primary" onClick={() => setShowExtra(true)}>
            显示晚挂载字段
          </Button>
        </Space>
        {showExtra ? <Input name="extra" label="extra（晚挂载）" /> : <Alert type="info" message="字段尚未挂载" />}
      </ReactForm>
    </Card>
  );
};

const DefaultLengthCard = () => {
  const formRef = useRef(null);
  const listRef = useRef(null);
  return (
    <Card
      type="inner"
      title={
        <Space>
          5. defaultLength 不垫短数组<Tag color="green">1 条不被垫成 2 条</Tag>
        </Space>
      }
      style={{ marginBottom: 16 }}>
      <Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
        GroupList defaultLength=2，点按钮写入 1 条数据后应只显示 1 项。
      </Text>
      <ReactForm ref={formRef}>
        <Button
          style={{ marginBottom: 12 }}
          onClick={() => formRef.current.setFormData({ users: [{ name: '仅一条' }] })}>
          setFormData 1 条
        </Button>
        <GroupList ref={listRef} name="users" defaultLength={2} reverseOrder={false}>
          {({ index, onRemove }) => (
            <Space key={index} style={{ display: 'flex', marginBottom: 8 }}>
              <Tag>项 {index + 1}</Tag>
              <Input name="name" label="名称" />
              <Button size="small" danger onClick={onRemove}>
                删除
              </Button>
            </Space>
          )}
        </GroupList>
      </ReactForm>
    </Card>
  );
};

const PARENT_DATA = { users: [{ name: '张三' }, { name: '李四' }] };

const SpliceCard = () => {
  const listRef = useRef(null);
  const [parentJson, setParentJson] = useState(() => JSON.stringify(PARENT_DATA, null, 2));
  return (
    <Card
      type="inner"
      title={
        <Space>
          6. 增删不改外部 data<Tag color="orange">clone 写回 init</Tag>
        </Space>
      }
      style={{ marginBottom: 16 }}>
      <Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
        删除第一项后，左侧表单少一行；右侧外部 PARENT_DATA 仍是张三、李四。
      </Text>
      <ReactForm data={PARENT_DATA} onFormDataChange={() => setParentJson(JSON.stringify(PARENT_DATA, null, 2))}>
        <div style={{ display: 'flex', gap: 16 }}>
          <div style={{ flex: 1 }}>
            <GroupList ref={listRef} name="users" defaultLength={0} reverseOrder={false}>
              {({ index, onRemove }) => (
                <Space key={index} style={{ display: 'flex', marginBottom: 8 }}>
                  <Tag>项 {index + 1}</Tag>
                  <Input name="name" label="名称" />
                  <Button size="small" danger onClick={onRemove}>
                    删除
                  </Button>
                </Space>
              )}
            </GroupList>
          </div>
          <pre style={{ flex: 1, margin: 0, padding: 12, background: '#fafafa', fontSize: 12 }}>{parentJson}</pre>
        </div>
      </ReactForm>
    </Card>
  );
};

const MemoizeCard = () => {
  const [snapshot, setSnapshot] = useState('{}');
  return (
    <Card
      type="inner"
      title={
        <Space>
          7. getFormData 随输入更新<Tag color="purple">不再 memoize 旧 Map</Tag>
        </Space>
      }
      style={{ marginBottom: 16 }}>
      <Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
        输入时右侧 JSON 应立刻变化。onFormDataChange 同一轮多次 set-value 会合并成一次。
      </Text>
      <ReactForm
        onFormDataChange={formData => {
          setSnapshot(JSON.stringify(formData, null, 2));
        }}>
        <Input name="title" label="标题" />
        <Input name="count" label="数量" />
        <pre style={{ marginTop: 8, padding: 12, background: '#fafafa', fontSize: 12 }}>{snapshot}</pre>
      </ReactForm>
    </Card>
  );
};

const AssociationsCard = () => {
  const [source, setSource] = useState('name');
  return (
    <Card
      type="inner"
      title={
        <Space>
          9. 动态 associations<Tag>改 fields 后跟随新源</Tag>
        </Space>
      }
      style={{ marginBottom: 16 }}>
      <Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
        切换「跟随字段」后，改对应源字段，昵称应抄新源，不再跟旧源。
      </Text>
      <ReactForm>
        <Space style={{ marginBottom: 12 }}>
          <Text>跟随</Text>
          <AntSelect
            value={source}
            style={{ width: 160 }}
            onChange={setSource}
            options={[
              { label: '名称', value: 'name' },
              { label: '备注', value: 'remark' }
            ]}
          />
        </Space>
        <Input name="name" label="名称" />
        <Input name="remark" label="备注" />
        <Input
          name="nickname"
          label="昵称"
          associations={{
            fields: [{ name: source }],
            callback: ({ target, origin, openApi }) => {
              openApi.setFieldValue(target, origin.value);
            }
          }}
        />
      </ReactForm>
    </Card>
  );
};

const BaseExample = () => {
  const [changeLog, setChangeLog] = useState('尚未变化');
  return (
    <div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
      <Card title="修复确认示例" bordered={false}>
        <Card
          type="inner"
          title={
            <Space>
              1 / 2. onFormDataChange 与 onReady<Tag color="cyan">已挂载立即回调</Tag>
            </Space>
          }
          style={{ marginBottom: 16 }}>
          <ReactForm
            onFormDataChange={formData => {
              setChangeLog(JSON.stringify(formData));
            }}
            onSubmit={data => message.success(JSON.stringify(data))}>
            <Input name="name" label="名称" />
            <ReadyProbe />
            <div style={{ margin: '12px 0' }}>
              <Text type="secondary">onFormDataChange：</Text>
              <Text code>{changeLog}</Text>
            </div>
            <FormDataPreview />
            <Space style={{ marginTop: 12 }}>
              <SubmitButton>提交</SubmitButton>
              <ResetButton />
            </Space>
          </ReactForm>
        </Card>

        <DataRefCard />
        <DefaultLengthCard />
        <SpliceCard />
        <MemoizeCard />
        <AssociationsCard />
      </Card>
    </div>
  );
};

render(<BaseExample />);

```

- 远程验证规则
- 自定义远程异步验证规则，模拟接口调用进行字段校验
- _ReactForm(@kne/current-lib_react-form),antd(antd)

```jsx
const { default: ReactForm, useField, useSubmit, useReset } = _ReactForm;
const { useState, useEffect } = React;
const { Button, Space, Card, Input: AntInput, Typography, Alert, message, List, Tag, Divider, Descriptions } = antd;
const { Text } = Typography;

const Input = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;
  const isValidating = fieldProps.errState === 3;

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
      </div>
      <div>
        <AntInput ref={fieldProps.fieldRef} type="text" value={fieldProps.value || ''} onChange={e => fieldProps.onChange(e.target.value)} onBlur={fieldProps.triggerValidate} status={isError ? 'error' : undefined} style={{ width: 200 }} />
        {fieldProps.errMsg && (
          <Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
            {fieldProps.errMsg}
          </Text>
        )}
        {isValidating && (
          <Text type="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
            验证中...
          </Text>
        )}
      </div>
    </div>
  );
};

const SubmitButton = ({ children }) => {
  const { isLoading, onClick } = useSubmit();
  return (
    <Button type="primary" onClick={onClick} loading={isLoading} style={{ marginRight: 8 }}>
      {children}
    </Button>
  );
};

const ResetButton = () => {
  const { onClick } = useReset();
  return <Button onClick={onClick}>重置</Button>;
};

// 校验日志组件
const ValidationLog = () => {
  const [logs, setLogs] = useState([]);
  const listRef = React.useRef(null);

  useEffect(() => {
    // 拦截 console.log 来捕获验证日志
    const originalLog = console.log;
    console.log = (...args) => {
      const message = args.join(' ');
      if (message.includes('验证')) {
        const timestamp = new Date().toLocaleTimeString();
        setLogs(prev => [...prev, { id: Date.now() + Math.random(), timestamp, message }]);
      }
      originalLog.apply(console, args);
    };
    return () => {
      console.log = originalLog;
    };
  }, []);

  // 自动滚动到底部
  useEffect(() => {
    if (listRef.current) {
      listRef.current.scrollTop = listRef.current.scrollHeight;
    }
  }, [logs]);

  const clearLogs = () => setLogs([]);

  return (
    <Card title="远程校验过程" size="small" style={{ marginBottom: 20 }}>
      <Space style={{ marginBottom: 12 }}>
        <Button size="small" onClick={clearLogs}>清空日志</Button>
        <Text type="secondary" style={{ fontSize: 12 }}>共 {logs.length} 条记录</Text>
      </Space>
      <div ref={listRef} style={{ height: 300, overflowY: 'auto', border: '1px solid #f0f0f0', borderRadius: 4 }}>
        {logs.length === 0 ? (
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
            <Text type="secondary" style={{ fontSize: 12 }}>
              暂无验证日志，请在上方输入框中输入内容触发验证
            </Text>
          </div>
        ) : (
          <List
            size="small"
            dataSource={logs}
            renderItem={item => {
              const isStart = item.message.includes('开始验证');
              const isComplete = item.message.includes('验证完成');
              return (
                <List.Item style={{ padding: '8px 12', borderBottom: '1px solid #f0f0f0' }}>
                  <Space>
                    <Text type="secondary" style={{ fontSize: 11, minWidth: 70 }}>{item.timestamp}</Text>
                    <Tag color={isStart ? 'blue' : isComplete ? 'green' : 'default'} style={{ margin: 0 }}>
                      {isStart ? '开始' : isComplete ? '完成' : '其他'}
                    </Tag>
                    <Text style={{ fontSize: 12 }}>{item.message}</Text>
                  </Space>
                </List.Item>
              );
            }}
          />
        )}
      </div>
    </Card>
  );
};

// 模拟用户名重复检查的远程验证规则
const checkUsernameUnique = async value => {
  if (!value) {
    return { result: false, errMsg: '用户名不能为空' };
  }

  // 模拟已存在的用户名
  const existingUsernames = ['admin', 'test', 'user', 'root'];

  console.log(&#96;开始验证用户名: ${value}&#96;);
  await new Promise(resolve => setTimeout(resolve, 1500));
  console.log(&#96;验证完成用户名: ${value}&#96;);

  if (existingUsernames.includes(value)) {
    return { result: false, errMsg: '该用户名已被占用' };
  }

  return { result: true, errMsg: '' };
};

// 模拟手机号有效性检查的远程验证规则
const checkPhoneValid = async value => {
  if (!value) {
    return { result: false, errMsg: '手机号不能为空' };
  }

  console.log(&#96;开始验证手机号: ${value}&#96;);
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log(&#96;验证完成手机号: ${value}&#96;);

  // 模拟手机号黑名单
  const blackList = ['13800138000', '13900139000'];

  if (blackList.includes(value)) {
    return { result: false, errMsg: '该手机号已被注册' };
  }

  return { result: true, errMsg: '' };
};

const BaseExample = () => {
  return (
    <div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
      <Card title="远程验证规则示例" bordered={false}>
        <Alert message="异步验证说明" description="示例展示了如何使用自定义的异步验证规则来模拟远程接口验证" type="info" showIcon style={{ marginBottom: 20 }} />

        <ValidationLog />

        <Card type="inner" title="特殊输入值说明" size="small" style={{ marginBottom: 20, backgroundColor: '#fff7e6' }}>
          <Descriptions column={1} size="small">
            <Descriptions.Item label="已占用用户名">
              <Space>
                <Tag color="red">admin</Tag>
                <Tag color="red">test</Tag>
                <Tag color="red">user</Tag>
                <Tag color="red">root</Tag>
              </Space>
            </Descriptions.Item>
            <Descriptions.Item label="已注册手机号">
              <Space>
                <Tag color="red">13800138000</Tag>
                <Tag color="red">13900139000</Tag>
              </Space>
            </Descriptions.Item>
          </Descriptions>
          <Text type="secondary" style={{ fontSize: 12 }}>
            💡 输入以上值会触发验证失败，用于测试错误提示和校验过程
          </Text>
        </Card>

        <ReactForm
          debug
          rules={{
            CHECK_USERNAME: checkUsernameUnique,
            CHECK_PHONE: checkPhoneValid
          }}
          data={{
            username: '',
            phone: ''
          }}
          onSubmit={async data => {
            console.log('提交数据:', data);
            await new Promise(resolve => setTimeout(resolve, 2000));
            message.success('注册成功: ' + JSON.stringify(data, null, 2));
          }}>
          <Input name="username" label="用户名" rule="REQ LEN-3-20 CHECK_USERNAME" placeholder="请输入用户名" />
          <Input name="phone" label="手机号" rule="REQ TEL CHECK_PHONE" placeholder="请输入手机号" />

          <div style={{ marginTop: 20 }}>
            <Space>
              <SubmitButton>注册</SubmitButton>
              <ResetButton />
            </Space>
          </div>
        </ReactForm>
      </Card>
    </div>
  );
};

render(<BaseExample />);

```

- falsy 值显示
- 展示 Form 的 data 中字段值为 0 或 false 时能正确显示和提交
- _ReactForm(@kne/current-lib_react-form),antd(antd)

```jsx
const { default: ReactForm, useField, useSubmit, useReset, useFormApi } = _ReactForm;
const { useRef } = React;
const { Button, Space, Card, Input: AntInput, InputNumber, Select, Switch, Typography, message, Divider, Descriptions, Alert } = antd;
const { Text } = Typography;

// ========================================
// 通用组件
// ========================================

const Input = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
      </div>
      <div>
        <AntInput
          ref={fieldProps.fieldRef}
          type={props.type || 'text'}
          value={fieldProps.value ?? ''}
          onChange={e => {
            fieldProps.onChange(e.target.value);
            fieldProps.triggerValidate();
          }}
          onBlur={fieldProps.triggerValidate}
          placeholder={props.placeholder}
          status={isError ? 'error' : undefined}
          style={{ width: props.width || 200 }}
        />
        {fieldProps.errMsg && (
          <Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
            {fieldProps.errMsg}
          </Text>
        )}
      </div>
    </div>
  );
};

const NumberInput = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
      </div>
      <div>
        <InputNumber
          ref={fieldProps.fieldRef}
          value={fieldProps.value}
          onChange={val => {
            fieldProps.onChange(val);
            fieldProps.triggerValidate();
          }}
          placeholder={props.placeholder}
          status={isError ? 'error' : undefined}
          style={{ width: props.width || 200 }}
          min={props.min}
          max={props.max}
        />
        {fieldProps.errMsg && (
          <Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
            {fieldProps.errMsg}
          </Text>
        )}
      </div>
    </div>
  );
};

const SelectField = props => {
  const fieldProps = useField(props);
  const isError = fieldProps.errState === 2;

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text type={isError ? 'danger' : undefined}>{fieldProps.label}</Text>
      </div>
      <div>
        <Select
          ref={fieldProps.fieldRef}
          value={fieldProps.value}
          onChange={val => {
            fieldProps.onChange(val);
            fieldProps.triggerValidate();
          }}
          placeholder={props.placeholder}
          status={isError ? 'error' : undefined}
          style={{ width: props.width || 200 }}
          options={props.options}
        />
        {fieldProps.errMsg && (
          <Text type="danger" style={{ marginLeft: 8, fontSize: 12 }}>
            {fieldProps.errMsg}
          </Text>
        )}
      </div>
    </div>
  );
};

const SwitchField = props => {
  const fieldProps = useField(props);

  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ marginBottom: 4 }}>
        <Text>{fieldProps.label}</Text>
      </div>
      <div>
        <Switch
          ref={fieldProps.fieldRef}
          checked={!!fieldProps.value}
          onChange={val => {
            fieldProps.onChange(val);
          }}
        />
        <Text type="secondary" style={{ marginLeft: 8 }}>
          当前值: {String(fieldProps.value)}
        </Text>
      </div>
    </div>
  );
};

const SubmitButton = ({ children }) => {
  const { isLoading, onClick } = useSubmit();
  return (
    <Button type="primary" onClick={onClick} loading={isLoading}>
      {children}
    </Button>
  );
};

const ResetButton = () => {
  const { onClick } = useReset();
  return <Button onClick={onClick} style={{ marginLeft: 8 }}>重置</Button>;
};

// ========================================
// FormInner 组件：展示表单当前数据
// ========================================

const FormInner = () => {
  const { openApi: formApi } = useFormApi();
  const formData = formApi.getFormData();

  return (
    <Card type="inner" title="当前表单数据" style={{ marginBottom: 16 }}>
      <Descriptions column={1} size="small" bordered>
        <Descriptions.Item label="表单数据">
          <Text code>{JSON.stringify(formData)}</Text>
        </Descriptions.Item>
      </Descriptions>
    </Card>
  );
};

// ========================================
// 示例：data 中包含数字 0 的表单
// ========================================

const ZeroValueExample = () => {
  return (
    <Card title="data 中值为 falsy (0/false) 的示例" extra={
      <Text type="secondary" style={{ fontSize: 12 }}>
        验证 data 中字段值为 0 时能正确显示
      </Text>
    }>
      <Space direction="vertical" style={{ width: '100%' }}>
        <Alert
          message="说明"
          description='当 Form 的 data 属性中字段值为 0 或 false 等 falsy 值时，表单需要正确识别并赋值，而非将其视为空值。'
          type="info"
          showIcon
          style={{ marginBottom: 16 }}
        />

        <ReactForm
          debug
          data={{
            name: '测试项目',
            count: 0,
            score: 0,
            level: 0,
            enabled: 0,
            isActive: false,
            isPublic: false,
            description: ''
          }}
          onSubmit={async data => {
            await new Promise(resolve => {
              setTimeout(() => resolve(), 1000);
            });
            console.log('submit:', data);
            message.success('提交成功: ' + JSON.stringify(data, null, 2));
          }}
        >
          <FormInner />

          <Divider>值为 0 的字段</Divider>

          <Input name="name" label="名称" rule="REQ" placeholder="请输入名称" />

          <NumberInput name="count" label="数量 (data=0)" rule="REQ" placeholder="请输入数量" />

          <NumberInput name="score" label="分数 (data=0)" rule="REQ" placeholder="请输入分数" min={0} max={100} />

          <SelectField
            name="level"
            label="等级 (data=0)"
            rule="REQ"
            placeholder="请选择等级"
            options={[
              { value: 0, label: '0 - 最低' },
              { value: 1, label: '1 - 普通' },
              { value: 2, label: '2 - 良好' },
              { value: 3, label: '3 - 优秀' }
            ]}
          />

          <SelectField
            name="enabled"
            label="状态 (data=0)"
            rule="REQ"
            placeholder="请选择状态"
            options={[
              { value: 0, label: '0 - 禁用' },
              { value: 1, label: '1 - 启用' }
            ]}
          />

          <Divider>值为 false 的字段</Divider>

          <SwitchField name="isActive" label="是否激活 (data=false)" />

          <SwitchField name="isPublic" label="是否公开 (data=false)" />

          <Input name="description" label="描述 (data='')" placeholder="请输入描述" />

          <Divider />

          <Space>
            <SubmitButton>提交</SubmitButton>
            <ResetButton />
          </Space>
        </ReactForm>
      </Space>
    </Card>
  );
};

render(<ZeroValueExample />);

```

### API

#### Form 组件 API

##### 属性

| 属性名 | 说明 | 类型 | 默认值 |
|-----|----|----|-----|
| data | 表单初始值（只影响尚未挂载的字段；不会重放已挂载字段）。按**内容**比较，仅引用变化且内容相同不重置 initFormData | object | {} |
| rules | 自定义验证规则 | object | {} |
| interceptors | 表单拦截器配置 | object | {} |
| debug | 是否开启调试模式 | boolean | false |
| noFilter | 是否关闭空值过滤 | boolean | false |
| onPrevSubmit | 提交前回调 | function(values, form) | - |
| onSubmit | 提交回调 | function(values) | Promise |
| onError | 错误回调 | function(errors) | - |
| onFormDataChange | 表单数据变化回调（同一轮多次值变更合并为一次；参数为当前已挂载字段汇总） | function(formData) | - |
| children | 表单内容 | React.ReactNode | - |

##### Ref 暴露的方法

| 方法名 | 说明 | 参数 | 返回值 |
|-----|----|----|-----|
| submit | 提交表单 | - | void |
| data | 获取表单数据 | - | object |
| set data(data) | 设置表单数据 | object | void |
| reset | 重置表单 | - | void |
| errors | 获取错误信息 | - | array |
| isPass | 表单是否通过验证 | - | boolean |
| setFormData | 设置表单数据 | (data, runValidate?) | void |
| getFormData | 获取表单数据 | - | object |
| setFields | 批量设置字段属性 | (fields, options?) | void |
| setField | 设置单个字段属性 | (field, options?) | void |
| setFieldValue | 设置字段值 | (target, value, options?) | void |
| setFieldValidate | 设置字段验证状态 | (target, validate) | void |
| getField | 获取指定字段 | (target) | Field |
| getFields | 获取匹配的字段列表 | (target) | Field[] |
| validateField | 验证指定字段 | (target) | void |
| validateAll | 验证所有字段 | - | void |
| onReady | 表单就绪回调（若已 mount 则立即执行） | callback | - |
| onDestroy | 表单销毁回调（若已卸载则立即执行） | callback | - |
| forgetField | 清除某字段 pending，重建时不回填 | (target) | void |
| forgetFields | 批量 forget | (targets) | void |
| registerDeclaredPaths | FieldList 登记声明 path（内部） | (sourceId, paths) | void |
| unregisterDeclaredPaths | 取消声明源（内部） | (sourceId) | void |

#### useField Hook API

##### 参数

| 参数名 | 说明 | 类型 | 默认值 |
|-----|----|----|-----|
| name | 字段名称 | string | - |
| label | 字段标签 | string | - |
| rule | 验证规则字符串 | string | - |
| interceptor | 字段拦截器配置 | object | {} |
| associations | 字段关联配置（`fields` 变化会同步到已挂载字段；callback 始终读最新） | object | {} |
| noTrim | 是否不自动去空格 | boolean | false |
| debounce | 防抖延迟时间 | number | 0 |
| defaultValue | 默认值 | any | - |
| preserve | 卸载时是否写入 pending 以便重建回填，默认 true | boolean | true |
| errMsg | 自定义错误信息 | string | - |
| onChange | 值变化回调 | function(value) | - |

##### 返回值

| 属性名 | 说明 | 类型 |
|-----|----|----|
| id | 字段唯一标识 | string |
| name | 字段名称 | string |
| label | 字段标签 | string |
| value | 字段值 | any |
| fieldRef | 字段 ref | RefObject |
| formData | 表单数据 | object |
| formState | 表单状态 | Map |
| rule | 验证规则 | string |
| groupName | 分组名称 | string |
| groupIndex | 分组索引 | number |
| onChange | 值变化处理函数 | function |
| isValueChanged | 是否值已改变 | boolean |
| triggerValidate | 触发验证 | function |
| associationOptions | 关联选项 | object |
| errState | 错误状态 (0未验证, 1通过, 2错误, 3验证中) | number |
| errMsg | 错误信息 | string |

#### useSubmit Hook API

##### 返回值

| 属性名 | 说明 | 类型 |
|-----|----|----|
| isLoading | 是否正在提交 | boolean |
| isPass | 表单是否通过验证 | boolean |
| onClick | 提交点击处理函数 | function |

#### useReset Hook API

##### 返回值

| 属性名 | 说明 | 类型 |
|-----|----|----|
| onClick | 重置点击处理函数 | function |

#### useFormApi Hook API

##### 返回值

与 Form 组件 ref 暴露的方法相同，返回 openApi 对象。

#### Group 组件 API

##### 属性

| 属性名 | 说明 | 类型 |
|-----|----|----|
| id | 分组唯一标识 | string |
| name | 分组名称 | string |
| defaultValue | 分组默认值 | object |
| children | 渲染函数 | function({ id, name, group, index }) |

##### children 参数

| 参数名 | 说明 | 类型 |
|-----|----|----|
| id | 分组唯一标识 | string |
| name | 完整分组名称 | string |
| group | 分组数据 | object |
| index | 分组索引 | number |

#### GroupList 组件 API

##### 属性

| 属性名 | 说明 | 类型 | 默认值 |
|-----|----|----|-----|
| name | 分组列表名称 | string | - |
| defaultLength | 没有数组时的初始条数；已有数组（含空数组）以数组长度为准，不再垫长 | number | 1 |
| empty | 空列表时显示的内容 | ReactNode | - |
| reverseOrder | 是否倒序显示 | boolean | true |
| children | 渲染函数 | function | - |
| ref | ref 对象 | RefObject | - |

##### children 参数

| 参数名 | 说明 | 类型 |
|-----|----|----|
| id | 分组项唯一标识 | string |
| index | 分组项索引 | number |
| length | 分组列表总长度 | number |
| onAdd | 添加分组项 | function(options) |
| onRemove | 删除当前分组项 | function |

##### ref 暴露的方法

| 方法名 | 说明 | 参数 |
|-----|----|----|
| onAdd | 添加分组项 | function({ isUnshift?, defaultValue? }) |
| onRemove | 删除分组项 | function(id) |

#### 内置验证规则

| 规则名 | 说明 | 格式 | 示例 |
|-----|----|----|-----|
| REQ | 必填验证 | REQ | rule="REQ" |
| TEL | 手机号验证 | TEL | rule="TEL" |
| EMAIL | 邮箱验证 | EMAIL | rule="EMAIL" |
| LEN | 长度验证 | LEN-{min}-{max} | rule="LEN-3-10" |

##### 验证规则格式说明

- 验证规则通过空格分隔，可组合多个规则
- LEN 规则格式：LEN-{最小长度}-{最大长度}，若最小等于最大则表示精确长度

#### 全局拦截器 API

##### 注册拦截器

| 方法名 | 说明 | 参数 | 返回值 |
|-----|----|----|-----|
| interceptors.input.use | 注册输入拦截器 | (name, function) | number |
| interceptors.output.use | 注册输出拦截器 | (name, function) | number |

##### 使用示例

```javascript
import { interceptors } from '@kne/react-form';

// 注册输入拦截器（在值存入表单前执行）
interceptors.input.use('trim', value => value.trim());
interceptors.input.use('number', value => Number(value));

// 注册输出拦截器（在值从表单取出时执行）
interceptors.output.use('formatDate', value => {
  return value ? new Date(value).toISOString() : value;
});
```

#### 赋值与字段重建

- `setField` / `setFormData` 会写入 pending。字段尚未注册或仍处于 PRE_INIT 时不会丢弃；挂载后按 **pending > data 初值 > defaultValue** 回填。
- 字段卸载时（`preserve !== false`）把当前值写入 pending。React key 变化导致的重建会回填；`getFormData` 仍只汇总**当前已挂载**字段。
- FieldList 会把未 `filter` 的 list（含 `display={false}` / `hidden`）登记为声明 path。pending 里有、声明和挂载都没有的 path，对账后 forget（从 list 里 spread 掉或 GroupList 删除）。
- 类型切换后必须清空下游时，在 `onChange` 里调用 `forgetField` / `setField({ name, value: undefined })`，不要依赖卸载推断。
- `hidden`：仍在声明内，值保留。`display={false}`：仍在声明内，再显示能回填。从 list 删除：离开声明，forget。
- `reset` 与 Form 卸载会清空 pending。`debug` 下 `setField` 未匹配到字段且**没有**写入 pending 时才会 `console.warn`。

稳定性单测（`src/__tests__`，入口 `npm test`）：

- `createSetFieldsEvent`：未注册 / PRE_INIT / ready / falsy / 分组 path / debug 写入 pending 不 warn
- `FormEvent` / `useOpenApi`：onFormDataChange 合并触发；已 mount 的 onReady 立即回调
- `formFixes`：data 仅引用变化不重置 init
- `GroupList`：defaultLength 不垫短数组；增删 clone 写回 init、不 splice 原数组
- `Field`：computedFormDataFormState / stateToError 同一 Map 原地改值可读到新值
- `associations` / `useFieldInit` / `createFieldChangeEvent`：空 fields 不匹配；fields 变化后同步
- `createFieldRemoveEvent`：preserve 默认写入 pending、`preserve={false}`、已 forget 不再写回
- `createForgetGroupEvent` / `createResetEvent`
- `fieldAssignLifecycle`：先 setField 后挂载、同 path 卸载再挂、声明消失、forget 后不回填
- `pendingFormData`：对账、forgetByPrefix、mergeFormData

---
_Source: https://npm.io/package/@kne/react-form · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
