需求
- 首先这是基于antd的Form组件,
- 需求1: 单选按钮组 选择设置时间 展示时间选择器
- 需求2: 动态添加时间选择器(最多添加10个、时间为空校验、时间段重叠校验)
- 需求3: 开关
- 需求4:编辑时赋值
1. 单选钮组
<Radio.Group>
组件中放置<Radio >
组件就可以形成单选按钮组。<Radio >
需要有value,并且通过<Space>
改为纵向(实际上就是添加了div)。- defaultValue 默认值,默认选中不限制,当然还有个作用就是回显时的赋值(这里我们通过判断时间选择器的数组是否有数据来展示选中状态的)。这个值我们并不需要提交给后端,只是用于判断是否展示时间选择器。
onChange
事件去触发我们定义的changeBuyTimeRadio
方法,这里onChange的值通过e?.target?.value
获得。如果选中值为1(也就是设置时间单选按钮),通过antd的form.setFieldsValue
(以对象作为参数)设置表单的buyPeriods
(也就是我们时间选择器的数组)值。因为选中设置时间后,我们要有一个时间选择器输入框,所以在数组中要添加一个空对象。如果选中的是0(不限制)则将buyPeriods
置空。
const changeBuyTimeRadio = (type) => {
if (type) {
form.setFieldsValue({ buyPeriods: [{}] });
} else {
form.setFieldsValue({ buyPeriods: [] });
}
};
<Form.Item label="购买时间">
<Radio.Group
defaultValue={step3FormData?.buyPeriods?.length > 0 ? 1 : 0}
onChange={(e) => {
changeBuyTimeRadio(e?.target?.value);
}}
>
<Space direction="vertical">
<Radio value={0}>
<Form.Item style={{ marginBottom: 0 }}>
<span>不限制</span>
</Form.Item>
</Radio>
<Radio value={1}>
<Form.Item style={{ marginBottom: 0 }}>
<span>设置时间(最多可设置10个购买时间)</span>
</Form.Item>
</Radio>
</Space>
</Radio.Group>
</Form.Item>
2. 动态添加时间选择器
2.1 antd 的动态增减表单项
<Form.List>
name用于Form收集字段,rules是对应字段的校验规则- 回调函数中用于遍历输入项 ,三个参数分别是:fields、{ add, remove } (解构出add和remove方法)、和 { errors }(解构出错误信息)。
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input } from 'antd';
import React from 'react';
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 4 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 20 },
},
};
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: { span: 24, offset: 0 },
sm: { span: 20, offset: 4 },
},
};
const App: React.FC = () => {
const onFinish = (values: any) => {
console.log('Received values of form:', values);
};
return (
<Form name="dynamic_form_item" {...formItemLayoutWithOutLabel} onFinish={onFinish}>
<Form.List
name="names"
rules={[
{
validator: async (_, names) => {
if (!names || names.length < 2) {
return Promise.reject(new Error('At least 2 passengers'));
}
},
},
]}
>
{(fields, { add, remove }, { errors }) => (
<>
{fields.map((field, index) => (
<Form.Item
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
label={index === 0 ? 'Passengers' : ''}
required={false}
key={field.key}
>
<Form.Item
{...field}
validateTrigger={['onChange', 'onBlur']}
rules={[
{
required: true,
whitespace: true,
message: "Please input passenger's name or delete this field.",
},
]}
noStyle
>
<Input placeholder="passenger name" style={{ width: '60%' }} />
</Form.Item>
{fields.length > 1 ? (
<MinusCircleOutlined
className="dynamic-delete-button"
onClick={() => remove(field.name)}
/>
) : null}
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add()}
style={{ width: '60%' }}
icon={<PlusOutlined />}
>
Add field
</Button>
<Button
type="dashed"
onClick={() => {
add('The head item', 0);
}}
style={{ width: '60%', marginTop: '20px' }}
icon={<PlusOutlined />}
>
Add field at head
</Button>
<Form.ErrorList errors={errors} />
</Form.Item>
</>
)}
</Form.List>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
export default App;
2.2 修改成我们的
先看下代码
{
<Form.List name="buyPeriods">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Space key={key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
<Form.Item
{...restField}
name={[name, 'date']}
rules={[
{ required: true, message: '请设置时间' },
{
validator: async (rule, value) => {
const index = rule?.field?.split('.')[1];
if (timeRangeVaild(value, index)) {
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject('每个购买时间段之间不可时间交叉');
}
return Promise.resolve();
},
},
]}
className="step_cover_global"
>
<RangePicker
showTime
/>
</Form.Item>
{fields.length > 1 && (
<MinusCircleFilled
onClick={() => remove(name)}
className={styles.minusIcon}
/>
)}
{fields.length - 1 === name && (
<PlusCircleFilled
onClick={() => {
if (fields.length < 10) {
add();
} else {
message.error('最多可设置10个购买时间');
}
}}
className={styles.plusIcon}
/>
)}
</Space>
))}
</>
)}
</Form.List>
}
- 我们Form 提交的字段名是buyPeriods
- 校验规则:1.不为空,2.时间段交叉校验
我们说一下时间段交叉校验,validatorr: async (rule, value) =>{} 用于自定义校验规则,然后使用Promise去返回校验响应。
我们打印看一下rule和value分别是什么
rule中包含着当前字段field,其中包含着当前字段(时间选择器)的索引值,value是我们当前时间选择器中选中的时间。首先,我们要用当前选中时间和buyPeriods
中的所以时间段比较(但是不要和其自身比较),所以收集一下当前选中的时间选择器的索引值const index = rule?.field?.split('.')[1];
将value和index传给我们自定义的timeRangeVaild方法。
如下是时间范围检验的方法,用到了moment的isBetween
// 时间范围校验
const timeRangeVaild = (date, sortIndex) => {
// 通过form.getFieldValue获得当前的buyPeriods值(是个数组) 下面我们打印了
const dateJson = form.getFieldValue('buyPeriods');
let flag = false;
// 当 buyPeriods 中的内容多余1项时我们开始比较
if (dateJson.length > 1) {
dateJson?.map((item, index) => {
// 不比较自身(根据上面传入的sortIndex和当前遍历的index进行比较)
if (Number(index) !== Number(sortIndex)) {
// 当前选中时间选择器的起始时间 格式化为YYYY-MM-DD HH:mm:ss格式
const beginRange = item.date[0].format('YYYY-MM-DD HH:mm:ss');
// 当前选中时间选择器的结束时间
const endRange = item.date[1].format('YYYY-MM-DD HH:mm:ss');
// 当前选中时间选择器的开始时间或结束时间只要在 其它时间段内就 将flag置为true
flag = date[0].isBetween(beginRange, endRange) || date[1].isBetween(beginRange, endRange);
}
});
}
return flag;
};
如下当检验函数返回true 我们会进行提示每个”购买时间段之间不可时间交叉“(这里用到Promise.reject)。如果通过校验则返回Promise.resolve();
<Form.Item
{...restField}
name={[name, 'date']}
rules={[
{ required: true, message: '请设置时间' },
{
validator: async (rule, value) => {
console.log(rule,value,'🐷');
const index = rule?.field?.split('.')[1];
if (timeRangeVaild(value, index)) {
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject('每个购买时间段之间不可时间交叉');
}
return Promise.resolve();
},
},
]}
className="step_cover_global"
>