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()
,就包含 schoolId
和 schoolName
。但是编辑的时候注意,从 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
尽量使用函数方式,因为actionType
在add
、edit
、preview
、copy
,在不同模式下,需要展示不同的字段和样式,而且有时候外层其他的值也会影响字段的展示。
借用 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,
})
);
};