@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.
@@ -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,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 { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
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
- return /*#__PURE__*/_jsx(_Fragment, {
11
- children: name && virtualKey ? /*#__PURE__*/_jsx(Form.Item, {
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);
@@ -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
- const tableBody = tableRef.current?.children[0]?.querySelector(`.${prefixCls}-table-body`);
227
- if (tableBody) {
228
- // 等待新行渲染完毕再读取 scrollHeight,确保能滚动到真正的底部
227
+ if (virtual) {
228
+ // 虚拟表格无 .ant-table-body 原生滚动容器,使用 antd Table ref 的 scrollTo 按行索引定位
229
+ // 不传 align:新增行在视口外时虚拟列表会自动贴边(尾部插入贴底、头部插入贴顶)
229
230
  requestAnimationFrame(() => {
230
- tableBody.scrollTo?.({
231
- top: insertType === 'before' ? 0 : tableBody.scrollHeight,
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) {