react-andDesign-table之可编辑表格集成封装组件

介绍

  1. 基于官网进行二次封装而成
  2. 目前内置两种常用的编辑操作(输入框与下拉框)(默认双击表格后编辑为输入框模式)
  3. 虽然只支持两种内置功能,但是可以自己添加其他功能(需要的参数基本上都暴露出来了)
  4. 解决form表单重复key值bug
  5. 最新更新时间:2023年2月17日
  6. 2023年2月17日-更新内容:
    (1)表格新增模式更换(之前为一行全部可编辑,目前为单个可编辑),更换完毕后界面会存在的卡顿效果消失。
    (2)表格支持多行新增需求。
    (3)时间插件不支持验证功能bug修复
    (4)新增三个文件:由于需要使用到 ant-design 的表单底层验证模块功能,所以我这边抽取出了 ant-design 表单底层 rc-field-form.js的底层验证源码。(./utils/index.ts, ./utils/interface.ts, ./utils/messages.ts)
    (5)…

使用介绍

// 表头更换成这种格式
const columns = [
	{
        title: '测试',
        dataIndex: 'test',
        width: 120,
        align: 'center',
        lhParams: {
          // 这个是规定双击后出现的是输入框(type为input)还是下拉框(type为select)
          type: 'select',
          // 这里是你对标签的属性添加,名字(select)则是type的名字,目前只支持两种格式,下拉框与输入框
          select: {
            defaultValue: 'xxx1',
            placeholder: '请输入设备IP',
            style: {
              width: '100px'
            }
          },
          // 编辑元素节点 自定义
          editRender: (t, record, save) => {
            return <Input
              onPressEnter={save}
              onBlur={save}
              placeholder={t}
            />
          },
          // 这里是你在表格回显的时候自己做的处理。默认返回值
          render: (t, record) => <div>{t}</div>,
          // 这块是select的配置项
          options: [
            {
              // 此处建议label和值一样
              label: '测试1',
              value: 'xxx1'
            },
            {
              label: '测试2',
              value: 'xxx2'
            },
          ]
        }
      },
      {
        title: '测试2',
        dataIndex: 'test2',
        width: 140,
        align: 'center',
        lhParams: {
          type: 'select',
          select: {
          	// 这里是antdesign select组件的属性描写处
            mode: "multiple",
          },
          options: [
            {
              label: '测试2',
              value: 'xxx2'
            },
          ]
        }
      },
      {
        title: '测试3',
        dataIndex: 'test3',
        width: 140,
        align: 'center',
        // 不写 lhParams 则会自动变成 输入框
      },
      {
        title: '测试4',
        dataIndex: 'test4',
        width: 140,
        align: 'center',
        // 这个属性是不走组件内部的编辑功能
        editable: false,
      },
]

table组件

import moment from 'moment';
import type { InputRef } from 'antd';
import type { FormInstance } from 'antd/es/form';
import { Select, Table, Form, Input } from 'antd';
import React, { useEffect, useState, useRef, useContext, useMemo } from 'react';
import styles from './index.less';
import _ from 'lodash';
import { useModel } from 'umi';
import { validateRules } from './utils'

/**
 *
 * @param index {number} 表格当前行索引
 * @param pageNum {number} 表格当前所在页
 * @param pageSize {number} 表格当前所在页查询多少条数据
 * @returns
 */
const tableIndex = (index: number, pageNum: number, pageSize: number) =>
  (pageNum - 1) * pageSize + index + 1;

const { Option } = Select;
const EditableContext = React.createContext<FormInstance<any> | null>(null);

interface EditableRowProps {
	index: number;
}
let errorValidataTimer: any = null;
const resetError = (form: FormInstance) => {
	form.getFieldsError().forEach(error => {
		if (error.errors.length > 1) form.setFields([{ name: error.name[0], errors: [error.errors[0]] }])
	});
}
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => {
	const [form] = Form.useForm();
	resetError(form);
	return (
		<Form form={form} component={false} preserve={false}>
			<EditableContext.Provider value={form}>
				<tr {...props} />
			</EditableContext.Provider>
		</Form>
	);
};

const columnsLhParams: any = {};
interface EditableCellProps {
	title: React.ReactNode;
	editable: boolean;
	children: React.ReactNode;
	dataIndex: any;
	record: any;
	lhParams: any;
	handleSave: (record: any) => void;
}
const EditableCell: React.FC<EditableCellProps> = ({ title, editable, children, dataIndex, record, lhParams, handleSave, ...restProps }) => {
	// const [editing, setEditing] = useState(record?._edit ? true : false);
	const [editing, setEditing] = useState(false);
	const inputRef = useRef<InputRef>(null);
	const form = useContext(EditableContext)!;
	columnsLhParams[dataIndex] = lhParams;
	const _handleSave = (r: any) => {
		if (lhParams.handleSave) {
			handleSave(lhParams.handleSave(r));
		} else {
			handleSave(r);
		}
	};
	const _setFieldsValue = () => {
		if (lhParams.toggleEdit) {
			lhParams.toggleEdit(() => { }, form, record);
		} else {
			form.setFieldsValue({ [dataIndex]: record[dataIndex] });
		}
	};
	useEffect(() => {
		setEditing(record?._edit);
		if (record?._edit) {
			const fn = () => {
				errorValidataTimer = true;
				if (errorValidataTimer) return;
				validateFieldsFn();
				setTimeout(() => {
					errorValidataTimer = false;
				}, 40);
			};
			fn();
			_setFieldsValue();
		}
	}, [record?._edit]);
	const validateFieldsAllFn = (validateFn: any) => {
		validateFn.catch((errors: any) => {
			console.log(errors, errors.errorFields, 'rrrr-ex')
			if (errors.errorFields.length > 0) {
				errors.errorFields.forEach((item: any) => {
					const key = item.name[0];
					record._validateFields = {
						...record?._validateFields,
						[key]: item.errors
					};
				});
				record._validateFields._loading && delete record._validateFields._loading;
				_handleSave(record);
			}
		});
	};
	// init
	const validateTableRowInfo = () => {
		first.current = false
		if (!lhParams.rules) return Promise.resolve();
		console.log(record[dataIndex], 'dataIndex')
		return new Promise((resolve, reject) => {
			validateRules([dataIndex], record[dataIndex] || '', lhParams?.rules || [], {}, false).catch((v) => reject({
				errorFields: [
					{
						errors: v,
						name: [dataIndex]
					}
				]
			}))
			form.setFieldValue(dataIndex, '')
		})
	}
	const first = useRef(true);
	useEffect(() => {
		if (editing) {
			// inputRef?.current?.focus();
			validateFieldsAllFn(form.validateFields([dataIndex]));
		} else {
			if (record?._key) {
				first.current && validateFieldsAllFn(validateTableRowInfo());
			}
		}
		if (record?._edit) {
			record._edit = false;
		}
	}, [editing]);

	const toggleEdit = () => {
		if (!editable) return;
		setEditing(!editing);
		form.setFieldsValue({ [dataIndex]: record[dataIndex] });
	};
	const validateFieldsFn = async () => {
		const errors = await form.getFieldsError();
		errors.forEach(error => {
			if (error.errors.length > 0) {
				form.setFields([{ name: error.name[0], errors: [error.errors[0]] }]);
				record._validateFields = {
					...record._validateFields,
					[error.name[0]]: [error.errors[0]]
				};
				_handleSave({
					...record
				});
			} else {
				error.name.forEach(name => record?._validateFields?.[name] && delete record._validateFields[name]);
				_handleSave(record);
			}
		});
	};
	const save = async (cb: Function | null = null) => {
		try {
			validateFieldsFn();
			if (_.isFunction(cb)) {
				cb(_handleSave, setEditing, form, {
					editable,
					lhParams
				});
			} else {
				let allValue = await form.getFieldsValue();
				if (record._validateFields && Object.keys(record._validateFields).length === 0) {
					form.setFieldsValue(allValue);
				}
				const values = { [dataIndex]: allValue[dataIndex] };
				if (lhParams?.input?.type === 'number') values[dataIndex] = +values[dataIndex];
				toggleEdit();
				Object.keys(allValue).forEach((key: any) => {
					if (columnsLhParams[key]?.handleSave) {
						allValue = {
							...allValue,
							...columnsLhParams[key]?.handleSave(allValue)
						};
					}
				});
				_handleSave({ ...record, ...allValue });
			}
		} catch (errInfo) {
			console.log('Save failed:', errInfo);
		}
	};
	let childNode = children;
	const FormItem = (children: any) => (
		<Form.Item
			style={{ margin: 0 }}
			name={dataIndex}
			rules={
				lhParams?.rules ||
				[
					// {
					//   required: true,
					//   message: `${title}为必填项`,
					// },
				]
			}
		>
			{children}
		</Form.Item>
	);
	const nodeComponent: any = {
		input: (_param: any) => FormItem(<Input ref={inputRef} onPressEnter={save} onBlur={save} key={`${record?.id || record?._key}2`} {...lhParams.input} {..._param} />),
		select: (_param: any) =>
			FormItem(
				<Select
					showSearch
					filterOption={(input, option) => (option!.children as unknown as string)?.toLowerCase?.()?.includes(input?.toLowerCase?.())}
					{...lhParams.select}
					{..._param}
					key={`${record?.id || record?._key}1`}
					onBlur={save}
					onDeselect={save}
				>
					{lhParams.options?.map((item: any) => (
						<Option key={item.value} value={item.value}>
							{item.label}
						</Option>
					))}
				</Select>
			)
	};
	if (editable && lhParams) {
		childNode =
			editing && dataIndex ? (
				lhParams.editRender ? (
					FormItem(lhParams.editRender(record[dataIndex], record, save, form, { lhParams }))
				) : (
					nodeComponent[lhParams.type]()
				)
			) : // @ts-ignore
				restProps.ellipsis ? (
					lhParams.render ? (
						lhParams.render(record[dataIndex], record)
					) : (
						children
					)
				) : (
					<div
						className="editable-cell-value-wrap flex-center flex-column"
						style={{ minHeight: '32px', wordBreak: 'break-all' }}
						onDoubleClick={() => (lhParams?.toggleEdit ? lhParams?.toggleEdit(setEditing, form, record) : toggleEdit())}
					>
						{lhParams.render ? lhParams.render(record[dataIndex], record) : children}
						<span style={{ color: 'red' }}>{record?._validateFields?.[dataIndex]?.[0]}</span>
					</div>
				);
	}

	return (
		<td
			{...restProps}
			onDoubleClick={() => {
				// @ts-ignore
				if (!restProps.ellipsis) return;
				lhParams?.toggleEdit ? lhParams?.toggleEdit(setEditing, form, record) : toggleEdit();
			}}
		>
			{childNode}
		</td>
	);
};
interface DataType {
	id?: React.Key;
	_key?: React.Key;
	name: string;
	age: string;
	address: string;
	_change?: boolean;
}

const editableFn = (col: any) => {
	return col.map((item: any) =>
		_.isBoolean(item.editable)
			? item
			: {
				lhParams: {
					type: 'input',
					input: {
						placeholder: '请输入' + item.title
					}
				},
				...item,
				editable: true
			}
	);
};
/**
 * @param table {} 里面内容为表格原生配置内容,建议分页在这个里面配置而不是使用 pageChange 【逻辑会走不通】。
 * @param flag 判断是否符合条件来提供显示表格筛选功能
 * @param data 表格数据内容
 * @param columns 表格头部信息
 * @param setDataSource {Function} 为修改表格数据的函数
 * @param showPaging 是否分页
 * @param total 分页总数 不建议使用
 * @param pageChange 分页的页码改变 不建议使用
 * @param rowSelectionList 当前行的选中值(值为双向绑定的,意味着这个值改变了,选中内容就会变)
 * @returns
 */
const ItemTable = function (props: any) {
	const {
		table,
		flag = false,
		data: _data_,
		columns: _columns_,
		setDataSource,
		setTableIdArr,
		showPaging = true,
		total,
		pageChange,
		key,
		setSelectId,
		// 选中key值初始化及更新
		rowSelectionList = []
	} = props;
	useEffect(() => {
		console.log(table, 'table');
	}, [props.table]);
	// 表头更变则置空选中id
	useEffect(() => {
		setSelectedRowKeys([]);
	}, [_columns_, table?.columns]);

	const [selectedRowKeys, setSelectedRowKeys] = useState(_.cloneDeep(rowSelectionList));
	useEffect(() => {
		setSelectedRowKeys(rowSelectionList || []);
		console.log(props.rowSelectionList, 'setRowSelectionListsetRowSelectionList');
	}, [props.rowSelectionList]);

	const [currentNum, setCurrentNum] = useState<number>(1);
	const onSelectChange = (selectedRowKeys: any, selectedRows: any) => {
		console.log(selectedRowKeys, 'selectedRowKeys');

		setSelectedRowKeys(selectedRowKeys);
		setTableIdArr({ idList: selectedRowKeys });
	};
	const rowSelection = {
		selectedRowKeys,
		onChange: onSelectChange
	};
	const onSelectRow = (record: any) => {
		const selectedList: any = [...selectedRowKeys];
		const idListObj: any = {};
		if (selectedList.indexOf(record.id) >= 0) {
			selectedList.splice(selectedList.indexOf(record.id), 1);
		} else if (selectedList.indexOf(record._key) >= 0) {
			selectedList.splice(selectedList.indexOf(record._key), 1);
		} else {
			selectedList.push(record.id || record._key);
		}

		idListObj.idList = selectedList;

		setSelectedRowKeys(selectedList);
		setTableIdArr(idListObj);
	};

	const handlePageChange = (page: any) => {
		setCurrentNum(page);
		pageChange && pageChange(page);
	};
	const data: any = [];
	for (let i = 0; i < 105; i++) {
		data.push({
			deviceName: 1 + i
		});
	}

	const _data = table?.dataSource || _data_ || [];
	const _columns = editableFn(table?.columns || _columns_ || []);
	const components = {
		body: {
			row: EditableRow,
			cell: EditableCell
		}
	};
	const handleSave = (row: DataType) => {
		const newData = [..._data];
		const index = newData.findIndex(item => {
			return row?._key ? row._key === item._key : row.id === item.id
		});
		const item = newData[index];
		// 给行内添加 此行发生变动标签
		row._change = true;
		newData.splice(index, 1, {
			...item,
			...row
		});
		setDataSource(newData);
	};
	const { stationShowFlag } = useModel('useStationInfo', ({ stationShowFlag }) => ({ stationShowFlag }))
	const columns = useMemo(() => _columns.map((col: any) => {
		if (col.title === '序号' && table?.pagination) {
			const { current, pageSize } = table.pagination;
			col.render = (t: any, r: any, i: number) => tableIndex(i, current, pageSize)
			return col
		};
		if (!col.editable) return col;
		return {
			...col,
			onCell: (record: DataType) => ({
				record,
				editable: col.editable,
				dataIndex: col.dataIndex,
				title: col.title,
				lhParams: col.lhParams,
				ellipsis: col.ellipsis,
				handleSave
			})
		};
	}).filter((v: any) => ['stationId', 'stationName'].includes(v.dataIndex) ? stationShowFlag : true), [_columns, table.pagination, stationShowFlag]);
	return (
		<Table
			className={styles['item-table']}
			style={{ height: 'calc(100%)', maxHeight: 'calc(100%)' }}
			components={components}
			columns={columns}
			dataSource={_data}
			pagination={
				showPaging && {
					defaultPageSize: 10,
					current: currentNum,
					onChange: page => handlePageChange(page),
					total
				}
			}
			bordered={true}
			rowKey={row => {
				return row.id || row._key;
			}}
			rowSelection={flag ? rowSelection : null}
			onHeaderRow={(columns, index) => {
				return {
					onChange: (e: any) => {
						if (index === 0) {
							const rowKeys = _data.map((v: any) => v.id || v._key);
							const creatFn = (idList: any) => ({
								idList
							});
							if (e.target?.checked) {
								setSelectedRowKeys(rowKeys);
								setTableIdArr(creatFn(rowKeys));
							} else {
								setSelectedRowKeys(selectedRowKeys.filter((v: any) => !rowKeys.includes(v)));
								setTableIdArr(creatFn(selectedRowKeys.filter((v: any) => !rowKeys.includes(v))));
							}
						}
					}
				};
			}}
			onRow={(record, i) => {
				return {
					onClick: flag
						? e => {
							e.ctrlKey && onSelectRow(record);
						}
						: null
					// onChange: (e) => {
					//   onSelectRow(record);
					// }
				};
			}}
			{...table}
		/>
	);
};
export default ItemTable;

新增加的文件引用

  1. ./utils
import RawAsyncValidator from 'async-validator';
import * as React from 'react';
import {
  InternalNamePath,
  ValidateOptions,
  ValidateMessages,
  RuleObject,
  StoreValue,
} from './interface';
import { defaultValidateMessages } from './messages';
import _ from 'lodash';
const isObject = _.isObject;
const warning = console.warn;
/**
 * Copy values into store and return a new values object
 * ({ a: 1, b: { c: 2 } }, { a: 4, b: { d: 5 } }) => { a: 4, b: { c: 2, d: 5 } }
 */
function internalSetValues<T>(store: T, values: any): T {
  const newStore: any = (Array.isArray(store) ? [...store] : { ...store }) as T;

  if (!values) {
    return newStore;
  }

  Object.keys(values).forEach(key => {
    const prevValue = newStore[key];
    const value = values[key];

    // If both are object (but target is not array), we use recursion to set deep value
    const recursive = isObject(prevValue) && isObject(value);
    newStore[key] = recursive ? internalSetValues(prevValue, value || {}) : value;
  });

  return newStore;
}

function setValues<T>(store: T, ...restValues: T[]): T {
  return restValues.reduce(
    (current: T, newStore: T): T => internalSetValues<T>(current, newStore),
    store,
  );
}

// Remove incorrect original ts define
const AsyncValidator: any = RawAsyncValidator;

/**
 * Replace with template.
 *   `I'm ${name}` + { name: 'bamboo' } = I'm bamboo
 */
function replaceMessage(template: string, kv: Record<string, string>): string {
  return template.replace(/\$\{\w+\}/g, (str: string) => {
    const key = str.slice(2, -1);
    return kv[key];
  });
}

/**
 * We use `async-validator` to validate rules. So have to hot replace the message with validator.
 * { required: '${name} is required' } => { required: () => 'field is required' }
 */
function convertMessages(
  messages: ValidateMessages,
  name: string,
  rule: RuleObject,
  messageVariables?: Record<string, string>,
): ValidateMessages {
  const kv = {
    ...(rule as Record<string, string | number>),
    name,
    enum: (rule.enum || []).join(', '),
  };

  const replaceFunc = (template: string, additionalKV?: Record<string, string>) => () =>
    replaceMessage(template, { ...kv, ...additionalKV });

  /* eslint-disable no-param-reassign */
  function fillTemplate(source: any, target: any = {}) {
    Object.keys(source).forEach(ruleName => {
      const value = source[ruleName];
      if (typeof value === 'string') {
        target[ruleName] = replaceFunc(value, messageVariables);
      } else if (value && typeof value === 'object') {
        target[ruleName] = {};
        fillTemplate(value, target[ruleName]);
      } else {
        target[ruleName] = value;
      }
    });

    return target;
  }
  /* eslint-enable */

  return fillTemplate(setValues({}, defaultValidateMessages, messages)) as ValidateMessages;
}

async function validateRule(
  name: string,
  value: StoreValue,
  rule: RuleObject,
  options: any,
  messageVariables?: Record<string, string>,
): Promise<string[]> {
  const cloneRule = { ...rule };
  // We should special handle array validate
  let subRuleField: any = null;
  if (cloneRule && cloneRule.type === 'array' && cloneRule.defaultField) {
    subRuleField = cloneRule.defaultField;
    delete cloneRule.defaultField;
  }

  const validator = new AsyncValidator({
    [name]: [cloneRule],
  });

  const messages: ValidateMessages = convertMessages(
    options.validateMessages,
    name,
    cloneRule,
    messageVariables,
  );
  validator.messages(messages);

  let result = [];

  try {
    await Promise.resolve(validator.validate({ [name]: value }, { ...options }));
  } catch (errObj: any) {
    if (errObj.errors) {
      result = errObj.errors.map(({ message }: any, index: any) =>
        // Wrap ReactNode with `key`
        React.isValidElement(message)
          ? React.cloneElement(message, { key: `error_${index}` })
          : message,
      );
    } else {
      console.error(errObj);
      result = [(messages.default as () => string)()];
    }
  }

  if (!result.length && subRuleField) {
    const subResults: string[][] = await Promise.all(
      (value as StoreValue[]).map((subValue: StoreValue, i: number) =>
        validateRule(`${name}.${i}`, subValue, subRuleField, options, messageVariables),
      ),
    );

    return subResults.reduce((prev, errors) => [...prev, ...errors], []);
  }

  return result;
}

/**
 * We use `async-validator` to validate the value.
 * But only check one value in a time to avoid namePath validate issue.
 */
export function validateRules(
  namePath: InternalNamePath,
  value: StoreValue,
  rules: RuleObject[],
  options: ValidateOptions,
  validateFirst: boolean,
  messageVariables?: Record<string, string>,
) {
  const name = namePath.join('.');

  // Fill rule with context
  const filledRules: RuleObject[] = rules.map(currentRule => {
    const originValidatorFunc = currentRule.validator;

    if (!originValidatorFunc) {
      return currentRule;
    }
    return {
      ...currentRule,
      validator(rule: RuleObject, val: StoreValue, callback: (error?: string) => void) {
        let hasPromise: any = false;

        // Wrap callback only accept when promise not provided
        const wrappedCallback: any = (...args: string[]) => {
          // Wait a tick to make sure return type is a promise
          Promise.resolve().then(() => {
            warning(
              !hasPromise,
              'Your validator function has already return a promise. `callback` will be ignored.',
            );

            if (!hasPromise) {
              callback(...args);
            }
          });
        };

        // Get promise
        const promise = originValidatorFunc(rule, val, wrappedCallback);
        hasPromise =
          promise && typeof promise.then === 'function' && typeof promise.catch === 'function';

        /**
         * 1. Use promise as the first priority.
         * 2. If promise not exist, use callback with warning instead
         */
        warning(hasPromise, '`callback` is deprecated. Please return a promise instead.');

        if (hasPromise) {
          (promise as Promise<void>)
            .then(() => {
              callback();
            })
            .catch(err => {
              callback(err);
            });
        }
      },
    };
  });

  const rulePromises = filledRules.map(rule =>
    validateRule(name, value, rule, options, messageVariables),
  );

  const summaryPromise: Promise<string[]> = (validateFirst
    ? finishOnFirstFailed(rulePromises)
    : finishOnAllFailed(rulePromises)
  ).then((errors: string[]): string[] | Promise<string[]> => {
    if (!errors.length) {
      return [];
    }

    return Promise.reject<string[]>(errors);
  });

  // Internal catch error to avoid console error log.
  summaryPromise.catch(e => e);

  return summaryPromise;
}

async function finishOnAllFailed(rulePromises: Promise<string[]>[]): Promise<string[]> {
  return Promise.all(rulePromises).then((errorsList: any): string[] | Promise<string[]> => {
    const errors: string[] = [].concat(...errorsList);

    return errors;
  });
}

async function finishOnFirstFailed(rulePromises: Promise<string[]>[]): Promise<string[]> {
  let count = 0;

  return new Promise(resolve => {
    rulePromises.forEach(promise => {
      promise.then(errors => {
        if (errors.length) {
          resolve(errors);
        }

        count += 1;
        if (count === rulePromises.length) {
          resolve([]);
        }
      });
    });
  });
}

  1. ./utils/interface.ts
import { ReactElement } from 'react';

interface UpdateAction {
  type: 'updateValue';
  namePath: InternalNamePath;
  value: StoreValue;
}

interface ValidateAction {
  type: 'validateField';
  namePath: InternalNamePath;
  triggerName: string;
}

export type ReducerAction = UpdateAction | ValidateAction;

export type InternalNamePath = (string | number)[];
export type NamePath = string | number | InternalNamePath;

export type StoreValue = any;
export interface Store {
  [name: string]: StoreValue;
}

export interface Meta {
  touched: boolean;
  validating: boolean;
  errors: string[];
  name: InternalNamePath;
}

/**
 * Used by `setFields` config
 */
export interface FieldData extends Partial<Omit<Meta, 'name'>> {
  name: NamePath;
  value?: StoreValue;
}

export type RuleType =
  | 'string'
  | 'number'
  | 'boolean'
  | 'method'
  | 'regexp'
  | 'integer'
  | 'float'
  | 'object'
  | 'enum'
  | 'date'
  | 'url'
  | 'hex'
  | 'email';

type Validator = (
  rule: RuleObject,
  value: StoreValue,
  callback: (error?: string) => void,
) => Promise<void> | void;

export type RuleRender = (form: FormInstance) => RuleObject;

interface BaseRule {
  enum?: StoreValue[];
  len?: number;
  max?: number;
  message?: string | ReactElement;
  min?: number;
  pattern?: RegExp;
  required?: boolean;
  transform?: (value: StoreValue) => StoreValue;
  type?: RuleType;
  validator?: Validator;
  whitespace?: boolean;

  /** Customize rule level `validateTrigger`. Must be subset of Field `validateTrigger` */
  validateTrigger?: string | string[];
}

interface ArrayRule extends Omit<BaseRule, 'type'> {
  type: 'array';
  defaultField?: RuleObject;
}

export type RuleObject = BaseRule | ArrayRule;

export type Rule = RuleObject | RuleRender;

export interface ValidateErrorEntity {
  values: Store;
  errorFields: { name: InternalNamePath; errors: string[] }[];
  outOfDate: boolean;
}

export interface FieldEntity {
  onStoreChange: (store: Store, namePathList: InternalNamePath[] | null, info: NotifyInfo) => void;
  isFieldTouched: () => boolean;
  isFieldValidating: () => boolean;
  validateRules: (options?: ValidateOptions) => Promise<string[]>;
  getMeta: () => Meta;
  getNamePath: () => InternalNamePath;
  getErrors: () => string[];
  props: {
    name?: NamePath;
    rules?: Rule[];
    dependencies?: NamePath[];
  };
}

export interface FieldError {
  name: InternalNamePath;
  errors: string[];
}

export interface ValidateOptions {
  triggerName?: string;
  validateMessages?: ValidateMessages;
}

export type InternalValidateFields = (
  nameList?: NamePath[],
  options?: ValidateOptions,
) => Promise<Store>;
export type ValidateFields = (nameList?: NamePath[]) => Promise<Store>;

interface ValueUpdateInfo {
  type: 'valueUpdate';
  source: 'internal' | 'external';
}

export type NotifyInfo =
  | ValueUpdateInfo
  | {
    type: 'validateFinish' | 'reset';
  }
  | {
    type: 'setField';
    data: FieldData;
  }
  | {
    type: 'dependenciesUpdate';
    /**
     * Contains all the related `InternalNamePath[]`.
     * a <- b <- c : change `a`
     * relatedFields=[a, b, c]
     */
    relatedFields: InternalNamePath[];
  };

export interface Callbacks {
  onValuesChange?: (changedValues: Store, values: Store) => void;
  onFieldsChange?: (changedFields: FieldData[], allFields: FieldData[]) => void;
  onFinish?: (values: Store) => void;
  onFinishFailed?: (errorInfo: ValidateErrorEntity) => void;
}

export interface InternalHooks {
  dispatch: (action: ReducerAction) => void;
  registerField: (entity: FieldEntity) => () => void;
  useSubscribe: (subscribable: boolean) => void;
  setInitialValues: (values: Store, init: boolean) => void;
  setCallbacks: (callbacks: Callbacks) => void;
  getFields: (namePathList?: InternalNamePath[]) => FieldData[];
  setValidateMessages: (validateMessages: ValidateMessages) => void;
}

export interface FormInstance {
  // Origin Form API
  getFieldValue: (name: NamePath) => StoreValue;
  getFieldsValue: (nameList?: NamePath[] | true, filterFunc?: (meta: Meta) => boolean) => Store;
  getFieldError: (name: NamePath) => string[];
  getFieldsError: (nameList?: NamePath[]) => FieldError[];
  isFieldsTouched(nameList?: NamePath[], allFieldsTouched?: boolean): boolean;
  isFieldsTouched(allFieldsTouched?: boolean): boolean;
  isFieldTouched: (name: NamePath) => boolean;
  isFieldValidating: (name: NamePath) => boolean;
  isFieldsValidating: (nameList: NamePath[]) => boolean;
  resetFields: (fields?: NamePath[]) => void;
  setFields: (fields: FieldData[]) => void;
  setFieldsValue: (value: Store) => void;
  validateFields: ValidateFields;

  // New API
  submit: () => void;
}

export type InternalFormInstance = Omit<FormInstance, 'validateFields'> & {
  validateFields: InternalValidateFields;

  /**
   * Passed by field context props
   */
  prefixName?: InternalNamePath;

  /**
   * Form component should register some content into store.
   * We pass the `HOOK_MARK` as key to avoid user call the function.
   */
  getInternalHooks: (secret: string) => InternalHooks | null;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type EventArgs = any[];

type ValidateMessage = string | (() => string);
export interface ValidateMessages {
  default?: ValidateMessage;
  required?: ValidateMessage;
  enum?: ValidateMessage;
  whitespace?: ValidateMessage;
  date?: {
    format?: ValidateMessage;
    parse?: ValidateMessage;
    invalid?: ValidateMessage;
  };
  types?: {
    string?: ValidateMessage;
    method?: ValidateMessage;
    array?: ValidateMessage;
    object?: ValidateMessage;
    number?: ValidateMessage;
    date?: ValidateMessage;
    boolean?: ValidateMessage;
    integer?: ValidateMessage;
    float?: ValidateMessage;
    regexp?: ValidateMessage;
    email?: ValidateMessage;
    url?: ValidateMessage;
    hex?: ValidateMessage;
  };
  string?: {
    len?: ValidateMessage;
    min?: ValidateMessage;
    max?: ValidateMessage;
    range?: ValidateMessage;
  };
  number?: {
    len?: ValidateMessage;
    min?: ValidateMessage;
    max?: ValidateMessage;
    range?: ValidateMessage;
  };
  array?: {
    len?: ValidateMessage;
    min?: ValidateMessage;
    max?: ValidateMessage;
    range?: ValidateMessage;
  };
  pattern?: {
    mismatch?: ValidateMessage;
  };
}
  1. ./utils/messages.ts
const typeTemplate = "'${name}' is not a valid ${type}";

export const defaultValidateMessages = {
  default: "Validation error on field '${name}'",
  required: "'${name}'是必填项",
  enum: "'${name}' must be one of [${enum}]",
  whitespace: "'${name}' cannot be empty",
  date: {
    format: "'${name}' is invalid for format date",
    parse: "'${name}' could not be parsed as date",
    invalid: "'${name}' is invalid date",
  },
  types: {
    string: typeTemplate,
    method: typeTemplate,
    array: typeTemplate,
    object: typeTemplate,
    number: typeTemplate,
    date: typeTemplate,
    boolean: typeTemplate,
    integer: typeTemplate,
    float: typeTemplate,
    regexp: typeTemplate,
    email: typeTemplate,
    url: typeTemplate,
    hex: typeTemplate,
  },
  string: {
    len: "'${name}' must be exactly ${len} characters",
    min: "'${name}' must be at least ${min} characters",
    max: "'${name}' cannot be longer than ${max} characters",
    range: "'${name}' must be between ${min} and ${max} characters",
  },
  number: {
    len: "'${name}' must equal ${len}",
    min: "'${name}' cannot be less than ${min}",
    max: "'${name}' cannot be greater than ${max}",
    range: "'${name}' must be between ${min} and ${max}",
  },
  array: {
    len: "'${name}' must be exactly ${len} in length",
    min: "'${name}' cannot be less than ${min} in length",
    max: "'${name}' cannot be greater than ${max} in length",
    range: "'${name}' must be between ${min} and ${max} in length",
  },
  pattern: {
    mismatch: "'${name}' does not match pattern ${pattern}",
  },
};

遇到的问题

  1. 界面切换重新渲染表格后,无限验证解决方案
// 找到渲染表格当前 行 的地方!
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => {
  const [form] = Form.useForm();
  // 主要解决方案就是这一步
  form.getFieldsError().forEach((error) => {
    if (error.errors.length > 1)
      form.setFields([{ name: error.name[0], errors: [error.errors[0]] }]);
  });
  // -------------------
  return (
    <Form form={form} component={false} preserve={false}>
      <EditableContext.Provider value={form}>
        <tr {...props} />
      </EditableContext.Provider>
    </Form>
  );
};

后续增加的插件

  1. 多选下拉框
import { Select } from 'antd'
const { Option } = Select;
export const selectMultiplePlugin = (keyDataIndex: string, needShowList: Array<any>, config: any) => {
  const { splitSymbol } = config || {
    splitSymbol: ','
  }
  return {
    toggleEdit: (setEditing: any, form: any, record: any) => {
      setEditing(true)
      form.setFieldsValue({ [keyDataIndex]: record[keyDataIndex]?.split?.(splitSymbol)?.map((v: string) => +v) });
    },
    render: (t: any, record: any) => {
      const idArr = record?.[keyDataIndex]?.split?.(splitSymbol) || [];
      return idArr.map((id: string) => {
        return needShowList.find((v: any) => v.value == id)?.label || ''
      })?.join(splitSymbol)
    },
    editRender: (t: any, r: any, save: any) => {
      const defaultText = t?.split?.(splitSymbol) || []
      return <Select
        mode="multiple"
        defaultValue={defaultText}
        onBlur={() => save(async (handleSave: any, setEditing: any, form: any) => {
          const values = await form.validateFields();
          values[keyDataIndex] = values[keyDataIndex]?.join?.(splitSymbol) || null
          handleSave({ ...r, ...values })
          form.setFieldsValue({ [keyDataIndex]: r[keyDataIndex] });
          setEditing(false);
        })}
      >
        {
          needShowList?.map((item: any) => <Option
            key={item.value}
            value={item.value}
          >
            {item.label}
          </Option>)
        }
      </Select>
    }
  }
}
  1. 时间选择框 — 单个
import { DatePicker } from 'antd';
import moment from 'moment';

// 时间选择框 --- 单个
export const datePickerPlugin = (
  _key: string,
  config: any = {
    type: 'YYYY-MM-DD HH:mm:ss',
  },
) => {
  return {
    toggleEdit: (setEditing: any, form: any, record: any) => {
      setEditing(true);
      form.setFieldsValue({ [_key]: moment(record[_key] || new Date()) });
    },
    render: (t: any, record: any) => {
      return t;
    },
    editRender: (t: any, r: any, save: any) => {
      const change = () => {
        save(async (handleSave: any, setEditing: any, form: any) => {
          const allValue = await form.getFieldsValue();
          setEditing(false);
          form.setFieldsValue({ [_key]: allValue[_key] });
          handleSave({
            ...r,
            [_key]: allValue[_key]?.format?.(config.type || 'YYYY-MM-DD HH:mm:ss') || undefined,
          });
        });
      };
      // @ts-ignore
      return <DatePicker onChange={change} {...config} />;
    },
    handleSave(r: any) {
      // console.log('提交表格信息:', _key, r)
      return {
        ...r,
        [_key]: moment(r[_key])?.format?.(config.type || 'YYYY-MM-DD HH:mm:ss') || undefined,
      }
    }
  };
};

插件的使用

  1. 多选下拉框插件
// ------
const [columns, setColumns] = useState([
	{ title: '名称', dataIndex: 'name', align: 'center' }
])
const [data, setData] = useState([
	{ name: '1,2' }
])
const nameList = [
	{label: 'zs', value: '1'},
	{label: 'ls', value: '2'},
]
columns[0].lhParams = selectMultiplePlugin('name', nameList, void 0)
setColumns(columns)
// ------
  1. 时间选择框 — 单个 插件使用
// ----------------
// columns[0].lhParams = datePickerPlugin('time')
columns[0].lhParams = datePickerPlugin('time', { type: 'YYYY-MM-DD HH:mm' })
// ----------------

你可能感兴趣的:(react,react.js,javascript,前端)