Преглед на файлове

feat(编辑器): 优化表格组件,增强选择范围逻辑,改进表格宽度处理

Zhang Yice преди 1 месец
родител
ревизия
5f8dd19e47

+ 29 - 30
src/components/Editor/blocks/TableBlock.tsx

@@ -11,7 +11,8 @@ import { TableCell } from './TableCell';
11 11
 import { TableToolbar } from './TableToolbar';
12 12
 import { TableResizeHandle } from './TableResizeHandle';
13 13
 import { useTableResize } from '../../../hooks/useTableResize';
14
-import { getTableVisualCellPositions } from '../../../utils/blockOperations';
14
+import { getTableCellRangeForVisualBounds, getTableVisualCellPositions } from '../../../utils/blockOperations';
15
+import { tableWidthToPt } from '../../../utils/tableUtils';
15 16
 import './TableBlock.css';
16 17
 
17 18
 // ══════════════════════════════════════════════════════════════════════════════
@@ -32,6 +33,19 @@ interface VisualCellPosition {
32 33
 
33 34
 const getCellKey = (rowIndex: number, colIndex: number) => `${rowIndex}-${colIndex}`;
34 35
 
36
+function getVisualSelection(
37
+  anchor: VisualCellPosition | undefined,
38
+  target: VisualCellPosition | undefined,
39
+) {
40
+  if (!anchor || !target) return null;
41
+  return {
42
+    rowStart: Math.min(anchor.rowStart, target.rowStart),
43
+    rowEnd: Math.max(anchor.rowEnd, target.rowEnd),
44
+    colStart: Math.min(anchor.colStart, target.colStart),
45
+    colEnd: Math.max(anchor.colEnd, target.colEnd),
46
+  };
47
+}
48
+
35 49
 /**
36 50
  * TableBlock - 表格块(完整实现)
37 51
  */
@@ -113,6 +127,7 @@ export const TableBlock: React.FC<TableBlockProps> = ({
113 127
     colWidths: block.metadata.col_widths,
114 128
     rowHeights,
115 129
     tableWidth: block.metadata.table_width,
130
+    tableWidthPt: tableWidthToPt(block.metadata.table_width, block.metadata.table_width_unit),
116 131
     readOnly,
117 132
     onColumnResize: handleColumnResize,
118 133
     onRowResize: handleRowResize,
@@ -201,21 +216,12 @@ export const TableBlock: React.FC<TableBlockProps> = ({
201 216
 
202 217
     if (shiftKey && selectedCell) {
203 218
       // Shift+点击:选择范围
204
-      const startRow = Math.min(selectedCell.row, rowIndex);
205
-      const endRow = Math.max(selectedCell.row, rowIndex);
206
-      const startCol = Math.min(selectedCell.col, colIndex);
207
-      const endCol = Math.max(selectedCell.col, colIndex);
208
-      
209
-      setSelectedRange({ startRow, startCol, endRow, endCol });
210 219
       const anchorPosition = visualCellPositions.get(getCellKey(selectedCell.row, selectedCell.col));
211 220
       const targetPosition = visualCellPositions.get(getCellKey(rowIndex, colIndex));
212
-      if (anchorPosition && targetPosition) {
213
-        setSelectedVisualRange({
214
-          rowStart: Math.min(anchorPosition.rowStart, targetPosition.rowStart),
215
-          rowEnd: Math.max(anchorPosition.rowEnd, targetPosition.rowEnd),
216
-          colStart: Math.min(anchorPosition.colStart, targetPosition.colStart),
217
-          colEnd: Math.max(anchorPosition.colEnd, targetPosition.colEnd),
218
-        });
221
+      const visualRange = getVisualSelection(anchorPosition, targetPosition);
222
+      if (visualRange) {
223
+        setSelectedVisualRange(visualRange);
224
+        setSelectedRange(getTableCellRangeForVisualBounds(block, visualRange));
219 225
       }
220 226
     } else {
221 227
       // 普通点击:选择单个单元格
@@ -223,30 +229,22 @@ export const TableBlock: React.FC<TableBlockProps> = ({
223 229
       setSelectedRange(null);
224 230
       setSelectedVisualRange(null);
225 231
     }
226
-  }, [selectedCell, visualCellPositions]);
232
+  }, [block, selectedCell, visualCellPositions]);
227 233
 
228 234
   const updateSelectedRange = useCallback((rowIndex: number, colIndex: number) => {
229 235
     const anchor = selectionAnchorRef.current;
230 236
     if (!anchor) return;
231 237
 
232
-    const startRow = Math.min(anchor.row, rowIndex);
233
-    const endRow = Math.max(anchor.row, rowIndex);
234
-    const startCol = Math.min(anchor.col, colIndex);
235
-    const endCol = Math.max(anchor.col, colIndex);
236 238
     setSelectedCell({ row: rowIndex, col: colIndex });
237
-    setSelectedRange({ startRow, startCol, endRow, endCol });
238 239
 
239 240
     const anchorPosition = selectionAnchorVisualRef.current;
240 241
     const targetPosition = visualCellPositions.get(getCellKey(rowIndex, colIndex));
241
-    if (anchorPosition && targetPosition) {
242
-      setSelectedVisualRange({
243
-        rowStart: Math.min(anchorPosition.rowStart, targetPosition.rowStart),
244
-        rowEnd: Math.max(anchorPosition.rowEnd, targetPosition.rowEnd),
245
-        colStart: Math.min(anchorPosition.colStart, targetPosition.colStart),
246
-        colEnd: Math.max(anchorPosition.colEnd, targetPosition.colEnd),
247
-      });
242
+    const visualRange = getVisualSelection(anchorPosition ?? undefined, targetPosition);
243
+    if (visualRange) {
244
+      setSelectedVisualRange(visualRange);
245
+      setSelectedRange(getTableCellRangeForVisualBounds(block, visualRange));
248 246
     }
249
-  }, [visualCellPositions]);
247
+  }, [block, visualCellPositions]);
250 248
 
251 249
   const handleCellMouseDown = useCallback((
252 250
     rowIndex: number,
@@ -335,12 +333,13 @@ export const TableBlock: React.FC<TableBlockProps> = ({
335 333
   
336 334
   // 如果没有metadata.col_widths,从content.col_widths推算百分比
337 335
   const effectiveColWidths = useMemo(() => {
338
-    if (colWidths && colWidths.length > 0) return colWidths;
336
+    if (colWidths && colWidths.length === block.metadata.cols) return colWidths;
339 337
     if (block.content.col_widths) {
340 338
       const totalPt = block.content.col_widths.reduce((sum, width) => sum + width, 0);
341
-      return totalPt > 0
339
+      const widths = totalPt > 0
342 340
         ? block.content.col_widths.map((width) => (width / totalPt) * 100)
343 341
         : block.content.col_widths.map(() => 100 / block.content.col_widths!.length);
342
+      if (widths.length === block.metadata.cols) return widths;
344 343
     }
345 344
     const columnCount = Math.max(block.metadata.cols, 1);
346 345
     return Array(columnCount).fill(100 / columnCount);

+ 22 - 13
src/components/Editor/blocks/TableToolbar.tsx

@@ -30,6 +30,7 @@ import {
30 30
   splitCell,
31 31
   getTableSelectionBounds,
32 32
   getTableCellRangeForVisualBounds,
33
+  getTableVisualCellPositions,
33 34
 } from '../../../utils/blockOperations';
34 35
 import './TableToolbar.css';
35 36
 
@@ -115,6 +116,11 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
115 116
   const selectedCellData = hasValidSelectedCell
116 117
     ? block.content.rows[selectedCell.row]?.cells[selectedCell.col]
117 118
     : undefined;
119
+  const selectedVisualPosition = getTableVisualCellPositions(block).get(
120
+    `${selectedCell.row}-${selectedCell.col}`,
121
+  );
122
+  const operationRow = selectedVisualPosition?.rowStart ?? selectedCell.row;
123
+  const operationCol = selectedVisualPosition?.colStart ?? selectedCell.col;
118 124
   const canEditTableStructure = hasValidSelectedCell;
119 125
   const isMergedCell = hasValidSelectedCell
120 126
     && ((selectedCellData?.rowspan ?? 1) > 1 || (selectedCellData?.colspan ?? 1) > 1);
@@ -123,7 +129,7 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
123 129
   const handleInsertRow = useCallback(() => {
124 130
     if (!canEditTableStructure) return;
125 131
     try {
126
-      const newBlock = insertTableRow(block, selectedCell.row);
132
+      const newBlock = insertTableRow(block, operationRow);
127 133
       updateBlock(block.id, {
128 134
         content: newBlock.content,
129 135
         metadata: newBlock.metadata,
@@ -132,13 +138,13 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
132 138
     } catch (error: unknown) {
133 139
       message.error(getOperationError(error, '插入行失败'));
134 140
     }
135
-  }, [block, canEditTableStructure, selectedCell.row, updateBlock]);
141
+  }, [block, canEditTableStructure, operationRow, updateBlock]);
136 142
 
137 143
   // 在上方插入行
138 144
   const handleInsertRowBefore = useCallback(() => {
139 145
     if (!canEditTableStructure) return;
140 146
     try {
141
-      const newBlock = insertTableRowBefore(block, selectedCell.row);
147
+      const newBlock = insertTableRowBefore(block, operationRow);
142 148
       updateBlock(block.id, {
143 149
         content: newBlock.content,
144 150
         metadata: newBlock.metadata,
@@ -147,13 +153,13 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
147 153
     } catch (error: unknown) {
148 154
       message.error(getOperationError(error, '插入行失败'));
149 155
     }
150
-  }, [block, canEditTableStructure, selectedCell.row, updateBlock]);
156
+  }, [block, canEditTableStructure, operationRow, updateBlock]);
151 157
 
152 158
   // 插入列
153 159
   const handleInsertColumn = useCallback(() => {
154 160
     if (!canEditTableStructure) return;
155 161
     try {
156
-      const newBlock = insertTableColumn(block, selectedCell.col);
162
+      const newBlock = insertTableColumn(block, operationCol);
157 163
       updateBlock(block.id, {
158 164
         content: newBlock.content,
159 165
         metadata: newBlock.metadata,
@@ -162,13 +168,13 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
162 168
     } catch (error: unknown) {
163 169
       message.error(getOperationError(error, '插入列失败'));
164 170
     }
165
-  }, [block, canEditTableStructure, selectedCell.col, updateBlock]);
171
+  }, [block, canEditTableStructure, operationCol, updateBlock]);
166 172
 
167 173
   // 在左侧插入列
168 174
   const handleInsertColumnBefore = useCallback(() => {
169 175
     if (!canEditTableStructure) return;
170 176
     try {
171
-      const newBlock = insertTableColumnBefore(block, selectedCell.col);
177
+      const newBlock = insertTableColumnBefore(block, operationCol);
172 178
       updateBlock(block.id, {
173 179
         content: newBlock.content,
174 180
         metadata: newBlock.metadata,
@@ -177,13 +183,13 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
177 183
     } catch (error: unknown) {
178 184
       message.error(getOperationError(error, '插入列失败'));
179 185
     }
180
-  }, [block, canEditTableStructure, selectedCell.col, updateBlock]);
186
+  }, [block, canEditTableStructure, operationCol, updateBlock]);
181 187
 
182 188
   // 删除行
183 189
   const handleDeleteRow = useCallback(() => {
184 190
     if (!canEditTableStructure) return;
185 191
     try {
186
-      const newBlock = deleteTableRow(block, selectedCell.row);
192
+      const newBlock = deleteTableRow(block, operationRow);
187 193
       updateBlock(block.id, {
188 194
         content: newBlock.content,
189 195
         metadata: newBlock.metadata,
@@ -193,13 +199,13 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
193 199
     } catch (error: unknown) {
194 200
       message.error(getOperationError(error, '删除行失败'));
195 201
     }
196
-  }, [block, canEditTableStructure, selectedCell.row, updateBlock, onClose]);
202
+  }, [block, canEditTableStructure, operationRow, updateBlock, onClose]);
197 203
 
198 204
   // 删除列
199 205
   const handleDeleteColumn = useCallback(() => {
200 206
     if (!canEditTableStructure) return;
201 207
     try {
202
-      const newBlock = deleteTableColumn(block, selectedCell.col);
208
+      const newBlock = deleteTableColumn(block, operationCol);
203 209
       updateBlock(block.id, {
204 210
         content: newBlock.content,
205 211
         metadata: newBlock.metadata,
@@ -209,7 +215,7 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
209 215
     } catch (error: unknown) {
210 216
       message.error(getOperationError(error, '删除列失败'));
211 217
     }
212
-  }, [block, canEditTableStructure, selectedCell.col, updateBlock, onClose]);
218
+  }, [block, canEditTableStructure, operationCol, updateBlock, onClose]);
213 219
 
214 220
   // 合并单元格
215 221
   const handleMergeCells = useCallback(() => {
@@ -260,7 +266,10 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
260 266
     <div className="table-toolbar">
261 267
       <Space size="small">
262 268
         {/* 表格宽度控制 */}
263
-        <TableWidthControl block={block} />
269
+        <TableWidthControl
270
+          key={`${block.id}-${block.metadata.table_width}-${block.metadata.table_width_unit}`}
271
+          block={block}
272
+        />
264 273
         
265 274
         <Divider type="vertical" style={{ margin: '0 4px' }} />
266 275
 

+ 11 - 26
src/components/Editor/blocks/TableWidthControl.tsx

@@ -11,6 +11,7 @@ import { InputNumber, Select, Space, Button } from 'antd';
11 11
 import { ColumnWidthOutlined } from '@ant-design/icons';
12 12
 import type { TableBlock } from '../../../types/editor';
13 13
 import { useEditorStore } from '../../../stores/editorStore';
14
+import { normalizePercents, percentToPtWidths, ptToPercentWidths, tableWidthToPt } from '../../../utils/tableUtils';
14 15
 
15 16
 // ══════════════════════════════════════════════════════════════════════════════
16 17
 // Component Props
@@ -44,16 +45,14 @@ export const TableWidthControl: React.FC<TableWidthControlProps> = ({ block }) =
44 45
   // 应用宽度变更
45 46
   const applyWidthChange = useCallback(() => {
46 47
     const safeWidth = Number.isFinite(width) && width > 0 ? width : 100;
47
-    const sourceColWidths = block.metadata.col_widths.length > 0
48
+    const sourceColWidths = block.metadata.col_widths.length === block.metadata.cols
48 49
       ? block.metadata.col_widths
49
-      : Array(Math.max(block.metadata.cols, 1)).fill(100 / Math.max(block.metadata.cols, 1));
50
-    const totalPercent = sourceColWidths.reduce(
51
-      (sum, value) => sum + (Number.isFinite(value) && value > 0 ? value : 0),
52
-      0,
50
+      : ptToPercentWidths(block.content.col_widths || []).slice(0, block.metadata.cols);
51
+    const normalizedColWidths = normalizePercents(
52
+      sourceColWidths.length === block.metadata.cols
53
+        ? sourceColWidths
54
+        : Array(Math.max(block.metadata.cols, 1)).fill(100 / Math.max(block.metadata.cols, 1)),
53 55
     );
54
-    const normalizedColWidths = totalPercent > 0
55
-      ? sourceColWidths.map((value) => (Number.isFinite(value) && value > 0 ? value : 0) * 100 / totalPercent)
56
-      : Array(sourceColWidths.length).fill(100 / sourceColWidths.length);
57 56
 
58 57
     // 更新metadata
59 58
     const newMetadata = {
@@ -63,24 +62,10 @@ export const TableWidthControl: React.FC<TableWidthControlProps> = ({ block }) =
63 62
       col_widths: normalizedColWidths,
64 63
     };
65 64
     
66
-    // 重新计算content.col_widths(pt单位)
67
-    // 根据新的表格宽度和百分比col_widths计算pt值
68
-    const pageWidthPt = 478; // A4默认宽度(减去边距)
69
-    
70
-    let tableActualWidthPt: number;
71
-    if (unit === 'percent') {
72
-      tableActualWidthPt = pageWidthPt * (safeWidth / 100);
73
-    } else if (unit === 'cm') {
74
-      tableActualWidthPt = safeWidth * 28.35; // 1cm = 28.35pt
75
-    } else if (unit === 'inch') {
76
-      tableActualWidthPt = safeWidth * 72; // 1inch = 72pt
77
-    } else {
78
-      tableActualWidthPt = pageWidthPt * (width / 100);
79
-    }
80
-    
81
-    // 根据metadata.col_widths(百分比)重新计算content.col_widths(pt)
82
-    const newColWidthsPt = normalizedColWidths.map(
83
-      (percent) => tableActualWidthPt * percent / 100
65
+    const newColWidthsPt = percentToPtWidths(
66
+      normalizedColWidths,
67
+      100,
68
+      tableWidthToPt(safeWidth, unit),
84 69
     );
85 70
     
86 71
     updateBlock(block.id, {

+ 47 - 26
src/hooks/useTableResize.ts

@@ -5,7 +5,7 @@
5 5
  * 参考Notion、飞书等主流编辑器的最佳实践
6 6
  */
7 7
 
8
-import { useState, useEffect, useCallback } from 'react';
8
+import { useState, useEffect, useCallback, useRef } from 'react';
9 9
 import type { RefObject } from 'react';
10 10
 import { 
11 11
   getResizeBoundaries, 
@@ -55,6 +55,8 @@ export interface UseTableResizeOptions {
55 55
   rowHeights: number[];
56 56
   /** 表格总宽度百分比 */
57 57
   tableWidth: number;
58
+  /** 表格实际宽度(磅) */
59
+  tableWidthPt?: number;
58 60
   /** 是否只读 */
59 61
   readOnly?: boolean;
60 62
   /** 列宽变更回调 */
@@ -96,7 +98,7 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
96 98
     tableRef,
97 99
     colWidths,
98 100
     rowHeights,
99
-    tableWidth,
101
+    tableWidthPt,
100 102
     readOnly = false,
101 103
     onColumnResize,
102 104
     onRowResize,
@@ -112,6 +114,19 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
112 114
     boundary: null,
113 115
   });
114 116
   const [resizeLinePosition, setResizeLinePosition] = useState<number | null>(null);
117
+  const previousBodyCursorRef = useRef<string | null>(null);
118
+  const previousBodyUserSelectRef = useRef<string | null>(null);
119
+
120
+  const restoreBodyStyles = useCallback(() => {
121
+    if (previousBodyCursorRef.current !== null) {
122
+      document.body.style.cursor = previousBodyCursorRef.current;
123
+      previousBodyCursorRef.current = null;
124
+    }
125
+    if (previousBodyUserSelectRef.current !== null) {
126
+      document.body.style.userSelect = previousBodyUserSelectRef.current;
127
+      previousBodyUserSelectRef.current = null;
128
+    }
129
+  }, []);
115 130
 
116 131
   // ────────────────────────────────────────────────────────────────────────────
117 132
   // Hover Detection
@@ -193,10 +208,14 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
193 208
       if (!table) return;
194 209
 
195 210
       const boundary = hoverState.boundary;
196
-      const originalSize =
197
-        boundary.type === 'column'
198
-          ? colWidths[boundary.index]
199
-          : rowHeights[boundary.index];
211
+      const columnCount = table.querySelectorAll('col').length;
212
+      const safeColWidths = colWidths.length === columnCount
213
+        ? colWidths
214
+        : Array(Math.max(columnCount, 1)).fill(100 / Math.max(columnCount, 1));
215
+      const originalSize = boundary.type === 'column'
216
+        ? safeColWidths[boundary.index]
217
+        : rowHeights[boundary.index];
218
+      if (!Number.isFinite(originalSize)) return;
200 219
 
201 220
       setResizeState({
202 221
         isResizing: true,
@@ -213,7 +232,8 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
213 232
       // 更新拖拽线初始位置
214 233
       setResizeLinePosition(boundary.position);
215 234
 
216
-      // 添加全局样式
235
+      previousBodyCursorRef.current = document.body.style.cursor;
236
+      previousBodyUserSelectRef.current = document.body.style.userSelect;
217 237
       document.body.style.cursor = boundary.type === 'column' ? 'col-resize' : 'row-resize';
218 238
       document.body.style.userSelect = 'none';
219 239
     },
@@ -245,7 +265,12 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
245 265
       if (!resizeState?.isResizing) return;
246 266
 
247 267
       const table = tableRef.current;
248
-      if (!table) return;
268
+      if (!table) {
269
+        setResizeState(null);
270
+        setResizeLinePosition(null);
271
+        restoreBodyStyles();
272
+        return;
273
+      }
249 274
 
250 275
       // 计算最终偏移量
251 276
       const finalOffset =
@@ -261,13 +286,15 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
261 286
         if (tableWidthPx <= 0) {
262 287
           setResizeState(null);
263 288
           setResizeLinePosition(null);
264
-          document.body.style.cursor = '';
265
-          document.body.style.userSelect = '';
289
+          restoreBodyStyles();
266 290
           return;
267 291
         }
268 292
         const deltaPercent = (finalOffset / tableWidthPx) * 100;
269 293
 
270
-        const newColWidths = [...colWidths];
294
+        const columnCount = table.querySelectorAll('col').length;
295
+        const newColWidths = colWidths.length === columnCount
296
+          ? [...colWidths]
297
+          : Array(Math.max(columnCount, 1)).fill(100 / Math.max(columnCount, 1));
271 298
         let newWidth = resizeState.originalSize + deltaPercent;
272 299
 
273 300
         // 调整相邻列以保持总宽度不变
@@ -294,7 +321,7 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
294 321
         const normalizedWidths = normalizePercents(newColWidths);
295 322
 
296 323
         // 计算pt单位的列宽
297
-        const colWidthsPt = percentToPtWidths(normalizedWidths, tableWidth);
324
+        const colWidthsPt = percentToPtWidths(normalizedWidths, 100, tableWidthPt);
298 325
 
299 326
         // 触发回调
300 327
         onColumnResize?.(normalizedWidths, colWidthsPt);
@@ -316,7 +343,7 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
316 343
       document.body.style.cursor = '';
317 344
       document.body.style.userSelect = '';
318 345
     },
319
-    [resizeState, tableRef, colWidths, tableWidth, onColumnResize, onRowResize]
346
+    [resizeState, tableRef, colWidths, tableWidthPt, onColumnResize, onRowResize, restoreBodyStyles]
320 347
   );
321 348
 
322 349
   // ────────────────────────────────────────────────────────────────────────────
@@ -344,28 +371,22 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
344 371
    */
345 372
   useEffect(() => {
346 373
     if (resizeState?.isResizing) {
347
-      // 开始拖拽时添加mousedown监听
348
-      const table = tableRef.current;
349
-      if (table && hoverState.boundary) {
350
-        table.addEventListener('mousedown', handleResizeStart);
351
-      }
352
-
353 374
       window.addEventListener('mousemove', handleResizeMove);
354 375
       window.addEventListener('mouseup', handleResizeEnd);
355 376
 
356 377
       return () => {
357
-        if (table) {
358
-          table.removeEventListener('mousedown', handleResizeStart);
359
-        }
360 378
         window.removeEventListener('mousemove', handleResizeMove);
361 379
         window.removeEventListener('mouseup', handleResizeEnd);
362
-        document.body.style.cursor = '';
363
-        document.body.style.userSelect = '';
380
+        restoreBodyStyles();
364 381
       };
365 382
     }
366 383
 
367 384
     return undefined;
368
-  }, [resizeState, tableRef, hoverState, handleResizeStart, handleResizeMove, handleResizeEnd]);
385
+  }, [resizeState, handleResizeMove, handleResizeEnd, restoreBodyStyles]);
386
+
387
+  useEffect(() => () => {
388
+    restoreBodyStyles();
389
+  }, [restoreBodyStyles]);
369 390
 
370 391
   /**
371 392
    * Hover状态变化时绑定mousedown
@@ -402,7 +423,7 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
402 423
           const newWidth = resizeState.originalSize + deltaPercent;
403 424
           
404 425
           // 计算pt值
405
-          const widthPt = Math.round((tableWidth / 100) * newWidth * 478 / 100 * 10) / 10;
426
+          const widthPt = Math.round((tableWidthPt || 478) * newWidth / 100 * 10) / 10;
406 427
           return `宽度: ${widthPt}pt`;
407 428
         } else {
408 429
           const offset = resizeState.currentPos.y - resizeState.startPos.y;

+ 27 - 10
src/utils/tableUtils.ts

@@ -6,6 +6,23 @@
6 6
 
7 7
 import type { TableBlock, TableContent } from '../types/editor';
8 8
 
9
+export const DEFAULT_PAGE_WIDTH_PT = 478;
10
+
11
+export function tableWidthToPt(
12
+  width: number,
13
+  unit: TableBlock['metadata']['table_width_unit'] = 'percent',
14
+  pageWidthPt: number = DEFAULT_PAGE_WIDTH_PT,
15
+): number {
16
+  const safeWidth = Number.isFinite(width) && width > 0 ? width : 100;
17
+  const safePageWidth = Number.isFinite(pageWidthPt) && pageWidthPt > 0
18
+    ? pageWidthPt
19
+    : DEFAULT_PAGE_WIDTH_PT;
20
+
21
+  if (unit === 'cm') return safeWidth * 28.35;
22
+  if (unit === 'inch') return safeWidth * 72;
23
+  return safePageWidth * safeWidth / 100;
24
+}
25
+
9 26
 /**
10 27
  * 计算列宽百分比转pt
11 28
  * 
@@ -17,13 +34,13 @@ import type { TableBlock, TableContent } from '../types/editor';
17 34
 export function percentToPtWidths(
18 35
   percentWidths: number[],
19 36
   tableWidthPercent: number = 100,
20
-  pageWidthPt: number = 478 // A4纸宽度595pt - 左右边距各71pt ≈ 453pt
37
+  pageWidthPt: number = DEFAULT_PAGE_WIDTH_PT // A4纸宽度595pt - 左右边距各71pt ≈ 453pt
21 38
 ): number[] {
22 39
   const safePageWidthPt = Number.isFinite(pageWidthPt) && pageWidthPt > 0 ? pageWidthPt : 478;
23 40
   const safeTableWidthPercent = Number.isFinite(tableWidthPercent) && tableWidthPercent > 0
24 41
     ? tableWidthPercent
25 42
     : 100;
26
-  const tableActualWidthPt = safePageWidthPt * (safeTableWidthPercent / 100);
43
+  const tableActualWidthPt = tableWidthToPt(safeTableWidthPercent, 'percent', safePageWidthPt);
27 44
   const validWidths = percentWidths.map((percent) => Number.isFinite(percent) && percent > 0 ? percent : 0);
28 45
   const totalPercent = validWidths.reduce((sum, percent) => sum + percent, 0);
29 46
   const fallbackPercent = validWidths.length > 0 ? 100 / validWidths.length : 0;
@@ -200,21 +217,21 @@ export function getResizeBoundaries(table: HTMLTableElement): ResizeBoundary[] {
200 217
   const boundaries: ResizeBoundary[] = [];
201 218
   const MIN_CELL_SIZE = 40; // 最小单元格尺寸(px)
202 219
   
203
-  // 获取列边界
204
-  const firstRow = table.rows[0];
205
-  if (firstRow) {
220
+  // 使用 colgroup 获取逻辑列,避免首行包含 rowspan/colspan 时丢失边界。
221
+  const columns = Array.from(table.querySelectorAll('col'));
222
+  if (columns.length > 1) {
223
+    const tableRect = table.getBoundingClientRect();
206 224
     let cumulativeX = 0;
207
-    for (let i = 0; i < firstRow.cells.length - 1; i++) {
208
-      const cell = firstRow.cells[i];
209
-      const cellWidth = cell.offsetWidth;
225
+    for (let i = 0; i < columns.length - 1; i += 1) {
226
+      const columnRect = columns[i].getBoundingClientRect();
227
+      const cellWidth = columnRect.width || (tableRect.width / columns.length);
210 228
       cumulativeX += cellWidth;
211
-      
212 229
       boundaries.push({
213 230
         type: 'column',
214 231
         index: i,
215 232
         position: cumulativeX,
216 233
         minPosition: cumulativeX - cellWidth + MIN_CELL_SIZE,
217
-        maxPosition: cumulativeX + firstRow.cells[i + 1].offsetWidth - MIN_CELL_SIZE,
234
+        maxPosition: cumulativeX + (columns[i + 1].getBoundingClientRect().width || cellWidth) - MIN_CELL_SIZE,
218 235
       });
219 236
     }
220 237
   }