npm.io
3.1.13 • Published 3 weeks ago

@kne/react-form

Licence
ISC
Version
3.1.13
Deps
6
Size
524 kB
Vulns
0
Weekly
0

react-form

描述

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

安装

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
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 })

// 同步验证规则
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 实现表单字段联动的核心机制,通过监听关联字段的变化,自动更新目标字段的值。

配置结构
{
  fields: [{ name: 'field1' }, { name: 'field2' }],
  callback: ({ target, origin, openApi }) => {
    // 当关联字段变化时,更新目标字段
    openApi.setFieldValue(target, computedValue);
  }
}
属性 类型 必填 说明
fields Array<FieldReference> 是 关联字段列表,当这些字段变化时触发回调
callback Function 是 关联回调函数,接收变化信息并更新目标字段
FieldReference 引用类型
// 单字段引用
{ name: 'fieldName' }

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

// 分组特定项引用
{
  name: 'fieldName',
  groupName: 'groupName',
  groupIndex: 0  // 索引从 0 开始
}
Callback 回调参数
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() 批量更新数据
执行顺序

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

避免循环关联
// ❌ 错误:循环依赖
<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 值从表单取出时 数据格式化、转换 内部 → 外部
注册机制
// 全局拦截器(所有表单共享)
import { interceptors } from 'react-form';

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

// 注册输出拦截器
interceptors.output.use('formatDate', value => {
  return value ? new Date(value).toISOString() : null;
});
表单级拦截器(优先级高于全局)
<Form
  interceptors={{
    input: [
      {
        name: 'customTrim',
        exec: value => value.trim()
      }
    ],
    output: [
      {
        name: 'uppercase',
        exec: value => value.toUpperCase()
      }
    ]
  }}
>
字段级拦截器配置
<Input
  name="email"
  rule="EMAIL"
  interceptor="trim"  // 指定使用的拦截器
/>
优先级
优先级从高到低:
1. 字段指定拦截器
2. 表单级拦截器
3. 全局拦截器

同名拦截器:后注册的覆盖先注册的
常用拦截器
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 组件

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

<GroupList ref={ref} name="groupName" defaultLength={2}>
  {({ index, onAdd, onRemove, length }) => (
    // 分组项内容
  )}
</GroupList>
回调参数
参数 类型 说明
index number 当前项的索引
onAdd Function 添加新项的函数
onRemove Function 移除当前项的函数
length number 分组当前项数
表单 API
useSubmit Hook
const { isLoading, isPass, onClick } = useSubmit();
参数 类型 说明
isLoading boolean 提交中状态
isPass boolean 表单是否通过验证
onClick Function 提交函数
useReset Hook
const { onClick } = useReset();
参数 类型 说明
onClick Function 重置函数
useFormApi Hook
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 表单卸载时 -
监听方式
// 在组件内通过 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();
  };
}, []);
生命周期钩子
const formApiRef = useRef();
formApiRef.current.onReady(() => {
  console.log('表单已就绪');
  // 可以在这里执行初始化操作
});

formApiRef.current.onDestroy(() => {
  console.log('表单即将销毁');
  // 可以在这里执行清理操作
});
生命周期
表单生命周期
阶段 触发时机 执行操作
挂载 Form 组件首次渲染 - 初始化 formState (Map 结构)
- 创建事件发射器
- 初始化 openApi
- 设置初始数据
- 绑定事件监听器
更新 Form 组件属性变化 - 更新 rules 配置
- 更新 interceptors 配置
- data 只更新后挂载字段的初始值,不重放已挂载字段
卸载 Form 组件销毁 - 清理所有事件监听器
- 清理异步任务
- 释放内存资源
字段生命周期
阶段 状态 说明 可执行操作
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)
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)
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)
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)
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)
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)
/**
 * 验证:外层 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)
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)
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)
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)
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)
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
使用示例
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