基于 Ant Design 的配置化表单开发指南

在现代前端开发中,表单是用户交互的重要组成部分。传统的表单开发方式需要编写大量的 JSX 代码,维护成本高,开发效率低。而配置化表单通过声明式的配置对象,让开发者能够快速构建复杂的企业级表单应用。

BetaSchemaForm 是 Ant Design ProComponents 生态中的核心表单组件,它基于 Ant Design 设计语言,提供了丰富的内置组件和强大的扩展能力。通过配置化的方式,开发者可以轻松实现字段依赖、动态渲染、自定义组件等复杂功能,特别适合构建企业级应用中的复杂表单场景。

为什么选择配置化表单?

  • 开发效率: 通过配置对象快速构建表单,减少重复代码
  • 维护性: 配置化的方式使表单逻辑更清晰,便于维护和修改
  • 一致性: 统一的配置规范确保表单样式和交互的一致性
  • 扩展性: 支持自定义组件和复杂业务逻辑的扩展
  • 团队协作: 配置化的方式降低了团队协作的复杂度
  • 配置复用: 表单配置与表格列配置保持一致,减少重复代码
  • 学习成本低: 掌握一套配置规范,即可同时用于表单和表格开发

效果展示

以下是一个基于 BetaSchemaForm 构建的复杂表单的实际效果:

从效果图中可以看到:

  • 动态表单: 根据用户选择动态显示/隐藏相关字段
  • 字段联动: 不同字段之间的依赖关系和联动效果
  • 丰富组件: 包含输入框、选择器、日期选择器等多种组件类型
  • 实时验证: 表单字段的实时验证和错误提示

官方信息

  • 官方网站 : procomponents.ant.design/components/...
  • 核心特性: 支持 20+ 种内置组件,支持自定义组件,支持表单联动,支持表单校验
  • 适用场景: 中后台表单、动态表单、复杂业务表单
  • 技术栈: 基于 Ant Design,支持 React
  • 组件类型: ProComponents 生态中的表单组件,与 ProTable、ProList 等组件协同使用
  • 版本支持: 支持 React 16.9+,TypeScript 支持完善
  • 性能优化: 内置虚拟滚动、懒加载等性能优化特性

文档说明

本文档基于实际项目中的使用经验,详细介绍基于 Ant Design 的配置化表单开发方法。文档涵盖了 BetaSchemaForm 的高级用法,包括依赖字段、字段值转换、自定义组件等特殊场景。通过丰富的实际代码示例和详细说明,帮助开发者快速掌握配置化表单的核心概念和最佳实践。

更多基础用法和组件说明,请参考 官方文档

目录

  1. 基础用法 - 了解配置化表单的基本配置方式
  2. [依赖字段 (Dependencies)](#依赖字段 (Dependencies) "#%E4%BE%9D%E8%B5%96%E5%AD%97%E6%AE%B5-dependencies") - 掌握字段间的依赖关系配置
  3. [字段值转换 (Transform & ConvertValue)](#字段值转换 (Transform & ConvertValue) "#%E5%AD%97%E6%AE%B5%E5%80%BC%E8%BD%AC%E6%8D%A2-transform--convertvalue") - 学习数据格式转换技巧
  4. [自定义组件 (renderFormItem)](#自定义组件 (renderFormItem) "#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6-renderformitem") - 扩展表单组件能力
  5. [条件渲染 (Conditional Rendering)](#条件渲染 (Conditional Rendering) "#%E6%9D%A1%E4%BB%B6%E6%B8%B2%E6%9F%93-conditional-rendering") - 实现动态表单显示
  6. [表单验证 (Validation)](#表单验证 (Validation) "#%E8%A1%A8%E5%8D%95%E9%AA%8C%E8%AF%81-validation") - 构建完善的表单验证体系
  7. 实际案例解析 - 通过真实案例深入理解
  8. 最佳实践 - 提升代码质量和开发效率

💡 提示 : 本文档专注于高级用法和实际项目经验,基础用法请参考 官方文档

基础用法

基本字段配置

在 BetaSchemaForm 中,每个字段都是一个配置对象,包含了字段的所有属性。一个典型的字段配置包含以下几个核心属性:

  • title: 字段的显示标签
  • dataIndex: 字段的数据键名,对应表单数据的属性名
  • valueType: 字段类型,决定渲染哪种表单组件
  • formItemProps: 传递给 Form.Item 的属性,主要用于验证规则
  • fieldProps: 传递给具体表单组件的属性,如 placeholder、disabled 等

下面是一个基本的输入框字段配置示例:

typescript 复制代码
{
  title: '活动名称',
  dataIndex: 'name',
  formItemProps: {
    rules: [{ required: true, message: '请选择活动类型' }],
  },
  valueType: 'input',
  fieldProps: {
    placeholder: '请输入活动名称',
    maxLength: 20,
  },
}

这个配置会渲染一个带有验证规则的输入框,用户必须输入内容,且最大长度为20个字符。

配置化表单的优势

通过上面的效果图可以看到,配置化表单相比传统表单开发方式具有以下优势:

  1. 代码简洁: 一个配置对象就能定义复杂的表单字段
  2. 功能丰富: 内置多种组件类型和交互效果
  3. 易于维护: 配置化的方式使表单逻辑更清晰
  4. 高度可扩展: 支持自定义组件和复杂业务逻辑
  5. 配置复用: 同一套配置可用于表单和表格,减少重复代码

配置复用示例

在 ProComponents 生态中,表单配置和表格列配置使用相同的配置规范,这大大提高了开发效率:

typescript 复制代码
// 定义通用字段配置
const activityNameColumn = {
  title: '活动名称',
  dataIndex: 'activityName',
  valueType: 'text',
  formItemProps: {
    rules: [{ required: true, message: '请输入活动名称' }]
  },
  fieldProps: {
    placeholder: '请输入活动名称'
  }
};

// 在 BetaSchemaForm 中使用
const formColumns = [activityNameColumn];

// 在 ProTable 的搜索表单中使用
const searchColumns = [activityNameColumn];

// 在 ProTable 的列配置中使用
const tableColumns = [activityNameColumn];

这种配置复用带来的优势:

  • 减少重复代码: 同一套配置可用于多种场景
  • 保持一致性: 确保表单和表格的字段行为一致
  • 降低维护成本: 修改配置时只需要改一个地方
  • 提高开发效率: 开发者只需要掌握一套配置规范

ProComponents SchemaForm 特色功能

根据 ProComponents SchemaForm 官方文档,BetaSchemaForm 具有以下特色功能:

1. 智能布局

  • 自动布局: 根据字段类型自动选择最佳的布局方式
  • 响应式设计: 支持不同屏幕尺寸的自适应布局
  • 栅格系统: 内置 Ant Design 的栅格系统,支持灵活的列布局

2. 高级表单功能

  • 表单联动: 支持字段间的依赖关系和联动效果
  • 动态表单: 支持根据条件动态显示/隐藏字段
  • 表单分组: 支持将相关字段进行分组显示
  • 步骤表单: 支持多步骤表单的配置和管理

3. 性能优化

  • 懒加载: 支持表单字段的懒加载,提升大型表单的性能
  • 虚拟滚动: 对于大量字段的表单,支持虚拟滚动
  • 缓存机制: 内置表单数据的缓存机制,减少重复渲染

4. 开发体验

  • TypeScript 支持: 完整的 TypeScript 类型定义
  • 调试工具: 内置表单调试工具,便于开发调试
  • 错误处理: 完善的错误处理和提示机制

💡 提示 : 以上示例展示了 BetaSchemaForm 的基础配置方式。每个字段都支持丰富的配置属性,包括显示控制、值处理、表单配置等。详细的通用配置说明请参考 ProComponents SchemaForm 官方文档

表单项通用配置

在 BetaSchemaForm 中,每个表单项都支持一系列通用配置属性,这些属性可以控制字段的显示、行为、验证等各个方面。以下是常用的通用配置:

完整的配置说明请参考 ProComponents SchemaForm 官方文档

基础配置属性

typescript 复制代码
{
  // 字段标识
  dataIndex: 'fieldName',           // 字段名,对应表单数据的键名
  title: '字段标题',                // 字段显示标题
  
  // 显示控制
  hideInForm: false,               // 在表单中隐藏
  hideInTable: false,              // 在表格中隐藏
  hideInSearch: false,             // 在搜索中隐藏
  hideInDescriptions: false,       // 在描述列表中隐藏
  
  // 值处理
  valueType: 'text',               // 字段类型,决定渲染的组件
  initialValue: '默认值',          // 初始值
  transform: (value) => value,     // 提交时转换值
  convertValue: (value) => value,  // 显示时转换值
  
  // 表单配置
  formItemProps: {                 // Form.Item 的属性
    rules: [{ required: true }],   // 验证规则
    label: '自定义标签',           // 自定义标签
    tooltip: '提示信息',           // 提示信息
  },
  fieldProps: {                    // 字段组件的属性
    placeholder: '请输入',         // 占位符
    disabled: false,               // 是否禁用
    style: { width: '100%' },      // 样式
  },
  
  // 依赖关系
  dependencies: ['field1', 'field2'], // 依赖的字段
  
  // 自定义渲染
  renderFormItem: (schema, config, form) => <CustomComponent />,
  render: (text, record, index) => <CustomRender />,
}

常用配置示例

1. 基础文本字段

typescript 复制代码
{
  title: '活动名称',
  dataIndex: 'activityName',
  valueType: 'text',
  formItemProps: {
    rules: [
      { required: true, message: '请输入活动名称' },
      { max: 50, message: '活动名称不能超过50个字符' }
    ]
  },
  fieldProps: {
    placeholder: '请输入活动名称',
    maxLength: 50
  }
}

2. 带依赖的字段

typescript 复制代码
{
  title: '接收人邮箱',
  dataIndex: 'receiverEmail',
  valueType: 'text',
  dependencies: ['pushCrm', 'pushSystem', 'receiverFlag'],
  formItemProps: {
    rules: [{ required: true, message: '请输入接收人邮箱' }]
  },
  fieldProps: {
    placeholder: '请输入接收人邮箱',
    disabled: (form) => form.getFieldValue('receiverFlag') === 1
  }
}

3. 带值转换的字段

typescript 复制代码
{
  title: '活动类型',
  dataIndex: 'activityType',
  valueType: 'radio',
  initialValue: 1,
  convertValue: (value) => {
    // 显示时将数字转换为选项
    const options = [
      { label: '学习测评', value: 1 },
      { label: '作业练习', value: 2 }
    ];
    return options.find(opt => opt.value === value)?.label || value;
  },
  transform: (value) => {
    // 提交时将选项转换为数字
    return typeof value === 'object' ? value.value : value;
  }
}

常用字段类型

BetaSchemaForm 支持 20+ 种内置组件,每种类型都有其特定的用途和配置方式。以下是项目中最常用的几种字段类型:

完整的组件列表和配置说明请参考 官方文档 - 组件列表

1. 单选按钮 (radio)

单选按钮适用于从多个选项中选择一个的场景,比如活动类型的选择:

typescript 复制代码
{
  title: '活动类型',
  dataIndex: 'activityType',
  valueType: 'radio',
  fieldProps: {
    options: typeOptionsCreate, // 选项数组,格式: [{label: '显示文本', value: '值'}]
  },
}

2. 多选框 (checkbox)

多选框允许用户选择多个选项,在项目中用于作答方式的选择:

typescript 复制代码
{
  title: '作答方式',
  dataIndex: 'answerDeviceType',
  valueType: 'checkbox',
  fieldProps: {
    options: answerDeviceTypeOptions, // 多选选项数组
  },
}

3. 下拉选择 (select)

下拉选择是最常用的字段类型,支持远程数据加载和搜索功能:

typescript 复制代码
{
  title: '所属学校',
  dataIndex: 'schoolId',
  valueType: 'select',
  request: getSchoolList, // 异步获取选项数据
  fieldProps: {
    placeholder: '请选择所属学校',
    allowClear: false, // 不允许清空
    showSearch: true, // 支持搜索
    labelInValue: true, // 返回 {label, value} 格式
  },
}

4. 日期范围选择 (dateRange)

日期范围选择器用于选择时间区间,在项目中用于活动时间范围的设置:

typescript 复制代码
{
  title: '活动时间范围',
  dataIndex: 'timeRange',
  valueType: 'dateRange',
  fieldProps: {
    placeholder: ['开始日期', '结束日期'],
    format: 'YYYY-MM-DD', // 日期格式
  },
}

5. 文本域 (textarea)

文本域用于输入较长的文本内容,支持字符计数:

typescript 复制代码
{
  title: '活动描述',
  dataIndex: 'description',
  valueType: 'textarea',
  fieldProps: {
    placeholder: '请输入活动描述',
    maxLength: 100, // 最大字符数
    showCount: true, // 显示字符计数
    rows: 4, // 显示行数
  },
}

依赖字段 (Dependencies)

依赖字段是 BetaSchemaForm 的核心特性之一,它允许字段根据其他字段的值动态显示或隐藏。这种机制使得表单能够根据用户的选择动态调整,提供更好的用户体验。

在实际业务场景中,我们经常遇到这样的需求:当用户选择了某个选项后,需要显示相关的子选项。比如选择活动类型后,需要显示对应的配置选项;选择推送系统后,需要显示对应的学校列表等。

基础依赖

最简单的依赖关系是单字段依赖,即一个字段依赖于另一个字段的值。当依赖的字段值发生变化时,依赖字段会重新渲染或隐藏。

在项目中,一级渠道字段依赖于活动默认渠道字段:

typescript 复制代码
{
  title: '一级渠道',
  dataIndex: 'firstLevelId',
  valueType: 'select',
  dependencies: ['businessType'], // 依赖 businessType 字段
  request: (params: any) => apiGetFirstLevelListFormat({ 
    businessType: params.businessType 
  }),
  fieldProps: {
    placeholder: '请选择一级渠道',
    onChange: () => {
      // 当一级渠道变化时,清空下级字段
      formRef.current?.resetFields(['secondLevelId', 'thirdLevelId'])
    }
  },
}

在这个例子中,dependencies: ['businessType'] 表示一级渠道字段依赖于 businessType 字段。当用户选择活动默认渠道后,系统会自动调用 apiGetFirstLevelListFormat 接口获取对应的一级渠道列表。

多级依赖

在实际业务中,我们经常遇到多级依赖的情况,即一个字段依赖于多个其他字段的值。这种场景在级联选择中特别常见。

在项目中,二级渠道字段同时依赖于活动默认渠道和一级渠道:

typescript 复制代码
{
  title: '二级渠道',
  dataIndex: 'secondLevelId',
  dependencies: ['businessType', 'firstLevelId'], // 依赖多个字段
  request: (params: any) => apiGetSecondLevelListFormat(params),
  valueType: 'select',
}

这里 dependencies: ['businessType', 'firstLevelId'] 表示二级渠道字段依赖于活动默认渠道和一级渠道两个字段。只有当这两个字段都有值时,系统才会调用接口获取二级渠道列表。

复杂依赖逻辑

对于更复杂的依赖关系,我们可以使用 valueType: 'dependency' 来实现条件渲染。这种方式允许我们根据依赖字段的值来决定显示哪些字段。

在项目中,推送CRM相关的字段组就是一个典型的复杂依赖例子:

typescript 复制代码
// 使用 valueType: 'dependency' 实现复杂的条件渲染
{
  valueType: 'dependency',
  name: ['pushSystem'],
  columns: ({ pushSystem }: { pushSystem: string }) => {
    if (pushSystem === SYSTEM_TYPE.CRM) {
      return [
        intentionalDeptColumn, 
        intentionalProjectColumn, 
        receiverFlagColumn
      ]
    }
    return []
  },
}

这个配置的含义是:当推送系统选择为CRM时,显示意向部门、意向项目和线索接收人字段;否则不显示任何字段。columns 函数接收依赖字段的值作为参数,返回要显示的字段配置数组。

嵌套依赖

在实际项目中,我们经常遇到嵌套依赖的情况,即依赖字段本身又依赖于其他字段。这种多层嵌套的依赖关系需要仔细设计,以确保字段的显示逻辑正确。

在推送CRM配置中,就存在这样的嵌套依赖关系:

typescript 复制代码
// 推送CRM字段组
const pushCRMColumns = {
  valueType: 'dependency',
  name: ['pushCrm'],
  columns: ({ pushCrm }: { pushCrm: number }) => {
    if (!pushCrm) {
      return []
    }
    return [
      pushSystemColumn, 
      pushSchoolColumn, 
      {
        valueType: 'dependency',
        name: ['pushSystem'],
        columns: ({ pushSystem }: { pushSystem: string }) => {
          if (pushSystem === SYSTEM_TYPE.CRM) {
            return [intentionalDeptColumn, intentionalProjectColumn]
          }
          return []
        },
      }
    ]
  },
}

这个嵌套依赖的逻辑是:

  1. 首先检查是否推送CRM(pushCrm 字段)
  2. 如果推送CRM,显示推送系统和推送学校字段
  3. 在推送系统字段内部,再次检查是否选择了CRM系统
  4. 如果选择了CRM系统,显示意向部门和意向项目字段

这种嵌套结构使得表单能够根据用户的选择逐步展开,提供更好的用户体验。

字段值转换 (Transform & ConvertValue)

在实际开发中,前端表单的数据格式往往与后端API期望的格式不一致。BetaSchemaForm 提供了两个重要的转换函数来解决这个问题:

  • Transform: 在表单提交时将前端数据转换为后端需要的格式
  • ConvertValue: 在表单回显时将后端数据转换为前端组件需要的格式

这两个函数确保了数据在前后端之间的正确流转,是构建健壮表单系统的关键。

Transform - 提交时转换

Transform 函数在表单提交时被调用,它将用户输入的数据转换为后端API期望的格式。这个转换过程是自动的,开发者只需要定义转换逻辑即可。

重要提示 :当字段配置中使用了 transform 函数时,需要先调用 FormRef.current.validateFields() 进行表单验证,然后使用 FormRef.current.getFieldsFormatValue() 来获取格式化后的值。这是因为 transform 函数只在调用 getFieldsFormatValue() 时才会执行,但在此之前需要确保表单验证通过。

ID-Name 转换

在项目中,很多下拉选择组件使用 labelInValue: true 配置,返回的是 {label: '显示文本', value: '实际值'} 格式的对象。但后端通常只需要ID值,这时就需要使用 Transform 进行转换:

typescript 复制代码
// ID-Name 转换工具函数
export const createIdNameTransform = (idField: string, nameField?: string) => {
  return (value: any) => {
    if (!value) {
      return {
        [idField]: null,
        [nameField || `${idField}Name`]: null
      };
    }
    
    if (typeof value === 'object' && value.label && value.value) {
      return {
        [idField]: value.value,
        [nameField || `${idField}Name`]: value.label
      };
    }
    
    return {
      [idField]: value,
      [nameField || `${idField}Name`]: null
    };
  };
};

// 使用示例
{
  title: '所属学校',
  dataIndex: 'schoolId',
  transform: createIdNameTransform('schoolId', 'schoolName'),
}

这个工具函数的作用是:

  • 当用户选择了学校时,将 {label: '北京学校', value: '123'} 转换为 {schoolId: '123', schoolName: '北京学校'}
  • 当没有选择时,返回 {schoolId: null, schoolName: null}

这样后端就能同时获得ID和名称信息,便于后续的数据处理和显示。

日期范围转换

日期范围选择器返回的是 dayjs 对象数组,但后端通常需要字符串格式的日期。Transform 函数可以处理这种转换:

typescript 复制代码
{
  title: '活动时间范围',
  dataIndex: 'timeRange',
  transform: (value: any) => {
    if (!value || !Array.isArray(value) || value.length !== 2) {
      return {
        timeStart: null,
        timeEnd: null
      };
    }

    const [startDate, endDate] = value;
    const startDayjs = startDate ? (startDate.$d ? startDate : dayjs(startDate)) : null;
    const endDayjs = endDate ? (endDate.$d ? endDate : dayjs(endDate)) : null;

    return {
      timeStart: startDayjs ? `${startDayjs.format('YYYY-MM-DD')} 00:00:00` : null,
      timeEnd: endDayjs ? `${endDayjs.format('YYYY-MM-DD')} 23:59:59` : null,
    };
  },
}

这个转换函数的作用是:

  • 将用户选择的日期范围 [dayjs('2024-01-01'), dayjs('2024-01-31')] 转换为后端需要的格式
  • 开始时间设置为当天的 00:00:00,结束时间设置为当天的 23:59:59
  • 如果没有选择日期,返回 null 值

复杂值转换

有些字段需要更复杂的转换逻辑,比如多选框的值转换。在项目中,作答方式字段需要将数组转换为特定的数字值:

typescript 复制代码
// 作答方式转换:数组转数字
{
  title: '作答方式',
  dataIndex: 'answerDeviceType',
  transform: (value: any) => {
    if (value === undefined || value === null) {
      return undefined;
    }
    if(Array.isArray(value)){
      // 长度为1 直接返回数组中的值
      if(value.length === 1){
        return value[0];
      }else if(value.length === 2){
        return 3; // 表示两种方式都选择
      }
    }
    return value;
  },
}

这个转换逻辑的含义是:

  • 如果用户只选择了手机作答,返回 1
  • 如果用户只选择了学习机作答,返回 2
  • 如果用户同时选择了两种方式,返回 3
  • 如果没有选择,返回 undefined

ConvertValue - 回显时转换

ConvertValue 函数与 Transform 相反,它在表单回显时将后端数据转换为前端组件需要的格式。这个函数在编辑模式下特别重要,确保数据能够正确显示在表单中。

多选值回显

对于多选框,需要将后端存储的数字值转换回数组格式:

typescript 复制代码
// 作答方式回显:数字转数组
{
  title: '作答方式',
  dataIndex: 'answerDeviceType',
  convertValue: (value: any) => {
    if (value === undefined || value === null) {
      return undefined;
    }
    if(value === 3){
      return [1, 2]; // 两种方式都选择
    }else if(value === 1){
      return [1]; // 只选择手机
    }else if(value === 2){
      return [2]; // 只选择学习机
    }
    return value;
  },
}

这个转换函数的作用是:

  • 将后端存储的数字值转换回多选框需要的数组格式
  • 确保编辑时能够正确显示之前的选择状态

自定义组件 (renderFormItem)

虽然 BetaSchemaForm 提供了 20+ 种内置组件,但在实际业务中,我们经常需要一些特殊的功能,比如复杂的搜索、弹窗选择、富文本编辑等。这时就需要使用 renderFormItem 来自定义组件。

自定义组件支持

BetaSchemaForm 支持通过 renderFormItem 完全自定义字段的渲染方式,这为复杂业务场景提供了强大的扩展能力。

renderFormItem 函数接收三个参数:

  • schema: 字段的配置对象
  • config: 字段的配置信息,包含 value、onChange 等
  • form: 表单实例,用于操作表单数据

通过这个函数,我们可以完全自定义字段的渲染方式,实现任何复杂的交互逻辑。

更多自定义组件的用法请参考 官方文档 - 自定义组件

邮箱选择组件

在项目中,邮箱选择组件需要支持搜索企业微信用户,这是一个典型的自定义组件场景。用户输入邮箱后按回车键,系统会搜索对应的企业微信用户信息:

typescript 复制代码
// 自定义邮箱选择组件
const EmailQiWei = (props: any, ref: any) => {
  const [dataList, setDataList] = useState([]);
  const [value, setValue] = useState();
  
  const handleSearch = (newValue: any) => {
    if (newValue.keyCode == '13') { // 回车键
      getData(newValue.target.value)
    }
  };

  const getData = (value: any) => {
    getQiweiInfoList({ email: value }).then((res: any) => {
      const dataQ11 = res.data?.map((item: any) => ({
        label: item.name,
        value: <><img src={item.thumbAvatar} width={25} /> {item.name} </>,
        enterpriseWechat: item
      }));
      setDataList(dataQ11)
    });
  };

  return (
    <Select
      showSearch
      value={value}
      placeholder={props.placeholder}
      onInputKeyDown={handleSearch}
      onChange={(value) => {
        setValue(value);
        if (typeof value === 'string' && value) {
          props.onChange?.(JSON.parse(value));
        }
      }}
    >
      {dataList?.map((item: any) => (
        <Select.Option 
          key={JSON.stringify(item.enterpriseWechat)} 
          value={JSON.stringify(item.enterpriseWechat)}
        >
          {item.value}
        </Select.Option>
      ))}
    </Select>
  );
};

// 在字段配置中使用
{
  title: '邮箱',
  dataIndex: 'enterpriseWechat',
  fieldProps: {
    placeholder: '请输入邮箱',
  },
  formItemProps: {
    rules: [{ required: true, message: '请输入邮箱回车后选择' }]
  },
  renderFormItem: (schema: any, config: any, form: any) => (
    <EmailQiWei 
      schema={schema} 
      config={config} 
      form={form} 
      placeholder="xxx@df.cn回车后选择" 
      style={{ width: '190px' }} 
    />
  )
}

这个自定义组件的核心功能是:

  1. 搜索触发: 用户输入邮箱后按回车键触发搜索
  2. 数据获取 : 调用 getQiweiInfoList 接口获取企业微信用户信息
  3. 结果展示: 在下拉列表中显示用户头像和姓名
  4. 数据传递: 将选中的用户信息以JSON字符串形式传递给表单

标签选择组件

标签选择组件需要支持弹窗选择,用户点击"设置"按钮后打开弹窗,在弹窗中选择标签,选择完成后关闭弹窗并显示选中的标签:

typescript 复制代码
// 自定义标签选择组件
const UserTag = ({config, form}: any) => {
  const [modalTagVisible, setModalTagVisible] = useState(false)
  const [innerValue, setInnerValue] = useState(config.value || [])
  
  const selectTagFn = (value: any) => {
    setInnerValue(value)
    form.setFieldValue('userTag', value)
    setModalTagVisible(false)
  }
 
  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'flex-start' }}>
        <Button type='primary' onClick={() => setModalTagVisible(true)}>
          设置
        </Button>
        <span style={{ marginLeft: 10, marginTop: 4 }}>
          {innerValue.map((item: any) => (
            <Tag key={item.id}>{item.tagName}</Tag>
          ))}
        </span>
      </div>
      <Modal 
        title="设置标签"
        open={modalTagVisible}
        onCancel={() => setModalTagVisible(false)}
        footer={null}
        width={1000}
      >
        <CompanyTagComponent 
          closeModal={() => setModalTagVisible(false)} 
          selectFn={selectTagFn} 
          tagList={innerValue} 
        />
      </Modal>
    </div>
  )
}

// 在字段配置中使用
{
  title: '给客户打标签:',
  dataIndex: 'userTag',
  formItemProps: {
    rules: [{ required: true, message: '请选择标签' }]
  },
  renderFormItem: (schema: any, config: any, form: any) => (
    <UserTag config={config} form={form} />
  )
}

这个自定义组件的核心功能是:

  1. 弹窗交互: 点击"设置"按钮打开标签选择弹窗
  2. 标签展示: 在按钮旁边显示已选择的标签
  3. 数据同步: 选择标签后自动更新表单数据
  4. 状态管理: 管理弹窗的显示/隐藏状态

表单收集组件

表单收集组件是一个更复杂的自定义组件,它需要支持单选按钮、表格展示和弹窗选择。用户可以选择是否收集表单,如果选择收集,则显示已选择的表单信息,并支持编辑功能:

typescript 复制代码
// 自定义表单收集组件
const CollectForm = ({ value, onChange }: any) => {
  const [collectFormInfo, setCollectFormInfo] = useState(value?.[0] || {})
  const [radioValue, setRadioValue] = useState(value?.length > 0 ? TRUE : FALSE)
  const [visibleModal, setVisibleModal] = useState(false)

  const handleRadioChange = (e: any) => {
    const value = e.target.value
    setRadioValue(value)

    if (value === TRUE) {
      setVisibleModal(true)
    } else {
      setCollectFormInfo({})
    }
  }

  return (
    <div>
      <Radio.Group
        value={radioValue}
        onChange={handleRadioChange}
        options={trueOrFalseOptions}
      />
      
      {collectFormInfo?.name && (
        <Table
          columns={columns}
          dataSource={[collectFormInfo]}
          pagination={false}
          size="small"
        />
      )}

      <ModalFormRelevance
        visible={visibleModal}
        onCancel={() => setVisibleModal(false)}
        onSelect={(value) => {
          setCollectFormInfo(value)
          setVisibleModal(false)
        }}
      />
    </div>
  )
}

// 在字段配置中使用
{
  title: '是否收集表单',
  dataIndex: 'form',
  valueType: 'radio',
  formItemProps: {
    rules: [{ required: true, validator: (rule: any, value: any, callback: any) => {
      callback()
    }}],
  },
  initialValue: [],
  renderFormItem: (schema: any, config: any, form: any) => (
    <CollectForm config={config} form={form} />
  ),
}

这个自定义组件的核心功能是:

  1. 单选控制: 通过单选按钮控制是否收集表单
  2. 表格展示: 当选择收集表单时,显示已选择的表单信息
  3. 弹窗选择: 点击"是"时打开表单选择弹窗
  4. 数据管理: 管理表单信息的状态和更新

通过这三个自定义组件的例子,我们可以看到 renderFormItem 的强大之处:它允许我们完全自定义字段的渲染方式,实现任何复杂的交互逻辑,而不受内置组件的限制。

条件渲染 (Conditional Rendering)

条件渲染是 BetaSchemaForm 的另一个重要特性,它允许我们根据不同的条件来显示或隐藏字段,或者改变字段的配置。在实际业务中,我们经常需要根据用户的操作类型、数据状态、权限等条件来动态调整表单的显示。

基于操作类型的条件渲染

在项目中,同一个表单需要支持多种操作类型:新增、编辑、预览、复制。不同的操作类型需要不同的字段配置,比如编辑时某些字段不可修改,预览时所有字段都只读等。

我们可以通过一个工具函数来根据操作类型动态设置字段的禁用状态:

typescript 复制代码
// 根据操作类型获取字段禁用状态
const getFieldDisabled = (actionType: ActionType, fieldName: string): boolean => {
  switch (actionType) {
    case ACTION_TYPE.ADD:
      return false; // 新增时所有字段可编辑
    case ACTION_TYPE.PREVIEW:
      return true; // 预览时所有字段禁用
    case ACTION_TYPE.EDIT:
      // 编辑时只有特定字段可编辑
      return !['description', 'schoolAddress'].includes(fieldName);
    case ACTION_TYPE.COPY:
      // 复制时活动类型不可编辑
      return fieldName === 'activityType';
    default:
      return false;
  }
};

// 使用示例
{
  title: '活动名称',
  dataIndex: 'name',
  fieldProps: {
    disabled: getFieldDisabled(actionType, 'name'),
  },
}

这个工具函数的作用是:

  • 新增模式: 所有字段都可以编辑,用户可以自由填写
  • 预览模式: 所有字段都禁用,用户只能查看不能修改
  • 编辑模式: 只有活动描述和校区地址可以编辑,其他字段保持只读
  • 复制模式: 活动类型不可编辑,其他字段可以修改

基于场景类型的条件渲染

在项目中,活动分为线上和线下两种场景,不同场景需要显示不同的字段。线下活动需要显示批改方式和校区地址,线上活动需要显示作答方式。

我们可以通过条件判断来动态添加不同的字段:

typescript 复制代码
// 根据场景类型添加不同字段
const getBasicInfoColumns = ({ answerScene, actionType }) => {
  const baseFields = [/* 基础字段 */];
  
  // 线下场景:显示批改方式和校区地址
  if (answerScene === SCENE_TYPE.OFFLINE) {
    baseFields.push(
      {
        title: '批改方式',
        dataIndex: 'correctOrigin',
        valueType: 'radio',
        fieldProps: {
          options: modifyWayOptions,
          disabled: true,
        },
        initialValue: MODIFY_WAY.ONLINE,
      },
      {
        title: '校区地址',
        dataIndex: 'schoolAddress',
        valueType: 'textarea',
        fieldProps: {
          disabled: getFieldDisabled(actionType, 'schoolAddress'),
        },
      }
    );
  }

  // 线上场景:显示作答方式
  if (answerScene === SCENE_TYPE.ONLINE) {
    baseFields.push({
      title: '作答方式',
      dataIndex: 'answerDeviceType',
      valueType: 'checkbox',
      fieldProps: {
        options: answerDeviceTypeOptions,
        disabled: getFieldDisabled(actionType, 'answerDeviceType'),
      },
    });
  }

  return baseFields;
};

这种条件渲染的方式使得表单能够根据不同的场景动态调整,提供更精准的用户体验。线下活动关注的是批改方式和校区地址,线上活动关注的是作答方式,这样的设计更符合实际业务需求。

动态字段插入

有时候我们需要在特定的位置插入字段,而不是简单地添加到数组末尾。在项目中,我们需要在活动类型字段后面插入场景相关的字段:

typescript 复制代码
// 在特定位置插入字段
const finalFields = [...baseFields];
const activityTypeIndex = finalFields.findIndex(field => field.dataIndex === 'activityType');

if (answerScene === SCENE_TYPE.OFFLINE) {
  // 在活动类型后插入批改方式
  finalFields.splice(activityTypeIndex + 1, 0, sceneSpecificFields[0]);
  // 在活动描述后插入校区地址
  finalFields.push(sceneSpecificFields[1]);
}

这种动态插入的方式确保了字段在表单中的位置符合业务逻辑,用户能够按照自然的顺序填写表单,提升了用户体验。

表单验证 (Validation)

表单验证是确保数据质量的关键环节。BetaSchemaForm 提供了多种验证方式,从简单的基础验证到复杂的自定义验证,能够满足各种业务场景的需求。

基础验证

基础验证是最常用的验证方式,通过 formItemProps.rules 配置验证规则:

typescript 复制代码
{
  title: '活动名称',
  dataIndex: 'name',
  formItemProps: {
    rules: [{ required: true, message: '请选择活动类型' }],
  },
}

这个配置表示活动名称字段是必填的,如果用户没有填写,会显示"请选择活动类型"的错误提示。

自定义验证器

对于更复杂的验证需求,我们可以使用自定义验证器。自定义验证器提供了更大的灵活性,可以处理各种复杂的验证逻辑。

在项目中,我们为活动名称创建了一个自定义验证器:

typescript 复制代码
// 名称验证器
export const nameValidator = (rule: any, value: any, callback: any) => {
  if (!value) {
    callback('活动名称不能为空');
    return;
  }
  if (!value.toString().trim()) {
    callback('活动名称不能只是空格');
    return;
  }
  if (value.length > 20) {
    callback('字数不能超过20');
    return;
  }
  callback();
};

// 使用自定义验证器
{
  title: '活动名称',
  dataIndex: 'name',
  formItemProps: {
    rules: [{ required: true, validator: nameValidator }],
  },
}

这个自定义验证器的作用是:

  1. 空值检查: 确保活动名称不为空
  2. 空格检查: 确保活动名称不只是空格
  3. 长度检查: 确保活动名称不超过20个字符
  4. 错误提示: 为每种错误情况提供具体的错误信息

条件验证

有些字段的验证规则需要根据其他字段的值来动态调整。在项目中,接收人邮箱字段的验证就依赖于线索接收人字段的值:

typescript 复制代码
{
  title: '接收人邮箱',
  dataIndex: 'receiverEmail',
  formItemProps: {
    rules: [{
      required: true,
      message: '请输入接收人邮箱',
      validator: (rule: any, value: any, callback: any) => {
        // 自定义配置时必须输入邮箱
        if (receiverFlagValue === FALSE && !value) {
          callback('请输入接收人邮箱')
        } else {
          callback()
        }
      }
    }],
  },
}

这个条件验证的逻辑是:

  • 当线索接收人选择"推广人"时,邮箱字段会自动填充,不需要验证
  • 当线索接收人选择"自定义配置"时,邮箱字段必须填写,否则显示错误提示

邮箱后缀自动补全

除了表单验证,我们还可以使用 transform 函数在表单提交时自动处理数据格式。在项目中,我们为接收人邮箱字段实现了自动补全 @df.cn 后缀的功能:

字段配置中的 Transform

typescript 复制代码
// 接收人邮箱字段配置
{
  title: '接收人邮箱',
  dataIndex: 'receiverEmail',
  fieldProps: {
    placeholder: '请输入接收人邮箱',
  },
  formItemProps: {
    rules: [{
      required: true,
      message: '请输入接收人邮箱',
    }],
  },
  transform: (value: any) => {
    if (!value || typeof value !== 'string') {
      return value;
    }
    
    const trimmedValue = value.trim();
    if (trimmedValue && !trimmedValue.endsWith('@df.cn')) {
      return trimmedValue + '@df.cn';
    }
    
    return trimmedValue;
  },
}

表单提交时的处理

typescript 复制代码
// 在表单提交时获取格式化后的数据
const handleFinish = async () => {
  try {
    // 1. 先进行表单验证
    await promotionFormRef.current?.validateFields();
    
    // 2. 再获取格式化后的值(包含 transform 处理后的邮箱)
    const thirdStepData = promotionFormRef.current?.getFieldsFormatValue();
    
    // 提交数据(邮箱已经自动补全了 @df.cn 后缀)
    const result = await apiCreateEvaluationActivityFormat(thirdStepData);
    
    if (result.success) {
      message.success('操作成功');
      onSuccess?.();
    }
  } catch (error) {
    console.error('表单验证失败:', error);
  }
};

功能效果

这个 transform 函数的作用是:

  • 自动去除空格 : 使用 trim() 去除用户输入的首尾空格
  • 后缀检查 : 检查邮箱是否以 @df.cn 结尾
  • 自动补全 : 如果没有后缀,自动添加 @df.cn
  • 提升用户体验: 用户只需要输入用户名,系统自动补全完整邮箱

使用示例

  • 用户输入:zhangsan → 自动变为:zhangsan@df.cn
  • 用户输入:lisi@ → 自动变为:lisi@@df.cn
  • 用户输入:wangwu@df.cn → 保持不变:wangwu@df.cn
  • 用户输入:zhaoliu → 自动变为:zhaoliu@df.cn(去除空格并添加后缀)

通过这种方式,我们可以在字段级别自动处理数据格式,而不需要在提交时手动处理,使代码更加简洁和可维护。

通过以上几种验证方式,我们可以构建出完善的表单验证体系,确保数据的准确性和完整性。

重要:Transform 函数的使用注意事项

在使用 transform 函数时,有一个非常重要的技术细节需要注意:

获取格式化后的值

当字段配置中使用了 transform 函数时,必须使用 FormRef.current.getFieldsFormatValue() 来获取格式化后的值,而不是普通的 validateFields()getFieldsValue()

typescript 复制代码
// ❌ 错误的方式 - 无法获取到 transform 后的值
const formData = formRef.current?.getFieldsValue();

// ✅ 正确的方式 - 先验证表单,再获取格式化后的值
await formRef.current?.validateFields(); // 先进行表单验证
const formData = formRef.current?.getFieldsFormatValue(); // 再获取格式化后的值

原因说明

  • validateFields() 用于验证表单,确保所有必填字段都已填写且格式正确
  • getFieldsValue() 返回的是表单的原始值,不会执行 transform 函数
  • getFieldsFormatValue() 会执行所有字段的 transform 函数,返回格式化后的值
  • 必须先调用 validateFields() 确保表单验证通过,再调用 getFieldsFormatValue() 获取格式化后的数据
  • 如果使用错误的方法,会导致提交给后端的数据格式不正确

实际应用示例

typescript 复制代码
// 字段配置中使用了 transform
{
  title: '所属学校',
  dataIndex: 'schoolId',
  transform: createIdNameTransform('schoolId', 'schoolName'),
}

// 提交时获取数据
const handleSubmit = async () => {
  try {
    // 1. 先进行表单验证
    await formRef.current?.validateFields();
    
    // 2. 再获取格式化后的数据
    const formData = formRef.current?.getFieldsFormatValue();
    
    // formData 会包含:
    // {
    //   schoolId: '123',
    //   schoolName: '北京学校'
    // }
    
    // 而不是原始值:
    // {
    //   schoolId: { label: '北京学校', value: '123' }
    // }
    
    submitToAPI(formData);
  } catch (error) {
    console.error('表单验证失败:', error);
  }
};

这个细节对于确保数据正确提交到后端非常重要,开发者在使用 transform 函数时必须牢记这一点。

实际案例解析

通过前面的介绍,我们已经了解了 BetaSchemaForm 的各种特性。现在让我们通过几个实际案例来深入理解这些特性是如何组合使用的,以及如何解决复杂的业务问题。

💡 说明: 以下案例均来自实际项目中的使用经验,展示了 BetaSchemaForm 在企业级应用中的实际应用场景。这些案例涵盖了复杂的业务逻辑、字段依赖、数据转换等高级用法。

案例1:级联选择

级联选择是表单中常见的需求,用户的选择会影响后续选项的内容。在项目中,我们实现了三级渠道的级联选择:活动默认渠道 → 一级渠道 → 二级渠道 → 三级渠道。

这个案例展示了如何通过依赖字段和动态请求来实现复杂的级联逻辑:

typescript 复制代码
// 三级渠道级联选择
const channelColumns = [
  {
    title: '活动默认渠道',
    dataIndex: 'businessType',
    valueType: 'select',
    fieldProps: {
      options: businessOptions,
      onChange: () => {
        // 清空下级字段
        formRef.current?.resetFields(['firstLevelId', 'secondLevelId', 'thirdLevelId'])
      }
    },
  },
  {
    title: '一级渠道',
    dataIndex: 'firstLevelId',
    dependencies: ['businessType'],
    request: (params: any) => apiGetFirstLevelListFormat({ 
      businessType: params.businessType 
    }),
    fieldProps: {
      onChange: () => {
        formRef.current?.resetFields(['secondLevelId', 'thirdLevelId'])
      }
    },
  },
  {
    title: '二级渠道',
    dataIndex: 'secondLevelId',
    dependencies: ['businessType', 'firstLevelId'],
    request: (params: any) => apiGetSecondLevelListFormat(params),
  },
  {
    title: '三级渠道',
    dataIndex: 'thirdLevelId',
    dependencies: ['businessType', 'secondLevelId', 'schoolId'],
    request: (params: any) => apiGetThirdLevelListFormat({
      businessType: params.businessType,
      level2Id: params.secondLevelId,
      schoolId: params.schoolId
    }),
  },
];

这个级联选择的实现逻辑是:

  1. 活动默认渠道: 用户选择后,清空所有下级字段
  2. 一级渠道: 依赖于活动默认渠道,选择后清空二级和三级渠道
  3. 二级渠道: 依赖于活动默认渠道和一级渠道
  4. 三级渠道: 依赖于活动默认渠道、二级渠道和学校ID

通过这种设计,确保了数据的完整性和用户体验的流畅性。

案例2:复杂依赖关系

推送CRM配置是一个典型的复杂依赖关系案例,它涉及多层嵌套的依赖逻辑。这个案例展示了如何通过 valueType: 'dependency' 来实现复杂的条件渲染:

typescript 复制代码
// 推送CRM配置的复杂依赖
const pushCRMColumns = {
  valueType: 'dependency',
  name: ['pushCrm'],
  columns: ({ pushCrm }: { pushCrm: number }) => {
    if (!pushCrm) return []
    
    return [
      // 推送系统
      {
        title: '推送系统',
        dataIndex: 'pushSystem',
        valueType: 'select',
        fieldProps: {
          options: pushSystemOptions,
        },
      },
      // 推送学校
      {
        title: '推送学校',
        dataIndex: 'pushSchoolId',
        dependencies: ['pushCrm', 'pushSystem'],
        request: (params: any) => apiGetPushSchoolListFormat({ 
          bizType: params.pushSystem 
        }),
      },
      // CRM特有字段
      {
        valueType: 'dependency',
        name: ['pushSystem'],
        columns: ({ pushSystem }: { pushSystem: string }) => {
          if (pushSystem === SYSTEM_TYPE.CRM) {
            return [
              // 意向部门
              {
                title: '意向部门',
                dataIndex: 'intentionalDeptCode',
                dependencies: ['pushCrm', 'pushSystem'],
                request: (params: any) => apiGetIntentionDeptListFormat({ 
                  bizType: params.pushSystem 
                }),
                fieldProps: {
                  onChange: (value: string) => {
                    formRef.current?.resetFields(['intentionalProjectCode'])
                  }
                },
              },
              // 意向项目
              {
                title: '意向项目',
                dataIndex: 'intentionalProjectCode',
                dependencies: ['pushCrm', 'pushSystem', 'intentionalDeptCode'],
                request: (params: any) => apiGetIntentionDeptListFormat({ 
                  bizType: params.pushSystem 
                }),
              },
            ]
          }
          return []
        },
      }
    ]
  },
}

这个复杂依赖关系的实现逻辑是:

  1. 第一层依赖: 检查是否推送CRM,如果不推送则隐藏所有相关字段
  2. 第二层依赖: 在推送CRM的前提下,显示推送系统和推送学校字段
  3. 第三层依赖: 在推送系统选择为CRM时,显示意向部门和意向项目字段
  4. 级联清空: 当意向部门变化时,自动清空意向项目字段

这种多层嵌套的依赖关系确保了表单的显示逻辑符合业务需求,用户能够按照正确的顺序填写表单。

案例3:条件字段显示

推广页面配置展示了如何根据操作类型来条件显示字段。在编辑和查看模式下,某些字段需要隐藏,以简化用户界面:

typescript 复制代码
// 推广页面配置的条件显示
const qrCodeConfigColumns = {
  valueType: 'dependency',
  name: ['qrCodeConfig'],
  columns: ({ qrCodeConfig }: { qrCodeConfig: number }) => {
    if (!qrCodeConfig) return []
    
    // 在编辑和查看模式下,只显示邮箱字段
    if (actionType === ACTION_TYPE.EDIT || actionType === ACTION_TYPE.PREVIEW) {
      return [emailColumn]
    }
    
    // 在新增和复制模式下,显示所有字段
    return [emailColumn, userTagColumn, greetingWordColumn]
  },
}

这个条件字段显示的逻辑是:

  • 新增模式: 显示所有字段,用户可以完整配置推广页面
  • 复制模式: 显示所有字段,用户可以修改配置
  • 编辑模式: 只显示邮箱字段,其他字段不可修改
  • 查看模式: 只显示邮箱字段,所有字段只读

通过这种设计,我们为不同的操作模式提供了不同的界面,既保证了功能的完整性,又避免了不必要的复杂性。

最佳实践

通过前面的介绍和案例,我们已经了解了 BetaSchemaForm 的各种特性和用法。现在让我们总结一些最佳实践,帮助开发者更好地使用 BetaSchemaForm,提升代码质量和开发效率。

1. 字段配置组织

良好的字段配置组织是构建可维护表单系统的关键。建议使用函数返回配置,支持参数化,这样可以根据不同的场景动态调整字段配置。同时,要充分利用 ProComponents 生态的配置复用特性:

typescript 复制代码
// 使用函数返回配置,支持参数化
export const getBasicInfoColumns = ({
  formRef,
  actionType = ACTION_TYPE.ADD,
  answerScene = SCENE_TYPE.OFFLINE,
  currentData = {}
}: {
  formRef: any;
  actionType?: ActionType;
  answerScene?: AnswerScene;
  currentData?: any;
}) => {
  // 配置逻辑
  return columns;
};

这种组织方式的优势是:

  • 参数化: 可以根据不同的参数生成不同的配置
  • 复用性: 同一个函数可以在不同的场景中使用
  • 可维护性: 配置逻辑集中在一个地方,便于维护
  • 类型安全: 通过 TypeScript 类型定义确保参数的正确性

配置复用最佳实践

在 ProComponents 生态中,充分利用配置复用可以大大提高开发效率:

typescript 复制代码
// 定义通用字段配置
const createActivityColumns = (mode: 'form' | 'table' | 'search') => {
  const baseConfig = {
    title: '活动名称',
    dataIndex: 'activityName',
    valueType: 'text',
  };

  // 根据使用场景调整配置
  switch (mode) {
    case 'form':
      return {
        ...baseConfig,
        formItemProps: {
          rules: [{ required: true, message: '请输入活动名称' }]
        },
        fieldProps: {
          placeholder: '请输入活动名称'
        }
      };
    case 'search':
      return {
        ...baseConfig,
        fieldProps: {
          placeholder: '请输入活动名称进行搜索'
        }
      };
    case 'table':
      return {
        ...baseConfig,
        // 表格列配置
        ellipsis: true,
        copyable: true
      };
    default:
      return baseConfig;
  }
};

// 在不同组件中使用
const formColumns = [createActivityColumns('form')];
const searchColumns = [createActivityColumns('search')];
const tableColumns = [createActivityColumns('table')];

配置复用的优势:

  • 代码复用: 同一套配置逻辑可用于多种场景
  • 维护简单: 修改配置时只需要改一个地方
  • 一致性保证: 确保不同场景下的字段行为一致
  • 开发效率: 减少重复代码,提高开发速度

2. 合理使用通用配置属性

根据 ProComponents SchemaForm 文档,合理使用通用配置属性可以提升开发效率和代码质量:

typescript 复制代码
// ✅ 合理使用显示控制属性
const createConfig = (mode: 'create' | 'edit' | 'view') => [
  {
    title: '活动ID',
    dataIndex: 'activityId',
    valueType: 'text',
    hideInForm: mode === 'create', // 创建时隐藏
    hideInTable: false,
    fieldProps: {
      disabled: mode === 'view' // 查看时禁用
    }
  },
  {
    title: '创建时间',
    dataIndex: 'createTime',
    valueType: 'dateTime',
    hideInForm: true, // 表单中隐藏,只在表格中显示
    hideInTable: false
  }
];

// ✅ 合理使用值处理属性
const createValueConfig = () => [
  {
    title: '活动状态',
    dataIndex: 'status',
    valueType: 'select',
    initialValue: 1, // 设置默认值
    convertValue: (value) => {
      // 显示时转换
      const statusMap = { 1: '启用', 0: '禁用' };
      return statusMap[value] || value;
    },
    transform: (value) => {
      // 提交时转换
      return typeof value === 'object' ? value.value : value;
    }
  }
];

// ✅ 合理使用表单配置属性
const createFormConfig = () => [
  {
    title: '活动名称',
    dataIndex: 'activityName',
    valueType: 'text',
    formItemProps: {
      rules: [
        { required: true, message: '请输入活动名称' },
        { max: 50, message: '活动名称不能超过50个字符' }
      ],
      tooltip: '活动名称将显示在用户界面上'
    },
    fieldProps: {
      placeholder: '请输入活动名称',
      maxLength: 50,
      showCount: true
    }
  }
];

通用配置属性的使用原则:

  • 显示控制 : 使用 hideInFormhideInTable 等属性控制字段在不同场景下的显示
  • 值处理 : 使用 initialValueconvertValuetransform 处理数据的显示和提交
  • 表单配置 : 使用 formItemPropsfieldProps 分别配置 Form.Item 和字段组件的属性
  • 依赖关系 : 使用 dependencies 建立字段间的依赖关系
  • 自定义渲染 : 使用 renderFormItemrender 实现复杂的自定义渲染逻辑

3. 工具函数复用

在表单开发中,很多转换逻辑和验证规则是重复的。建议创建通用的工具函数,提高代码的复用性和一致性:

typescript 复制代码
// 创建通用的ID-Name转换函数
export const createIdNameTransform = (idField: string, nameField?: string) => {
  return (value: any) => {
    if (!value) {
      return {
        [idField]: null,
        [nameField || `${idField}Name`]: null
      };
    }
    
    if (typeof value === 'object' && value.label && value.value) {
      return {
        [idField]: value.value,
        [nameField || `${idField}Name`]: value.label
      };
    }
    
    return {
      [idField]: value,
      [nameField || `${idField}Name`]: null
    };
  };
};

// 使用
{
  title: '所属学校',
  dataIndex: 'schoolId',
  transform: createIdNameTransform('schoolId', 'schoolName'),
}

工具函数复用的优势是:

  • 一致性: 确保相同的逻辑在不同地方的行为一致
  • 可维护性: 修改逻辑时只需要修改一个地方
  • 可测试性: 可以单独测试工具函数的逻辑
  • 可读性: 通过函数名清楚地表达意图

3. 条件渲染优化

对于复杂的条件渲染逻辑,建议使用 IIFE(立即执行函数表达式)来封装,避免变量污染和提高代码的可读性:

typescript 复制代码
// 使用IIFE封装复杂逻辑
const pushSystemColumns = (() => {
  const intentionalDeptColumn = {
    title: '意向部门',
    dataIndex: 'intentionalDeptCode',
    // ... 配置
  };

  const intentionalProjectColumn = {
    title: '意向项目',
    dataIndex: 'intentionalProjectCode',
    // ... 配置
  };

  return {
    valueType: 'dependency',
    name: ['pushSystem'],
    columns: ({ pushSystem }: { pushSystem: string }) => {
      if (pushSystem === SYSTEM_TYPE.CRM) {
        return [intentionalDeptColumn, intentionalProjectColumn]
      }
      return []
    },
  }
})();

IIFE 的优势是:

  • 封装性: 将相关的逻辑封装在一个作用域内
  • 避免污染: 不会污染全局作用域
  • 可读性: 代码结构更清晰,逻辑更集中
  • 可维护性: 相关的配置集中在一起,便于维护

4. 表单引用管理

在组件中正确管理表单引用是确保表单功能正常工作的关键。建议使用 useRef 和 useMemo 来优化性能:

typescript 复制代码
// 在组件中管理表单引用
const DrawerUpdate = () => {
  const basicFormRef = useRef();
  const promotionFormRef = useRef();
  
  // 获取字段配置
  const basicInfoColumns = useMemo(() => 
    getBasicInfoColumns({
      formRef: basicFormRef,
      actionType,
      answerScene,
      currentData
    }), [actionType, answerScene, currentData]
  );

  return (
    <BetaSchemaForm
      formRef={basicFormRef}
      columns={basicInfoColumns}
      // ... 其他配置
    />
  );
};

表单引用管理的优势是:

  • 性能优化: 使用 useMemo 避免不必要的重新计算
  • 引用稳定: 使用 useRef 确保引用在组件生命周期内保持稳定
  • 依赖管理: 通过依赖数组控制何时重新计算配置

5. 数据转换处理

在表单提交时,建议统一处理数据转换和验证,确保数据的完整性和正确性:

typescript 复制代码
// 统一的数据转换处理
const handleFinish = async () => {
  try {
    // 1. 先进行表单验证
    await promotionFormRef.current?.validateFields();
    
    // 2. 再获取格式化后的值(如果字段使用了 transform)
    const formData = promotionFormRef.current?.getFieldsFormatValue();
    
    // 数据预处理
    if (formData.receiverEmail && !formData.receiverEmail.endsWith('@df.cn')) {
      formData.receiverEmail = formData.receiverEmail + '@df.cn';
    }
    
    // 提交数据
    const result = await apiCreateEvaluationActivityFormat(formData);
    
    if (result.success) {
      message.success('操作成功');
      onSuccess?.();
    }
  } catch (error) {
    console.error('提交失败:', error);
  }
};

数据转换处理的优势是:

  • 统一性: 所有数据转换逻辑集中在一个地方
  • 可维护性: 修改转换逻辑时只需要修改一个地方
  • 错误处理: 统一的错误处理机制
  • 数据完整性: 确保提交的数据格式正确

重要提醒 :在使用 transform 函数时,务必先调用 validateFields() 进行表单验证,然后使用 getFieldsFormatValue() 获取格式化后的数据,否则无法获取到正确的格式化值。

6. 性能优化

对于复杂的表单,建议使用 useCallback 和 useMemo 来优化性能,避免不必要的重新渲染:

typescript 复制代码
// 使用 useMemo 缓存字段配置
const basicInfoColumns = useMemo(() => 
  getBasicInfoColumns({
    formRef: basicFormRef,
    actionType,
    answerScene,
    currentData
  }), [actionType, answerScene, currentData]
);

// 使用 useCallback 缓存事件处理函数
const handleFieldChange = useCallback((fieldName: string) => {
  return () => {
    // 清空相关字段
    formRef.current?.resetFields([/* 相关字段 */]);
  };
}, [formRef]);

性能优化的优势是:

  • 减少重新渲染: 避免不必要的组件重新渲染
  • 提升用户体验: 表单响应更快,交互更流畅
  • 资源节约: 减少不必要的计算和内存占用

总结

配置化表单是现代前端开发中的重要技术,通过合理使用依赖字段、值转换、自定义组件等特性,可以构建出复杂而灵活的表单系统。BetaSchemaForm 作为基于 Ant Design 的配置化表单解决方案,为开发者提供了强大的工具和丰富的功能。

关键是要理解各种配置选项的作用和使用场景,并根据实际需求进行合理的组合使用。通过本文档的学习,开发者应该能够掌握配置化表单的核心概念,并能够在实际项目中灵活运用。

核心优势

  • 声明式配置: 通过配置对象快速构建表单,减少重复代码
  • 丰富的组件: 支持 20+ 种内置组件,满足大部分业务需求
  • 强大的扩展性: 支持自定义组件,适应复杂业务场景
  • 表单联动: 支持字段依赖和动态渲染
  • 完善的校验: 内置多种校验规则,支持自定义校验
  • 配置复用: 表单配置与表格列配置保持一致,一套配置多场景使用
  • ProComponents 生态: 与 ProTable、ProList 等组件无缝集成,提供完整的企业级解决方案
  • 性能优化: 内置虚拟滚动、懒加载等性能优化特性
  • TypeScript 支持: 完整的类型定义,提供更好的开发体验

开发建议

在实际开发中,建议:

  1. 合理组织配置:使用函数返回配置,支持参数化
  2. 复用工具函数:创建通用的转换和验证函数
  3. 优化性能:使用 useMemo 和 useCallback 缓存配置和函数
  4. 处理复杂逻辑:使用 IIFE 封装复杂的条件渲染逻辑
  5. 统一数据流:在提交时统一处理数据转换和验证

学习资源

  • 官方网站 : procomponents.ant.design/components/...
  • 组件文档: 查看完整的组件列表和配置说明
  • 示例代码: 参考官方提供的各种使用示例
  • 社区支持: 遇到问题可以在官方仓库提交 Issue
  • ProComponents 生态: 了解与 ProTable、ProList、ProCard 等组件的协同使用
  • Ant Design: 基于 Ant Design 组件库,熟悉 Ant Design 有助于更好地使用 BetaSchemaForm

通过以上最佳实践,可以构建出既灵活又易维护的表单系统,提升开发效率和用户体验。

相关推荐
niusir几秒前
Zustand 实战:10 行代码搞定全局状态
前端·javascript·react.js
niusir几秒前
React 状态管理的演进与最佳实践
前端·javascript·react.js
张愚歌1 分钟前
快速上手Leaflet:轻松创建你的第一个交互地图
前端
唐某人丶10 分钟前
教你如何用 JS 实现 Agent 系统(3)—— 借鉴 Cursor 的设计模式实现深度搜索
前端·人工智能·aigc
看到我请叫我铁锤14 分钟前
vue3使用leaflet的时候高亮显示省市区
前端·javascript·vue.js
南囝coding24 分钟前
Vercel 发布 AI Gateway 神器!可一键访问数百个模型,助力零门槛开发 AI 应用
前端·后端
AI大模型30 分钟前
前端学 AI 不用愁!手把手教你用 LangGraph 实现 ReAct 智能体(附完整流程 + 代码)
前端·llm·agent
小红1 小时前
网络通信基石:从IP地址到子网划分的完整指南
前端·网络协议
一枚前端小能手1 小时前
🔥 前端储存这点事 - 5个存储方案让你的数据管理更优雅
前端·javascript
willlzq1 小时前
深入探索Swift的Subscript机制和最佳实践
前端