基于Antd的SchemaForm 的表单复杂配置

1. 传统 Form 组件的配置

传统的Form 组件,代码大致如下

tsx 复制代码
<Form
  name="basic"
  labelCol={{ span: 8 }}
  wrapperCol={{ span: 16 }}
  style={{ maxWidth: 600 }}
  initialValues={{ remember: true }}
  onFinish={onFinish}
  onFinishFailed={onFinishFailed}
  autoComplete="off"
>
  <Form.Item<FieldType>
    label="Username"
    name="username"
    rules={[{ required: true, message: 'Please input your username!' }]}
  >
    <Input />
  </Form.Item>

  <Form.Item<FieldType>
    label="Password"
    name="password"
    rules={[{ required: true, message: 'Please input your password!' }]}
  >
    <Input.Password />
  </Form.Item>

  <Form.Item label={null}>
    <Button type="primary" htmlType="submit">
      Submit
    </Button>
  </Form.Item>
</Form>

2. BetaSchemaForm 的配置

使用schemaForm的话,代码如下

tsx 复制代码
import { BetaSchemaForm } from '@ant-design/pro-components';
const columns = [
  {
    title: 'Username',
    dataIndex: 'username',
    valueType: 'input',
    rules: [{ required: true, message: 'Please input your username!' }],
  },
  {
    title: 'Password',
    dataIndex: 'password',
    valueType: 'input',
    rules: [{ required: true, message: 'Please input your password!' }],
  },
];
const onFinish = (values: any) => {
  console.log(values);
};

const initialValues = {
  username: 'admin',
};
<BetaSchemaForm columns={columns} onFinish={onFinish} />;

3. 配置化的优点

配置化的优点,代码更加简洁,可读性更高,更容易维护。

除此之外,因为 column 使用和表格 column 基本一致的配置,在查询页面,可以定义一个 column,同时可以用在查询、表格和创建功能中。

column 的配置可以参考官网BetaSchemaForm

这里我想通过最近做的学习机测评表单,来说些偏复杂的配置功能。

基础核心属性

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

  • title: 字段的显示标签
  • dataIndex: 字段的数据键名,对应表单数据的属性名
  • valueType: 字段类型,决定渲染哪种表单组件
  • formItemProps: 传递给 Form.Item 的属性,主要用于验证规则
  • fieldProps: 传递给具体表单组件的属性,如 placeholder、disabled 等
  • initialValue: 字段的初始值
  • transform: 字段值的转换
  • convertValue: 字段值的转换
js 复制代码
// 输入
{
  title: '活动名称',
  dataIndex: 'name',
  formItemProps: {
    rules: [{ required: true, message: '请选择活动名称' }],
  },
  // radio/checkbox/select/input/text/rangepicker/datePicker/timePicker/upload/password/textarea/radioButton
  valueType: 'input',
  fieldProps: {
    placeholder: '请输入活动名称',
    maxLength: 20,
    // options: 单选、多选、下拉框的选项
  },
}

选择类的字段

提交的数据既要包含 value,又要包含 label

两个方面,一个需要加 labelInValue: true,一个需要加 transform

拿学校来说,schoolId 也要包含 schoolName。

js 复制代码
{
  title: '所属学校',
  dataIndex: 'schoolId',
  valueType: 'select',
  fieldProps: {
      placeholder: '请选择所属学校',
      labelInValue: true,
  },
  transform: (obj:{value: number, label: string}) => {
    return {
      schoolId: obj.value,
      schoolName: obj.label,
    }
  }
}

获取值的时候,formRef.current.getFieldsFormatValue(),就包含 schoolIdschoolName。但是编辑的时候注意,从 record 获取的 schoolName 需要保存,因为只有修改选项的时候才会同时有 schoolId 和 schoolName,{schoolName, ...formRef.current.getFieldsFormatValue()}

远程获取数据作为选项 - request

request 是一个函数,返回一个 Promise,Promise 的返回值是一个数组,数组中的每个元素是一个对象,对象中包含 value 和 label 两个属性。[{value: '1', label: '学校1'}, {value: '2', label: '学校2'}]

js 复制代码
{
  title: '所属学校',
  dataIndex: 'schoolId',
  formItemProps: {
    rules: [{ required: true, message: '请选择所属学校' }],
  },
  valueType: 'select',
  request: getSchoolList, // 如果不常变化,可以使用缓存
}

依赖别的表单项请求远程数据作为选项 - dependencies

dependencies 是一个数组,数组中的每个元素是一个字符串,表示依赖的字段名称。 比如部门依赖学校,当学校发生变化时,部门会自动重新请求远程数据,且默认会带上学校作为参数。

js 复制代码
{
  title: '所属部门',
  dataIndex: 'deptCode',
  dependencies: ['schoolId'],
  formItemProps: {
    rules: [{ required: true, message: '请选择所属部门' }],
  },
  valueType: 'select',
  request: ({schoolId}) => getDeptList(schoolId),

}

这里注意,一旦有依赖项,尽量加一个依赖项 onChange 事件,重置其他依赖项的值。

js 复制代码
{
  dataIndex: 'schoolId',
  onChange: () => {
    formRef.current?.setFieldValue('deptCode', null)
  }
}

动态表单项,某些表单项是否存在,依赖于别的表单项的值 - valueType: dependency

比如选择招生端,就有渠道的字段,其他的就没有。

那么这些表单项需要单独配置,并放在 dependency 中。

js 复制代码
{
    valueType: 'dependency',
    name: ['activityScene'],
    columns: ({ activityScene }: { activityScene: number }) => {
        if (activityScene === ACTIVITY_SCENE_SCOPE.STUDENT) {
            return [channel1Column, channel2Column]
        }
        return []
    },
}

表单值的转换 transform/convertValue

比如作答方式,交互上是,复选框默认是数组类型,但是提交的时候,需要是数字。同样回显的时候,需要将数字转换为数组。这就需要用到 transform/convertValue。

js 复制代码
{
    title: '作答方式',
    dataIndex: 'answerDeviceType',
    valueType: 'checkbox',
    formItemProps: {
        rules: [{ required: true, message: '请选择作答方式' }],
    },
    fieldProps: {
        options: answerDeviceTypeOptions,
        disabled: getFieldDisabled(actionType, 'answerDeviceType'),
    },
    initialValue: [ANSWER_DEVICE_TYPE.PHONE, ANSWER_DEVICE_TYPE.STUDY_MACHINE],
    transform: (value: any) => {
        if (Array.isArray(value)) {
            // 长度为1 直接返回数组中的值
            if (value.length === 1) return value[0]
            if (value.length === 2) return 3;
            return value;
        }
        return value;
    },
    convertValue: (value: any) => {
        if (value === 3) return [1, 2];
        if (value === 1) return [1];
        if (value === 2) return [2];
        return value;
    },
}

注意,transform 的话,一定使用formRef.current.getFieldsFormatValue()获取值,不然会拿到原始值。

还有种加后缀的,比如输入邮箱,自动追加@df.cn 等等。

善用隐藏字段-hidden/initialValue

编辑的时候有 id 字段,需要提交,但不需要展示在表单中,所以需要善用隐藏字段。这样formRef.current.setFieldsValue(record)设置值的时候,id 就被赋值了,且不展示在表单中。

js 复制代码
{
    title: 'id',
    dataIndex: 'id',
    formItemProps: {
        hidden: true
    }
}

或者一些别的字段,不在表单的配置中,但是需要提交,比如海报pushBackgroundUrl,可以同样设置隐藏字段,当海报修改的时候,formRef.current.setFieldValue('pushBackgroundUrl', value)设置值,这样提交的时候,就会包含pushBackgroundUrl

表单项的值的变化决定另一个表单项属性的变化 - onChange 和 valueType: dependency 结合

比如线索接收人,选择推广人,则邮箱自动填充,且设置为 disabled;选择自定义配置,则邮箱清空

js 复制代码
{
  title: '线索接收人',
  dataIndex: 'receiverFlag',
  onChange: (value: any) => {
    formRef.current.setFieldValue('receiverEmail', value === TRUE ? loginUserEmail : '')
  }
}

const emailColumn =  {
  valueType: 'dependency',
  name: ['receiverFlag'],
  columns: ({ receiverFlag }: { receiverFlag: number }) => {
    return [{
      title: '接收人邮箱',
      dataIndex: 'receiverEmail',
      dependencies: ['receiverFlag'],
      fieldProps: {
        placeholder: '请输入接收人邮箱',
        disabled: receiverFlagValue === TRUE, // 推广人时禁用编辑
      },
      formItemProps: {
        rules: [{
          required: true,
          message: '请输入接收人邮箱',
          validator: (rule: any, value: any, callback: any) => {
            // 自定义配置时必须输入邮箱
            if (receiverFlagValue === FALSE && !value) {
              callback('请输入接收人邮箱')
            } else {
              callback()
            }
          }
        }],
      },
}]}
}

自定义表单组件 - renderFormItem

如果表单项比较复杂,比如有弹框,或者样式比较复杂,可以自定义表单组件 renderFormItem 配置。

比如收集表单,需要点开弹框选择值,同时还需要显示一个表格,展示收集表单的详情。

js 复制代码
const collectFormColumn = {
  title: '收集表单',
  dataIndex: 'form',
  valueType: 'radio',
  formItemProps: {
    rules: [
      {
        required: true,
        validator: (rule: any, value: any, callback: any) => {
          // 必填项,选择否的时候form就是空数组,不需要校验
          callback();
        },
      },
    ],
  },
  initialValue: [],
  renderFormItem: (schema: any, config: any, form: any) => (
    <CollectForm config={config} form={form} schema={schema} />
  ),
};

schema 其实就是collectFormColumn,config 是整个表单层面的配置,form 是表单实例也就是formRef.current

value 和 onChange 属性,一般不在 renderFormItem 中配置,而是在组件内部配置,也是必传项,onChange 的时候就是外层表单获取到的值,value 是外层表单传递该表单项的值。

jsx 复制代码
const CollectForm: React.FC<CollectFormProps> = ({ value, onChange }: any) => {
  // 拿到表单项的值,进行处理回显
  const [collectFormInfo, setCollectFormInfo] = useState(
    value?.[0] || { ...CollectFormInfoInit }
  );

  useEffect(() => {
    // 当collectFormInfo发生变化时,调用onChange,外层表单获取到该表单项的值
    onChange(collectFormInfo?.name ? [collectFormInfo] : []);
  }, [collectFormInfo]);

  const [radioValue, setRadioValue] =
    useState < number > (value?.length > 0 ? TRUE : FALSE);
  const handleSelectForm = (value: { id: number, name: string }) => {
    console.log('value', value);
    const isRelated = value.name !== '';
    if (!isRelated) {
      setRadioValue(FALSE);
      setCollectFormInfo({ ...CollectFormInfoInit });
      return;
    }
    setRadioValue(TRUE);
    // 更新表单信息
    setCollectFormInfo(value);
  };

  return (
    <div
      className="select-add-container"
      style={{
        display: 'flex',
        flexDirection: 'column',
        minHeight: collectFormInfo?.name ? '110px' : '30px',
        gap: '10px',
      }}
    >
      {/* 单选按钮 - 占满一行 */}
      <div style={{ width: '100%' }}>
        <Radio.Group
          value={radioValue}
          onChange={handleRadioChange}
          options={trueOrFalseOptions}
          style={{ width: '100%' }}
        />
      </div>

      {/* 表格展示已选择的表单信息 */}
      {collectFormInfo?.name && (
        <div style={{ width: '100%' }}>
          <Table
            columns={columns}
            dataSource={tableData}
            pagination={false}
            size="small"
            bordered
          />
        </div>
      )}

      <ModalFormRelevance />
    </div>
  );
};

这样组件逻辑略复杂,但是外层表单不关心,只关心 onChange 和 value。 因此即便逻辑复杂,外层表单也可以很简洁,而且也容易复用。

其他一些实践

columns尽量使用函数方式,因为actionTypeaddeditpreviewcopy,在不同模式下,需要展示不同的字段和样式,而且有时候外层其他的值也会影响字段的展示。

借用 cursor,有个能快速定义接口的方式,这里分享下。

js 复制代码
import { BASE_URL } from '@/utils/define';
import { request } from '@umijs/max';

// 试卷任务相关类型定义
export interface PaperTaskItem {
  /** 创建时间 */
  createTime: string;
  // ...
}

export interface PaperTaskPageData {
  /** 是否最后一页 */
  isLastPage: boolean;
  /** 数据列表 */
  list: PaperTaskItem[][];
  //  ...
}

export interface PaperTaskResponse {
  /** 响应码 */
  code: string;
  // ...
}

export interface PaperTaskQueryParams {
  /** 部门代码 */
  deptCode?: string;
  // ...
}

/**
 * 获取试卷任务列表
 * @description 根据查询条件获取试卷任务分页列表
 * @param params 查询参数
 * @returns Promise<PaperTaskResponse>
 */
export const apiGetPaperList = (params: PaperTaskQueryParams) => {
  return (
    request <
    PaperTaskResponse >
    (`${BASE_URL}/k1-manager/ist`,
    {
      method: 'POST',
      data: params,
    })
  );
};
相关推荐
专注VB编程开发20年2 小时前
vb.net COM DLL 示例,实现了所有 VB6 X86 数据类型的对应
开发语言·前端·vb.net·com·vb6·activex dll
要加油哦~2 小时前
vue 构建工具如何选择 | vue-cli 和 vite的区别
前端·javascript·vue.js
李剑一3 小时前
为了免受再来一刀的痛苦,我耗时两天开发了一款《提肛助手》
前端·vue.js·rust
红尘散仙3 小时前
使用 Tauri Plugin-Store 实现 Zustand 持久化与多窗口数据同步
前端·rust·electron
沙白猿3 小时前
npm启动项目报错“无法加载文件……”
前端·npm·node.js
tyro曹仓舒4 小时前
彻底讲透as const + keyof typeof
前端·typescript
徐小夕@趣谈前端4 小时前
pxcharts多维表格编辑器Ultra版:支持二开 + 本地化部署的多维表格解决方案
大数据·javascript·react.js·编辑器·开源软件·r-tree·多维表格
蛋黄液4 小时前
【黑马程序员】后端Web基础--Maven基础和基础知识
前端·log4j·maven
睡不着的可乐4 小时前
uniapp 支付宝小程序 扩展组件 component 节点的class不生效
前端·微信小程序·支付宝