@hzab/edit-table 0.1.12 → 0.1.13-alpha.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/CHANGELOG.md +18 -0
- package/dist/common/useDynamicHeight.d.ts +8 -0
- package/dist/index.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +436 -261
- package/dist/index.js.map +1 -1
- package/dist/project-manifest.json +5 -5
- package/dist/project.json +3 -3
- package/package.json +5 -5
- package/src/Template/GetBaseTemplate.tsx +2 -2
- package/src/common/WidthWrapHoc.tsx +56 -0
- package/src/common/useDynamicHeight.ts +239 -0
- package/src/index.less +20 -2
- package/src/index.tsx +111 -56
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export const useDynamicHeight = (props) => {
|
|
4
|
+
const { wrapRef, rowHeight, setRows } = props
|
|
5
|
+
/** 高度上报按帧合并缓冲,一帧内多次上报只触发一次 state 更新 */
|
|
6
|
+
const pendingHeightsRef = useRef<any>({});
|
|
7
|
+
const flushHeightsRafRef = useRef<any>();
|
|
8
|
+
/** 用户滚动位置记忆:持续跟踪,供编辑/重建后恢复 */
|
|
9
|
+
const scrollMemoryRef = useRef({ top: 0, left: 0 });
|
|
10
|
+
/** 滚动重置保护窗口截止时间戳:窗口内"回到顶部/最左"的程序化重置被纠正回记忆位置 */
|
|
11
|
+
const scrollProtectUntilRef = useRef(0);
|
|
12
|
+
/** rows 的镜像 ref:updater 执行时机不确定,编辑等纯计算需读取即时值 */
|
|
13
|
+
const rowsRef = useRef<any>([]);
|
|
14
|
+
/** 附件等动态行高:rowId -> { [columnId]: height },同行多附件列时取最大值 */
|
|
15
|
+
const [rowHeights, setRowHeights] = useState<any>({});
|
|
16
|
+
/** 编辑提交时开启的长保护窗(ms),覆盖 onEditSubmit → getList 全量刷新 → rows 重建的完整链路 */
|
|
17
|
+
const SCROLL_RESET_GUARD_MS = 3000;
|
|
18
|
+
|
|
19
|
+
const armScrollProtect = useCallback((duration = 500) => {
|
|
20
|
+
const el = wrapRef.current;
|
|
21
|
+
if (!el) return;
|
|
22
|
+
scrollMemoryRef.current = { top: el.scrollTop, left: el.scrollLeft };
|
|
23
|
+
// 只延长不缩短:编辑链路开的 3000ms 长窗不能被 rows 重建 effect 的 500ms 短窗覆盖
|
|
24
|
+
scrollProtectUntilRef.current = Math.max(
|
|
25
|
+
scrollProtectUntilRef.current || 0,
|
|
26
|
+
Date.now() + duration,
|
|
27
|
+
);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const restoreScroll = useCallback(() => {
|
|
31
|
+
const el = wrapRef.current;
|
|
32
|
+
if (!el) return;
|
|
33
|
+
const { top, left } = scrollMemoryRef.current;
|
|
34
|
+
// 双层 rAF:等 ReactGrid componentDidUpdate 的聚焦滚动写入先执行,再恢复用户位置
|
|
35
|
+
requestAnimationFrame(() => {
|
|
36
|
+
requestAnimationFrame(() => {
|
|
37
|
+
if (el.scrollTop !== top) el.scrollTop = top;
|
|
38
|
+
if (el.scrollLeft !== left) el.scrollLeft = left;
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}, []);
|
|
42
|
+
/**
|
|
43
|
+
* reactgrid 每次 pointerdown(点击单元格)都会对隐藏的键盘捕获 input(.rg-hidden-element)
|
|
44
|
+
* 调用 focus();该元素 position:fixed,静态位置落在滚动内容末尾(视口下边缘之外),
|
|
45
|
+
* 浏览器原生 scroll-into-view 会把表格滚动容器往下带一点(数据多到出现滚动条时可见)。
|
|
46
|
+
* 此处统一改为 preventScroll,焦点行为不变,仅禁止滚动。
|
|
47
|
+
*/
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const root = wrapRef.current;
|
|
50
|
+
if (!root) return;
|
|
51
|
+
const patchHiddenFocus = () => {
|
|
52
|
+
root.querySelectorAll(".rg-hidden-element").forEach((el: any) => {
|
|
53
|
+
if (el.__focusScrollPatched) return;
|
|
54
|
+
el.__focusScrollPatched = true;
|
|
55
|
+
const rawFocus = el.focus.bind(el);
|
|
56
|
+
el.focus = (options?: FocusOptions) => rawFocus({ preventScroll: true, ...(options || {}) });
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
patchHiddenFocus();
|
|
60
|
+
const mo = new MutationObserver(patchHiddenFocus);
|
|
61
|
+
mo.observe(root, { childList: true, subtree: true });
|
|
62
|
+
return () => mo.disconnect();
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 滚动守卫:点击单元格时 reactgrid 会在 componentDidUpdate 里程序化写 scrollTop
|
|
67
|
+
* 做"聚焦行跟随"(scrollIntoView)。动态行高在编辑态切换时被反复重新测量、行高收敛
|
|
68
|
+
* 过程中每次状态更新都会触发一次 1~2px 的修正性滚动(focus-scroll-into-view 的效果)。
|
|
69
|
+
* 用户滚轮/触摸滚动是原生行为、不经过 JS scrollTop 赋值,因此这里拦截元素上的
|
|
70
|
+
* scrollTop/scrollTo/scrollBy 写入只影响程序化滚动:忽略 ≤3px 的微量修正,
|
|
71
|
+
* 大步长滚动(方向键导航等)不受影响。
|
|
72
|
+
*/
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const el: any = wrapRef.current;
|
|
75
|
+
if (!el || el.__scrollGuard) return;
|
|
76
|
+
el.__scrollGuard = true;
|
|
77
|
+
const desc = Object.getOwnPropertyDescriptor(Element.prototype, "scrollTop") as PropertyDescriptor;
|
|
78
|
+
const MAX_CORRECTION = 3;
|
|
79
|
+
|
|
80
|
+
Object.defineProperty(el, "scrollTop", {
|
|
81
|
+
configurable: true,
|
|
82
|
+
get() {
|
|
83
|
+
return desc.get!.call(el);
|
|
84
|
+
},
|
|
85
|
+
set(v: any) {
|
|
86
|
+
const next = Number(v) || 0;
|
|
87
|
+
const cur = desc.get!.call(el);
|
|
88
|
+
const delta = next - cur;
|
|
89
|
+
if (delta !== 0 && Math.abs(delta) <= MAX_CORRECTION) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (next === 0 && cur > 50) {
|
|
93
|
+
console.warn("[CellEditTable] 检测到程序化滚动回顶部: ", cur, "->", next, new Error().stack);
|
|
94
|
+
}
|
|
95
|
+
desc.set!.call(el, next);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const rawScrollTo = el.scrollTo.bind(el);
|
|
100
|
+
el.scrollTo = (x: any, y?: any) => {
|
|
101
|
+
if (typeof x === "object" && x !== null) {
|
|
102
|
+
if (x.top != null) {
|
|
103
|
+
const cur = desc.get!.call(el);
|
|
104
|
+
const delta = Number(x.top) - cur;
|
|
105
|
+
if (delta !== 0 && Math.abs(delta) <= MAX_CORRECTION) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return rawScrollTo(x);
|
|
110
|
+
}
|
|
111
|
+
if (typeof x === "number" && typeof y === "number") {
|
|
112
|
+
const cur = desc.get!.call(el);
|
|
113
|
+
const delta = y - cur;
|
|
114
|
+
if (delta !== 0 && Math.abs(delta) <= MAX_CORRECTION) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return rawScrollTo(x, y);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const rawScrollBy = el.scrollBy.bind(el);
|
|
122
|
+
el.scrollBy = (x: any, y?: any) => {
|
|
123
|
+
if (typeof x === "object" && x !== null) {
|
|
124
|
+
if (x.top != null && Math.abs(Number(x.top)) <= MAX_CORRECTION) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
return rawScrollBy(x);
|
|
128
|
+
}
|
|
129
|
+
if (typeof x === "number" && typeof y === "number" && Math.abs(y) <= MAX_CORRECTION) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
return rawScrollBy(x, y);
|
|
133
|
+
};
|
|
134
|
+
}, []);
|
|
135
|
+
/**
|
|
136
|
+
* 滚动位置记忆与重置纠正:持续跟踪用户滚动位置;
|
|
137
|
+
* 保护窗口内若出现"回到顶部/最左"式跳变(scrollTop 归 0 且记忆位置较深),
|
|
138
|
+
* 视为 ReactGrid/浏览器的程序化重置,立即纠正回记忆位置。
|
|
139
|
+
* 只拦截归零式重置,不影响窗口内的正常滚动与键盘导航滚动。
|
|
140
|
+
*/
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const el: any = wrapRef.current;
|
|
143
|
+
if (!el) return;
|
|
144
|
+
const onScroll = () => {
|
|
145
|
+
if (
|
|
146
|
+
Date.now() < scrollProtectUntilRef.current &&
|
|
147
|
+
((el.scrollTop === 0 && scrollMemoryRef.current.top > 50) ||
|
|
148
|
+
(el.scrollLeft === 0 && scrollMemoryRef.current.left > 50))
|
|
149
|
+
) {
|
|
150
|
+
el.scrollTop = scrollMemoryRef.current.top;
|
|
151
|
+
el.scrollLeft = scrollMemoryRef.current.left;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
scrollMemoryRef.current = { top: el.scrollTop, left: el.scrollLeft };
|
|
155
|
+
};
|
|
156
|
+
el.addEventListener("scroll", onScroll, true);
|
|
157
|
+
return () => el.removeEventListener("scroll", onScroll, true);
|
|
158
|
+
}, []);
|
|
159
|
+
const flushPendingHeights = useCallback(() => {
|
|
160
|
+
flushHeightsRafRef.current = undefined;
|
|
161
|
+
const pending = pendingHeightsRef.current;
|
|
162
|
+
if (Object.keys(pending).length === 0) return;
|
|
163
|
+
pendingHeightsRef.current = {};
|
|
164
|
+
setRowHeights((prev: any) => {
|
|
165
|
+
let next = prev;
|
|
166
|
+
for (const rowId in pending) {
|
|
167
|
+
const cols = { ...(next[rowId] || {}) };
|
|
168
|
+
let rowChanged = false;
|
|
169
|
+
for (const colId in pending[rowId]) {
|
|
170
|
+
const h = pending[rowId][colId];
|
|
171
|
+
if (cols[colId] !== h) {
|
|
172
|
+
cols[colId] = h;
|
|
173
|
+
rowChanged = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (rowChanged) {
|
|
177
|
+
if (next === prev) next = { ...prev };
|
|
178
|
+
next[rowId] = cols;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return next === prev ? prev : next;
|
|
182
|
+
});
|
|
183
|
+
}, []);
|
|
184
|
+
|
|
185
|
+
/** 供单元格(如 Upload 附件)回传内容高度,撑开/恢复所在行高 */
|
|
186
|
+
const handleRowHeightChange = useCallback(
|
|
187
|
+
(rowId: any, height?: number, columnId?: string) => {
|
|
188
|
+
const h = Math.max(Number(height) || 0, rowHeight);
|
|
189
|
+
const rId = String(rowId);
|
|
190
|
+
const cId = columnId || "_";
|
|
191
|
+
const row = (pendingHeightsRef.current[rId] = pendingHeightsRef.current[rId] || {});
|
|
192
|
+
if (row[cId] === h) return;
|
|
193
|
+
row[cId] = h;
|
|
194
|
+
if (!flushHeightsRafRef.current) {
|
|
195
|
+
flushHeightsRafRef.current = requestAnimationFrame(flushPendingHeights);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
[rowHeight, flushPendingHeights],
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
/** 组件卸载时取消未执行的合帧刷新 */
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
return () => {
|
|
204
|
+
if (flushHeightsRafRef.current) cancelAnimationFrame(flushHeightsRafRef.current);
|
|
205
|
+
};
|
|
206
|
+
}, []);
|
|
207
|
+
|
|
208
|
+
/** 行高变化时仅补丁式更新受影响行的 height,避免整表重建 */
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
setRows((prev: any) => {
|
|
211
|
+
if (!Array.isArray(prev) || prev.length === 0) return prev;
|
|
212
|
+
let changed = false;
|
|
213
|
+
const next = prev.map((row) => {
|
|
214
|
+
// 表头行高度固定,不参与动态行高
|
|
215
|
+
if (typeof row?.rowId === "string" && row.rowId.indexOf("header-") === 0) return row;
|
|
216
|
+
const heightMap = rowHeights[row.rowId];
|
|
217
|
+
const h =
|
|
218
|
+
heightMap && Object.keys(heightMap).length > 0
|
|
219
|
+
? Math.max(rowHeight, ...Object.values(heightMap).map(Number))
|
|
220
|
+
: rowHeight;
|
|
221
|
+
if (row.height === h) return row;
|
|
222
|
+
changed = true;
|
|
223
|
+
return { ...row, height: h };
|
|
224
|
+
});
|
|
225
|
+
const result = changed ? next : prev;
|
|
226
|
+
rowsRef.current = result;
|
|
227
|
+
return result;
|
|
228
|
+
});
|
|
229
|
+
}, [rowHeights, rowHeight]);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
handleRowHeightChange,
|
|
233
|
+
armScrollProtect,
|
|
234
|
+
restoreScroll,
|
|
235
|
+
SCROLL_RESET_GUARD_MS,
|
|
236
|
+
rowsRef,
|
|
237
|
+
rowHeights
|
|
238
|
+
}
|
|
239
|
+
}
|
package/src/index.less
CHANGED
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
|
|
36
36
|
.rg-cell {
|
|
37
37
|
padding: 0px !important;
|
|
38
|
+
/** hidden 兜底,clip 不建立滚动容器:单元格内部 focus 时浏览器不会再把它内部滚动 */
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
overflow: clip;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
.rg-cell-focus {
|
|
@@ -48,18 +51,33 @@
|
|
|
48
51
|
|
|
49
52
|
.base-hoc {
|
|
50
53
|
width: 100%;
|
|
51
|
-
|
|
54
|
+
/** 高度由内容决定(换行内容撑开),但不小于单元格高度;
|
|
55
|
+
border-box 保证 scrollHeight 恒等于"内容+padding",行高测量不会自我反馈 */
|
|
56
|
+
height: auto;
|
|
57
|
+
min-height: 100%;
|
|
58
|
+
box-sizing: border-box;
|
|
52
59
|
display: flex;
|
|
53
60
|
align-items: center;
|
|
54
61
|
justify-content: center;
|
|
55
62
|
z-index: 1;
|
|
56
|
-
|
|
63
|
+
}
|
|
64
|
+
.rg-cell > .base-hoc {
|
|
65
|
+
/** 单元格内边距(回显态与编辑态一致;行高测量会按此 padding 补偿,见 WidthWrapHoc) */
|
|
66
|
+
padding: 8px;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
.base-hoc-edit {
|
|
60
70
|
background-color: #fff;
|
|
61
71
|
}
|
|
62
72
|
|
|
73
|
+
/** 组件回显/编辑内容容器:纯内容高度,不设 min-height(保证高度测量双向真实) */
|
|
74
|
+
.base-hoc-inner {
|
|
75
|
+
width: 100%;
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
}
|
|
80
|
+
|
|
63
81
|
.validator-icon {
|
|
64
82
|
position: relative;
|
|
65
83
|
background: #fff;
|
package/src/index.tsx
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
2
10
|
import { Checkbox, Empty, Spin } from "antd";
|
|
3
11
|
import { ReactGrid } from "@silevis/reactgrid";
|
|
4
12
|
import TextTemplate from "./Template/TextCellTemplate";
|
|
@@ -9,6 +17,7 @@ import { ExpandListTemplate } from "./Template/ExpandListTemplate";
|
|
|
9
17
|
import { convertColumnsToRows, flattenColumns, isReactElement } from "./common/utils";
|
|
10
18
|
import { childAntdComponent } from "./common/childAntdComponent";
|
|
11
19
|
import { GetBaseTemplate } from "./Template/GetBaseTemplate";
|
|
20
|
+
import { useDynamicHeight } from "./common/useDynamicHeight";
|
|
12
21
|
import "./index.less";
|
|
13
22
|
import "@silevis/reactgrid/styles.css";
|
|
14
23
|
|
|
@@ -66,9 +75,26 @@ function CellEditTable(props: CellEditTableProps, ref: any) {
|
|
|
66
75
|
const [headerCustomTemplate, setHeaderCustomTemplate] = useState<any>([]);
|
|
67
76
|
const [highlights, setHighlights] = useState([]);
|
|
68
77
|
const highlightsRowIdRef = useRef();
|
|
69
|
-
|
|
70
78
|
const gridRef = useRef<any>();
|
|
79
|
+
const wrapRef = useRef<HTMLDivElement>(null);
|
|
71
80
|
const currentCell = useRef<any>();
|
|
81
|
+
/** 展开态镜像 ref:dataSource 同步 effect 读取最新 expandKeys 用(不加进 deps,
|
|
82
|
+
* 避免展开/收起时又触发全量同步——展开/收起由 ExpandListTemplate 直接维护 newDataSource) */
|
|
83
|
+
const expandKeysRef = useRef<any>([]);
|
|
84
|
+
expandKeysRef.current = expandKeys;
|
|
85
|
+
/** 动态测量单元格高度及滚动逻辑 */
|
|
86
|
+
const {
|
|
87
|
+
handleRowHeightChange,
|
|
88
|
+
armScrollProtect,
|
|
89
|
+
restoreScroll,
|
|
90
|
+
SCROLL_RESET_GUARD_MS,
|
|
91
|
+
rowsRef,
|
|
92
|
+
rowHeights,
|
|
93
|
+
} = useDynamicHeight({
|
|
94
|
+
wrapRef,
|
|
95
|
+
rowHeight,
|
|
96
|
+
setRows,
|
|
97
|
+
});
|
|
72
98
|
|
|
73
99
|
useImperativeHandle(ref, () => {
|
|
74
100
|
return {
|
|
@@ -84,10 +110,28 @@ function CellEditTable(props: CellEditTableProps, ref: any) {
|
|
|
84
110
|
}
|
|
85
111
|
});
|
|
86
112
|
};
|
|
113
|
+
|
|
87
114
|
/** 洗数据 */
|
|
88
115
|
useEffect(() => {
|
|
89
116
|
handleDataSource(dataSource);
|
|
90
|
-
|
|
117
|
+
// 同步时必须保留展开态:把已展开父行的 children 重新插入到对应位置。
|
|
118
|
+
// 直接 setNewDataSource(dataSource) 会把列表重置为仅父行,子行瞬间从矩阵消失,
|
|
119
|
+
// ReactGrid 内部 validateLocation(focusedLocation) 在 rowId 查不到时会被
|
|
120
|
+
// idx-clamp 永久替换成原索引处的其他行,导致聚焦框/整行高亮错位。
|
|
121
|
+
const flattenWithExpansion = (arr: any) => {
|
|
122
|
+
const result: any = [];
|
|
123
|
+
const loop = (list: any) => {
|
|
124
|
+
for (const item of list || []) {
|
|
125
|
+
result.push(item);
|
|
126
|
+
if (item?.children?.length && expandKeysRef.current?.includes(item?.[rowKey])) {
|
|
127
|
+
loop(item.children);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
loop(arr);
|
|
132
|
+
return result;
|
|
133
|
+
};
|
|
134
|
+
setNewDataSource(flattenWithExpansion(dataSource));
|
|
91
135
|
}, [dataSource]);
|
|
92
136
|
|
|
93
137
|
/** 判断当前选中行是否还存在 */
|
|
@@ -182,9 +226,14 @@ function CellEditTable(props: CellEditTableProps, ref: any) {
|
|
|
182
226
|
const columnsIds = tableColumns?.map((el) => el?.columnId);
|
|
183
227
|
|
|
184
228
|
const tempDataSource = newDataSource?.map((item, index) => {
|
|
229
|
+
const heightMap = rowHeights[item[rowKey]];
|
|
230
|
+
const dynamicRowHeight =
|
|
231
|
+
heightMap && Object.keys(heightMap).length > 0
|
|
232
|
+
? Math.max(rowHeight, ...Object.values(heightMap).map(Number))
|
|
233
|
+
: rowHeight;
|
|
185
234
|
return {
|
|
186
235
|
rowId: item[rowKey],
|
|
187
|
-
height:
|
|
236
|
+
height: dynamicRowHeight,
|
|
188
237
|
cells: columnsIds?.map((el, ind) => {
|
|
189
238
|
let newCell = {};
|
|
190
239
|
const columnsItem = tableColumns[ind];
|
|
@@ -227,72 +276,73 @@ function CellEditTable(props: CellEditTableProps, ref: any) {
|
|
|
227
276
|
rowKey,
|
|
228
277
|
expandKeys,
|
|
229
278
|
Slots,
|
|
279
|
+
onRowHeightChange: handleRowHeightChange,
|
|
230
280
|
...newCell,
|
|
231
281
|
};
|
|
232
282
|
}),
|
|
233
283
|
};
|
|
234
284
|
});
|
|
235
285
|
const mergerList = [...headerList, ...tempDataSource];
|
|
286
|
+
// 整表重建会触发 ReactGrid 内部程序化写 scrollTop(滚动回顶部):
|
|
287
|
+
// 重建前记录用户滚动位置并开启保护窗口,重建完成后双层 rAF 恢复;
|
|
288
|
+
// 窗口内若再发生归零式重置,由 scroll 监听纠正回记忆位置
|
|
289
|
+
rowsRef.current = mergerList;
|
|
290
|
+
armScrollProtect();
|
|
236
291
|
setRows(mergerList);
|
|
292
|
+
restoreScroll();
|
|
293
|
+
// 注:此处读取的 rowHeights 为本次渲染时的快照;行高的后续增量变化由下方补丁 effect 处理,避免全量重建
|
|
237
294
|
}, [newDataSource, tableColumns, headerList]);
|
|
238
|
-
|
|
239
|
-
if (loading) {
|
|
240
|
-
const item = rows?.find((el) => el?.rowId == currentCell.current?.rowId);
|
|
241
|
-
let newItem = item?.cells?.find((it) => it?.columnId == currentCell.current?.columnId);
|
|
242
|
-
newItem = { ...currentCell.current };
|
|
243
|
-
initRowsRef.current = rows;
|
|
244
|
-
}
|
|
245
|
-
}, [loading, rows]);
|
|
295
|
+
|
|
246
296
|
// 单元格编辑回调
|
|
247
297
|
const handleCellsChanged = async (changes) => {
|
|
248
|
-
const batchQuery: any = [];
|
|
249
298
|
if (!Array.isArray(changes) || changes.length === 0) return;
|
|
250
|
-
|
|
251
|
-
let isBatchSubmit = false;
|
|
252
|
-
if (changes?.length > 1) {
|
|
253
|
-
isBatchSubmit = true;
|
|
254
|
-
}
|
|
299
|
+
const batchQuery: any = [];
|
|
255
300
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
} catch (err) {
|
|
285
|
-
getList && getList();
|
|
286
|
-
}
|
|
301
|
+
// 基于 rowsRef 当前值在 updater 外纯计算(updater 执行时机不确定且可能执行两次,
|
|
302
|
+
// 不能在 updater 内收集提交数据),一次性生成新数组后同步 rowsRef 再 setRows
|
|
303
|
+
const prevRows = rowsRef.current || [];
|
|
304
|
+
const nextRows = prevRows.map((row) => ({ ...row, cells: [...row.cells] }));
|
|
305
|
+
changes.forEach((change) => {
|
|
306
|
+
const changeRowIdx = nextRows.findIndex((el) => el.rowId == change.rowId);
|
|
307
|
+
const changeColumnIdx = tableColumns.findIndex((el) => el.columnId == change.columnId);
|
|
308
|
+
if (changeRowIdx === -1 || changeColumnIdx === -1) return;
|
|
309
|
+
nextRows[changeRowIdx].cells[changeColumnIdx] = { ...change.newCell };
|
|
310
|
+
currentCell.current = { ...change.newCell };
|
|
311
|
+
batchQuery.push(handleArrayToObj(nextRows[changeRowIdx]));
|
|
312
|
+
if (formulaConfig) {
|
|
313
|
+
for (const key in formulaConfig) {
|
|
314
|
+
const item = formulaConfig[key];
|
|
315
|
+
const resultColumnIdx = tableColumns.findIndex((el) => el.columnId == key);
|
|
316
|
+
if (resultColumnIdx === -1) continue;
|
|
317
|
+
const newCell = {
|
|
318
|
+
...nextRows[changeRowIdx].cells[resultColumnIdx],
|
|
319
|
+
text: item?.setCellData && item?.setCellData(nextRows[changeRowIdx]),
|
|
320
|
+
value: item?.setCellData && item?.setCellData(nextRows[changeRowIdx]),
|
|
321
|
+
style:
|
|
322
|
+
item?.setCellStyle &&
|
|
323
|
+
item?.setCellStyle(
|
|
324
|
+
(item?.setCellData && item?.setCellData(nextRows[changeRowIdx])) ||
|
|
325
|
+
nextRows[changeRowIdx],
|
|
326
|
+
),
|
|
327
|
+
};
|
|
328
|
+
nextRows[changeRowIdx].cells[resultColumnIdx] = newCell;
|
|
287
329
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return [...prevRows];
|
|
330
|
+
}
|
|
331
|
+
onChange && onChange(nextRows[changeRowIdx]);
|
|
291
332
|
});
|
|
333
|
+
rowsRef.current = nextRows;
|
|
334
|
+
// 编辑提交后 list-render 的 onEditSubmit 会全量 getList 刷新并触发 rows 重建,
|
|
335
|
+
// 期间任意时刻都可能发生程序化回顶部:开启长保护窗,覆盖 提交→refetch→重建 全链路
|
|
336
|
+
armScrollProtect(SCROLL_RESET_GUARD_MS);
|
|
337
|
+
setRows(nextRows);
|
|
338
|
+
restoreScroll();
|
|
292
339
|
|
|
293
|
-
|
|
340
|
+
// 提交成功后由 list-render 的 onEditSubmit 全量刷新列表;
|
|
341
|
+
// 滚动位置由 rows 重建 effect 中的快照/恢复机制保持;
|
|
342
|
+
// 失败时仍 getList() 回滚恢复
|
|
343
|
+
if (onEditSubmit) {
|
|
294
344
|
try {
|
|
295
|
-
|
|
345
|
+
await onEditSubmit(batchQuery);
|
|
296
346
|
} catch (err) {
|
|
297
347
|
getList && getList();
|
|
298
348
|
}
|
|
@@ -435,10 +485,15 @@ function CellEditTable(props: CellEditTableProps, ref: any) {
|
|
|
435
485
|
|
|
436
486
|
return (
|
|
437
487
|
<Spin spinning={loading}>
|
|
438
|
-
<div className={`grid-container ${className}`} style={reactGridStyle}>
|
|
488
|
+
<div ref={wrapRef} className={`grid-container ${className}`} style={reactGridStyle}>
|
|
439
489
|
<ReactGrid
|
|
440
490
|
ref={gridRef}
|
|
441
|
-
rows
|
|
491
|
+
// 直接渲染 rows state:loading 期间它本就持有完整数据(编辑提交时已 setRows)。
|
|
492
|
+
// 历史上曾用 initRowsRef 快照在 loading 期间代替 rows,但该 ref 由 effect 写入、
|
|
493
|
+
// 晚于渲染消费——loading 翻 true 的当次渲染读到的是过期快照(行数少于当前),
|
|
494
|
+
// 矩阵瞬态缩小、展开子行消失,ReactGrid 内部 validateLocation 的 idx-clamp
|
|
495
|
+
// 会把 focusedLocation 永久替换成错误行(聚焦框/整行高亮错位)。
|
|
496
|
+
rows={rows}
|
|
442
497
|
key={dataSource?.length == 0 ? 0 : 1}
|
|
443
498
|
columns={tableColumns}
|
|
444
499
|
onCellsChanged={handleCellsChanged}
|