zmdms-webui 2.3.8 → 2.4.0

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.
@@ -155,7 +155,6 @@ function CanvasTable(props) {
155
155
  }), maxScrollTop = _1.maxScrollTop, maxScrollLeft = _1.maxScrollLeft;
156
156
  // 滚动管理(使用预先计算的maxScrollTop和maxScrollLeft)
157
157
  var _2 = useTableScroll({
158
- canvasRef: canvasRef,
159
158
  containerRef: containerRef,
160
159
  maxScrollTop: maxScrollTop,
161
160
  maxScrollLeft: maxScrollLeft,
@@ -269,9 +268,10 @@ function CanvasTable(props) {
269
268
  return processedColumns.filter(function (item) { return !item.key.includes("__"); });
270
269
  }, [processedColumns]);
271
270
  // Excel导出功能
272
- var exportExcel = useExcelExport(processedDataSource, {
271
+ var exportExcel = useExcelExport(newDataSource, {
273
272
  columns: exportColumns,
274
273
  isAutoMerge: isAutoMerge,
274
+ summaryConfig: [],
275
275
  });
276
276
  // 交互事件处理(使用baseScrollbarMetrics的maxScrollTop/maxScrollLeft以保持稳定)
277
277
  var _5 = useTableInteraction({
@@ -90,28 +90,38 @@ var useCopyToClipboard = function (params) {
90
90
  }
91
91
  return rows.join("\n");
92
92
  });
93
- // 处理复制事件
94
- var handleCopy = useMemoizedFn(function (e) {
95
- var _a;
96
- if (!cellSelection)
97
- return;
98
- var text = getSelectedCellsText();
99
- if (text) {
100
- e.preventDefault();
101
- (_a = e.clipboardData) === null || _a === void 0 ? void 0 : _a.setData("text/plain", text);
93
+ // 兼容的复制到剪贴板函数(支持HTTP环境)
94
+ var copyToClipboard = useMemoizedFn(function (text) {
95
+ // 优先使用现代 clipboard API(仅在HTTPS/localhost下可用)
96
+ if (navigator.clipboard && window.isSecureContext) {
97
+ navigator.clipboard.writeText(text).catch(function (err) {
98
+ console.error("Failed to copy with clipboard API:", err);
99
+ });
100
+ return true;
101
+ }
102
+ // 降级方案:使用 execCommand(支持HTTP环境)
103
+ try {
104
+ var textarea = document.createElement("textarea");
105
+ textarea.value = text;
106
+ textarea.style.position = "fixed";
107
+ textarea.style.opacity = "0";
108
+ textarea.style.pointerEvents = "none";
109
+ document.body.appendChild(textarea);
110
+ textarea.select();
111
+ textarea.setSelectionRange(0, textarea.value.length);
112
+ var success = document.execCommand("copy");
113
+ document.body.removeChild(textarea);
114
+ if (!success) {
115
+ console.error("Failed to copy using execCommand");
116
+ }
117
+ return success;
118
+ }
119
+ catch (err) {
120
+ console.error("Failed to copy:", err);
121
+ return false;
102
122
  }
103
123
  });
104
- // 监听复制事件
105
- useEffect(function () {
106
- var container = containerRef.current;
107
- if (!container)
108
- return;
109
- container.addEventListener("copy", handleCopy);
110
- return function () {
111
- container.removeEventListener("copy", handleCopy);
112
- };
113
- }, [handleCopy, containerRef]);
114
- // 监听键盘事件(Ctrl+C / Cmd+C)- 只在表格容器内部处理
124
+ // 监听键盘事件(Ctrl+C / Cmd+C)处理复制
115
125
  useEffect(function () {
116
126
  var container = containerRef.current;
117
127
  if (!container)
@@ -125,25 +135,21 @@ var useCopyToClipboard = function (params) {
125
135
  if (isInContainer) {
126
136
  e.preventDefault(); // 阻止默认复制行为
127
137
  var text = getSelectedCellsText();
128
- if (text && navigator.clipboard) {
129
- navigator.clipboard.writeText(text).catch(function (err) {
130
- console.error("Failed to copy:", err);
131
- });
138
+ if (text) {
139
+ copyToClipboard(text);
132
140
  }
133
141
  }
134
142
  }
135
143
  };
136
- // 监听容器的keydown事件,而不是全局window事件
137
- container.addEventListener("keydown", handleKeyDown);
138
144
  // 同时也监听全局事件,但会检查焦点位置
139
145
  document.addEventListener("keydown", handleKeyDown);
140
146
  return function () {
141
- container.removeEventListener("keydown", handleKeyDown);
142
147
  document.removeEventListener("keydown", handleKeyDown);
143
148
  };
144
- }, [cellSelection, getSelectedCellsText, containerRef]);
149
+ }, [cellSelection, getSelectedCellsText, copyToClipboard, containerRef]);
145
150
  return {
146
151
  getSelectedCellsText: getSelectedCellsText,
152
+ copyToClipboard: copyToClipboard,
147
153
  };
148
154
  };
149
155
 
@@ -79,12 +79,11 @@ var useScrollbarMetrics = function (params) {
79
79
  * 表格滚动管理 Hook
80
80
  *
81
81
  * 处理滚动相关的所有事件:
82
- * - Canvas 滚轮事件(使用 RAF 节流)
83
- * - Container 滚轮事件(阻止默认行为)
82
+ * - Container 滚轮事件(统一处理滚动,支持CellOverlay等覆盖层)
84
83
  * - 全局鼠标事件(拖拽滚动条)
85
84
  */
86
85
  var useTableScroll = function (params) {
87
- var canvasRef = params.canvasRef, containerRef = params.containerRef, maxScrollTop = params.maxScrollTop, maxScrollLeft = params.maxScrollLeft, onScroll = params.onScroll;
86
+ var containerRef = params.containerRef, maxScrollTop = params.maxScrollTop, maxScrollLeft = params.maxScrollLeft, onScroll = params.onScroll;
88
87
  var _a = useState({
89
88
  scrollLeft: 0,
90
89
  scrollTop: 0,
@@ -98,14 +97,23 @@ var useTableScroll = function (params) {
98
97
  // 用于节流滚轮事件的refs
99
98
  var rafIdRef = useRef(null);
100
99
  var pendingDeltaRef = useRef({ deltaX: 0, deltaY: 0 });
101
- // 滚轮事件处理(Canvas)- 使用 requestAnimationFrame 节流
100
+ // 注意:滚轮事件统一在Container级别处理,以确保在CellOverlay等覆盖层上也能正常滚动
101
+ // 滚轮事件处理(Container)
102
102
  useEffect(function () {
103
- var canvas = canvasRef.current;
104
- if (!canvas)
103
+ var container = containerRef.current;
104
+ if (!container)
105
105
  return;
106
- var handleNativeWheel = function (e) {
106
+ var handleNativeContainerWheel = function (e) {
107
+ // 检查事件是否来自过滤弹框,如果是,允许滚动
108
+ var target = e.target;
109
+ var isInFilterPopover = target.closest(".canvas-table-filter-popover");
110
+ if (isInFilterPopover) {
111
+ // 允许过滤弹框内部滚动
112
+ return;
113
+ }
107
114
  e.preventDefault();
108
115
  e.stopPropagation();
116
+ // 当鼠标在容器内任何位置(包括CellOverlay)时都能滚动
109
117
  // 累积滚动增量
110
118
  pendingDeltaRef.current.deltaX += e.deltaX;
111
119
  pendingDeltaRef.current.deltaY += e.deltaY;
@@ -134,39 +142,18 @@ var useTableScroll = function (params) {
134
142
  });
135
143
  });
136
144
  };
137
- canvas.addEventListener("wheel", handleNativeWheel, { passive: false });
145
+ container.addEventListener("wheel", handleNativeContainerWheel, {
146
+ passive: false,
147
+ });
138
148
  return function () {
139
- canvas.removeEventListener("wheel", handleNativeWheel);
149
+ container.removeEventListener("wheel", handleNativeContainerWheel);
140
150
  // 清理pending的动画帧
141
151
  if (rafIdRef.current !== null) {
142
152
  cancelAnimationFrame(rafIdRef.current);
143
153
  rafIdRef.current = null;
144
154
  }
145
155
  };
146
- }, [maxScrollTop, maxScrollLeft, onScroll, canvasRef, setScrollState]);
147
- // 滚轮事件处理(Container)
148
- useEffect(function () {
149
- var container = containerRef.current;
150
- if (!container)
151
- return;
152
- var handleNativeContainerWheel = function (e) {
153
- // 检查事件是否来自过滤弹框,如果是,允许滚动
154
- var target = e.target;
155
- var isInFilterPopover = target.closest(".canvas-table-filter-popover");
156
- if (isInFilterPopover) {
157
- // 允许过滤弹框内部滚动
158
- return;
159
- }
160
- e.preventDefault();
161
- e.stopPropagation();
162
- };
163
- container.addEventListener("wheel", handleNativeContainerWheel, {
164
- passive: false,
165
- });
166
- return function () {
167
- container.removeEventListener("wheel", handleNativeContainerWheel);
168
- };
169
- }, [containerRef]);
156
+ }, [containerRef, maxScrollTop, maxScrollLeft, onScroll, setScrollState]);
170
157
  // 全局鼠标事件处理(拖拽滚动条)
171
158
  useEffect(function () {
172
159
  var handleGlobalMouseUp = function () {
@@ -1,5 +1,5 @@
1
1
  import { __assign, __spreadArray } from '../../_virtual/_tslib.js';
2
- import { useEffect } from 'react';
2
+ import { useRef, useEffect } from 'react';
3
3
  import { useMemoizedFn } from 'ahooks';
4
4
  import { FONT_SIZE, FONT_FAMILY } from '../utils/constants.js';
5
5
  import { isTextTruncated } from '../utils/canvasDrawHelpers.js';
@@ -706,10 +706,11 @@ var useTableInteraction = function (params) {
706
706
  // 清理状态
707
707
  setState(function (prev) {
708
708
  var updates = {};
709
+ // 注释:不再在鼠标离开时停止框选,只在鼠标松开时停止
709
710
  // 结束框选
710
- if (prev.isSelecting) {
711
- updates.isSelecting = false;
712
- }
711
+ // if (prev.isSelecting) {
712
+ // updates.isSelecting = false;
713
+ // }
713
714
  // 清理 hover
714
715
  if (prev.hoverRowIndex !== null) {
715
716
  updates.hoverRowIndex = null;
@@ -738,14 +739,25 @@ var useTableInteraction = function (params) {
738
739
  // 显示右键菜单
739
740
  menuShow({ event: e });
740
741
  });
741
- // 在document上监听拖拽事件,以支持鼠标移出canvas后继续拖拽
742
+ // 使用ref存储最新的状态和函数引用,避免闭包问题
743
+ var stateRef = useRef(state);
744
+ stateRef.current = state;
745
+ var scrollStateRef = useRef(scrollState);
746
+ scrollStateRef.current = scrollState;
747
+ var resizeStateRef = useRef(resizeState);
748
+ resizeStateRef.current = resizeState;
749
+ var columnRenderInfosRef = useRef(columnRenderInfos);
750
+ columnRenderInfosRef.current = columnRenderInfos;
751
+ // 在document上监听拖拽事件和框选事件,以支持鼠标移出canvas后继续操作
742
752
  useEffect(function () {
743
753
  var isDragging = scrollState.isDraggingVertical ||
744
754
  scrollState.isDraggingHorizontal ||
745
755
  (resizeState === null || resizeState === void 0 ? void 0 : resizeState.isResizing);
746
- if (!isDragging)
756
+ var isSelecting = state.isSelecting;
757
+ if (!isDragging && !isSelecting)
747
758
  return;
748
759
  var handleDocumentMouseMove = function (e) {
760
+ var _a;
749
761
  // 获取当前实例的canvas
750
762
  var canvas = canvasRef.current;
751
763
  if (!canvas)
@@ -753,26 +765,50 @@ var useTableInteraction = function (params) {
753
765
  var rect = canvas.getBoundingClientRect();
754
766
  var x = e.clientX - rect.left;
755
767
  var y = e.clientY - rect.top;
768
+ // 使用ref获取最新状态,避免闭包问题
769
+ var currentScrollState = scrollStateRef.current;
770
+ var currentResizeState = resizeStateRef.current;
771
+ var currentState = stateRef.current;
756
772
  // 处理列宽调整拖拽
757
- if ((resizeState === null || resizeState === void 0 ? void 0 : resizeState.isResizing) && updateResize) {
773
+ if ((currentResizeState === null || currentResizeState === void 0 ? void 0 : currentResizeState.isResizing) && updateResize) {
758
774
  updateResize(x);
759
775
  return;
760
776
  }
761
777
  // 处理垂直滚动条拖拽
762
- if (scrollState.isDraggingVertical) {
763
- handleVerticalScrollDrag(y - scrollState.dragStartY);
778
+ if (currentScrollState.isDraggingVertical) {
779
+ handleVerticalScrollDrag(y - currentScrollState.dragStartY);
764
780
  return;
765
781
  }
766
782
  // 处理水平滚动条拖拽
767
- if (scrollState.isDraggingHorizontal) {
768
- handleHorizontalScrollDrag(x - scrollState.dragStartX);
783
+ if (currentScrollState.isDraggingHorizontal) {
784
+ handleHorizontalScrollDrag(x - currentScrollState.dragStartX);
785
+ return;
786
+ }
787
+ // 处理框选(在canvas外也能继续框选)
788
+ if (currentState.isSelecting) {
789
+ var cell = getCellFromPosition(x, y);
790
+ if (cell) {
791
+ // 检查是否在选择框列,如果是,则不更新框选
792
+ var isSelectionColumn = ((_a = columnRenderInfosRef.current[cell.col]) === null || _a === void 0 ? void 0 : _a.column.key) ===
793
+ "__selection__";
794
+ if (!isSelectionColumn) {
795
+ updateSelection(cell);
796
+ }
797
+ }
769
798
  }
770
799
  };
771
800
  var handleDocumentMouseUp = function () {
801
+ // 使用ref获取最新状态
802
+ var currentResizeState = resizeStateRef.current;
803
+ var currentState = stateRef.current;
772
804
  // 结束列宽调整
773
- if ((resizeState === null || resizeState === void 0 ? void 0 : resizeState.isResizing) && endResize) {
805
+ if ((currentResizeState === null || currentResizeState === void 0 ? void 0 : currentResizeState.isResizing) && endResize) {
774
806
  endResize();
775
807
  }
808
+ // 结束框选
809
+ if (currentState.isSelecting) {
810
+ setState(function (prev) { return (__assign(__assign({}, prev), { isSelecting: false })); });
811
+ }
776
812
  setScrollState(function (prev) { return (__assign(__assign({}, prev), { isDraggingVertical: false, isDraggingHorizontal: false })); });
777
813
  };
778
814
  document.addEventListener("mousemove", handleDocumentMouseMove);
@@ -781,16 +817,16 @@ var useTableInteraction = function (params) {
781
817
  document.removeEventListener("mousemove", handleDocumentMouseMove);
782
818
  document.removeEventListener("mouseup", handleDocumentMouseUp);
783
819
  };
820
+ // 优化:减少依赖项,避免频繁重注册事件监听器
784
821
  // eslint-disable-next-line react-hooks/exhaustive-deps
785
822
  }, [
786
823
  scrollState.isDraggingVertical,
787
824
  scrollState.isDraggingHorizontal,
788
825
  resizeState === null || resizeState === void 0 ? void 0 : resizeState.isResizing,
789
- updateResize,
790
- endResize,
791
- handleVerticalScrollDrag,
792
- handleHorizontalScrollDrag,
793
- canvasRef,
826
+ state.isSelecting,
827
+ // 移除函数依赖,通过 useMemoizedFn 和 useRef 来稳定引用
828
+ // updateResize, endResize, handleVerticalScrollDrag, handleHorizontalScrollDrag,
829
+ // updateSelection, setState, getCellFromPosition, columnRenderInfos,
794
830
  ]);
795
831
  return {
796
832
  handleCanvasMouseDown: handleCanvasMouseDown,
@@ -1,4 +1,4 @@
1
- import { useMemo, isValidElement, useEffect } from 'react';
1
+ import { useRef, useMemo, isValidElement, useEffect } from 'react';
2
2
  import { useMemoizedFn } from 'ahooks';
3
3
  import { IS_SUMMARY } from '../../table/constant.js';
4
4
  import '../../_virtual/_tslib.js';
@@ -19,6 +19,13 @@ import { calculateSelectionState, calculateIconArea } from '../utils/interaction
19
19
  * 表格渲染 Hook
20
20
  */
21
21
  var useTableRender = function (params) {
22
+ // 添加节流机制,避免频繁重绘
23
+ var rafIdRef = useRef(null);
24
+ var isDrawingRef = useRef(false);
25
+ // 内存优化:缓存上次的渲染参数,避免不必要的重绘
26
+ var lastRenderParamsRef = useRef("");
27
+ // 内存优化:缓存Canvas context,避免重复获取
28
+ var canvasContextRef = useRef(null);
22
29
  var canvasRef = params.canvasRef, processedDataSource = params.processedDataSource, columnRenderInfos = params.columnRenderInfos, columns = params.columns, state = params.state, scrollState = params.scrollState, rowSelection = params.rowSelection, containerWidth = params.containerWidth, containerHeight = params.containerHeight, headerHeight = params.headerHeight, baseHeaderHeight = params.baseHeaderHeight, rowHeight = params.rowHeight, bordered = params.bordered, striped = params.striped, _a = params.headerAlign, headerAlign = _a === void 0 ? "center" : _a, needVerticalScrollbar = params.needVerticalScrollbar, needHorizontalScrollbar = params.needHorizontalScrollbar, verticalScrollbarTop = params.verticalScrollbarTop, verticalScrollbarHeight = params.verticalScrollbarHeight, horizontalScrollbarLeft = params.horizontalScrollbarLeft, horizontalScrollbarWidth = params.horizontalScrollbarWidth, getRowKey = params.getRowKey, resizeState = params.resizeState, getColumnWidth = params.getColumnWidth, _b = params.RESIZE_HANDLE_WIDTH, RESIZE_HANDLE_WIDTH = _b === void 0 ? 8 : _b, mergeCellMap = params.mergeCellMap, _c = params.hasSummaryRow, hasSummaryRow = _c === void 0 ? false : _c, fixedRowsCount = params.fixedRowsCount, fixedRowsConfig = params.fixedRowsConfig, _d = params.summaryFixed, summaryFixed = _d === void 0 ? false : _d;
23
30
  // 判断是否是多级表头
24
31
  var maxDepth = useMemo(function () { return getMaxDepth(columns); }, [columns]);
@@ -1323,157 +1330,245 @@ var useTableRender = function (params) {
1323
1330
  renderBorder(true);
1324
1331
  }
1325
1332
  });
1326
- // 绘制表格
1327
- var drawTable = useMemoizedFn(function () {
1328
- var canvas = canvasRef.current;
1329
- if (!canvas)
1330
- return;
1331
- var ctx = canvas.getContext("2d");
1332
- if (!ctx)
1333
- return;
1334
- // 设置 canvas 尺寸
1335
- var displayWidth = containerWidth;
1336
- var displayHeight = containerHeight;
1337
- canvas.width = displayWidth * dpr;
1338
- canvas.height = displayHeight * dpr;
1339
- canvas.style.width = "".concat(displayWidth, "px");
1340
- canvas.style.height = "".concat(displayHeight, "px");
1341
- ctx.scale(dpr, dpr);
1342
- // 优化文字渲染质量,防止模糊
1343
- ctx.imageSmoothingEnabled = true;
1344
- ctx.imageSmoothingQuality = "high";
1345
- // 清空画布
1346
- ctx.clearRect(0, 0, displayWidth, displayHeight);
1347
- // 绘制背景
1348
- ctx.fillStyle = COLORS.white;
1349
- ctx.fillRect(0, 0, displayWidth, displayHeight);
1350
- // 计算固定行的数量(支持多种配置方式)
1351
- var fixedTopRowsCount = fixedRowsCount || (fixedRowsConfig === null || fixedRowsConfig === void 0 ? void 0 : fixedRowsConfig.topCount) || 0;
1352
- var fixedBottomRowsCount = (fixedRowsConfig === null || fixedRowsConfig === void 0 ? void 0 : fixedRowsConfig.bottomCount) || 0;
1353
- // 计算固定行占用的高度
1354
- var fixedTopRowsHeight = fixedTopRowsCount * rowHeight;
1355
- var fixedBottomRowsHeight = fixedBottomRowsCount * rowHeight;
1356
- var fixedSummaryRowHeight = summaryFixed && hasSummaryRow ? rowHeight : 0;
1357
- // 计算可视区域的数据区域高度(不包括表头、固定行和固定合计行)
1358
- var dataAreaHeight = displayHeight -
1359
- headerHeight -
1360
- fixedTopRowsHeight -
1361
- fixedBottomRowsHeight -
1362
- (needHorizontalScrollbar ? SCROLLBAR_SIZE : 0) -
1363
- fixedSummaryRowHeight;
1364
- // 计算可视区域(从固定行之后开始)
1365
- var scrollStartRow = fixedTopRowsCount + Math.floor(scrollState.scrollTop / rowHeight);
1366
- var scrollEndRow = Math.min(fixedTopRowsCount +
1367
- Math.ceil((scrollState.scrollTop + dataAreaHeight) / rowHeight), processedDataSource.length -
1368
- fixedBottomRowsCount -
1369
- (summaryFixed && hasSummaryRow ? 1 : 0) // 如果合计行固定,需要排除它
1370
- );
1371
- // 关键修复:向前扩展渲染范围,检查是否有合并单元格延伸到可视区域
1372
- // 策略:对每一列独立检查,找到该列最靠前的需要渲染的主合并单元格
1373
- if (mergeCellMap && scrollStartRow > fixedTopRowsCount) {
1374
- var originalStartRow = scrollStartRow;
1375
- var minStartRow = scrollStartRow;
1376
- // 对每一列独立检查
1377
- for (var colIndex = 0; colIndex < columnRenderInfos.length; colIndex++) {
1378
- // 向前查找这一列的主合并单元格,最多向前查找20行
1379
- for (var checkRow = originalStartRow - 1; checkRow >= fixedTopRowsCount && checkRow >= originalStartRow - 20; checkRow--) {
1380
- var mergeCellKey = "".concat(checkRow, "-").concat(colIndex);
1381
- var mergeInfo = mergeCellMap.get(mergeCellKey);
1382
- // 如果找到主单元格(不是被合并的单元格)
1383
- if (mergeInfo && !mergeInfo.skip && mergeInfo.rowSpan > 1) {
1384
- // 计算合并单元格的底部行索引
1385
- var mergeEndRow = checkRow + mergeInfo.rowSpan - 1;
1386
- // 如果这个合并单元格延伸到可视区域
1387
- if (mergeEndRow >= originalStartRow) {
1388
- // 更新最小起始行
1389
- minStartRow = Math.min(minStartRow, checkRow);
1333
+ // 绘制表格核心函数
1334
+ var drawTableCore = useMemoizedFn(function () {
1335
+ try {
1336
+ var canvas = canvasRef.current;
1337
+ if (!canvas)
1338
+ return;
1339
+ // 防止重复绘制
1340
+ if (isDrawingRef.current)
1341
+ return;
1342
+ isDrawingRef.current = true;
1343
+ // 内存优化:复用context对象
1344
+ var ctx_1 = canvasContextRef.current;
1345
+ if (!ctx_1 || ctx_1.canvas !== canvas) {
1346
+ ctx_1 = canvas.getContext("2d");
1347
+ if (!ctx_1) {
1348
+ isDrawingRef.current = false;
1349
+ return;
1350
+ }
1351
+ canvasContextRef.current = ctx_1;
1352
+ }
1353
+ // 性能优化:检查是否需要重绘(基于关键参数hash)
1354
+ var renderParamsHash = JSON.stringify({
1355
+ width: containerWidth,
1356
+ height: containerHeight,
1357
+ scrollTop: scrollState.scrollTop,
1358
+ scrollLeft: scrollState.scrollLeft,
1359
+ dataLength: processedDataSource.length,
1360
+ columnsLength: columnRenderInfos.length,
1361
+ hoverRow: state.hoverRowIndex,
1362
+ selection: state.cellSelection,
1363
+ resizing: resizeState === null || resizeState === void 0 ? void 0 : resizeState.isResizing,
1364
+ });
1365
+ // 如果关键参数没有变化,跳过重绘
1366
+ if (lastRenderParamsRef.current === renderParamsHash) {
1367
+ isDrawingRef.current = false;
1368
+ return;
1369
+ }
1370
+ lastRenderParamsRef.current = renderParamsHash;
1371
+ // 设置 canvas 尺寸
1372
+ var displayWidth_1 = containerWidth;
1373
+ var displayHeight_1 = containerHeight;
1374
+ canvas.width = displayWidth_1 * dpr;
1375
+ canvas.height = displayHeight_1 * dpr;
1376
+ canvas.style.width = "".concat(displayWidth_1, "px");
1377
+ canvas.style.height = "".concat(displayHeight_1, "px");
1378
+ ctx_1.scale(dpr, dpr);
1379
+ // 优化文字渲染质量,防止模糊
1380
+ ctx_1.imageSmoothingEnabled = true;
1381
+ ctx_1.imageSmoothingQuality = "high";
1382
+ // 清空画布
1383
+ ctx_1.clearRect(0, 0, displayWidth_1, displayHeight_1);
1384
+ // 绘制背景
1385
+ ctx_1.fillStyle = COLORS.white;
1386
+ ctx_1.fillRect(0, 0, displayWidth_1, displayHeight_1);
1387
+ // 计算固定行的数量(支持多种配置方式)
1388
+ var fixedTopRowsCount_1 = fixedRowsCount || (fixedRowsConfig === null || fixedRowsConfig === void 0 ? void 0 : fixedRowsConfig.topCount) || 0;
1389
+ var fixedBottomRowsCount_1 = (fixedRowsConfig === null || fixedRowsConfig === void 0 ? void 0 : fixedRowsConfig.bottomCount) || 0;
1390
+ // 计算固定行占用的高度
1391
+ var fixedTopRowsHeight = fixedTopRowsCount_1 * rowHeight;
1392
+ var fixedBottomRowsHeight = fixedBottomRowsCount_1 * rowHeight;
1393
+ var fixedSummaryRowHeight = summaryFixed && hasSummaryRow ? rowHeight : 0;
1394
+ // 计算可视区域的数据区域高度(不包括表头、固定行和固定合计行)
1395
+ var dataAreaHeight = displayHeight_1 -
1396
+ headerHeight -
1397
+ fixedTopRowsHeight -
1398
+ fixedBottomRowsHeight -
1399
+ (needHorizontalScrollbar ? SCROLLBAR_SIZE : 0) -
1400
+ fixedSummaryRowHeight;
1401
+ // 计算可视区域(从固定行之后开始)
1402
+ var scrollStartRow_1 = fixedTopRowsCount_1 + Math.floor(scrollState.scrollTop / rowHeight);
1403
+ var scrollEndRow_1 = Math.min(fixedTopRowsCount_1 +
1404
+ Math.ceil((scrollState.scrollTop + dataAreaHeight) / rowHeight), processedDataSource.length -
1405
+ fixedBottomRowsCount_1 -
1406
+ (summaryFixed && hasSummaryRow ? 1 : 0) // 如果合计行固定,需要排除它
1407
+ );
1408
+ // 关键修复:向前扩展渲染范围,检查是否有合并单元格延伸到可视区域
1409
+ // 策略:对每一列独立检查,找到该列最靠前的需要渲染的主合并单元格
1410
+ if (mergeCellMap && scrollStartRow_1 > fixedTopRowsCount_1) {
1411
+ var originalStartRow = scrollStartRow_1;
1412
+ var minStartRow = scrollStartRow_1;
1413
+ // 对每一列独立检查
1414
+ for (var colIndex = 0; colIndex < columnRenderInfos.length; colIndex++) {
1415
+ // 向前查找这一列的主合并单元格,最多向前查找20行
1416
+ for (var checkRow = originalStartRow - 1; checkRow >= fixedTopRowsCount_1 && checkRow >= originalStartRow - 20; checkRow--) {
1417
+ var mergeCellKey = "".concat(checkRow, "-").concat(colIndex);
1418
+ var mergeInfo = mergeCellMap.get(mergeCellKey);
1419
+ // 如果找到主单元格(不是被合并的单元格)
1420
+ if (mergeInfo && !mergeInfo.skip && mergeInfo.rowSpan > 1) {
1421
+ // 计算合并单元格的底部行索引
1422
+ var mergeEndRow = checkRow + mergeInfo.rowSpan - 1;
1423
+ // 如果这个合并单元格延伸到可视区域
1424
+ if (mergeEndRow >= originalStartRow) {
1425
+ // 更新最小起始行
1426
+ minStartRow = Math.min(minStartRow, checkRow);
1427
+ }
1428
+ // 找到这一列的主单元格后,停止向前查找该列
1429
+ break;
1390
1430
  }
1391
- // 找到这一列的主单元格后,停止向前查找该列
1392
- break;
1431
+ // 如果是skip的单元格或没有合并信息,继续向前查找
1432
+ // 因为主单元格可能在更前面
1393
1433
  }
1394
- // 如果是skip的单元格或没有合并信息,继续向前查找
1395
- // 因为主单元格可能在更前面
1396
1434
  }
1435
+ scrollStartRow_1 = minStartRow;
1397
1436
  }
1398
- scrollStartRow = minStartRow;
1399
- }
1400
- // 1. 绘制可滚动区域的数据行(包括边框)
1401
- renderWithFixedLayer(ctx,
1402
- // 渲染行内容
1403
- function (onlyFixed) {
1404
- for (var i = scrollStartRow; i < scrollEndRow; i++) {
1405
- drawRow(ctx, i, displayWidth, displayHeight, onlyFixed);
1406
- }
1407
- },
1408
- // 渲染边框
1409
- function (onlyFixed) {
1410
- drawBorders(ctx, scrollStartRow, scrollEndRow, displayWidth, displayHeight, onlyFixed);
1411
- });
1412
- // 2. 绘制固定的顶部行(如果有)
1413
- if (fixedTopRowsCount > 0) {
1414
- ctx.save();
1415
- renderWithFixedLayer(ctx,
1437
+ // 1. 绘制可滚动区域的数据行(包括边框)
1438
+ renderWithFixedLayer(ctx_1,
1416
1439
  // 渲染行内容
1417
1440
  function (onlyFixed) {
1418
- for (var i = 0; i < fixedTopRowsCount; i++) {
1419
- drawFixedTopRow(ctx, i, displayWidth, displayHeight, onlyFixed);
1441
+ for (var i = scrollStartRow_1; i < scrollEndRow_1; i++) {
1442
+ drawRow(ctx_1, i, displayWidth_1, displayHeight_1, onlyFixed);
1420
1443
  }
1421
1444
  },
1422
1445
  // 渲染边框
1423
1446
  function (onlyFixed) {
1424
- drawFixedTopRowsBorder(ctx, fixedTopRowsCount, displayWidth, displayHeight, onlyFixed);
1447
+ drawBorders(ctx_1, scrollStartRow_1, scrollEndRow_1, displayWidth_1, displayHeight_1, onlyFixed);
1425
1448
  });
1426
- ctx.restore();
1449
+ // 2. 绘制固定的顶部行(如果有)
1450
+ if (fixedTopRowsCount_1 > 0) {
1451
+ ctx_1.save();
1452
+ renderWithFixedLayer(ctx_1,
1453
+ // 渲染行内容
1454
+ function (onlyFixed) {
1455
+ for (var i = 0; i < fixedTopRowsCount_1; i++) {
1456
+ drawFixedTopRow(ctx_1, i, displayWidth_1, displayHeight_1, onlyFixed);
1457
+ }
1458
+ },
1459
+ // 渲染边框
1460
+ function (onlyFixed) {
1461
+ drawFixedTopRowsBorder(ctx_1, fixedTopRowsCount_1, displayWidth_1, displayHeight_1, onlyFixed);
1462
+ });
1463
+ ctx_1.restore();
1464
+ }
1465
+ // 3. 绘制固定的底部行(如果有,不包括合计行)
1466
+ if (fixedBottomRowsCount_1 > 0) {
1467
+ ctx_1.save();
1468
+ var bottomRowsStartIndex_1 = processedDataSource.length -
1469
+ fixedBottomRowsCount_1 -
1470
+ (summaryFixed && hasSummaryRow ? 1 : 0);
1471
+ renderWithFixedLayer(ctx_1,
1472
+ // 渲染行内容
1473
+ function (onlyFixed) {
1474
+ for (var i = 0; i < fixedBottomRowsCount_1; i++) {
1475
+ var rowIndex = bottomRowsStartIndex_1 + i;
1476
+ drawFixedBottomRow(ctx_1, rowIndex, i, displayWidth_1, displayHeight_1, onlyFixed);
1477
+ }
1478
+ },
1479
+ // 渲染边框
1480
+ function (onlyFixed) {
1481
+ drawFixedBottomRowsBorder(ctx_1, fixedBottomRowsCount_1, displayWidth_1, displayHeight_1, onlyFixed);
1482
+ });
1483
+ ctx_1.restore();
1484
+ }
1485
+ // 7. 绘制固定列阴影
1486
+ drawFixedColumns(ctx_1, scrollStartRow_1, scrollEndRow_1, displayWidth_1, displayHeight_1);
1487
+ // 4. 绘制固定合计行(如果启用)
1488
+ if (summaryFixed && hasSummaryRow) {
1489
+ var summaryRowIndex_1 = processedDataSource.length - 1;
1490
+ var summaryRowY_1 = displayHeight_1 -
1491
+ (needHorizontalScrollbar ? SCROLLBAR_SIZE : 0) -
1492
+ rowHeight;
1493
+ ctx_1.save();
1494
+ renderWithFixedLayer(ctx_1,
1495
+ // 渲染行内容
1496
+ function (onlyFixed) {
1497
+ drawFixedSummaryRow(ctx_1, summaryRowIndex_1, summaryRowY_1, displayWidth_1, displayHeight_1, onlyFixed);
1498
+ },
1499
+ // 渲染边框
1500
+ function (onlyFixed) {
1501
+ drawFixedSummaryRowBorder(ctx_1, summaryRowIndex_1, summaryRowY_1, displayWidth_1, displayHeight_1, onlyFixed);
1502
+ });
1503
+ ctx_1.restore();
1504
+ }
1505
+ // 6. 绘制表头(最后绘制,确保始终在最上层,不会被覆盖)
1506
+ drawHeader(ctx_1, displayWidth_1, displayHeight_1);
1507
+ // 绘制滚动条(最后绘制,在表头之上)
1508
+ drawScrollbars(ctx_1, displayWidth_1, displayHeight_1);
1509
+ // 绘制框选区域
1510
+ if (state.cellSelection) {
1511
+ drawCellSelection(ctx_1, displayWidth_1, displayHeight_1);
1512
+ }
1513
+ // 重置绘制标志
1514
+ isDrawingRef.current = false;
1427
1515
  }
1428
- // 3. 绘制固定的底部行(如果有,不包括合计行)
1429
- if (fixedBottomRowsCount > 0) {
1430
- ctx.save();
1431
- var bottomRowsStartIndex_1 = processedDataSource.length -
1432
- fixedBottomRowsCount -
1433
- (summaryFixed && hasSummaryRow ? 1 : 0);
1434
- renderWithFixedLayer(ctx,
1435
- // 渲染行内容
1436
- function (onlyFixed) {
1437
- for (var i = 0; i < fixedBottomRowsCount; i++) {
1438
- var rowIndex = bottomRowsStartIndex_1 + i;
1439
- drawFixedBottomRow(ctx, rowIndex, i, displayWidth, displayHeight, onlyFixed);
1516
+ catch (error) {
1517
+ // 错误边界:捕获渲染异常,防止组件崩溃
1518
+ console.error("CanvasTable: 渲染过程中发生异常:", error);
1519
+ // 重置状态,防止卡死
1520
+ isDrawingRef.current = false;
1521
+ // 清理可能的异常状态
1522
+ try {
1523
+ var canvas = canvasRef.current;
1524
+ if (canvas) {
1525
+ var ctx = canvas.getContext("2d");
1526
+ if (ctx) {
1527
+ // 清空画布,显示错误状态
1528
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1529
+ ctx.fillStyle = "#f5f5f5";
1530
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1531
+ // 显示错误提示
1532
+ ctx.fillStyle = "#ff4d4f";
1533
+ ctx.font = "14px Arial";
1534
+ ctx.textAlign = "center";
1535
+ ctx.fillText("表格渲染异常,请检查数据或刷新页面", canvas.width / 2, canvas.height / 2);
1536
+ }
1440
1537
  }
1441
- },
1442
- // 渲染边框
1443
- function (onlyFixed) {
1444
- drawFixedBottomRowsBorder(ctx, fixedBottomRowsCount, displayWidth, displayHeight, onlyFixed);
1445
- });
1446
- ctx.restore();
1447
- }
1448
- // 7. 绘制固定列阴影
1449
- drawFixedColumns(ctx, scrollStartRow, scrollEndRow, displayWidth, displayHeight);
1450
- // 4. 绘制固定合计行(如果启用)
1451
- if (summaryFixed && hasSummaryRow) {
1452
- var summaryRowIndex_1 = processedDataSource.length - 1;
1453
- var summaryRowY_1 = displayHeight -
1454
- (needHorizontalScrollbar ? SCROLLBAR_SIZE : 0) -
1455
- rowHeight;
1456
- ctx.save();
1457
- renderWithFixedLayer(ctx,
1458
- // 渲染行内容
1459
- function (onlyFixed) {
1460
- drawFixedSummaryRow(ctx, summaryRowIndex_1, summaryRowY_1, displayWidth, displayHeight, onlyFixed);
1461
- },
1462
- // 渲染边框
1463
- function (onlyFixed) {
1464
- drawFixedSummaryRowBorder(ctx, summaryRowIndex_1, summaryRowY_1, displayWidth, displayHeight, onlyFixed);
1465
- });
1466
- ctx.restore();
1538
+ }
1539
+ catch (fallbackError) {
1540
+ console.error("CanvasTable: 错误恢复失败:", fallbackError);
1541
+ }
1542
+ // 可以触发错误回调,通知外部组件
1543
+ // props.onRenderError?.(error);
1467
1544
  }
1468
- // 6. 绘制表头(最后绘制,确保始终在最上层,不会被覆盖)
1469
- drawHeader(ctx, displayWidth, displayHeight);
1470
- // 绘制滚动条(最后绘制,在表头之上)
1471
- drawScrollbars(ctx, displayWidth, displayHeight);
1472
- // 绘制框选区域
1473
- if (state.cellSelection) {
1474
- drawCellSelection(ctx, displayWidth, displayHeight);
1545
+ });
1546
+ // 节流的绘制函数,使用 requestAnimationFrame
1547
+ var drawTable = useMemoizedFn(function () {
1548
+ // 如果已经有pending的绘制请求,取消之前的
1549
+ if (rafIdRef.current !== null) {
1550
+ cancelAnimationFrame(rafIdRef.current);
1475
1551
  }
1552
+ // 使用 requestAnimationFrame 进行节流
1553
+ rafIdRef.current = requestAnimationFrame(function () {
1554
+ drawTableCore();
1555
+ rafIdRef.current = null;
1556
+ });
1476
1557
  });
1558
+ // 清理函数 - 释放资源
1559
+ useEffect(function () {
1560
+ return function () {
1561
+ // 清理动画帧
1562
+ if (rafIdRef.current !== null) {
1563
+ cancelAnimationFrame(rafIdRef.current);
1564
+ rafIdRef.current = null;
1565
+ }
1566
+ // 清理缓存的引用,释放内存
1567
+ canvasContextRef.current = null;
1568
+ lastRenderParamsRef.current = "";
1569
+ isDrawingRef.current = false;
1570
+ };
1571
+ }, []);
1477
1572
  // 监听变化,自动重绘
1478
1573
  useEffect(function () {
1479
1574
  drawTable();
@@ -1493,7 +1588,9 @@ var useTableRender = function (params) {
1493
1588
  columnRenderInfos,
1494
1589
  bordered,
1495
1590
  striped,
1496
- resizeState,
1591
+ resizeState === null || resizeState === void 0 ? void 0 : resizeState.isResizing,
1592
+ resizeState === null || resizeState === void 0 ? void 0 : resizeState.resizingColumnIndex,
1593
+ resizeState === null || resizeState === void 0 ? void 0 : resizeState.hoverResizeColumnIndex,
1497
1594
  drawTable,
1498
1595
  ]);
1499
1596
  return {
@@ -390,7 +390,9 @@ var useNumKeys = function (columns) {
390
390
  var numKeys = useMemo(function () {
391
391
  var leafColumns = getLeafColumns(columns);
392
392
  return Array.from(new Set(leafColumns
393
- .filter(function (item) { return item.precision !== undefined; })
393
+ .filter(function (item) {
394
+ return item.precision !== undefined || item.thousand !== undefined;
395
+ })
394
396
  .map(function (item) { return item.key; })));
395
397
  }, [columns]);
396
398
  return numKeys;
@@ -415,30 +417,66 @@ function useAutoMerge(dataSource, columns, _a) {
415
417
  .filter(function (item) { return item.isDimensionSum; })
416
418
  .map(function (item) { return item.key; });
417
419
  }, [columns]);
418
- var newDataSource = useMemo(function () {
419
- if (!isAutoMerge)
420
- return dataSource;
421
- else {
422
- // 多字段合并
423
- if (!mergeKeys)
424
- return dataSource;
425
- else {
426
- var newDataSource_2 = flattenRecordsOptimized(__spreadArray([], (dataSource || []), true), mergeKeys, dimensionSummaryKeys, summaryKeys, isDimensionDynamic, order, dimensionCustomSumKeys, columns // 添加 columns 参数,支持自定义排序
427
- );
428
- return newDataSource_2;
420
+ // 添加性能监控和异常处理
421
+ var processDataWithOptimization = useCallback(function (sourceData, mergeKeysParam) {
422
+ try {
423
+ // 数据有效性检查
424
+ if (!Array.isArray(sourceData)) {
425
+ console.warn("CanvasTable: 数据源不是数组类型");
426
+ return [];
427
+ }
428
+ if (!Array.isArray(mergeKeysParam) || mergeKeysParam.length === 0) {
429
+ console.warn("CanvasTable: 合并字段配置无效");
430
+ return sourceData;
431
+ }
432
+ // 性能检查:数据量过大时启用优化策略
433
+ var dataSize = sourceData.length;
434
+ var mergeKeysCount = mergeKeysParam.length;
435
+ var complexity = dataSize * mergeKeysCount;
436
+ // 时间复杂度过高时的优化策略
437
+ if (complexity > 10000) {
438
+ console.warn("CanvasTable: \u68C0\u6D4B\u5230\u9AD8\u590D\u6742\u5EA6\u6570\u636E\u5904\u7406 (".concat(complexity, ")\uFF0C\u5EFA\u8BAE\u4F18\u5316\u6570\u636E\u91CF\u6216\u51CF\u5C11\u5408\u5E76\u5B57\u6BB5"));
439
+ // 对于超大数据,可以考虑分页处理或虚拟化
440
+ if (dataSize > 5000) {
441
+ console.warn("CanvasTable: \u6570\u636E\u91CF\u8FC7\u5927 (".concat(dataSize, " \u884C)\uFF0C\u6027\u80FD\u53EF\u80FD\u53D7\u5F71\u54CD"));
442
+ }
443
+ }
444
+ var startTime = performance.now();
445
+ var result = flattenRecordsOptimized(__spreadArray([], sourceData, true), mergeKeysParam, dimensionSummaryKeys, summaryKeys, isDimensionDynamic, order, dimensionCustomSumKeys, columns // 添加 columns 参数,支持自定义排序
446
+ );
447
+ var endTime = performance.now();
448
+ var duration = endTime - startTime;
449
+ // 性能监控:超过阈值时警告
450
+ if (duration > 100) {
451
+ console.warn("CanvasTable: \u6570\u636E\u5904\u7406\u8017\u65F6\u8F83\u957F (".concat(duration.toFixed(2), "ms)\uFF0C\u5EFA\u8BAE\u4F18\u5316"));
429
452
  }
453
+ // 结果有效性检查
454
+ if (!Array.isArray(result)) {
455
+ console.error("CanvasTable: 数据处理结果异常");
456
+ return sourceData; // 降级返回原始数据
457
+ }
458
+ return result;
459
+ }
460
+ catch (error) {
461
+ console.error("CanvasTable: 数据处理过程中发生异常:", error);
462
+ // 异常降级:返回原始数据,避免组件崩溃
463
+ return sourceData;
430
464
  }
431
465
  }, [
432
- dataSource,
433
- isAutoMerge,
434
- mergeKeys,
435
466
  dimensionSummaryKeys,
436
467
  summaryKeys,
437
468
  isDimensionDynamic,
438
469
  order,
439
470
  dimensionCustomSumKeys,
440
- columns, // 添加 columns 依赖
471
+ columns,
441
472
  ]);
473
+ var newDataSource = useMemo(function () {
474
+ if (!isAutoMerge)
475
+ return dataSource;
476
+ if (!mergeKeys)
477
+ return dataSource;
478
+ return processDataWithOptimization(dataSource || [], mergeKeys);
479
+ }, [dataSource, isAutoMerge, mergeKeys, processDataWithOptimization]);
442
480
  var newColumns = useMemo(function () {
443
481
  var _columns = columns;
444
482
  // 递归处理多级表头,自动补充render/合并/编辑等逻辑
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zmdms-webui",
3
- "version": "2.3.8",
3
+ "version": "2.4.0",
4
4
  "private": false,
5
5
  "main": "dist/index.es.js",
6
6
  "module": "dist/index.es.js",