@zat-design/sisyphus-react 4.4.3-beta.2 → 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/README.md +1 -42
- package/es/ProEditTable/components/RenderField/ListChangedWrapper.d.ts +1 -14
- package/es/ProEditTable/components/RenderField/ListChangedWrapper.js +27 -102
- package/es/ProEditTable/components/RenderField/index.js +313 -366
- package/es/ProEditTable/components/RenderField/propsType.d.ts +30 -0
- package/es/ProEditTable/components/RenderField/propsType.js +1 -0
- package/es/ProEditTable/components/RenderField/tools.d.ts +22 -0
- package/es/ProEditTable/components/RenderField/tools.js +203 -0
- 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 +25 -25
- package/es/ProTree/components/ProTreeSelect/index.js +3 -2
- package/es/ProTree/utils.d.ts +9 -0
- package/es/ProTree/utils.js +31 -0
- package/package.json +1 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { FormInstance } from 'antd';
|
|
2
|
+
import { InternalNamePath, NamePath } from 'antd/lib/form/interface';
|
|
3
|
+
import { DiffOriginalParams } from '../../utils/diffOriginal';
|
|
4
|
+
export interface GetOriginalValueParams {
|
|
5
|
+
namePath: InternalNamePath;
|
|
6
|
+
originalName: InternalNamePath;
|
|
7
|
+
originalNames?: InternalNamePath[];
|
|
8
|
+
originalValues?: any;
|
|
9
|
+
rowKeyPath?: InternalNamePath;
|
|
10
|
+
form: FormInstance;
|
|
11
|
+
}
|
|
12
|
+
export interface Props {
|
|
13
|
+
name?: NamePath;
|
|
14
|
+
names?: NamePath[];
|
|
15
|
+
originalName?: NamePath;
|
|
16
|
+
originalNames?: NamePath[];
|
|
17
|
+
namesStr?: NamePath;
|
|
18
|
+
originalValues?: any;
|
|
19
|
+
form: FormInstance;
|
|
20
|
+
onDiff?: DiffOriginalParams['onDiff'];
|
|
21
|
+
[name: string]: any;
|
|
22
|
+
}
|
|
23
|
+
/** RenderField 的 props 多为表格运行期动态结构(column/config/record),保留显式 any */
|
|
24
|
+
export interface RenderFieldComparableProps {
|
|
25
|
+
text?: any;
|
|
26
|
+
record?: any;
|
|
27
|
+
index: number;
|
|
28
|
+
column?: any;
|
|
29
|
+
config?: any;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { InternalNamePath, NamePath } from 'antd/lib/form/interface';
|
|
2
|
+
import { GetOriginalValueParams, RenderFieldComparableProps } from './propsType';
|
|
3
|
+
/** 列配置中不应透传到 Form.Item 或 DOM 的字段(与 ProForm Render 保持一致) */
|
|
4
|
+
export declare const OMIT_FORM_ITEM_AND_DOM_KEYS: string[];
|
|
5
|
+
/** 克隆子元素时需从 props 中剔除,避免透传到 div 等 DOM 导致 React 警告 */
|
|
6
|
+
export declare const OMIT_FROM_DOM_KEYS: string[];
|
|
7
|
+
export declare const toNamePath: (name: NamePath) => InternalNamePath;
|
|
8
|
+
export declare const toNamePaths: (names: NamePath[]) => InternalNamePath[];
|
|
9
|
+
export declare const getOriginalValue: ({ namePath, originalName, originalNames, originalValues, rowKeyPath, form, }: GetOriginalValueParams) => {
|
|
10
|
+
originalValue: any;
|
|
11
|
+
originRecord: any;
|
|
12
|
+
record: any;
|
|
13
|
+
} | {
|
|
14
|
+
originalValue: any;
|
|
15
|
+
originRecord: any;
|
|
16
|
+
record?: undefined;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* memo 自定义比较函数:只有关键 props 变化时才重新渲染。
|
|
20
|
+
* 从 RenderField 拆出,行为与原内联实现保持一致。
|
|
21
|
+
*/
|
|
22
|
+
export declare const arePropsEqual: (prevProps: RenderFieldComparableProps, nextProps: RenderFieldComparableProps) => boolean;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import _isFunction from "lodash/isFunction";
|
|
2
|
+
import _isEqualWith from "lodash/isEqualWith";
|
|
3
|
+
import _isEqual from "lodash/isEqual";
|
|
4
|
+
import _get from "lodash/get";
|
|
5
|
+
import { customEqualForFunction } from "../../../utils";
|
|
6
|
+
import { getNamePath } from "../../utils/tools";
|
|
7
|
+
/** 列配置中不应透传到 Form.Item 或 DOM 的字段(与 ProForm Render 保持一致) */
|
|
8
|
+
export const OMIT_FORM_ITEM_AND_DOM_KEYS = ['format', 'toISOString', 'toCSTString', 'switchValue', 'precision', 'clearNotShow', 'dependNames', 'shouldCellUpdate' // 表格内部性能优化属性,不应传递给 Form.Item
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
/** 克隆子元素时需从 props 中剔除,避免透传到 div 等 DOM 导致 React 警告 */
|
|
12
|
+
export const OMIT_FROM_DOM_KEYS = ['disabled',
|
|
13
|
+
// div 不支持 disabled,且值可能非 string/number
|
|
14
|
+
'toCSTString', 'toISOString', 'format', 'switchValue', 'precision', 'clearNotShow', 'dependNames'];
|
|
15
|
+
export const toNamePath = name => {
|
|
16
|
+
if (Array.isArray(name)) {
|
|
17
|
+
return name;
|
|
18
|
+
}
|
|
19
|
+
return [name];
|
|
20
|
+
};
|
|
21
|
+
export const toNamePaths = names => {
|
|
22
|
+
return names.map(name => toNamePath(name));
|
|
23
|
+
};
|
|
24
|
+
export const getOriginalValue = ({
|
|
25
|
+
namePath,
|
|
26
|
+
originalName,
|
|
27
|
+
originalNames,
|
|
28
|
+
originalValues,
|
|
29
|
+
rowKeyPath,
|
|
30
|
+
form
|
|
31
|
+
}) => {
|
|
32
|
+
if (!originalValues) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
let originRecord;
|
|
36
|
+
if (rowKeyPath) {
|
|
37
|
+
const rowValueNamePath = namePath.slice(0, rowKeyPath.length - 1); // 表单中变动值所在行
|
|
38
|
+
const rowKeyName = rowKeyPath[rowKeyPath.length - 1]; // rowKey在行内的name
|
|
39
|
+
|
|
40
|
+
const rowValue = form.getFieldValue(rowValueNamePath);
|
|
41
|
+
if (!rowValue) return undefined;
|
|
42
|
+
const keyValue = rowValue[rowKeyName]; // 获取表单中rowKey值
|
|
43
|
+
if (!keyValue) return undefined;
|
|
44
|
+
const originalValueList = _get(originalValues, originalName.slice(0, rowKeyPath.length - 2));
|
|
45
|
+
originRecord = originalValueList?.find(item => {
|
|
46
|
+
return item[rowKeyPath[rowKeyPath.length - 1]] === keyValue;
|
|
47
|
+
});
|
|
48
|
+
let originalValue;
|
|
49
|
+
if (originalNames?.length) {
|
|
50
|
+
const originalNamesValue = originalNames.map(originalName => {
|
|
51
|
+
return _get(originRecord, originalName.slice(rowKeyPath.length - 1));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// 有可能出现数组中全是undefined的情况 视为没有值
|
|
55
|
+
const fillUndefined = originalNamesValue.every(valItem => valItem === undefined);
|
|
56
|
+
originalValue = fillUndefined ? undefined : originalNamesValue;
|
|
57
|
+
} else {
|
|
58
|
+
originalValue = _get(originRecord, originalName.slice(rowKeyPath.length - 1));
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
originalValue,
|
|
62
|
+
originRecord,
|
|
63
|
+
record: rowValue
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 这个方法是给editTable专用的 暂时不存在不传rowKey的情况
|
|
68
|
+
return {
|
|
69
|
+
originalValue: originalNames?.length ? originalNames.map(originalName => _get(originalValues, originalName)) : _get(originalValues, originalName),
|
|
70
|
+
originRecord
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* memo 自定义比较函数:只有关键 props 变化时才重新渲染。
|
|
76
|
+
* 从 RenderField 拆出,行为与原内联实现保持一致。
|
|
77
|
+
*/
|
|
78
|
+
export const arePropsEqual = (prevProps, nextProps) => {
|
|
79
|
+
const {
|
|
80
|
+
text: prevText,
|
|
81
|
+
record: prevRecord,
|
|
82
|
+
index: prevIndex,
|
|
83
|
+
column: prevColumn,
|
|
84
|
+
config: prevConfig
|
|
85
|
+
} = prevProps;
|
|
86
|
+
const {
|
|
87
|
+
text: nextText,
|
|
88
|
+
record: nextRecord,
|
|
89
|
+
index: nextIndex,
|
|
90
|
+
column: nextColumn,
|
|
91
|
+
config: nextConfig
|
|
92
|
+
} = nextProps;
|
|
93
|
+
|
|
94
|
+
// 函数型动态属性(component/editRender/fieldProps/rules/required/disabled/isEditable/valueType)
|
|
95
|
+
// 可能依赖行内兄弟字段;行对象为同引用(原地修改)时,浅比较无法区分,需强制重渲染
|
|
96
|
+
const hasFunctionDependency = _isFunction(prevColumn?.component) || _isFunction(prevColumn?.editRender) || _isFunction(prevColumn?.fieldProps) || _isFunction(prevColumn?.rules) || _isFunction(prevColumn?.required) || _isFunction(prevColumn?.disabled) || _isFunction(prevColumn?.isEditable) || _isFunction(prevColumn?.valueType) || _isFunction(nextColumn?.component) || _isFunction(nextColumn?.editRender) || _isFunction(nextColumn?.fieldProps) || _isFunction(nextColumn?.rules) || _isFunction(nextColumn?.required) || _isFunction(nextColumn?.disabled) || _isFunction(nextColumn?.isEditable) || _isFunction(nextColumn?.valueType);
|
|
97
|
+
if (hasFunctionDependency && prevRecord === nextRecord) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 构建新的参数格式(与 ProForm 保持一致)
|
|
102
|
+
const prevNamePath = getNamePath(prevConfig?.name, prevConfig?.virtualKey);
|
|
103
|
+
const nextNamePath = getNamePath(nextConfig?.name, nextConfig?.virtualKey);
|
|
104
|
+
const prevReactiveParams = {
|
|
105
|
+
form: prevConfig?.form,
|
|
106
|
+
index: prevIndex,
|
|
107
|
+
namePath: [...prevNamePath, prevIndex]
|
|
108
|
+
};
|
|
109
|
+
const nextReactiveParams = {
|
|
110
|
+
form: nextConfig?.form,
|
|
111
|
+
index: nextIndex,
|
|
112
|
+
namePath: [...nextNamePath, nextIndex]
|
|
113
|
+
};
|
|
114
|
+
if (_isFunction(prevColumn?.disabled) && _isFunction(nextColumn?.disabled)) {
|
|
115
|
+
if (prevColumn?.disabled(prevRecord, prevReactiveParams) !== nextColumn?.disabled(nextRecord, nextReactiveParams)) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// fieldProps 函数化直接更新,无法比对返回值是否一致
|
|
121
|
+
// 优化:不直接调用函数,而是比较输入参数(record 和 reactiveParams)
|
|
122
|
+
// 如果输入参数相同,fieldProps 的返回值应该相同(纯函数假设)
|
|
123
|
+
if (_isFunction(prevColumn?.fieldProps) && _isFunction(nextColumn?.fieldProps)) {
|
|
124
|
+
// 比较 record 数据是否变化
|
|
125
|
+
if (!_isEqualWith(prevRecord, nextRecord, customEqualForFunction)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
// reactiveParams 中的 form、index、namePath 已经在外层比较过了,无需重复比较
|
|
129
|
+
}
|
|
130
|
+
// fieldProps 为对象时,必须比较配置变化(如 dataSource 异步更新)
|
|
131
|
+
if (!_isFunction(prevColumn?.fieldProps) && !_isFunction(nextColumn?.fieldProps)) {
|
|
132
|
+
const isFieldPropsEqual = _isEqualWith(prevColumn?.fieldProps, nextColumn?.fieldProps, customEqualForFunction);
|
|
133
|
+
if (!isFieldPropsEqual) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 通用函数比较方法
|
|
139
|
+
// 优化:不直接调用函数,而是比较函数引用和输入参数
|
|
140
|
+
const compareFunctionResult = (prevCol, nextCol, prevValues, nextValues, _prevReactiveParams, _nextReactiveParams, functionName) => {
|
|
141
|
+
const prevFunc = prevCol?.[functionName];
|
|
142
|
+
const nextFunc = nextCol?.[functionName];
|
|
143
|
+
if (_isFunction(prevFunc) && _isFunction(nextFunc)) {
|
|
144
|
+
// 函数引用变化说明闭包可能捕获了新的外部状态(如异步加载的 list),必须重渲染
|
|
145
|
+
if (prevFunc !== nextFunc) return false;
|
|
146
|
+
// 同引用函数,比较输入参数(纯函数假设:输入相同则输出相同)
|
|
147
|
+
return _isEqualWith(prevValues, nextValues, customEqualForFunction);
|
|
148
|
+
}
|
|
149
|
+
return true; // 如果不是函数或只有一个是函数,认为相等
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// 使用简化后的比较逻辑
|
|
153
|
+
const functionFields = ['rules', 'component', 'desensitization', 'isEditable', 'required'];
|
|
154
|
+
if (functionFields.some(field => !compareFunctionResult(prevColumn, nextColumn, prevRecord, nextRecord, prevReactiveParams, nextReactiveParams, field))) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 基本props比较
|
|
159
|
+
if (!_isEqual(prevText, nextText) || prevIndex !== nextIndex) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 记录关键字段比较(避免完整对象比较的性能开销)
|
|
164
|
+
if (prevRecord?.rowKey !== nextRecord?.rowKey || prevRecord?._addFlag !== nextRecord?._addFlag) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 列配置比较(只比较关键字段),required,disabled为函数时,需要重新计算
|
|
169
|
+
if (prevColumn?.dataIndex !== nextColumn?.dataIndex || prevColumn?.type !== nextColumn?.type || prevColumn?.name !== nextColumn?.name) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// config中关键字段比较
|
|
174
|
+
if (prevConfig?._isEditing !== nextConfig?._isEditing || prevConfig?.mode !== nextConfig?.mode) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 当前单元格的值比较
|
|
179
|
+
const prevDataIndex = prevColumn?.dataIndex;
|
|
180
|
+
const nextDataIndex = nextColumn?.dataIndex;
|
|
181
|
+
if (prevDataIndex && nextDataIndex && prevDataIndex === nextDataIndex) {
|
|
182
|
+
if (prevRecord?.[prevDataIndex] !== nextRecord?.[nextDataIndex]) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 特殊处理:当使用自定义 component 函数时,比较整个 record 对象
|
|
188
|
+
// 因为自定义组件可能依赖 record 中的其他字段(不只是当前列的 dataIndex)
|
|
189
|
+
if (_isFunction(prevColumn?.component) || _isFunction(nextColumn?.component)) {
|
|
190
|
+
// 浅比较 record 的所有属性
|
|
191
|
+
const prevKeys = Object.keys(prevRecord || {});
|
|
192
|
+
const nextKeys = Object.keys(nextRecord || {});
|
|
193
|
+
if (prevKeys.length !== nextKeys.length) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 使用 some 方法代替 for 循环
|
|
198
|
+
if (prevKeys.some(key => prevRecord?.[key] !== nextRecord?.[key])) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
};
|
|
@@ -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) {
|