npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kne/react-form

v3.1.9

Published

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

Readme

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 - 提交和错误回调

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. 分组字段聚合 - 对分组内的多个字段值进行汇总

拦截器

拦截器类型

| 类型 | 执行时机 | 用途 | 方向 | |------|---------|------|------| | 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() | 表单就绪回调 | | | 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: '[email protected]' },
                  { 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="[email protected]" 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: '[email protected]' },
                { 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: '[email protected]' },
            { 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 />);
  • 字段关联
  • 通过 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 />);
  • 远程验证规则
  • 自定义远程异步验证规则,模拟接口调用进行字段校验
  • _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 | 表单初始值 | 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 | 表单就绪回调 | callback | - | | onDestroy | 表单销毁回调 | callback | - |

useField Hook API

参数

| 参数名 | 说明 | 类型 | 默认值 | |-----|----|----|-----| | name | 字段名称 | string | - | | label | 字段标签 | string | - | | rule | 验证规则字符串 | string | - | | interceptor | 字段拦截器配置 | object | {} | | associations | 字段关联配置 | object | {} | | noTrim | 是否不自动去空格 | boolean | false | | debounce | 防抖延迟时间 | number | 0 | | defaultValue | 默认值 | any | - | | 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;
});