@zat-design/sisyphus-react 4.4.3 → 4.5.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/ProEditTable/components/RenderField/index.js +46 -4
- package/es/ProEditTable/components/Validator/index.d.ts +9 -0
- package/es/ProEditTable/components/Validator/index.js +56 -6
- package/es/ProEditTable/index.js +16 -2
- package/es/ProEditTable/utils/config.d.ts +1 -1
- package/es/ProEditTable/utils/config.js +18 -7
- package/es/ProEditTable/utils/tools.d.ts +30 -1
- package/es/ProEditTable/utils/tools.js +95 -12
- package/es/ProEditTable/utils/validateAll.d.ts +86 -0
- package/es/ProEditTable/utils/validateAll.js +319 -0
- package/es/ProForm/components/combination/Group/utils/index.d.ts +21 -21
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@ import classNames from 'classnames';
|
|
|
12
12
|
import { compatStartTransition } from "../../../utils";
|
|
13
13
|
import valueTypeMap from "../../../ProForm/utils/valueType";
|
|
14
14
|
import transformMap from "../../utils/transform";
|
|
15
|
-
import { getNamePath, difference, getDisabled } from "../../utils/tools";
|
|
15
|
+
import { getNamePath, difference, getDisabled, resolveChangedFields, syncGroupCombinedValues } from "../../utils/tools";
|
|
16
16
|
import * as componentMap from "../../../ProForm/components";
|
|
17
17
|
import { useProConfig } from "../../../ProConfigProvider";
|
|
18
18
|
import Container from "../../../ProForm/components/Container";
|
|
@@ -74,7 +74,10 @@ const RenderField = ({
|
|
|
74
74
|
otherProps,
|
|
75
75
|
diffConfig,
|
|
76
76
|
getIsNew,
|
|
77
|
-
shouldUpdateDebounce
|
|
77
|
+
shouldUpdateDebounce,
|
|
78
|
+
virtual,
|
|
79
|
+
errorStore,
|
|
80
|
+
columns
|
|
78
81
|
} = config;
|
|
79
82
|
let lastFieldProps = fieldProps || {};
|
|
80
83
|
let lastRules = rules || [];
|
|
@@ -400,6 +403,10 @@ const RenderField = ({
|
|
|
400
403
|
// 使用 useCallback 创建稳定的 onChange 函数
|
|
401
404
|
const handleChange = useCallback(async (...args) => {
|
|
402
405
|
const executeChange = async (...innerArgs) => {
|
|
406
|
+
// 虚拟表格:用户编辑本单元格 → 清掉旧的常驻报错,提交时再以最新值统一重校
|
|
407
|
+
if (virtual && errorStore && record?.rowKey != null) {
|
|
408
|
+
errorStore.clearCell(record.rowKey, String(column?.dataIndex));
|
|
409
|
+
}
|
|
403
410
|
let callArgs = [...innerArgs];
|
|
404
411
|
const rowPath = [...namePath, index];
|
|
405
412
|
if (!onFieldChange && !onChange) {
|
|
@@ -425,7 +432,16 @@ const RenderField = ({
|
|
|
425
432
|
if (validateTrigger && validateTrigger.includes('onChange')) {
|
|
426
433
|
if (!_isEqual(orgRow, rowAfter)) {
|
|
427
434
|
const diff = difference(rowAfter, orgRow) || {};
|
|
428
|
-
const
|
|
435
|
+
const changedKeys = Object.keys(diff);
|
|
436
|
+
// 联动回填只写了子键,names 组合列的合并字段值未同步 → 先按子键组合写回,确保校验读到最新值
|
|
437
|
+
syncGroupCombinedValues(form, rowPath, changedKeys, columns);
|
|
438
|
+
// 把变更子键映射回所属列字段名(names 组合列 → 合并名),否则 group 列重校验路径对不上、旧报错不清
|
|
439
|
+
const changedFields = resolveChangedFields(changedKeys, columns);
|
|
440
|
+
// 虚拟表格:联动改写到的列同步清掉常驻报错,避免滚动重挂载后回填旧错
|
|
441
|
+
if (virtual && errorStore && record?.rowKey != null) {
|
|
442
|
+
changedFields.forEach(id => errorStore.clearCell(record.rowKey, id));
|
|
443
|
+
}
|
|
444
|
+
const validateFieldKeys = changedFields.map(id => [...rowPath, id]).concat(dependencies || []);
|
|
429
445
|
if (validateFieldKeys?.length) {
|
|
430
446
|
debounceValidate(validateFieldKeys);
|
|
431
447
|
}
|
|
@@ -450,7 +466,15 @@ const RenderField = ({
|
|
|
450
466
|
if (validateTrigger && validateTrigger.includes('onChange')) {
|
|
451
467
|
if (!_isEqual(orgRow, rowAfter)) {
|
|
452
468
|
const diff = difference(rowAfter, orgRow) || {};
|
|
453
|
-
const
|
|
469
|
+
const changedKeys = Object.keys(diff);
|
|
470
|
+
// 联动回填只写了子键,names 组合列的合并字段值未同步 → 先按子键组合写回,确保校验读到最新值
|
|
471
|
+
syncGroupCombinedValues(form, rowPath, changedKeys, columns);
|
|
472
|
+
// 把变更子键映射回所属列字段名(names 组合列 → 合并名),否则 group 列重校验路径对不上、旧报错不清
|
|
473
|
+
const changedFields = resolveChangedFields(changedKeys, columns);
|
|
474
|
+
if (virtual && errorStore && record?.rowKey != null) {
|
|
475
|
+
changedFields.forEach(id => errorStore.clearCell(record.rowKey, id));
|
|
476
|
+
}
|
|
477
|
+
const validateFieldKeys = changedFields.map(id => [...rowPath, id]).concat(dependencies || []);
|
|
454
478
|
if (validateFieldKeys?.length) {
|
|
455
479
|
debounceValidate(validateFieldKeys);
|
|
456
480
|
}
|
|
@@ -483,6 +507,24 @@ const RenderField = ({
|
|
|
483
507
|
};
|
|
484
508
|
}, []);
|
|
485
509
|
|
|
510
|
+
// 虚拟表格:单元格(重新)挂载时,回填 errorStore 中已存储的真实报错,实现报错常驻。
|
|
511
|
+
// 虚拟滚动会卸载视口外行的 Field 实体导致 antd 报错丢失,重新挂载后由此恢复红字。
|
|
512
|
+
useEffect(() => {
|
|
513
|
+
if (!virtual || !errorStore || record?.rowKey == null) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const cellKey = String(column?.dataIndex);
|
|
517
|
+
const errs = errorStore.get(record.rowKey, cellKey);
|
|
518
|
+
if (errs?.length) {
|
|
519
|
+
const fieldName = formNamePath ? cellName.slice(formNamePath?.length - 1) : cellName;
|
|
520
|
+
form.setFields([{
|
|
521
|
+
name: fieldName,
|
|
522
|
+
errors: errs
|
|
523
|
+
}]);
|
|
524
|
+
}
|
|
525
|
+
// 仅在挂载或行/列标识变化时回填
|
|
526
|
+
}, [virtual, record?.rowKey, column?.dataIndex]);
|
|
527
|
+
|
|
486
528
|
// 使用useCallback优化handleBlur函数
|
|
487
529
|
const handleBlur = useCallback(async (...args) => {
|
|
488
530
|
if (!onBlur) {
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import type { NamePath } from 'antd/es/form/interface';
|
|
3
|
+
import { type ErrorStore } from '../../utils/validateAll';
|
|
3
4
|
interface ValidatorProps {
|
|
4
5
|
name?: NamePath;
|
|
5
6
|
virtualKey?: string;
|
|
6
7
|
editingKeys?: string[];
|
|
7
8
|
onlyOneLineMsg?: string;
|
|
9
|
+
/** antd6 虚拟表格开关:开启时注册聚合校验字段,覆盖视口外未挂载的行 */
|
|
10
|
+
virtual?: boolean;
|
|
11
|
+
/** 原始列配置(含 name/names/rules/required/label 等),用于全量真实规则校验 */
|
|
12
|
+
columns?: any[];
|
|
13
|
+
/** 表格内部 config(提供 form/name/disabled/isView/otherProps 等) */
|
|
14
|
+
config?: any;
|
|
15
|
+
/** 外部错误存储,承载「报错常驻」的真实文案来源 */
|
|
16
|
+
errorStore?: ErrorStore;
|
|
8
17
|
}
|
|
9
18
|
declare const _default: import("react").NamedExoticComponent<ValidatorProps>;
|
|
10
19
|
export default _default;
|
|
@@ -1,14 +1,56 @@
|
|
|
1
|
-
import { memo } from 'react';
|
|
1
|
+
import { memo, useCallback } from 'react';
|
|
2
2
|
import { Form, message } from 'antd';
|
|
3
|
-
import {
|
|
3
|
+
import { runUnifiedValidation } from "../../utils/validateAll";
|
|
4
|
+
import { scrollVirtualToErrorColumn } from "../../utils/tools";
|
|
5
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
4
6
|
const Validator = ({
|
|
5
7
|
name,
|
|
6
8
|
virtualKey,
|
|
7
9
|
editingKeys,
|
|
8
|
-
onlyOneLineMsg
|
|
10
|
+
onlyOneLineMsg,
|
|
11
|
+
virtual,
|
|
12
|
+
columns,
|
|
13
|
+
config,
|
|
14
|
+
errorStore
|
|
9
15
|
}) => {
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
// 聚合校验:在 form.validateFields()(提交)时对全量数据跑真实规则,
|
|
17
|
+
// 既拦截视口外非法行(统一校验),又把真实报错写入 errorStore 供滚动到该行时回填(报错常驻)。
|
|
18
|
+
const aggregateValidator = useCallback(async () => {
|
|
19
|
+
if (!errorStore || !config?.form) {
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
}
|
|
22
|
+
const {
|
|
23
|
+
hasError,
|
|
24
|
+
firstErrorIndex,
|
|
25
|
+
firstErrorRowKey
|
|
26
|
+
} = await runUnifiedValidation({
|
|
27
|
+
form: config.form,
|
|
28
|
+
name: config.name,
|
|
29
|
+
columns: columns || [],
|
|
30
|
+
config,
|
|
31
|
+
errorStore
|
|
32
|
+
});
|
|
33
|
+
if (hasError) {
|
|
34
|
+
// 滚动首个错误行进入视口 → 其单元格挂载后由 RenderField 回填红字
|
|
35
|
+
if (firstErrorIndex != null) {
|
|
36
|
+
requestAnimationFrame(() => {
|
|
37
|
+
// 纵向:scrollTo({ index }) 仅定位行
|
|
38
|
+
config?.tableRef?.current?.scrollTo?.({
|
|
39
|
+
index: firstErrorIndex
|
|
40
|
+
});
|
|
41
|
+
// 横向:等错误行渲染后,下一帧把错误列滚入视口(虚拟表格横向需 scrollTo({ left }))
|
|
42
|
+
requestAnimationFrame(() => {
|
|
43
|
+
scrollVirtualToErrorColumn(config?.tableRef, firstErrorRowKey);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return Promise.reject(new Error(''));
|
|
48
|
+
}
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
}, [errorStore, config, columns]);
|
|
51
|
+
const nameKey = Array.isArray(name) ? name.filter(Boolean).join('-') : String(name);
|
|
52
|
+
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
53
|
+
children: [name && virtualKey ? /*#__PURE__*/_jsx(Form.Item, {
|
|
12
54
|
noStyle: true,
|
|
13
55
|
validateFirst: true,
|
|
14
56
|
name: `${name}-${virtualKey}`,
|
|
@@ -23,7 +65,15 @@ const Validator = ({
|
|
|
23
65
|
}
|
|
24
66
|
}],
|
|
25
67
|
children: /*#__PURE__*/_jsx(_Fragment, {})
|
|
26
|
-
}) : null
|
|
68
|
+
}) : null, name && virtual && !config?.isView && !config?.disabled ? /*#__PURE__*/_jsx(Form.Item, {
|
|
69
|
+
noStyle: true,
|
|
70
|
+
validateFirst: true,
|
|
71
|
+
name: `${nameKey}-virtual-validator`,
|
|
72
|
+
rules: [{
|
|
73
|
+
validator: aggregateValidator
|
|
74
|
+
}],
|
|
75
|
+
children: /*#__PURE__*/_jsx(_Fragment, {})
|
|
76
|
+
}) : null]
|
|
27
77
|
});
|
|
28
78
|
};
|
|
29
79
|
export default /*#__PURE__*/memo(Validator);
|
package/es/ProEditTable/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { useLocalStorageState, useSetState } from 'ahooks';
|
|
|
8
8
|
import { ConfigProvider, Form, Affix } from 'antd';
|
|
9
9
|
import classnames from 'classnames';
|
|
10
10
|
import { transformColumns } from "./utils";
|
|
11
|
+
import { createErrorStore } from "./utils/validateAll";
|
|
11
12
|
import { getRandom, getNamePath, handleScrollToError, onPageCheck, buildTableRowKey } from "./utils/tools";
|
|
12
13
|
import { Validator, Summary, RenderToolbar } from "./components";
|
|
13
14
|
import ProForm from "../ProForm";
|
|
@@ -69,6 +70,12 @@ const ProEditTable = ({
|
|
|
69
70
|
disabled = formFieldProps?.disabled || disabled; // formFieldProps?.disabled可能是函数??
|
|
70
71
|
}
|
|
71
72
|
const tableRef = useRef(null);
|
|
73
|
+
// 虚拟表格错误存储:脱离 antd Field 生命周期持久化真实报错文案,
|
|
74
|
+
// 解决滚动卸载后报错丢失(报错常驻)。按表格实例隔离。
|
|
75
|
+
const errorStoreRef = useRef(undefined);
|
|
76
|
+
if (!errorStoreRef.current) {
|
|
77
|
+
errorStoreRef.current = createErrorStore();
|
|
78
|
+
}
|
|
72
79
|
// shouldCellUpdate 缓存按表格实例隔离,避免 module-level Map 在多实例间交叉污染
|
|
73
80
|
const cellCachesRef = useRef({
|
|
74
81
|
dataSourceRef: new Map(),
|
|
@@ -207,11 +214,14 @@ const ProEditTable = ({
|
|
|
207
214
|
name: _isArray(name) ? name : [name],
|
|
208
215
|
// name需要处理成namePath形式,为兼容多路径
|
|
209
216
|
namePath,
|
|
217
|
+
columns,
|
|
218
|
+
// 原始列配置:供单元格变更后按所属列(含 names 合并名)重校验/清理常驻报错
|
|
210
219
|
tableRef,
|
|
211
220
|
max,
|
|
212
221
|
tableLength: value?.length,
|
|
213
222
|
page,
|
|
214
223
|
originalValues,
|
|
224
|
+
errorStore: errorStoreRef.current,
|
|
215
225
|
prefixCls,
|
|
216
226
|
rowDisabled,
|
|
217
227
|
actionDirection,
|
|
@@ -223,7 +233,7 @@ const ProEditTable = ({
|
|
|
223
233
|
getIsNew,
|
|
224
234
|
handlePageChange,
|
|
225
235
|
...resetProps
|
|
226
|
-
}), [actionDirection, actionProps, actionWidth, deletePoConfirmMsg, diffConfig, editingKeys, emptyBtnText, form,
|
|
236
|
+
}), [actionDirection, actionProps, actionWidth, columns, deletePoConfirmMsg, diffConfig, editingKeys, emptyBtnText, form,
|
|
227
237
|
// forceUpdate 不应该作为依赖项,因为它是触发刷新的信号,而非用于比较的数据
|
|
228
238
|
getIsNew, handlePageChange, insertType, isView, max, mode, mulDeletePoConfirmMsg, name, namePath, onlyOneLineMsg, originalValues, prefixCls, requiredAlign, resetProps, rowDisabled, selectedRowKeys, selectedRows, shouldUpdateDebounce, tableRef, toolbarProps, value?.length, viewEmpty, virtualKey, page]);
|
|
229
239
|
|
|
@@ -467,7 +477,11 @@ const ProEditTable = ({
|
|
|
467
477
|
name: name,
|
|
468
478
|
virtualKey: virtualKey,
|
|
469
479
|
editingKeys: editingKeys,
|
|
470
|
-
onlyOneLineMsg: onlyOneLineMsg
|
|
480
|
+
onlyOneLineMsg: onlyOneLineMsg,
|
|
481
|
+
virtual: resetProps?.virtual,
|
|
482
|
+
columns: columns,
|
|
483
|
+
config: config,
|
|
484
|
+
errorStore: errorStoreRef.current
|
|
471
485
|
})]
|
|
472
486
|
});
|
|
473
487
|
};
|
|
@@ -20,6 +20,6 @@ export declare const actions: {
|
|
|
20
20
|
save: ({ record, editingKeys, setState, form, rowName, virtualRowName, result }: any) => Promise<void>;
|
|
21
21
|
cancel: ({ name, record, editingKeys, setState, form, virtualRowName, virtualKey, rowName, }: any) => void;
|
|
22
22
|
delete: ({ name, record, editingKeys, setState, form, virtualKey, onlyOneLineMsg }: any) => boolean;
|
|
23
|
-
add: ({ result, insertType, editingKeys, setState, form, name, virtualName, virtualKey, onlyOneLineMsg, tableRef, prefixCls, page, handlePageChange, }: any) => Promise<boolean>;
|
|
23
|
+
add: ({ result, insertType, editingKeys, setState, form, name, virtualName, virtualKey, onlyOneLineMsg, tableRef, prefixCls, page, handlePageChange, virtual, }: any) => Promise<boolean>;
|
|
24
24
|
mulDelete: ({ form, name, virtualKey, setState, selectedRowKeys, editingKeys }: any) => void;
|
|
25
25
|
};
|
|
@@ -179,7 +179,8 @@ export const actions = {
|
|
|
179
179
|
tableRef,
|
|
180
180
|
prefixCls = 'ant',
|
|
181
181
|
page,
|
|
182
|
-
handlePageChange
|
|
182
|
+
handlePageChange,
|
|
183
|
+
virtual
|
|
183
184
|
}) => {
|
|
184
185
|
const nextData = [...(form.getFieldValue(name) || [])];
|
|
185
186
|
// 单行编辑时,需要先保存,才能进行下面的编辑
|
|
@@ -223,15 +224,25 @@ export const actions = {
|
|
|
223
224
|
});
|
|
224
225
|
}
|
|
225
226
|
form.setFieldValue(name, nextData);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
//
|
|
227
|
+
if (virtual) {
|
|
228
|
+
// 虚拟表格无 .ant-table-body 原生滚动容器,使用 antd Table ref 的 scrollTo 按行索引定位
|
|
229
|
+
// 不传 align:新增行在视口外时虚拟列表会自动贴边(尾部插入贴底、头部插入贴顶)
|
|
229
230
|
requestAnimationFrame(() => {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
behavior: 'smooth'
|
|
231
|
+
tableRef.current?.scrollTo?.({
|
|
232
|
+
index: insertType === 'before' ? 0 : nextData.length - 1
|
|
233
233
|
});
|
|
234
234
|
});
|
|
235
|
+
} else {
|
|
236
|
+
const tableBody = tableRef.current?.children[0]?.querySelector(`.${prefixCls}-table-body`);
|
|
237
|
+
if (tableBody) {
|
|
238
|
+
// 等待新行渲染完毕再读取 scrollHeight,确保能滚动到真正的底部
|
|
239
|
+
requestAnimationFrame(() => {
|
|
240
|
+
tableBody.scrollTo?.({
|
|
241
|
+
top: insertType === 'before' ? 0 : tableBody.scrollHeight,
|
|
242
|
+
behavior: 'smooth'
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
}
|
|
235
246
|
}
|
|
236
247
|
if (virtualKey) {
|
|
237
248
|
await form.validateFields([name]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Key } from 'react';
|
|
1
|
+
import React, { Key } from 'react';
|
|
2
2
|
import { FormInstance } from 'antd/es/form';
|
|
3
3
|
import { NamePath } from 'antd/es/form/interface';
|
|
4
4
|
/**
|
|
@@ -34,6 +34,24 @@ export declare const customValidate: (validateKeys: string[], form: FormInstance
|
|
|
34
34
|
* @returns 拆解后的字符串
|
|
35
35
|
*/
|
|
36
36
|
export declare const splitNames: (names: any[]) => string;
|
|
37
|
+
/**
|
|
38
|
+
* 把行内变更的子键映射回「所属列的字段标识」,并去重。
|
|
39
|
+
* - names 组合列:任一子键命中 → 该列合并名(splitNames(names)),与 Form.Item 注册名/dataIndex 对齐
|
|
40
|
+
* - 普通列:原键本身
|
|
41
|
+
*
|
|
42
|
+
* 用于行数据被联动改写(兄弟列 onFieldChange 回填)后,按真实字段名重校验与清理常驻报错,
|
|
43
|
+
* 修复 group 列旧报错不清的问题。
|
|
44
|
+
*/
|
|
45
|
+
export declare const resolveChangedFields: (changedKeys: string[], columns?: any[]) => string[];
|
|
46
|
+
/**
|
|
47
|
+
* 把联动回填后的子键,重新组合并写回 names 组合列的合并字段(store)。
|
|
48
|
+
*
|
|
49
|
+
* 背景:names 组合列真正被校验的是合并字段(splitNames(names),如 certType-certNo),
|
|
50
|
+
* 其值仅在渲染期由 getValueProps 从子键组合后写回 store。联动回填(兄弟列 onFieldChange)
|
|
51
|
+
* 只直接写入子键,不经过该字段的 normalize/渲染,导致合并字段 store 值未更新 →
|
|
52
|
+
* 按合并名校验时读到旧空值、红字不清。这里按当前行子键即时组合并写回,确保 validateFields 读到最新值。
|
|
53
|
+
*/
|
|
54
|
+
export declare const syncGroupCombinedValues: (form: FormInstance, rowPath: (string | number)[], changedKeys: string[], columns?: any[]) => void;
|
|
37
55
|
/**
|
|
38
56
|
* 获取中间formItem的name
|
|
39
57
|
* @param name 名称路径
|
|
@@ -71,6 +89,17 @@ export declare const getDisabled: ({ globalControl, formDisabled, column, tabled
|
|
|
71
89
|
* 表格自动滚动到报错位置
|
|
72
90
|
*/
|
|
73
91
|
export declare const handleScrollToError: () => void;
|
|
92
|
+
/**
|
|
93
|
+
* 虚拟表格:横向滚动定位到「首个错误行」内的报错单元格。
|
|
94
|
+
*
|
|
95
|
+
* 背景:虚拟表格的横向滚动不是原生 DOM scroll(holder 的 overflow-x 为 hidden),
|
|
96
|
+
* 由 rc-virtual-list 用 transform 模拟,只能通过 antd Table ref 的 scrollTo({ left }) 驱动;
|
|
97
|
+
* 而 scrollTo({ index }) 仅做纵向行定位。保存校验失败后纵向已滚到错误行,但错误列可能仍在
|
|
98
|
+
* 横向视口外(如错误列靠左、表格当前滚到最右)→ 看不到红字。此函数补齐横向定位。
|
|
99
|
+
*
|
|
100
|
+
* 目标 scrollLeft = 错误单元格前置兄弟列宽累计 - fixed-left 列宽(fixed 列悬浮,不占滚动偏移)。
|
|
101
|
+
*/
|
|
102
|
+
export declare const scrollVirtualToErrorColumn: (tableRef: React.MutableRefObject<any>, rowKey?: string | null) => void;
|
|
74
103
|
/**
|
|
75
104
|
* 深copy一个对象,并过滤掉其中的React节点
|
|
76
105
|
* @param value 需要深拷贝的对象
|
|
@@ -89,6 +89,59 @@ export const splitNames = names => {
|
|
|
89
89
|
return result;
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* 判断值是否为空
|
|
94
|
+
* @param value 需要判断的值
|
|
95
|
+
* @returns 是否为空
|
|
96
|
+
*/
|
|
97
|
+
const isNull = value => {
|
|
98
|
+
if (Array.isArray(value) && value.length) {
|
|
99
|
+
return value.some(item => isNull(item));
|
|
100
|
+
}
|
|
101
|
+
return value === '' || value === undefined || value === null || Array.isArray(value) && value.length === 0;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 把行内变更的子键映射回「所属列的字段标识」,并去重。
|
|
106
|
+
* - names 组合列:任一子键命中 → 该列合并名(splitNames(names)),与 Form.Item 注册名/dataIndex 对齐
|
|
107
|
+
* - 普通列:原键本身
|
|
108
|
+
*
|
|
109
|
+
* 用于行数据被联动改写(兄弟列 onFieldChange 回填)后,按真实字段名重校验与清理常驻报错,
|
|
110
|
+
* 修复 group 列旧报错不清的问题。
|
|
111
|
+
*/
|
|
112
|
+
export const resolveChangedFields = (changedKeys, columns = []) => {
|
|
113
|
+
const result = new Set();
|
|
114
|
+
(changedKeys || []).forEach(key => {
|
|
115
|
+
const groupCol = columns?.find?.(col => Array.isArray(col?.names) && col.names.includes(key));
|
|
116
|
+
result.add(groupCol ? splitNames(groupCol.names) : key);
|
|
117
|
+
});
|
|
118
|
+
return Array.from(result);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 把联动回填后的子键,重新组合并写回 names 组合列的合并字段(store)。
|
|
123
|
+
*
|
|
124
|
+
* 背景:names 组合列真正被校验的是合并字段(splitNames(names),如 certType-certNo),
|
|
125
|
+
* 其值仅在渲染期由 getValueProps 从子键组合后写回 store。联动回填(兄弟列 onFieldChange)
|
|
126
|
+
* 只直接写入子键,不经过该字段的 normalize/渲染,导致合并字段 store 值未更新 →
|
|
127
|
+
* 按合并名校验时读到旧空值、红字不清。这里按当前行子键即时组合并写回,确保 validateFields 读到最新值。
|
|
128
|
+
*/
|
|
129
|
+
export const syncGroupCombinedValues = (form, rowPath, changedKeys, columns = []) => {
|
|
130
|
+
const row = form.getFieldValue(rowPath) || {};
|
|
131
|
+
(columns || []).forEach(col => {
|
|
132
|
+
if (!Array.isArray(col?.names)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const touched = col.names.some(key => changedKeys.includes(key));
|
|
136
|
+
if (!touched) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const combined = col.names.map(key => _get(row, key));
|
|
140
|
+
const isAllEmpty = combined.every(item => isNull(item));
|
|
141
|
+
form.setFieldValue([...rowPath, splitNames(col.names)], isAllEmpty ? undefined : combined);
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
92
145
|
/**
|
|
93
146
|
* 获取中间formItem的name
|
|
94
147
|
* @param name 名称路径
|
|
@@ -272,6 +325,48 @@ export const handleScrollToError = () => {
|
|
|
272
325
|
}, 0);
|
|
273
326
|
};
|
|
274
327
|
|
|
328
|
+
/**
|
|
329
|
+
* 虚拟表格:横向滚动定位到「首个错误行」内的报错单元格。
|
|
330
|
+
*
|
|
331
|
+
* 背景:虚拟表格的横向滚动不是原生 DOM scroll(holder 的 overflow-x 为 hidden),
|
|
332
|
+
* 由 rc-virtual-list 用 transform 模拟,只能通过 antd Table ref 的 scrollTo({ left }) 驱动;
|
|
333
|
+
* 而 scrollTo({ index }) 仅做纵向行定位。保存校验失败后纵向已滚到错误行,但错误列可能仍在
|
|
334
|
+
* 横向视口外(如错误列靠左、表格当前滚到最右)→ 看不到红字。此函数补齐横向定位。
|
|
335
|
+
*
|
|
336
|
+
* 目标 scrollLeft = 错误单元格前置兄弟列宽累计 - fixed-left 列宽(fixed 列悬浮,不占滚动偏移)。
|
|
337
|
+
*/
|
|
338
|
+
export const scrollVirtualToErrorColumn = (tableRef, rowKey) => {
|
|
339
|
+
const ref = tableRef?.current;
|
|
340
|
+
const root = ref?.nativeElement;
|
|
341
|
+
if (!ref?.scrollTo || !root || rowKey == null) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const rowEl = root.querySelector(`[data-row-key="${rowKey}"]`);
|
|
345
|
+
// 取错误行内首个报错单元格(DOM 顺序 = 列顺序,首个即最靠左的错误列)
|
|
346
|
+
const errorCell = rowEl?.querySelector('[class*="form-item-has-error"]')?.closest('[class*="table-cell"]');
|
|
347
|
+
if (!errorCell) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
let offsetLeft = 0;
|
|
351
|
+
let fixedLeft = 0;
|
|
352
|
+
let previousSibling = errorCell.previousElementSibling;
|
|
353
|
+
while (previousSibling) {
|
|
354
|
+
if (previousSibling.nodeType === 1) {
|
|
355
|
+
const {
|
|
356
|
+
width = 0
|
|
357
|
+
} = previousSibling.getBoundingClientRect() || {};
|
|
358
|
+
offsetLeft += width;
|
|
359
|
+
if (previousSibling.classList?.contains?.('ant-table-cell-fix-left')) {
|
|
360
|
+
fixedLeft += width;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
previousSibling = previousSibling.previousElementSibling;
|
|
364
|
+
}
|
|
365
|
+
ref.scrollTo({
|
|
366
|
+
left: Math.max(0, offsetLeft - fixedLeft)
|
|
367
|
+
});
|
|
368
|
+
};
|
|
369
|
+
|
|
275
370
|
/**
|
|
276
371
|
* 深copy一个对象,并过滤掉其中的React节点
|
|
277
372
|
* @param value 需要深拷贝的对象
|
|
@@ -284,18 +379,6 @@ export function cloneDeepFilterNode(value) {
|
|
|
284
379
|
}
|
|
285
380
|
});
|
|
286
381
|
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* 判断值是否为空
|
|
290
|
-
* @param value 需要判断的值
|
|
291
|
-
* @returns 是否为空
|
|
292
|
-
*/
|
|
293
|
-
const isNull = value => {
|
|
294
|
-
if (Array.isArray(value) && value.length) {
|
|
295
|
-
return value.some(item => isNull(item));
|
|
296
|
-
}
|
|
297
|
-
return value === '' || value === undefined || value === null || Array.isArray(value) && value.length === 0;
|
|
298
|
-
};
|
|
299
382
|
const handleCheckCellValue = async (column, record) => {
|
|
300
383
|
let value = null;
|
|
301
384
|
if (column.name) {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { FormInstance } from 'antd';
|
|
2
|
+
import type { NamePath } from 'antd/es/form/interface';
|
|
3
|
+
/**
|
|
4
|
+
* 单元格规则装配 + 全量数据真实校验(脱离 DOM)。
|
|
5
|
+
*
|
|
6
|
+
* 背景:antd6 虚拟表格滚动时视口外的行会被卸载,其 Form.Item 字段未注册,
|
|
7
|
+
* form.validateFields 无法覆盖;本模块用 form store 里的全量数据 + 真实规则
|
|
8
|
+
* 逐行逐列离屏校验,解决「统一校验」缺口,并产出可常驻的真实报错文案。
|
|
9
|
+
*/
|
|
10
|
+
interface BuildCellRulesParams {
|
|
11
|
+
column: any;
|
|
12
|
+
rowData: any;
|
|
13
|
+
index: number;
|
|
14
|
+
form: FormInstance;
|
|
15
|
+
name: (string | number)[];
|
|
16
|
+
config: any;
|
|
17
|
+
}
|
|
18
|
+
interface BuiltCellRule {
|
|
19
|
+
/** 单元格 Form.Item 的 namePath(用于定位/messageVariables) */
|
|
20
|
+
namePath: (string | number)[];
|
|
21
|
+
/** 列在行内的稳定标识(与 transformColumns 的 dataIndex/key 一致),用于错误存储归集 */
|
|
22
|
+
cellKey: string;
|
|
23
|
+
/** 待校验值(names 列为组合数组) */
|
|
24
|
+
value: any;
|
|
25
|
+
/** 真实规则集合 */
|
|
26
|
+
rules: any[];
|
|
27
|
+
/** 字段 label,供 ${label} 文案插值 */
|
|
28
|
+
label: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 为单行单列装配真实校验规则与取值。
|
|
32
|
+
* 当该单元格处于查看/禁用态、或无任何规则时返回 null(跳过校验)。
|
|
33
|
+
*/
|
|
34
|
+
export declare const buildCellRules: ({ column, rowData, index, form, name, config, }: BuildCellRulesParams) => BuiltCellRule | null;
|
|
35
|
+
interface ValidateAllParams {
|
|
36
|
+
form: FormInstance;
|
|
37
|
+
name: NamePath;
|
|
38
|
+
columns: any[];
|
|
39
|
+
config: any;
|
|
40
|
+
}
|
|
41
|
+
export interface ValidateAllResult {
|
|
42
|
+
hasError: boolean;
|
|
43
|
+
/** rowKey -> { cellKey -> 错误文案数组 } */
|
|
44
|
+
errorMap: Record<string, Record<string, string[]>>;
|
|
45
|
+
/** 首个错误单元格的 namePath(用于滚动定位),无错误为 null */
|
|
46
|
+
firstErrorPath: (string | number)[] | null;
|
|
47
|
+
/** 首个错误行的 rowKey(用于横向滚动时定位错误行 DOM),无错误为 null */
|
|
48
|
+
firstErrorRowKey: string | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 对 form store 中 name 路径下的全量数组数据,逐行逐列用真实规则校验(脱离 DOM)。
|
|
52
|
+
*/
|
|
53
|
+
export declare const validateAllRows: ({ form, name, columns, config, }: ValidateAllParams) => Promise<ValidateAllResult>;
|
|
54
|
+
/**
|
|
55
|
+
* 外部错误存储:以 rowKey + cellKey 为键持久化真实报错文案。
|
|
56
|
+
* 它是「报错常驻」的真实来源——脱离 antd Field 实体生命周期,
|
|
57
|
+
* 行卸载后仍保留,重新挂载时回填显示。
|
|
58
|
+
*/
|
|
59
|
+
export interface ErrorStore {
|
|
60
|
+
/** 读取某单元格错误文案 */
|
|
61
|
+
get(rowKey: string | number, cellKey: string): string[] | undefined;
|
|
62
|
+
/** 读取整行错误 */
|
|
63
|
+
getRow(rowKey: string | number): Record<string, string[]> | undefined;
|
|
64
|
+
/** 全量替换(来自一次完整校验) */
|
|
65
|
+
setAll(errorMap: Record<string, Record<string, string[]>>): void;
|
|
66
|
+
/** 清理某单元格(用户修正后调用),整行清空时移除该行 */
|
|
67
|
+
clearCell(rowKey: string | number, cellKey: string): void;
|
|
68
|
+
/** 清空所有 */
|
|
69
|
+
clearAll(): void;
|
|
70
|
+
/** 是否为空 */
|
|
71
|
+
isEmpty(): boolean;
|
|
72
|
+
}
|
|
73
|
+
export declare const createErrorStore: () => ErrorStore;
|
|
74
|
+
interface RunUnifiedParams extends ValidateAllParams {
|
|
75
|
+
errorStore: ErrorStore;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 统一校验入口:对全量数据跑真实规则,写入 errorStore,返回是否有错与首个错误行索引。
|
|
79
|
+
* 供隐藏聚合校验字段在 form.validateFields() 时调用,从而覆盖视口外未挂载的行。
|
|
80
|
+
*/
|
|
81
|
+
export declare const runUnifiedValidation: ({ form, name, columns, config, errorStore, }: RunUnifiedParams) => Promise<{
|
|
82
|
+
hasError: boolean;
|
|
83
|
+
firstErrorIndex: number | null;
|
|
84
|
+
firstErrorRowKey: string | null;
|
|
85
|
+
}>;
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import _isString from "lodash/isString";
|
|
2
|
+
import _isFunction from "lodash/isFunction";
|
|
3
|
+
import _isBoolean from "lodash/isBoolean";
|
|
4
|
+
import _get from "lodash/get";
|
|
5
|
+
// 复用 antd 内部(@rc-component/form)的真实校验内核:与每个 Form.Item 字段走的是同一套
|
|
6
|
+
// async-validator 流程,保证脱离 DOM 校验时的文案/异步 validator 行为与渲染态完全一致。
|
|
7
|
+
import { validateRules } from '@rc-component/form/es/utils/validateUtil';
|
|
8
|
+
import { rulesCreator } from "../../ProForm/utils/rulesCreator";
|
|
9
|
+
import { isSelect, isNullArray, isNotFullArray } from "../../ProForm/utils";
|
|
10
|
+
import { getDisabled, splitNames } from "./tools";
|
|
11
|
+
import locale from "../../locale";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 单元格规则装配 + 全量数据真实校验(脱离 DOM)。
|
|
15
|
+
*
|
|
16
|
+
* 背景:antd6 虚拟表格滚动时视口外的行会被卸载,其 Form.Item 字段未注册,
|
|
17
|
+
* form.validateFields 无法覆盖;本模块用 form store 里的全量数据 + 真实规则
|
|
18
|
+
* 逐行逐列离屏校验,解决「统一校验」缺口,并产出可常驻的真实报错文案。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 纯函数版规则装配,镜像 ProForm/hooks/useRules(去掉 hook 外壳)+ 复用 rulesCreator,
|
|
23
|
+
* 确保与渲染态生成的 rules 完全一致,无文案漂移。
|
|
24
|
+
*/
|
|
25
|
+
const composeRules = ({
|
|
26
|
+
rules,
|
|
27
|
+
required,
|
|
28
|
+
isSelectField,
|
|
29
|
+
label,
|
|
30
|
+
names,
|
|
31
|
+
labelRequired
|
|
32
|
+
}) => {
|
|
33
|
+
const _label = _isString(label) ? label : '';
|
|
34
|
+
const requiredRule = Array.isArray(rules) && rules.find(rule => rule?.required === true);
|
|
35
|
+
const allRequired = required === true || Array.isArray(required) && required.every(item => item === true);
|
|
36
|
+
let resultRules = rules || [];
|
|
37
|
+
if (allRequired) {
|
|
38
|
+
if (!requiredRule) {
|
|
39
|
+
const message = isSelectField ? `${locale.ProForm.selectPlaceHolder}${_label}` : `${locale.ProForm.inputPlaceholder}${_label}`;
|
|
40
|
+
const rule = {
|
|
41
|
+
required: allRequired,
|
|
42
|
+
message
|
|
43
|
+
};
|
|
44
|
+
// names 字段的必填校验(与 useRules 保持一致)
|
|
45
|
+
if (names?.length) {
|
|
46
|
+
rule.validator = (_, value) => {
|
|
47
|
+
if (!value || isNullArray(value)) {
|
|
48
|
+
return Promise.reject(new Error(`${locale.ProForm.inputPlaceholder}${_label}`));
|
|
49
|
+
}
|
|
50
|
+
return Promise.resolve();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
resultRules = Array.isArray(rules) ? [...rules, rule] : [rule];
|
|
54
|
+
}
|
|
55
|
+
} else if (requiredRule && names?.length) {
|
|
56
|
+
// 不可变:克隆并替换 requiredRule 的 validator,避免污染外部 column.rules
|
|
57
|
+
resultRules = rules.map(rule => rule === requiredRule ? {
|
|
58
|
+
...rule,
|
|
59
|
+
validator: (_, value) => {
|
|
60
|
+
if (!value || isNullArray(value)) {
|
|
61
|
+
return Promise.reject(new Error(`${locale.ProForm.inputPlaceholder}${_label}`));
|
|
62
|
+
}
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
}
|
|
65
|
+
} : rule);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// type → 内置 rules(直接复用 rulesCreator 纯函数)
|
|
69
|
+
let finalRules = rulesCreator({
|
|
70
|
+
rules: resultRules,
|
|
71
|
+
label: _label,
|
|
72
|
+
isSelect: isSelectField,
|
|
73
|
+
names,
|
|
74
|
+
required
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// 完整性校验(镜像 useRules 的 useEffect 分支)
|
|
78
|
+
if (names && !_isBoolean(labelRequired)) {
|
|
79
|
+
const customRequired = {
|
|
80
|
+
validator: (_rules, value) => {
|
|
81
|
+
if (Array.isArray(value) && !isNullArray(value) && isNotFullArray(value, names.length, required)) {
|
|
82
|
+
return Promise.reject(new Error(`${locale.ProForm.completeText}${_label}`));
|
|
83
|
+
}
|
|
84
|
+
return Promise.resolve();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
finalRules = [customRequired, ...finalRules];
|
|
88
|
+
}
|
|
89
|
+
return finalRules;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 为单行单列装配真实校验规则与取值。
|
|
94
|
+
* 当该单元格处于查看/禁用态、或无任何规则时返回 null(跳过校验)。
|
|
95
|
+
*/
|
|
96
|
+
export const buildCellRules = ({
|
|
97
|
+
column,
|
|
98
|
+
rowData,
|
|
99
|
+
index,
|
|
100
|
+
form,
|
|
101
|
+
name,
|
|
102
|
+
config
|
|
103
|
+
}) => {
|
|
104
|
+
// 整表查看模式:不校验
|
|
105
|
+
if (config?.isView) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const reactiveParams = {
|
|
109
|
+
form,
|
|
110
|
+
index,
|
|
111
|
+
namePath: [...name, index]
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// 动态 required / rules / isEditable 按行求值
|
|
115
|
+
let lastRequired = column?.required ?? false;
|
|
116
|
+
if (_isFunction(column?.required)) {
|
|
117
|
+
lastRequired = column.required(rowData, reactiveParams);
|
|
118
|
+
}
|
|
119
|
+
let lastRules = column?.rules ?? [];
|
|
120
|
+
if (_isFunction(column?.rules)) {
|
|
121
|
+
lastRules = column.rules(rowData, reactiveParams);
|
|
122
|
+
}
|
|
123
|
+
let lastFieldProps = column?.fieldProps ?? {};
|
|
124
|
+
if (_isFunction(column?.fieldProps)) {
|
|
125
|
+
lastFieldProps = column.fieldProps(rowData, reactiveParams);
|
|
126
|
+
}
|
|
127
|
+
let isEditable = column?.isEditable ?? true;
|
|
128
|
+
if (_isFunction(column?.isEditable)) {
|
|
129
|
+
isEditable = column.isEditable(rowData, reactiveParams);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 与 RenderField 一致的 type 首字母大写
|
|
133
|
+
const type = column?.type?.replace?.(column.type[0], column.type[0].toUpperCase());
|
|
134
|
+
|
|
135
|
+
// 查看/禁用单元格跳过(镜像 RenderField.isView 的核心判定)
|
|
136
|
+
const disabled = getDisabled({
|
|
137
|
+
globalControl: config?.otherProps?.globalControl,
|
|
138
|
+
formDisabled: config?.otherProps?.formDisabled,
|
|
139
|
+
column,
|
|
140
|
+
tabledDisabled: config?.disabled,
|
|
141
|
+
columnFieldProps: lastFieldProps,
|
|
142
|
+
params: [rowData, reactiveParams],
|
|
143
|
+
rowDisabled: config?.rowDisabled || 'empty'
|
|
144
|
+
});
|
|
145
|
+
if (!isEditable || rowData?.['is-view'] || disabled) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const names = column?.names;
|
|
149
|
+
const columnName = column?.name || column?.dataIndex || column?.key;
|
|
150
|
+
const isSelectField = !!isSelect({
|
|
151
|
+
dataSource: lastFieldProps?.dataSource,
|
|
152
|
+
type
|
|
153
|
+
});
|
|
154
|
+
const rules = composeRules({
|
|
155
|
+
rules: lastRules,
|
|
156
|
+
required: lastRequired,
|
|
157
|
+
isSelectField,
|
|
158
|
+
label: column?.label,
|
|
159
|
+
names,
|
|
160
|
+
labelRequired: column?.labelRequired
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// 无实际约束(仅 [{ required: false }] 兜底)时跳过
|
|
164
|
+
const hasRealRule = rules.some(rule => rule?.required === true || rule?.validator || rule?.type || rule?.pattern);
|
|
165
|
+
if (!hasRealRule) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 取值与 cellKey:names 列取组合数组,cellKey 用 splitNames(与 transformColumns 对齐)
|
|
170
|
+
let value;
|
|
171
|
+
let cellKey;
|
|
172
|
+
if (names?.length) {
|
|
173
|
+
value = names.map(key => _get(rowData, key));
|
|
174
|
+
cellKey = splitNames(names);
|
|
175
|
+
} else {
|
|
176
|
+
const path = Array.isArray(columnName) ? columnName : [columnName];
|
|
177
|
+
value = _get(rowData, path);
|
|
178
|
+
cellKey = String(columnName);
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
namePath: [...name, index, ...(Array.isArray(columnName) ? columnName : [columnName])],
|
|
182
|
+
cellKey,
|
|
183
|
+
value,
|
|
184
|
+
rules,
|
|
185
|
+
label: _isString(column?.label) ? column.label : ''
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* 对 form store 中 name 路径下的全量数组数据,逐行逐列用真实规则校验(脱离 DOM)。
|
|
190
|
+
*/
|
|
191
|
+
export const validateAllRows = async ({
|
|
192
|
+
form,
|
|
193
|
+
name,
|
|
194
|
+
columns,
|
|
195
|
+
config
|
|
196
|
+
}) => {
|
|
197
|
+
const namePath = Array.isArray(name) ? name : [name];
|
|
198
|
+
const list = form.getFieldValue(namePath) || [];
|
|
199
|
+
const errorMap = {};
|
|
200
|
+
let firstErrorPath = null;
|
|
201
|
+
let firstErrorRowKey = null;
|
|
202
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
203
|
+
const rowData = list[index];
|
|
204
|
+
// 行可能为空位(单行编辑增删残留),跳过
|
|
205
|
+
if (!rowData) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const rowKey = String(rowData.rowKey ?? index);
|
|
209
|
+
for (const column of columns) {
|
|
210
|
+
const built = buildCellRules({
|
|
211
|
+
column,
|
|
212
|
+
rowData,
|
|
213
|
+
index,
|
|
214
|
+
form,
|
|
215
|
+
name: namePath,
|
|
216
|
+
config
|
|
217
|
+
});
|
|
218
|
+
if (!built) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// validateFirst=true:成功 resolve([]),首个失败 reject([{ errors, rule }])
|
|
223
|
+
// eslint-disable-next-line no-await-in-loop
|
|
224
|
+
const ruleErrors = await validateRules(built.namePath, built.value, built.rules, {
|
|
225
|
+
validateMessages: config?.validateMessages
|
|
226
|
+
}, true, {
|
|
227
|
+
label: built.label
|
|
228
|
+
}).then(() => [], err => Array.isArray(err) ? err : []);
|
|
229
|
+
const messages = [];
|
|
230
|
+
ruleErrors.forEach(({
|
|
231
|
+
errors = [],
|
|
232
|
+
rule
|
|
233
|
+
}) => {
|
|
234
|
+
// warningOnly 仅警告,不计入阻断性错误
|
|
235
|
+
if (!rule?.warningOnly) {
|
|
236
|
+
messages.push(...errors);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
if (messages.length) {
|
|
240
|
+
if (!errorMap[rowKey]) {
|
|
241
|
+
errorMap[rowKey] = {};
|
|
242
|
+
}
|
|
243
|
+
errorMap[rowKey][built.cellKey] = messages;
|
|
244
|
+
if (!firstErrorPath) {
|
|
245
|
+
firstErrorPath = built.namePath;
|
|
246
|
+
firstErrorRowKey = rowKey;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
hasError: !!firstErrorPath,
|
|
253
|
+
errorMap,
|
|
254
|
+
firstErrorPath,
|
|
255
|
+
firstErrorRowKey
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 外部错误存储:以 rowKey + cellKey 为键持久化真实报错文案。
|
|
261
|
+
* 它是「报错常驻」的真实来源——脱离 antd Field 实体生命周期,
|
|
262
|
+
* 行卸载后仍保留,重新挂载时回填显示。
|
|
263
|
+
*/
|
|
264
|
+
|
|
265
|
+
export const createErrorStore = () => {
|
|
266
|
+
const map = new Map();
|
|
267
|
+
return {
|
|
268
|
+
get: (rowKey, cellKey) => map.get(String(rowKey))?.[cellKey],
|
|
269
|
+
getRow: rowKey => map.get(String(rowKey)),
|
|
270
|
+
setAll: errorMap => {
|
|
271
|
+
map.clear();
|
|
272
|
+
Object.keys(errorMap || {}).forEach(rowKey => {
|
|
273
|
+
map.set(rowKey, errorMap[rowKey]);
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
clearCell: (rowKey, cellKey) => {
|
|
277
|
+
const row = map.get(String(rowKey));
|
|
278
|
+
if (row && cellKey in row) {
|
|
279
|
+
delete row[cellKey];
|
|
280
|
+
if (Object.keys(row).length === 0) {
|
|
281
|
+
map.delete(String(rowKey));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
clearAll: () => map.clear(),
|
|
286
|
+
isEmpty: () => map.size === 0
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* 统一校验入口:对全量数据跑真实规则,写入 errorStore,返回是否有错与首个错误行索引。
|
|
291
|
+
* 供隐藏聚合校验字段在 form.validateFields() 时调用,从而覆盖视口外未挂载的行。
|
|
292
|
+
*/
|
|
293
|
+
export const runUnifiedValidation = async ({
|
|
294
|
+
form,
|
|
295
|
+
name,
|
|
296
|
+
columns,
|
|
297
|
+
config,
|
|
298
|
+
errorStore
|
|
299
|
+
}) => {
|
|
300
|
+
const {
|
|
301
|
+
hasError,
|
|
302
|
+
errorMap,
|
|
303
|
+
firstErrorPath,
|
|
304
|
+
firstErrorRowKey
|
|
305
|
+
} = await validateAllRows({
|
|
306
|
+
form,
|
|
307
|
+
name,
|
|
308
|
+
columns,
|
|
309
|
+
config
|
|
310
|
+
});
|
|
311
|
+
errorStore.setAll(errorMap);
|
|
312
|
+
const namePath = Array.isArray(name) ? name : [name];
|
|
313
|
+
const firstErrorIndex = firstErrorPath && firstErrorPath.length > namePath.length ? Number(firstErrorPath[namePath.length]) : null;
|
|
314
|
+
return {
|
|
315
|
+
hasError,
|
|
316
|
+
firstErrorIndex,
|
|
317
|
+
firstErrorRowKey
|
|
318
|
+
};
|
|
319
|
+
};
|
|
@@ -75,58 +75,58 @@ export declare const useFormItemProps: (column: FlexibleGroupColumnType, context
|
|
|
75
75
|
confirm?: boolean | import("antd").ModalFuncProps | import("../../../render/propsType").FunctionArgs<any, boolean | import("antd").ModalFuncProps>;
|
|
76
76
|
show?: boolean | ReactiveFunction<any, boolean>;
|
|
77
77
|
component?: React.ReactNode | ReactiveFunction<any, React.ReactNode>;
|
|
78
|
+
isView?: boolean;
|
|
79
|
+
style?: React.CSSProperties;
|
|
80
|
+
children?: React.ReactNode | ((form: FormInstance<any>) => React.ReactNode);
|
|
78
81
|
className?: string;
|
|
79
82
|
hidden?: boolean;
|
|
80
83
|
id?: string;
|
|
81
|
-
style?: React.CSSProperties;
|
|
82
|
-
children?: React.ReactNode | ((form: FormInstance<any>) => React.ReactNode);
|
|
83
84
|
onReset?: () => void;
|
|
85
|
+
validateTrigger?: string | false | string[];
|
|
86
|
+
preserve?: boolean;
|
|
87
|
+
trim?: boolean;
|
|
88
|
+
normalize?: (value: any, prevValue: any, allValues: import("@rc-component/form/lib/interface").Store) => any;
|
|
89
|
+
clearNotShow?: boolean;
|
|
90
|
+
labelAlign?: import("antd/es/form/interface").FormLabelAlign;
|
|
84
91
|
prefixCls?: string;
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
colon?: boolean;
|
|
93
|
+
layout?: import("antd/es/form/Form").FormItemLayout;
|
|
94
|
+
labelCol?: import("antd").ColProps;
|
|
95
|
+
wrapperCol?: import("antd").ColProps;
|
|
87
96
|
rootClassName?: string;
|
|
88
|
-
status?: "" | "
|
|
97
|
+
status?: "" | "validating" | "warning" | "error" | "success";
|
|
89
98
|
vertical?: boolean;
|
|
90
|
-
getValueProps?: ((value: any) => Record<string, unknown>) & ((value: any) => Record<string, unknown>);
|
|
91
|
-
colon?: boolean;
|
|
92
99
|
htmlFor?: string;
|
|
93
|
-
labelAlign?: import("antd/es/form/interface").FormLabelAlign;
|
|
94
|
-
labelCol?: import("antd").ColProps;
|
|
95
100
|
getValueFromEvent?: (...args: import("@rc-component/form/lib/interface").EventArgs) => any;
|
|
96
|
-
normalize?: (value: any, prevValue: any, allValues: import("@rc-component/form/lib/interface").Store) => any;
|
|
97
101
|
shouldUpdate?: import("@rc-component/form/lib/Field").ShouldUpdate<any>;
|
|
98
102
|
trigger?: string;
|
|
99
|
-
validateTrigger?: string | false | string[];
|
|
100
103
|
validateDebounce?: number;
|
|
101
104
|
valuePropName?: string;
|
|
105
|
+
getValueProps?: ((value: any) => Record<string, unknown>) & ((value: any) => Record<string, unknown>);
|
|
102
106
|
messageVariables?: Record<string, string>;
|
|
103
107
|
initialValue?: any;
|
|
104
108
|
onMetaChange?: (meta: import("@rc-component/form/lib/Field").MetaEvent) => void;
|
|
105
|
-
preserve?: boolean;
|
|
106
109
|
isListField?: boolean;
|
|
107
110
|
isList?: boolean;
|
|
108
111
|
noStyle?: boolean;
|
|
109
112
|
hasFeedback?: boolean | {
|
|
110
113
|
icons: import("antd/es/form/FormItem").FeedbackIcons;
|
|
111
114
|
};
|
|
112
|
-
validateStatus?: "" | "
|
|
113
|
-
layout?: import("antd/es/form/Form").FormItemLayout;
|
|
114
|
-
wrapperCol?: import("antd").ColProps;
|
|
115
|
+
validateStatus?: "" | "validating" | "warning" | "error" | "success";
|
|
115
116
|
help?: React.ReactNode;
|
|
116
117
|
fieldId?: string;
|
|
117
118
|
valueType?: import("../../../render/propsType").ProFormValueType;
|
|
118
|
-
toISOString?: boolean;
|
|
119
|
-
toCSTString?: boolean;
|
|
120
119
|
switchValue?: [any, any];
|
|
121
|
-
clearNotShow?: boolean;
|
|
122
|
-
trim?: boolean;
|
|
123
|
-
upperCase?: boolean;
|
|
124
120
|
viewRender?: (value: any, record: any, { form, index, namePath, }: {
|
|
125
121
|
[key: string]: any;
|
|
126
122
|
form: FormInstance<any>;
|
|
127
123
|
index?: number;
|
|
128
124
|
}) => string | React.ReactElement<any, any>;
|
|
129
125
|
viewType?: import("../../../render/propsType").ViewType;
|
|
126
|
+
upperCase?: boolean;
|
|
127
|
+
toISOString?: boolean;
|
|
128
|
+
toCSTString?: boolean;
|
|
129
|
+
desensitization?: [number, number] | ReactiveFunction<any, [number, number]>;
|
|
130
130
|
name: any;
|
|
131
131
|
dependencies: any[];
|
|
132
132
|
tooltip: string | {
|
|
@@ -141,7 +141,7 @@ export declare const useFormItemProps: (column: FlexibleGroupColumnType, context
|
|
|
141
141
|
* 创建组件属性
|
|
142
142
|
*/
|
|
143
143
|
export declare const createComponentProps: (column: FlexibleGroupColumnType, formItemProps: any) => {
|
|
144
|
-
componentProps: import("lodash").Omit<any, "
|
|
144
|
+
componentProps: import("lodash").Omit<any, "clearNotShow" | "format" | "valueType" | "switchValue" | "dependNames" | "toISOString" | "toCSTString" | "precision">;
|
|
145
145
|
formItemTransform: {
|
|
146
146
|
getValueProps: any;
|
|
147
147
|
normalize: any;
|