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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@fastspace/schema-form

v0.0.15

Published

<p align="center"> <img src="./龙.png" alt="logo" width="120" /> </p>

Readme

Fastspace Schema Form 组件文档

基于 JSON Schema 驱动的动态表单组件,支持验证、联动、计算字段等功能。(Mui v7 + React Hook Form + Valibot)

license npm latest package npm downloads GitHub branch status

目录


快速开始

import { SchemaForm, type SchemaInput, type SchemaFormInstance } from './share/Schema';

const schema: SchemaInput = {
  fields: [
    {
      name: 'username',
      component: 'Text',
      ui: { label: '用户名', placeholder: '请输入用户名' },
      rules: [
        { type: 'required', message: '用户名必填' },
        { type: 'minLength', value: 3, message: '至少3个字符' },
      ],
    },
    {
      name: 'email',
      component: 'Text',
      ui: { label: '邮箱' },
      rules: [
        { type: 'required', message: '邮箱必填' },
        { type: 'email', message: '请输入有效的邮箱' },
      ],
    },
  ],
};

function App() {
  const formRef = useRef<SchemaFormInstance>(null);

  const handleSubmit = (values) => {
    console.log('提交数据:', values);
  };

  return (
    <SchemaForm
      ref={formRef}
      schema={schema}
      onSubmit={handleSubmit}
      spacing={2}
    >
      <Button onClick={() => formRef.current?.submit()}>提交</Button>
    </SchemaForm>
  );
}

组件类型

基础输入组件

| 组件名 | 说明 | 数据类型 | |--------|------|----------| | Text | 单行文本输入 | string | | Password | 密码输入 | string | | Number | 数字输入 | number | | Textarea | 多行文本 | string |

选择组件

| 组件名 | 说明 | 数据类型 | |--------|------|----------| | Select | 下拉选择(单选) | string \| number | | Autocomplete | 自动完成(支持多选) | string \| number \| array | | Checkbox | 复选框 | boolean | | Switch | 开关 | boolean | | Radio | 单选组 | string \| number |

选项配置详解 (Select / Autocomplete)

OptionItem 结构支持以下属性:

type OptionItem = {
  label: string;                  // 选中后输入框展示的文本
  value: string | number | boolean | null; // 提交到表单的值
  key?: string | number;          // [可选] 唯一标识 (当 value 不唯一或 label 重复时使用)
  listLabel?: React.ReactNode;    // [可选] 下拉列表展示的内容 (可与 label 不一致)
  disabled?: boolean;             // 是否禁用
};

场景 1:Label 重复导致 React Key 报错

如果选项中存在相同的 labelvalue,请显式提供 key

{
  label: '重复项',
  value: 'dup_1',
  key: 'unique_key_1'
},
{
  label: '重复项',
  value: 'dup_2',
  key: 'unique_key_2'
}

场景 2:列表展示与选中展示不一致

例如,列表中展示详细信息(如 "张三 (ID:001)"),选中后只展示姓名("张三"):

{
  label: '张三',                // 选中后展示
  value: '001',
  listLabel: (                  // 列表中展示
    <div>
      <strong>张三</strong>
      <span style={{ color: '#999', fontSize: 12 }}> (ID: 001)</span>
    </div>
  )
}

场景 3:选中后触发额外操作

可以在 fieldProps 中传入 onChange 回调:

{
  name: 'userSelect',
  component: 'Select',
  ui: {
    label: '选择用户',
    options: [
      { label: 'User A', value: 'a' },
      { label: 'User B', value: 'b' }
    ]
  },
  // 使用 ui.props 或 fieldProps 都可以透传属性
  fieldProps: {
    // 这里的 onChange 会在表单值更新后触发
    onChange: (event, option) => {
      console.log('选中项:', option);
      // 执行额外逻辑,例如:
      // 1. 调用接口获取详情
      // 2. 联动更新其他字段 (通过 form 实例)
      // 3. 弹窗提示
    }
  }
}

注意:在 v0.0.14 之前,fieldProps 需要写在 ui.props 中。从 v0.0.14 开始,支持直接在 Schema 顶层使用 fieldProps,这与 ui.props 是等效的。

日期时间组件

| 组件名 | 说明 | 数据类型 | |--------|------|----------| | Date | 日期选择 | string (YYYY-MM-DD) | | Time | 时间选择 | string | | DateTime | 日期时间选择 | string (YYYY-MM-DD HH:mm) |

其他组件

| 组件名 | 说明 | 数据类型 | |--------|------|----------| | Slider | 滑块 | number | | Rating | 评分 | number | | Hidden | 隐藏字段 | any | | Custom | 自定义组件 | any |

复合组件

| 组件名 | 说明 | |--------|------| | FormList | 动态列表(可增删行) | | Group | 字段分组(多字段一行) |


验证规则

规则类型一览

| 规则类型 | 说明 | 适用组件 | 参数 | |----------|------|----------|------| | required | 必填 | 所有 | message?: string | | minLength | 最小长度 | Text, Password, Textarea | value: number, message?: string | | maxLength | 最大长度 | Text, Password, Textarea | value: number, message?: string | | min | 最小值 | Number, Slider, Rating | value: number, message?: string | | max | 最大值 | Number, Slider, Rating | value: number, message?: string | | pattern | 正则匹配 | Text, Password, Textarea | value: string \| RegExp, message?: string | | email | 邮箱格式 | Text | message?: string | | url | URL格式 | Text | message?: string | | custom | 自定义验证 | 所有 | validate: (value, values) => boolean \| string | | array | 数组验证 | Upload, FormList | minItems?: number, maxItems?: number, message?: string |

使用示例

必填验证

{
  name: 'username',
  component: 'Text',
  rules: [
    { type: 'required', message: '用户名不能为空' }
  ]
}

长度验证

{
  name: 'password',
  component: 'Password',
  rules: [
    { type: 'required', message: '密码必填' },
    { type: 'minLength', value: 6, message: '密码至少6个字符' },
    { type: 'maxLength', value: 20, message: '密码最多20个字符' }
  ]
}

数值范围验证

{
  name: 'age',
  component: 'Number',
  rules: [
    { type: 'required', message: '年龄必填' },
    { type: 'min', value: 0, message: '年龄不能为负数' },
    { type: 'max', value: 150, message: '年龄不能超过150' }
  ]
}

正则验证

{
  name: 'phone',
  component: 'Text',
  rules: [
    { type: 'required', message: '手机号必填' },
    { 
      type: 'pattern', 
      value: '^1[3-9]\\d{9}$',  // 注意转义
      message: '请输入有效的手机号' 
    }
  ]
}

邮箱验证

{
  name: 'email',
  component: 'Text',
  rules: [
    { type: 'required', message: '邮箱必填' },
    { type: 'email', message: '请输入有效的邮箱地址' }
  ]
}

URL 验证

{
  name: 'website',
  component: 'Text',
  rules: [
    { type: 'url', message: '请输入有效的网址' }
  ]
}

自定义验证

{
  name: 'confirmPassword',
  component: 'Password',
  rules: [
    { type: 'required', message: '请确认密码' },
    {
      type: 'custom',
      validate: (value, values) => {
        if (value !== values.password) {
          return '两次密码输入不一致';  // 返回错误消息
        }
        return true;  // 返回 true 表示验证通过
      }
    }
  ]
}

Checkbox 必须勾选

{
  name: 'agreeTerms',
  component: 'Checkbox',
  ui: { label: '我已阅读并同意服务条款' },
  rules: [
    { type: 'required', message: '请同意服务条款' }
  ]
}

条件控制

visibleWhen - 条件显示

{
  name: 'companyName',
  component: 'Text',
  ui: { label: '公司名称' },
  // 当 accountType 等于 'business' 时显示
  visibleWhen: { field: 'accountType', eq: 'business' }
}

disabledWhen - 条件禁用

{
  name: 'discount',
  component: 'Number',
  // 当 isVip 为 false 时禁用
  disabledWhen: { field: 'isVip', eq: false }
}

requiredWhen - 条件必填

{
  name: 'taxId',
  component: 'Text',
  // 当 accountType 等于 'business' 时必填
  requiredWhen: { field: 'accountType', eq: 'business' }
}

条件表达式

简单条件

// 等于
{ field: 'status', eq: 'active' }

// 不等于
{ field: 'status', ne: 'inactive' }

// 大于
{ field: 'age', gt: 18 }

// 大于等于
{ field: 'age', gte: 18 }

// 小于
{ field: 'price', lt: 100 }

// 小于等于
{ field: 'price', lte: 100 }

// 在数组中
{ field: 'type', in: ['a', 'b', 'c'] }

// 不在数组中
{ field: 'type', notIn: ['x', 'y'] }

// 为空
{ field: 'name', empty: true }

// 非空
{ field: 'name', notEmpty: true }

复合条件

// AND 逻辑
{
  and: [
    { field: 'type', eq: 'business' },
    { field: 'country', eq: 'CN' }
  ]
}

// OR 逻辑
{
  or: [
    { field: 'type', eq: 'vip' },
    { field: 'points', gte: 1000 }
  ]
}

// NOT 逻辑
{
  not: { field: 'status', eq: 'disabled' }
}

布局配置

colSpan - 响应式布局

通过 colSpan 控制字段宽度(基于 12 栅格):

// 简单值 - 固定宽度
{ colSpan: 6 }  // 占 50%

// 响应式
{ 
  colSpan: { 
    xs: 12,   // 手机:100%
    sm: 6,    // 平板:50%
    md: 4,    // 桌面:33%
    lg: 3,    // 大屏:25%
  } 
}

spacing - 间距

<SchemaForm schema={schema} spacing={3} />

newLine - 强制换行

FieldSchema 中添加 newLine: true 可以强制字段从新的一行开始。

{
  name: 'is_super_admin',
  component: 'Radio',
  newLine: true,  // 👈 强制从新行开始
  ui: { ... },
  colSpan: { xs: 12, md: 6 },  // 即使宽度只有一半,也会另起一行
}

Radio 行内布局 (inline)

通过在 ui.props 中设置 inline: true,可以让 Radio 组件的 Label 和选项在同一行显示。

{
  name: 'is_super_admin',
  component: 'Radio',
  ui: {
    label: '超管状态',
    props: {
      inline: true,  // 👈 label 和 radio 选项在同一行显示
    },
    options: [
      { label: '普通用户', value: 0 },
      { label: '超级管理员', value: 1 },
    ],
  },
  colSpan: { xs: 12, md: 6 },
}

FormList 动态列表

用于动态添加/删除表单行。

{
  name: 'contacts',
  component: 'FormList',
  ui: { label: '联系人列表' },
  // 默认值:初始化时显示的行 (支持多行)
  defaultValue: [
    { name: '张三', phone: '13800000001' },
    { name: '李四', phone: '13800000002' }
  ],
  minItems: 1,      // 最少1行
  maxItems: 5,      // 最多5行
  addText: '添加联系人',  // 添加按钮文字
  copyable: true,   // 允许复制行
  columns: [
    {
      name: 'name',
      component: 'Text',
      ui: { label: '姓名' },
      rules: [{ type: 'required', message: '姓名必填' }],
      colSpan: { xs: 12, sm: 6 }
    },
    {
      name: 'phone',
      component: 'Text',
      ui: { label: '电话' },
      rules: [
        { type: 'required', message: '电话必填' },
        { type: 'pattern', value: '^1[3-9]\\d{9}$', message: '请输入有效的手机号' }
      ],
      colSpan: { xs: 12, sm: 6 }
    }
  ]
}

FormList 配置项

| 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | columns | FieldSchema[] | - | 子字段定义 | | defaultValue | array | [] | 初始值 | | minItems | number | 0 | 最少行数 | | maxItems | number | Infinity | 最多行数 | | addText | string | '添加一行' | 添加按钮文字 | | copyable | boolean | false | 是否可复制行 |


Group 字段分组

将多个字段组合在一行显示。

{
  name: 'addressGroup',
  component: 'Group',
  colSpan: { xs: 12 },
  columns: [
    {
      name: 'province',
      component: 'Select',
      ui: { label: '省份', options: [...] },
      colSpan: { xs: 12, sm: 4 }
    },
    {
      name: 'city',
      component: 'Select',
      ui: { label: '城市', options: [...] },
      colSpan: { xs: 12, sm: 4 }
    },
    {
      name: 'district',
      component: 'Select',
      ui: { label: '区县', options: [...] },
      colSpan: { xs: 12, sm: 4 }
    }
  ]
}

Custom 自定义组件

用于完全自定义渲染的场景。

方式1:children 函数(推荐)

可以访问 fieldformerror 等,实现完全自定义的表单控件:

{
  name: 'customInput',
  component: 'Custom',
  colSpan: { xs: 12 },
  ui: {
    label: '自定义输入',
    props: {
      // children 是一个函数,接收 field、form、label、error、helperText 等参数
      children: ({ field, form, label, error, helperText, fieldProps }) => {
        return (
          <div>
            <label>{label}</label>
            <input
              type="text"
              value={field.value ?? ''}
              onChange={(e) => field.onChange(e.target.value)}
              onBlur={field.onBlur}
              disabled={fieldProps?.disabled}
              style={{
                border: error ? '1px solid red' : '1px solid #ccc',
                padding: '8px',
                width: '100%',
              }}
            />
            {helperText && <span style={{ color: error ? 'red' : '#666' }}>{helperText}</span>}
          </div>
        );
      },
    },
  },
  rules: [{ type: 'required', message: '此字段必填' }],
}

children 函数参数

| 参数 | 类型 | 说明 | |------|------|------| | field | ControllerRenderProps | RHF 字段对象,包含 valueonChangeonBlurnameref | | form | UseFormReturn | 表单实例,可调用 form.setValue()form.trigger() 等 | | values | Record<string, any> | 当前表单值(通常仅包含被依赖的字段,如需全量可用 form.getValues()) | | label | string | 标签文本 | | error | boolean | 是否有错误 | | helperText | ReactNode | 帮助/错误文本 | | fieldProps | object | 包含 disabledrequiredreadOnly 等状态 |

方式2:children 静态内容

用于纯展示,不需要表单交互:

{
  name: 'notice',
  component: 'Custom',
  colSpan: { xs: 12 },
  noSubmit: true,  // 不参与表单提交
  ui: {
    props: {
      children: (
        <div style={{ padding: 16, background: '#f5f5f5', borderRadius: 8 }}>
          <strong>💡 提示:</strong>
          <span>这是一段提示信息</span>
        </div>
      ),
    },
  },
}

方式3:传入自定义组件

// 先定义自定义组件
const MyCustomInput = ({ field, form, values, label, error, helperText }) => {
  return (
    <div>
      <label>{label}</label>
      <input
        value={field.value ?? ''}
        onChange={(e) => field.onChange(e.target.value)}
      />
      {error && <span style={{ color: 'red' }}>{helperText}</span>}
      {/* 可以在这里使用 values 或 form 做更多逻辑 */}
    </div>
  );
};

// 在 schema 中使用
{
  name: 'myField',
  component: 'Custom',
  ui: {
    label: '我的字段',
    props: {
      component: MyCustomInput,  // 传入组件
      // 其他 props 会透传给 MyCustomInput
      customProp: 'value',
    },
  },
}

完整示例

{
  name: 'paymentAccount',
  component: 'Custom',
  colSpan: { xs: 12 },
  ui: {
    label: '付款账户',
    helperText: '请选择或输入付款账户',
    props: {
      children: ({ field, form, values, label, error, helperText, fieldProps }) => {
        const [accounts] = useState([
          { id: '1', name: '工商银行 **** 1234' },
          { id: '2', name: '建设银行 **** 5678' },
        ]);

        return (
          <div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
              <span style={{ fontWeight: 500 }}>{label}</span>
              <a href="#" style={{ fontSize: 12 }}>添加账户</a>
            </div>
            
            <select
              value={field.value ?? ''}
              onChange={(e) => field.onChange(e.target.value)}
              onBlur={field.onBlur}
              disabled={fieldProps?.disabled}
              style={{
                width: '100%',
                padding: '8px 12px',
                border: error ? '1px solid red' : '1px solid #ddd',
                borderRadius: 4,
              }}
            >
              <option value="">请选择账户</option>
              {accounts.map((acc) => (
                <option key={acc.id} value={acc.id}>{acc.name}</option>
              ))}
            </select>
            
            {helperText && (
              <div style={{ fontSize: 12, color: error ? 'red' : '#999', marginTop: 4 }}>
                {helperText}
              </div>
            )}
          </div>
        );
      },
    },
  },
  rules: [{ type: 'required', message: '请选择付款账户' }],
}

计算字段

自动计算字段值,支持简单的数学运算表达式,以及精度和舍入控制。

基础用法

{
  name: 'total',
  component: 'Number',
  readonly: true,
  ui: { 
    label: '总价',
    helperText: '自动计算: 单价 × 数量'
  },
  compute: {
    expr: 'price * quantity',  // 表达式,支持 + - * / % 等
    dependencies: ['price', 'quantity'] // 可选,指定依赖字段(默认自动分析)
  }
}

精度与舍入控制

通过 precisionroundMode 属性控制计算结果的精度。

  • precision: 保留的小数位数(整数)。
  • roundMode: 舍入模式,可选值:
    • 'round': 四舍五入(默认)。
    • 'ceil': 向上取整。
    • 'floor': 向下取整。
{
  name: 'discountedPrice',
  component: 'Number',
  readonly: true,
  ui: { label: '折后价(保留2位小数,向下取整)' },
  compute: {
    expr: 'price * discount',
    precision: 2,       // 保留两位小数
    roundMode: 'floor'  // 向下取整 (例如 10.559 -> 10.55)
  }
}

舍入模式示例

假设计算结果为 12.3456precision2

| roundMode | 结果 | 说明 | |-----------|------|------| | round (默认) | 12.35 | 四舍五入 | | ceil | 12.35 | 向上取整 (12.3456 -> 12.35) | | floor | 12.34 | 向下取整 (12.3456 -> 12.34) |

高级示例:互斥计算(含税/不含税)

这是一个复杂的业务场景示例:根据用户选择的"是否含税",自动计算"含税金额"或"不含税金额"。

  • 如果选择"含税",用户输入"含税金额",系统自动计算"不含税金额"。
  • 如果选择"不含税",用户输入"不含税金额",系统自动计算"含税金额"。
  • "增值税"始终由两者差值计算得出。
{
  name: 'is_include_tax',
  component: 'Radio',
  ui: { 
    label: '是否含税', 
    options: [{ label: '含税', value: 1 }, { label: '不含税', value: 2 }] 
  },
  defaultValue: 1,
},
{
  name: 'contract_amount',
  component: 'Number',
  ui: { label: '合同额(含税)' },
  // 当选择不含税(2)时,该字段禁用且由计算得出
  disabledWhen: { field: 'is_include_tax', eq: 2 },
  compute: {
    // 仅当 is_include_tax === 2 (不含税) 时才执行计算
    // 否则保持原值 (contract_amount)
    expr: 'is_include_tax === 2 ? exclud_tax_amount * (1 + tax_rate / 100) : contract_amount',
    dependencies: ['exclud_tax_amount', 'tax_rate', 'is_include_tax'],
    precision: 2,
    roundMode: 'round',
  },
},
{
  name: 'exclud_tax_amount',
  component: 'Number',
  ui: { label: '合同额(不含税)' },
  // 当选择含税(1)时,该字段禁用且由计算得出
  disabledWhen: { field: 'is_include_tax', eq: 1 },
  compute: {
    // 仅当 is_include_tax === 1 (含税) 时才执行计算
    // 否则保持原值 (exclud_tax_amount)
    expr: 'is_include_tax === 1 ? contract_amount / (1 + tax_rate / 100) : exclud_tax_amount',
    dependencies: ['contract_amount', 'tax_rate', 'is_include_tax'],
    precision: 2,
    roundMode: 'round',
  },
},
{
  name: 'tax_rate',
  component: 'Number',
  ui: { label: '税率(%)' },
},
{
  name: 'tax_amount',
  component: 'Number',
  ui: { label: '增值税额' },
  disabled: true,
  compute: {
    expr: 'contract_amount - exclud_tax_amount',
    dependencies: ['contract_amount', 'exclud_tax_amount'],
    precision: 2,
  },
}

自动完成 (Autocomplete)

Autocomplete 组件支持两种数据加载模式:一次性加载服务端搜索

1. 一次性加载 (Static / Async)

适用于数据量较小(如少于 1000 条),或者可以一次性加载所有选项的场景。

用法:

  • 静态选项:直接配置 ui.options
  • 异步加载:配置 ui.optionRequest,组件挂载时(或依赖变化时)会自动执行一次请求。

特点:

  • 前端过滤:组件内置了基于输入的本地过滤功能。
  • 简单:无需处理分页、防抖等逻辑。
// 示例:异步加载一次性数据
{
  name: 'userId',
  component: 'Autocomplete',
  ui: {
    label: '选择用户',
    // 方式 A: 静态数据
    // options: [{ label: '张三', value: 1 }, { label: '李四', value: 2 }],
    
    // 方式 B: 异步一次性加载
    optionRequest: async (values) => {
      // 这里的 values 是当前表单的值,可用于联动
      const users = await api.getAllUsers();
      return users.map(u => ({ label: u.name, value: u.id }));
    }
  }
}

2. 服务端搜索 (Remote Search)

适用于数据量极大(如用户库、商品库),无法一次性加载,需要根据用户输入实时搜索的场景。

用法:

  • 配置 ui.remoteConfig

特点:

  • 服务端过滤:每次输入都会发送请求给服务端。
  • 支持分页:滚动到底部自动加载下一页。
  • 支持防抖:内置防抖机制,减少请求频率。
  • 支持回显:通过 fetchById 解决默认值不在当前列表中的问题。

注意:如果配置了 remoteConfigui.optionsui.optionRequest 加载的数据将被忽略(除非作为初始值),组件将完全由 remoteConfig 接管数据流。

{
  name: 'userId',
  component: 'Autocomplete',
  ui: {
    label: '搜索用户',
    remoteConfig: {
      // 远程搜索函数
      fetchOptions: async (keyword, page, pageSize) => {
        const res = await api.searchUsers({ keyword, page, pageSize });
        return {
          data: res.list.map(u => ({ label: u.name, value: u.id })),
          total: res.total,
          hasMore: res.hasMore
        };
      },
      // 回显函数(用于处理默认值不在当前列表中的情况)
      fetchById: async (value) => {
        const user = await api.getUserById(value);
        return user ? { label: user.name, value: user.id } : null;
      },
      // 加载状态回调(可选,用于外部显示 loading)
      onLoadingChange: (loading) => {
        console.log('Loading:', loading);
      },
      pageSize: 20,       // 每页条数 (默认 20)
      debounceTimeout: 800 // 防抖时间 (默认 500ms)
    }
  }
}

RemoteConfig 类型定义

type RemoteConfig = {
  /** 
   * 远程获取选项列表 
   * @param keyword 搜索关键词
   * @param page 当前页码 (从1开始)
   * @param pageSize 每页条数
   */
  fetchOptions: (
    keyword: string,
    page: number,
    pageSize: number
  ) => Promise<{
    data: OptionItem[];
    total: number;
    hasMore: boolean;
  }>;

  /**
   * 根据 ID 获取单个选项(用于回显)
   * 当 field.value 有值但 options 中没有对应项时触发
   */
  fetchById?: (value: string | number) => Promise<OptionItem | null>;

  /** 加载状态变更回调 */
  onLoadingChange?: (loading: boolean) => void;

  /** 每页条数 (默认 20) */
  pageSize?: number;

  /** 搜索防抖时间 (ms, 默认 500) */
  debounceTimeout?: number;

  /** 最小搜索字符长度 (暂未实现) */
  minSearchLength?: number;
};

---

## 数据转换

### transform - 提交前转换

在表单提交前对数据进行转换,例如将数组转换为字符串,或格式化日期。

`transform` 函数接收两个参数:
1. `value`: 当前字段的值
2. `values`: 当前表单的所有值 (T)

```typescript
{
  name: 'tags',
  component: 'Select',
  ui: {
    label: '标签',
    options: [
      { label: '技术', value: 'tech' },
      { label: '生活', value: 'life' }
    ],
    props: { multiple: true } // 多选
  },
  // 提交时将数组转换为逗号分隔字符串: ['tech', 'life'] -> "tech,life"
  transform: (value) => Array.isArray(value) ? value.join(',') : value
}

noSubmit - 不参与提交

用于纯 UI 展示字段,或仅用于计算的中间变量,不包含在最终提交的数据中。

{
  name: 'tips',
  component: 'Custom',
  noSubmit: true, // 👈 提交时会自动过滤掉此字段
  ui: {
    props: {
      children: <div style={{ color: '#666' }}>请务必填写真实信息</div>
    }
  }
}

API 参考

SchemaFormProps

| 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | schema | SchemaInput | - | Schema 定义(必填) | | defaultValues | object | - | 默认值 | | onSubmit | (values) => void | - | 提交回调 | | onValuesChange | (values) => void | - | 值变化回调 | | grid | boolean | true | 是否使用 Grid 布局 | | spacing | number | 2 | 间距 | | disabled | boolean | false | 全局禁用 | | readOnly | boolean | false | 全局只读 | | widgets | Record<string, WidgetComponent> | - | 自定义组件 | | children | ReactNode | - | 子元素(如提交按钮) |

SchemaFormInstance (ref)

| 方法 | 说明 | |------|------| | submit() | 提交表单 | | reset() | 重置表单 | | getValues() | 获取所有值 | | getFormValues() | 获取表单值(排除 noSubmit 字段) | | setValue(name, value) | 设置单个值 | | setValues(values) | 批量设置值 | | trigger(name?) | 触发验证 | | clearErrors(name?) | 清除错误 |

FieldSchema

| 属性 | 类型 | 说明 | |------|------|------| | 基础配置 | | | | name | string | 字段名(必填,对应表单数据 key) | | component | ComponentType | 组件类型(Text, Number, Select, ...) | | defaultValue | any | 默认值 | | UI 配置 | | | | ui.label | string | 字段标签 | | ui.placeholder | string | 占位符 | | ui.helperText | ReactNode | 帮助/错误提示文本 | | ui.tooltip | string | 悬浮提示信息 | | ui.options | OptionItem[] | 静态选项列表 (Select/Radio/Checkbox) | | ui.optionRequest | (values) => Promise | 异步加载选项函数 | | ui.remoteConfig | RemoteConfig | 远程搜索配置 (Autocomplete) | | ui.props | object | 透传给底层组件的 props | | 布局配置 | | | | colSpan | GridSize \| object | 栅格列宽 (1-12),支持响应式对象 { xs, md } | | newLine | boolean | 是否强制换行 | | 验证与状态 | | | | rules | ValidationRule[] | 验证规则数组 | | readonly | boolean | 是否只读 | | disabled | boolean | 是否禁用 | | hidden | boolean | 是否隐藏 (不渲染但保留数据) | | 条件控制 | | | | visibleWhen | ConditionExpression | 条件显示表达式 | | disabledWhen | ConditionExpression | 条件禁用表达式 | | requiredWhen | ConditionExpression | 条件必填表达式 | | 计算与数据 | | | | compute | ComputeConfig | 自动计算配置 { expr, dependencies, precision } | | dependencies | string[] | 显式声明依赖字段 (触发重置或副作用) | | transform | (val, vals) => any | 提交前的数据转换函数 | | noSubmit | boolean | 是否从提交数据中排除 | | FormList/Group | | | | columns | FieldSchema[] | 子字段定义 (用于 Group/FormList) | | minItems | number | 最小行数 (FormList) | | maxItems | number | 最大行数 (FormList) | | addText | string | 添加按钮文案 (FormList) | | sortable | boolean | 是否可拖拽排序 (FormList) | | copyable | boolean | 是否可复制行 (FormList) |


完整示例

import { SchemaForm, type SchemaInput } from './share/Schema';

const schema: SchemaInput = {
  fields: [
    // 基础字段
    {
      name: 'username',
      component: 'Text',
      ui: { label: '用户名', placeholder: '请输入用户名' },
      colSpan: { xs: 12, md: 6 },
      rules: [
        { type: 'required', message: '用户名必填' },
        { type: 'minLength', value: 3, message: '用户名至少3个字符' },
      ],
    },

    // 邮箱验证
    {
      name: 'email',
      component: 'Text',
      ui: { label: '邮箱', placeholder: '请输入邮箱' },
      colSpan: { xs: 12, md: 6 },
      rules: [
        { type: 'required', message: '邮箱必填' },
        { type: 'email', message: '请输入有效的邮箱' },
      ],
    },

    // 下拉选择 + 条件显示
    {
      name: 'accountType',
      component: 'Select',
      defaultValue: 'personal',
      ui: {
        label: '账户类型',
        options: [
          { label: '个人账户', value: 'personal' },
          { label: '企业账户', value: 'business' },
        ],
      },
      colSpan: { xs: 12, md: 6 },
    },

    // 条件显示字段
    {
      name: 'companyName',
      component: 'Text',
      ui: { label: '公司名称' },
      colSpan: { xs: 12, md: 6 },
      visibleWhen: { field: 'accountType', eq: 'business' },
      requiredWhen: { field: 'accountType', eq: 'business' },
    },

    // 动态列表
    {
      name: 'contacts',
      component: 'FormList',
      ui: { label: '联系人' },
      colSpan: { xs: 12 },
      defaultValue: [{ name: '', phone: '' }],
      minItems: 1,
      maxItems: 3,
      columns: [
        {
          name: 'group',
          component: 'Group',
          colSpan: { xs: 12 },
          columns: [
            {
              name: 'name',
              component: 'Text',
              ui: { label: '姓名' },
              colSpan: { xs: 12, sm: 6 },
              rules: [{ type: 'required', message: '姓名必填' }],
            },
            {
              name: 'phone',
              component: 'Text',
              ui: { label: '电话' },
              colSpan: { xs: 12, sm: 6 },
              rules: [{ type: 'required', message: '电话必填' }],
            },
          ],
        },
      ],
    },

    // 计算字段
    {
      name: 'price',
      component: 'Number',
      defaultValue: 100,
      ui: { label: '单价' },
      colSpan: { xs: 12, md: 4 },
    },
    {
      name: 'quantity',
      component: 'Number',
      defaultValue: 1,
      ui: { label: '数量' },
      colSpan: { xs: 12, md: 4 },
    },
    {
      name: 'total',
      component: 'Number',
      readonly: true,
      ui: { label: '总价' },
      colSpan: { xs: 12, md: 4 },
      compute: { expr: 'price * quantity' },
    },

    // 协议勾选
    {
      name: 'agree',
      component: 'Checkbox',
      ui: { label: '我已阅读并同意服务条款' },
      colSpan: { xs: 12 },
      rules: [{ type: 'required', message: '请同意服务条款' }],
    },
  ],
};