1. Form 相关
1.1. 杂项
1.1.1. ant.design 的 Form.list 组件怎么默认显示多少条?
设置 initialValue={[{}]} 即可
1.1.2. shouldUpdate快速实现表单项之间联动
之前表单项联动都是通过 onValuesChange 观察改变的数据,来进行表单项的展示与否,shouldUpdate API能快速实现这一需求。
关键点在使用 shouldUpdate (dependencies),该方法主要用于表单项之间存在依赖,并且会渲染出不同组件时使用的:
...
const [form] = useForm();
<Form form={form}>
<Form.Item
name="gender"
label="Gender"
>
<Select
placeholder="Select a option and change input text above"
allowClear
>
<Option value="male">male</Option>
<Option value="female">female</Option>
<Option value="other">other</Option>
</Select>
</Form.Item>
<Form.Item
noStyle
dependencies={['gender']}
// 使用shouldUpdate也能实现 shouldUpdate={(prevValues, currentValues) => prevValues.gender !== currentValues.gender}
>
{
({ getFieldValue }) => (
<Form.Item
name="customizeGender"
label="Customize Gender"
rules={[
{
required: true,
},
]}
>
<Input disabled={getFieldValue('gender') === 'other'} />
</Form.Item>
)
}
</Form.Item>
</Form>
1.1.3. 自定义表单实现内容格式化
以上是一个带默认值的输入框,输入的内容前面自带前缀,且无法删除,借助自定义表单和自定义校验规则来实现:
export interface CustomeContractNameInputProps {
onChange: (value: string) => void;
value?: string;
}
const CustomeContractNameInput = (props: CustomeContractNameInputProps) => {
const { value, onChange, ...otherProps } = props;
const onValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value: inputValue } = e.target;
const reg = /^前缀内容-/;
if (inputValue === '') {
onChange('前缀内容-');
} else if (reg.test(inputValue)) {
onChange(inputValue);
}
}
React.useEffect(() => {
// 用于初次 form.setFieldsValue 的时候contractName的值不带前缀,在这里将内容格式化
if (value) {
onChange(value?.startsWith('前缀内容-') ? value : `前缀内容-${value ?? ''}`);
}
}, []);
return (
<Input
allowClear
{...otherProps}
value={value?.startsWith('前缀内容-') ? value : `前缀内容-${value ?? ''}`}
onChange={onValueChange}
/>
)
};
// 使用
<Form.Item
label="合同名称"
name="contractName"
rules={[
validator(_, value) { // 自定义校验规则
if (value === '前缀内容-' || !value) {
return Promise.reject(new Error('请输入合同名称'));
}
return Promise.resolve();
}
]}
>
<SupplementaryContractNameInput />
</Form.Item>
1.2. 验证相关
1.2.1. 清除字段验证状态
以上是一个字段联动的校验需求,当单选字段为是时,输入框必填,为否时,输入框非必填;如果不做任何处理,在校验状态下切换至否时,校验状态不会清除,我们需要切换至否的时候清除状态,并且不会清空已输入的内容,如果使用 resetFields() 不仅会清除状态,同时也会初始化表单项的内容,所以使用 setFields() 来重置表单的状态:
form.setFields([{ name: "需要重置的field字段", errors: undefined }]);
1.2.2. 验证防抖(debounce Promise)
实际开发过程中通常会有这样的需求:后台在服务器端校验名称是否重复,这个时候需要使用自定义校验功能进行远程校验,如下是一个远程校验的案例:
const handleValidate = (_, value) => {
if (!value) {
return Promise.reject(new Error("请输入名称"));
}
if (value === "123") {
// 模拟重复
return Promise.reject(new Error("名称重复"));
}
return Promise.resolve();
};
const MyForm = () => {
// ...
return (
<Form>
// ...
<Form.Item
label="Username"
name="username"
rules={[
{
validator: handleValidate,
},
]}
>
<Input />
</Form.Item>
</Form>
)
}
这种情况设置表单组件的校验时机为onBlur没有问题的,但是体验感并不好,如果在失去焦点后用户才知道名称重复了,需要再次获取焦点进行输入:
那么就需要在onChange的时机进行校验,这种情况下,如果不考虑防抖,会造成频繁的请求(注意右下角的控制台输出):
此时就需要加入防抖机制,但是因为自定义校验功能返回的为Promise,lodash的防抖功能在这种情况下是有问题的,会出现验证步频和debounce的result不对应的问题:
这个时候就需要借助另一个插件:p-debounce来实现promise的防抖:
实现一种比较完美的校验方案。
1.2.3. form.validateFields()会自动触发Form.Provider组件的onFormChange()以及Form组件的onFieldsChange()
这是因为 validateFields 时,表单项状态变了(值没变)。
1.3. Upload组件
1.3.1. Upload上传组件,如何阻止上传?
在 beforeUpload 中使用 return false 无法阻止文件继续上传,仍会触发请求。beforeUpload 同时还支持使用 Promise,通过返回一个 Promise 直接 reject 的方式,就不会再进行文件的上传了,也不会再触发 change 事件。如下:
beforeUpload: file =>
new Promise((resolve, reject) => {
if (file.size / 1048576 > 100) {
message.error('文件大小不能超过:100M');
reject(false);
return;
}
resolve(true);
}),
1.3.2. 被Form.Item包裹(控制)的Upload,fileList属性失效怎么办?
当用Form.Item对表单组件进行包装后,表单数据会被Form接管,这个时候再对上传列表进行扩展是比较困难的,想要实现一些自定义的功能,如列表拖拽,列表排序等等就比较麻烦,来看下官方对这两个功能的说明
被设置了 name 属性的 Form.Item 包装的控件,表单控件会自动添加 value(或 valuePropName 指定的其他属性) onChange(或 trigger 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果:
- 你不再需要也不应该用 onChange 来做数据收集同步(你可以使用 Form 的 onValuesChange),但还是可以继续监听 onChange 事件。
- 你不能用控件的 value 或 defaultValue 等属性来设置表单域的值,默认值可以用 Form 里的 initialValues 来设置。注意 initialValues 不能被 setState 动态更新,你需要用 setFieldsValue来更新。
- 你不应该用 setState,可以使用 form.setFieldsValue 来动态改变表单值。
可见,这两个属性是有冲突的,但是他们各有用处:
- 将
Upload组件用Fomr.Item进行包裹后,可以使用表单的方法控制上传组件的逻辑,如:form.getFieldsValue()获取表单项的值,form.setFiledsValue()设置表单项的值,以及antd表单自带的所有方法都可以,但是这个时候对上传组件设置fileList进行受控是无效的 ,也就无法扩展上传列表了 - 如果用
fileList来控制Upload组件,这时候无法使用表单的方法进行控制了
如果既想通过Form来控制上传组件,又想通过fileList来控制上传列表该怎么做呢,答案就是:自定义表单组件,通过自定义组件的方式将Upload进行二次封装,内部使用fileList进行控制,来扩展上传列表的功能,外层仍然用Form.Item进行包裹,用Form控制,就能实现需求了。
再来看下,自定义表单控件,需要遵循以下约定:
- 提供受控属性 value 或其它与 valuePropName 的值同名的属性。
- 提供 onChange 事件或 trigger 的值同名的事件。
根据约定,来实现自定表单控件
const CustomizedUpload: React.FC<UploadFileProps> = (props: UploadFileProps) => {
const { value, onChange, ...otherProps } = props;
const [files, setFiles] = useState([]);
const handleChange = (info) => {
let newFileList = [...info.fileList];
// 对上传数量进行限制,只能同时上传2个文件
newFileList = newFileList.slice(-2);
// 为上传的附件添加链接
newFileList = newFileList.map(file => {
if (file.response) {
file.url = file.response.url;
}
return file;
});
setFiles(fileList);
}
useEffect(() => {
if (files.length) {
// 再把 fileList 返回给 Form
onChange(fileList);
}
}, [files]);
useEffect(() => {
if (value?.length) {
// 内部使用 fileList 进行控制
setFiles(value);
}
}, [value]);
return (
<Upload
name="file"
action='上传路径'
headers={{
authorization: 'authorization-text',
}}
{...otherProps}
fileList={files}
onChange={(info) => {
handleChange(info);
}}
>
<Button icon={<UploadOutlined />}>上传</Button>
</Upload>
);
};
外层,直接使用Form.Item进行包裹即可:
<Form.Item
name="uploadFile"
label=上传附件
>
<UploadItem
{...otherFormProps}
/>
</Form.Item>
1.4. datePicker组件
1.4.1. RangePicker组件选定一个时间之后,限定可选时间范围
...
state = {
dates: [] // 时间范围
}
// 禁用的时间范围
disabledDate = current => {
const { dates } = this.state;
if (dates.length === 1) {
return moment(dates[0]).subtract(1, 'month') >= moment(current) || moment(dates[0]).add(1, 'month') <= moment(current);
}
}
// 日期切换
handleChangeDate = dates => {
this.setState({
dates
})
}
render() {
return (
<Form>
...
<Form.Item label="操作时间">
{getFieldDecorator('dates')(
<RangePicker
disabledDate={this.disabledDate}
// 这里使用的是待选日期发生变化的回调函数,在选中第一个时间时,确定disabledDate的范围
onCalendarChange={this.handleChangeDate}
/>
)}
</Form.Item>
...
</Form>
)
}
1.4.2. datePicker设置回显值
场景:表单编辑,后台返回的时间戳为毫秒,前端需要将其转换成 moment,才能在 datePicker 中使用
for (const key in formData) {
if (Object.hasOwnProperty.call(formData, key)) {
const ele = formData[key];
if (key.includes('Time')) {
formData[key] = moment(ele); // 需要先moment一下
}
}
}
form.setFieldsValue(formData);
1.4.3. datePicker常用disableDate
下列案例,moment() 传入某个日期表示指定日期,未传表示当前日期
- 某个日期前/后日期禁用
// 日期前禁用,不包括指定日期当天
disabledDate: current => current && moment().endOf('d') > current
// 日期前禁用,包括指定日期当天
disabledDate: current => current && moment().subtract(1, 'd').endOf('d') > current
// 日期后禁用,不包括指定日期当天
disabledDate: current => moment().endOf('d') < current
// 日期后禁用,包括指定日期当天
disabledDate: current => moment().subtract(1, 'd').endOf('d') < current
// 日期后多少天禁用,包括指定日期当天
disabledDate: current => current => moment().subtract(1, 'd').endOf('d') < moment(current).subtract(6, 'months').endOf('d')
1.4.4. datePicker 组件间取值赋值问题
一个 datePickerA 组件从另一个 datePickerB 组件中取值时需要注意,不能用另一个 datePickerB 的值直接执行 add() 方法或者其他改变 datePickerB 的值的方法,而是先用 moment() 方法对值进行一个复制,否则为 datePickerA 赋值后,datePickerB 的值也会改变:
const { contractStartTime } = form.getFieldsValue([ 'contractStartTime' ]);
...
// moment对值进行转化
form.setFieldsValue({ contractEndTime: moment(contractStartTime).add(1, 'd') });
1.4.5. datePicker设置默认空值的问题
需要设置 initialValue 为 null ,或 default 为 null
2. Table相关
2.1. 列设置单元格无效
给列添加className,通过样式控制
2.2. 表格多选
直接用下面的hooks即可
import { useState } from 'react';
/**
* 表格选中 hooks
* 适用于表格选中,传入行数据中唯一的 id 键即可,返回基础的rowSelection配置和选中的数据
* @param {string} rowUniqueId 指定行的唯一标识符,用于在选择行时确定哪些行被选中
* @return {Array} 返回一个数组,包含以下三个元素:
* rowSelection:table组件的选中项配置对象,可以在外部自行扩展其他属性和方法,这里只包含基础的全选和单选功能;
* selectedRows:被选中的行数据数组
* resetSelectedRows:重置已选中的行数据-用于初始选中项
*/
interface useRowSelectionParams {
rowUniqueId: string;
}
type UseRowSelection = (value: useRowSelectionParams) => [Record<any, any>, any[], () => void]
const useRowSelection: UseRowSelection = ({ rowUniqueId }) => {
const [selectedRows, setSelectedRows] = useState<any[]>([]);
// table选中项配置
const rowSelection = {
fixed: true,
selectedRowKeys: selectedRows.map(item => item[rowUniqueId]),
onSelect: (record, selectedStatus: boolean) => {
let selectedAttrs: any[];
if (selectedStatus) {
selectedAttrs = selectedRows.concat(record);
} else {
selectedAttrs = selectedRows.filter(item => item[rowUniqueId] !== record[rowUniqueId]);
}
setSelectedRows(selectedAttrs);
},
onSelectAll: (selectedStatus: boolean, curSelectedRows: any[], changeRows: any[]) => {
let selectedAttrs: any[];
if (selectedStatus) {
selectedAttrs = selectedRows.concat(changeRows);
} else {
const changeRowIds = changeRows.map(item => item[rowUniqueId]);
selectedAttrs = selectedRows.filter(item => !changeRowIds.includes(item[rowUniqueId]));
}
setSelectedRows(selectedAttrs);
},
};
// 重置已选中的行数据
const resetSelectedRows = () => {
setSelectedRows([]);
}
return [rowSelection, selectedRows, resetSelectedRows];
};
export default useRowSelection;
2.3. 固定列不生效
当Tabel组件的祖先元素为flex-item时(也就是flex项时),必须为table的其中一个祖先元素设置一个固定宽度,否则固定列无法生效。这是因为Table组件设置固定列情况下,table的样式为width: max-content;其意义为:
max-content尺寸关键字代表了内容的最大宽度或最大高度。对于文本内容而言,这意味着内容即便溢出也不会被换行。
也就是说表格会自动撑开祖先元素flex-item到最大宽度,导致固定列的样式失效。
2.4. 列宽不生效
2.4.1. 场景一:列包含隐藏列,列宽生效
在包含列样式为display: none的情况下,需要将该列或多列内容放在columns数组的最后,否则会导致设置列的width不生效。
3. 主题相关
3.1. 如何通过修改主题色统一修改 disabled 的表单颜色?
场景:客户反馈禁用的表单颜色过浅,无法看清字体,看了很多方案都是通过修改 antd 样式的方式来修改颜色,但是表单类型过多,修改起来比较麻烦,就考虑到使用修改主题色的方式,修改颜色变量,来统一进行修改。
解决方案:在 node_modules/antd 中找到对应的控件所使用的变量,如下,是 Input 组件使用的颜色:
.disabled() {
color: @input-disabled-color;
background-color: @input-disabled-bg;
border-color: @input-border-color;
box-shadow: none;
cursor: not-allowed;
opacity: 1;
&:hover {
.hover(@input-border-color);
}
}
// node_modules/antd/lib/style/theme.less 主题包下对应的变量
@input-disabled-color: @disabled-color;
@disabled-color: fade(#000, 25%); // 这里就找到了需要修改的主题色
之后在 umi 配置文件 /config.ts 中修改 theme 选项中的主题色即可:
...
theme: {
'@disabled-color': '#333'
}
...