介绍
- 基于官网进行二次封装而成
- 目前内置两种常用的编辑操作(输入框与下拉框)(默认双击表格后编辑为输入框模式)
- 虽然只支持两种内置功能,但是可以自己添加其他功能(需要的参数基本上都暴露出来了)
- 解决form表单重复key值bug
- 最新更新时间:
2023年2月17日
- 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: 'select',
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>,
options: [
{
label: '测试1',
value: 'xxx1'
},
{
label: '测试2',
value: 'xxx2'
},
]
}
},
{
title: '测试2',
dataIndex: 'test2',
width: 140,
align: 'center',
lhParams: {
type: 'select',
select: {
mode: "multiple",
},
options: [
{
label: '测试2',
value: 'xxx2'
},
]
}
},
{
title: '测试3',
dataIndex: 'test3',
width: 140,
align: 'center',
},
{
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'
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(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);
}
});
};
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) {
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 ||
[
]
}
>
{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]()
)
) :
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={() => {
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
}
);
};
const ItemTable = function (props: any) {
const {
table,
flag = false,
data: _data_,
columns: _columns_,
setDataSource,
setTableIdArr,
showPaging = true,
total,
pageChange,
key,
setSelectId,
rowSelectionList = []
} = props;
useEffect(() => {
console.log(table, 'table');
}, [props.table]);
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
};
}}
{...table}
/>
);
};
export default ItemTable;
新增加的文件引用
- ./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;
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];
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,
);
}
const AsyncValidator: any = RawAsyncValidator;
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];
});
}
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 });
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;
}
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 };
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) =>
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;
}
export function validateRules(
namePath: InternalNamePath,
value: StoreValue,
rules: RuleObject[],
options: ValidateOptions,
validateFirst: boolean,
messageVariables?: Record<string, string>,
) {
const name = namePath.join('.');
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;
const wrappedCallback: any = (...args: string[]) => {
Promise.resolve().then(() => {
warning(
!hasPromise,
'Your validator function has already return a promise. `callback` will be ignored.',
);
if (!hasPromise) {
callback(...args);
}
});
};
const promise = originValidatorFunc(rule, val, wrappedCallback);
hasPromise =
promise && typeof promise.then === 'function' && typeof promise.catch === 'function';
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);
});
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([]);
}
});
});
});
}
- ./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;
}
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;
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';
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 {
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;
submit: () => void;
}
export type InternalFormInstance = Omit<FormInstance, 'validateFields'> & {
validateFields: InternalValidateFields;
prefixName?: InternalNamePath;
getInternalHooks: (secret: string) => InternalHooks | null;
};
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;
};
}
- ./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}",
},
};
遇到的问题
- 界面切换重新渲染表格后,无限验证解决方案
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>
);
};
后续增加的插件
- 多选下拉框
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>
}
}
}
- 时间选择框 — 单个
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,
});
});
};
return <DatePicker onChange={change} {...config} />;
},
handleSave(r: any) {
return {
...r,
[_key]: moment(r[_key])?.format?.(config.type || 'YYYY-MM-DD HH:mm:ss') || undefined,
}
}
};
};
插件的使用
- 多选下拉框插件
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)
- 时间选择框 — 单个 插件使用
columns[0].lhParams = datePickerPlugin('time', { type: 'YYYY-MM-DD HH:mm' })