Переглянути джерело

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

Zhang Yice 1 місяць тому
батько
коміт
5f8dd19e47

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

@@ -11,7 +11,8 @@ import { TableCell } from './TableCell';
11
 import { TableToolbar } from './TableToolbar';
11
 import { TableToolbar } from './TableToolbar';
12
 import { TableResizeHandle } from './TableResizeHandle';
12
 import { TableResizeHandle } from './TableResizeHandle';
13
 import { useTableResize } from '../../../hooks/useTableResize';
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
 import './TableBlock.css';
16
 import './TableBlock.css';
16
 
17
 
17
 // ══════════════════════════════════════════════════════════════════════════════
18
 // ══════════════════════════════════════════════════════════════════════════════
@@ -32,6 +33,19 @@ interface VisualCellPosition {
32
 
33
 
33
 const getCellKey = (rowIndex: number, colIndex: number) => `${rowIndex}-${colIndex}`;
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
  * TableBlock - 表格块(完整实现)
50
  * TableBlock - 表格块(完整实现)
37
  */
51
  */
@@ -113,6 +127,7 @@ export const TableBlock: React.FC<TableBlockProps> = ({
113
     colWidths: block.metadata.col_widths,
127
     colWidths: block.metadata.col_widths,
114
     rowHeights,
128
     rowHeights,
115
     tableWidth: block.metadata.table_width,
129
     tableWidth: block.metadata.table_width,
130
+    tableWidthPt: tableWidthToPt(block.metadata.table_width, block.metadata.table_width_unit),
116
     readOnly,
131
     readOnly,
117
     onColumnResize: handleColumnResize,
132
     onColumnResize: handleColumnResize,
118
     onRowResize: handleRowResize,
133
     onRowResize: handleRowResize,
@@ -201,21 +216,12 @@ export const TableBlock: React.FC<TableBlockProps> = ({
201
 
216
 
202
     if (shiftKey && selectedCell) {
217
     if (shiftKey && selectedCell) {
203
       // Shift+点击:选择范围
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
       const anchorPosition = visualCellPositions.get(getCellKey(selectedCell.row, selectedCell.col));
219
       const anchorPosition = visualCellPositions.get(getCellKey(selectedCell.row, selectedCell.col));
211
       const targetPosition = visualCellPositions.get(getCellKey(rowIndex, colIndex));
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
     } else {
226
     } else {
221
       // 普通点击:选择单个单元格
227
       // 普通点击:选择单个单元格
@@ -223,30 +229,22 @@ export const TableBlock: React.FC<TableBlockProps> = ({
223
       setSelectedRange(null);
229
       setSelectedRange(null);
224
       setSelectedVisualRange(null);
230
       setSelectedVisualRange(null);
225
     }
231
     }
226
-  }, [selectedCell, visualCellPositions]);
232
+  }, [block, selectedCell, visualCellPositions]);
227
 
233
 
228
   const updateSelectedRange = useCallback((rowIndex: number, colIndex: number) => {
234
   const updateSelectedRange = useCallback((rowIndex: number, colIndex: number) => {
229
     const anchor = selectionAnchorRef.current;
235
     const anchor = selectionAnchorRef.current;
230
     if (!anchor) return;
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
     setSelectedCell({ row: rowIndex, col: colIndex });
238
     setSelectedCell({ row: rowIndex, col: colIndex });
237
-    setSelectedRange({ startRow, startCol, endRow, endCol });
238
 
239
 
239
     const anchorPosition = selectionAnchorVisualRef.current;
240
     const anchorPosition = selectionAnchorVisualRef.current;
240
     const targetPosition = visualCellPositions.get(getCellKey(rowIndex, colIndex));
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
   const handleCellMouseDown = useCallback((
249
   const handleCellMouseDown = useCallback((
252
     rowIndex: number,
250
     rowIndex: number,
@@ -335,12 +333,13 @@ export const TableBlock: React.FC<TableBlockProps> = ({
335
   
333
   
336
   // 如果没有metadata.col_widths,从content.col_widths推算百分比
334
   // 如果没有metadata.col_widths,从content.col_widths推算百分比
337
   const effectiveColWidths = useMemo(() => {
335
   const effectiveColWidths = useMemo(() => {
338
-    if (colWidths && colWidths.length > 0) return colWidths;
336
+    if (colWidths && colWidths.length === block.metadata.cols) return colWidths;
339
     if (block.content.col_widths) {
337
     if (block.content.col_widths) {
340
       const totalPt = block.content.col_widths.reduce((sum, width) => sum + width, 0);
338
       const totalPt = block.content.col_widths.reduce((sum, width) => sum + width, 0);
341
-      return totalPt > 0
339
+      const widths = totalPt > 0
342
         ? block.content.col_widths.map((width) => (width / totalPt) * 100)
340
         ? block.content.col_widths.map((width) => (width / totalPt) * 100)
343
         : block.content.col_widths.map(() => 100 / block.content.col_widths!.length);
341
         : block.content.col_widths.map(() => 100 / block.content.col_widths!.length);
342
+      if (widths.length === block.metadata.cols) return widths;
344
     }
343
     }
345
     const columnCount = Math.max(block.metadata.cols, 1);
344
     const columnCount = Math.max(block.metadata.cols, 1);
346
     return Array(columnCount).fill(100 / columnCount);
345
     return Array(columnCount).fill(100 / columnCount);

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

@@ -30,6 +30,7 @@ import {
30
   splitCell,
30
   splitCell,
31
   getTableSelectionBounds,
31
   getTableSelectionBounds,
32
   getTableCellRangeForVisualBounds,
32
   getTableCellRangeForVisualBounds,
33
+  getTableVisualCellPositions,
33
 } from '../../../utils/blockOperations';
34
 } from '../../../utils/blockOperations';
34
 import './TableToolbar.css';
35
 import './TableToolbar.css';
35
 
36
 
@@ -115,6 +116,11 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
115
   const selectedCellData = hasValidSelectedCell
116
   const selectedCellData = hasValidSelectedCell
116
     ? block.content.rows[selectedCell.row]?.cells[selectedCell.col]
117
     ? block.content.rows[selectedCell.row]?.cells[selectedCell.col]
117
     : undefined;
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
   const canEditTableStructure = hasValidSelectedCell;
124
   const canEditTableStructure = hasValidSelectedCell;
119
   const isMergedCell = hasValidSelectedCell
125
   const isMergedCell = hasValidSelectedCell
120
     && ((selectedCellData?.rowspan ?? 1) > 1 || (selectedCellData?.colspan ?? 1) > 1);
126
     && ((selectedCellData?.rowspan ?? 1) > 1 || (selectedCellData?.colspan ?? 1) > 1);
@@ -123,7 +129,7 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
123
   const handleInsertRow = useCallback(() => {
129
   const handleInsertRow = useCallback(() => {
124
     if (!canEditTableStructure) return;
130
     if (!canEditTableStructure) return;
125
     try {
131
     try {
126
-      const newBlock = insertTableRow(block, selectedCell.row);
132
+      const newBlock = insertTableRow(block, operationRow);
127
       updateBlock(block.id, {
133
       updateBlock(block.id, {
128
         content: newBlock.content,
134
         content: newBlock.content,
129
         metadata: newBlock.metadata,
135
         metadata: newBlock.metadata,
@@ -132,13 +138,13 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
132
     } catch (error: unknown) {
138
     } catch (error: unknown) {
133
       message.error(getOperationError(error, '插入行失败'));
139
       message.error(getOperationError(error, '插入行失败'));
134
     }
140
     }
135
-  }, [block, canEditTableStructure, selectedCell.row, updateBlock]);
141
+  }, [block, canEditTableStructure, operationRow, updateBlock]);
136
 
142
 
137
   // 在上方插入行
143
   // 在上方插入行
138
   const handleInsertRowBefore = useCallback(() => {
144
   const handleInsertRowBefore = useCallback(() => {
139
     if (!canEditTableStructure) return;
145
     if (!canEditTableStructure) return;
140
     try {
146
     try {
141
-      const newBlock = insertTableRowBefore(block, selectedCell.row);
147
+      const newBlock = insertTableRowBefore(block, operationRow);
142
       updateBlock(block.id, {
148
       updateBlock(block.id, {
143
         content: newBlock.content,
149
         content: newBlock.content,
144
         metadata: newBlock.metadata,
150
         metadata: newBlock.metadata,
@@ -147,13 +153,13 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
147
     } catch (error: unknown) {
153
     } catch (error: unknown) {
148
       message.error(getOperationError(error, '插入行失败'));
154
       message.error(getOperationError(error, '插入行失败'));
149
     }
155
     }
150
-  }, [block, canEditTableStructure, selectedCell.row, updateBlock]);
156
+  }, [block, canEditTableStructure, operationRow, updateBlock]);
151
 
157
 
152
   // 插入列
158
   // 插入列
153
   const handleInsertColumn = useCallback(() => {
159
   const handleInsertColumn = useCallback(() => {
154
     if (!canEditTableStructure) return;
160
     if (!canEditTableStructure) return;
155
     try {
161
     try {
156
-      const newBlock = insertTableColumn(block, selectedCell.col);
162
+      const newBlock = insertTableColumn(block, operationCol);
157
       updateBlock(block.id, {
163
       updateBlock(block.id, {
158
         content: newBlock.content,
164
         content: newBlock.content,
159
         metadata: newBlock.metadata,
165
         metadata: newBlock.metadata,
@@ -162,13 +168,13 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
162
     } catch (error: unknown) {
168
     } catch (error: unknown) {
163
       message.error(getOperationError(error, '插入列失败'));
169
       message.error(getOperationError(error, '插入列失败'));
164
     }
170
     }
165
-  }, [block, canEditTableStructure, selectedCell.col, updateBlock]);
171
+  }, [block, canEditTableStructure, operationCol, updateBlock]);
166
 
172
 
167
   // 在左侧插入列
173
   // 在左侧插入列
168
   const handleInsertColumnBefore = useCallback(() => {
174
   const handleInsertColumnBefore = useCallback(() => {
169
     if (!canEditTableStructure) return;
175
     if (!canEditTableStructure) return;
170
     try {
176
     try {
171
-      const newBlock = insertTableColumnBefore(block, selectedCell.col);
177
+      const newBlock = insertTableColumnBefore(block, operationCol);
172
       updateBlock(block.id, {
178
       updateBlock(block.id, {
173
         content: newBlock.content,
179
         content: newBlock.content,
174
         metadata: newBlock.metadata,
180
         metadata: newBlock.metadata,
@@ -177,13 +183,13 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
177
     } catch (error: unknown) {
183
     } catch (error: unknown) {
178
       message.error(getOperationError(error, '插入列失败'));
184
       message.error(getOperationError(error, '插入列失败'));
179
     }
185
     }
180
-  }, [block, canEditTableStructure, selectedCell.col, updateBlock]);
186
+  }, [block, canEditTableStructure, operationCol, updateBlock]);
181
 
187
 
182
   // 删除行
188
   // 删除行
183
   const handleDeleteRow = useCallback(() => {
189
   const handleDeleteRow = useCallback(() => {
184
     if (!canEditTableStructure) return;
190
     if (!canEditTableStructure) return;
185
     try {
191
     try {
186
-      const newBlock = deleteTableRow(block, selectedCell.row);
192
+      const newBlock = deleteTableRow(block, operationRow);
187
       updateBlock(block.id, {
193
       updateBlock(block.id, {
188
         content: newBlock.content,
194
         content: newBlock.content,
189
         metadata: newBlock.metadata,
195
         metadata: newBlock.metadata,
@@ -193,13 +199,13 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
193
     } catch (error: unknown) {
199
     } catch (error: unknown) {
194
       message.error(getOperationError(error, '删除行失败'));
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
   const handleDeleteColumn = useCallback(() => {
205
   const handleDeleteColumn = useCallback(() => {
200
     if (!canEditTableStructure) return;
206
     if (!canEditTableStructure) return;
201
     try {
207
     try {
202
-      const newBlock = deleteTableColumn(block, selectedCell.col);
208
+      const newBlock = deleteTableColumn(block, operationCol);
203
       updateBlock(block.id, {
209
       updateBlock(block.id, {
204
         content: newBlock.content,
210
         content: newBlock.content,
205
         metadata: newBlock.metadata,
211
         metadata: newBlock.metadata,
@@ -209,7 +215,7 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
209
     } catch (error: unknown) {
215
     } catch (error: unknown) {
210
       message.error(getOperationError(error, '删除列失败'));
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
   const handleMergeCells = useCallback(() => {
221
   const handleMergeCells = useCallback(() => {
@@ -260,7 +266,10 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
260
     <div className="table-toolbar">
266
     <div className="table-toolbar">
261
       <Space size="small">
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
         <Divider type="vertical" style={{ margin: '0 4px' }} />
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
 import { ColumnWidthOutlined } from '@ant-design/icons';
11
 import { ColumnWidthOutlined } from '@ant-design/icons';
12
 import type { TableBlock } from '../../../types/editor';
12
 import type { TableBlock } from '../../../types/editor';
13
 import { useEditorStore } from '../../../stores/editorStore';
13
 import { useEditorStore } from '../../../stores/editorStore';
14
+import { normalizePercents, percentToPtWidths, ptToPercentWidths, tableWidthToPt } from '../../../utils/tableUtils';
14
 
15
 
15
 // ══════════════════════════════════════════════════════════════════════════════
16
 // ══════════════════════════════════════════════════════════════════════════════
16
 // Component Props
17
 // Component Props
@@ -44,16 +45,14 @@ export const TableWidthControl: React.FC<TableWidthControlProps> = ({ block }) =
44
   // 应用宽度变更
45
   // 应用宽度变更
45
   const applyWidthChange = useCallback(() => {
46
   const applyWidthChange = useCallback(() => {
46
     const safeWidth = Number.isFinite(width) && width > 0 ? width : 100;
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
       ? block.metadata.col_widths
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
     // 更新metadata
57
     // 更新metadata
59
     const newMetadata = {
58
     const newMetadata = {
@@ -63,24 +62,10 @@ export const TableWidthControl: React.FC<TableWidthControlProps> = ({ block }) =
63
       col_widths: normalizedColWidths,
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
     updateBlock(block.id, {
71
     updateBlock(block.id, {

+ 47 - 26
src/hooks/useTableResize.ts

@@ -5,7 +5,7 @@
5
  * 参考Notion、飞书等主流编辑器的最佳实践
5
  * 参考Notion、飞书等主流编辑器的最佳实践
6
  */
6
  */
7
 
7
 
8
-import { useState, useEffect, useCallback } from 'react';
8
+import { useState, useEffect, useCallback, useRef } from 'react';
9
 import type { RefObject } from 'react';
9
 import type { RefObject } from 'react';
10
 import { 
10
 import { 
11
   getResizeBoundaries, 
11
   getResizeBoundaries, 
@@ -55,6 +55,8 @@ export interface UseTableResizeOptions {
55
   rowHeights: number[];
55
   rowHeights: number[];
56
   /** 表格总宽度百分比 */
56
   /** 表格总宽度百分比 */
57
   tableWidth: number;
57
   tableWidth: number;
58
+  /** 表格实际宽度(磅) */
59
+  tableWidthPt?: number;
58
   /** 是否只读 */
60
   /** 是否只读 */
59
   readOnly?: boolean;
61
   readOnly?: boolean;
60
   /** 列宽变更回调 */
62
   /** 列宽变更回调 */
@@ -96,7 +98,7 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
96
     tableRef,
98
     tableRef,
97
     colWidths,
99
     colWidths,
98
     rowHeights,
100
     rowHeights,
99
-    tableWidth,
101
+    tableWidthPt,
100
     readOnly = false,
102
     readOnly = false,
101
     onColumnResize,
103
     onColumnResize,
102
     onRowResize,
104
     onRowResize,
@@ -112,6 +114,19 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
112
     boundary: null,
114
     boundary: null,
113
   });
115
   });
114
   const [resizeLinePosition, setResizeLinePosition] = useState<number | null>(null);
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
   // Hover Detection
132
   // Hover Detection
@@ -193,10 +208,14 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
193
       if (!table) return;
208
       if (!table) return;
194
 
209
 
195
       const boundary = hoverState.boundary;
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
       setResizeState({
220
       setResizeState({
202
         isResizing: true,
221
         isResizing: true,
@@ -213,7 +232,8 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
213
       // 更新拖拽线初始位置
232
       // 更新拖拽线初始位置
214
       setResizeLinePosition(boundary.position);
233
       setResizeLinePosition(boundary.position);
215
 
234
 
216
-      // 添加全局样式
235
+      previousBodyCursorRef.current = document.body.style.cursor;
236
+      previousBodyUserSelectRef.current = document.body.style.userSelect;
217
       document.body.style.cursor = boundary.type === 'column' ? 'col-resize' : 'row-resize';
237
       document.body.style.cursor = boundary.type === 'column' ? 'col-resize' : 'row-resize';
218
       document.body.style.userSelect = 'none';
238
       document.body.style.userSelect = 'none';
219
     },
239
     },
@@ -245,7 +265,12 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
245
       if (!resizeState?.isResizing) return;
265
       if (!resizeState?.isResizing) return;
246
 
266
 
247
       const table = tableRef.current;
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
       const finalOffset =
276
       const finalOffset =
@@ -261,13 +286,15 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
261
         if (tableWidthPx <= 0) {
286
         if (tableWidthPx <= 0) {
262
           setResizeState(null);
287
           setResizeState(null);
263
           setResizeLinePosition(null);
288
           setResizeLinePosition(null);
264
-          document.body.style.cursor = '';
265
-          document.body.style.userSelect = '';
289
+          restoreBodyStyles();
266
           return;
290
           return;
267
         }
291
         }
268
         const deltaPercent = (finalOffset / tableWidthPx) * 100;
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
         let newWidth = resizeState.originalSize + deltaPercent;
298
         let newWidth = resizeState.originalSize + deltaPercent;
272
 
299
 
273
         // 调整相邻列以保持总宽度不变
300
         // 调整相邻列以保持总宽度不变
@@ -294,7 +321,7 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
294
         const normalizedWidths = normalizePercents(newColWidths);
321
         const normalizedWidths = normalizePercents(newColWidths);
295
 
322
 
296
         // 计算pt单位的列宽
323
         // 计算pt单位的列宽
297
-        const colWidthsPt = percentToPtWidths(normalizedWidths, tableWidth);
324
+        const colWidthsPt = percentToPtWidths(normalizedWidths, 100, tableWidthPt);
298
 
325
 
299
         // 触发回调
326
         // 触发回调
300
         onColumnResize?.(normalizedWidths, colWidthsPt);
327
         onColumnResize?.(normalizedWidths, colWidthsPt);
@@ -316,7 +343,7 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
316
       document.body.style.cursor = '';
343
       document.body.style.cursor = '';
317
       document.body.style.userSelect = '';
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
   useEffect(() => {
372
   useEffect(() => {
346
     if (resizeState?.isResizing) {
373
     if (resizeState?.isResizing) {
347
-      // 开始拖拽时添加mousedown监听
348
-      const table = tableRef.current;
349
-      if (table && hoverState.boundary) {
350
-        table.addEventListener('mousedown', handleResizeStart);
351
-      }
352
-
353
       window.addEventListener('mousemove', handleResizeMove);
374
       window.addEventListener('mousemove', handleResizeMove);
354
       window.addEventListener('mouseup', handleResizeEnd);
375
       window.addEventListener('mouseup', handleResizeEnd);
355
 
376
 
356
       return () => {
377
       return () => {
357
-        if (table) {
358
-          table.removeEventListener('mousedown', handleResizeStart);
359
-        }
360
         window.removeEventListener('mousemove', handleResizeMove);
378
         window.removeEventListener('mousemove', handleResizeMove);
361
         window.removeEventListener('mouseup', handleResizeEnd);
379
         window.removeEventListener('mouseup', handleResizeEnd);
362
-        document.body.style.cursor = '';
363
-        document.body.style.userSelect = '';
380
+        restoreBodyStyles();
364
       };
381
       };
365
     }
382
     }
366
 
383
 
367
     return undefined;
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
    * Hover状态变化时绑定mousedown
392
    * Hover状态变化时绑定mousedown
@@ -402,7 +423,7 @@ export function useTableResize(options: UseTableResizeOptions): UseTableResizeRe
402
           const newWidth = resizeState.originalSize + deltaPercent;
423
           const newWidth = resizeState.originalSize + deltaPercent;
403
           
424
           
404
           // 计算pt值
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
           return `宽度: ${widthPt}pt`;
427
           return `宽度: ${widthPt}pt`;
407
         } else {
428
         } else {
408
           const offset = resizeState.currentPos.y - resizeState.startPos.y;
429
           const offset = resizeState.currentPos.y - resizeState.startPos.y;

+ 27 - 10
src/utils/tableUtils.ts

@@ -6,6 +6,23 @@
6
 
6
 
7
 import type { TableBlock, TableContent } from '../types/editor';
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
  * 计算列宽百分比转pt
27
  * 计算列宽百分比转pt
11
  * 
28
  * 
@@ -17,13 +34,13 @@ import type { TableBlock, TableContent } from '../types/editor';
17
 export function percentToPtWidths(
34
 export function percentToPtWidths(
18
   percentWidths: number[],
35
   percentWidths: number[],
19
   tableWidthPercent: number = 100,
36
   tableWidthPercent: number = 100,
20
-  pageWidthPt: number = 478 // A4纸宽度595pt - 左右边距各71pt ≈ 453pt
37
+  pageWidthPt: number = DEFAULT_PAGE_WIDTH_PT // A4纸宽度595pt - 左右边距各71pt ≈ 453pt
21
 ): number[] {
38
 ): number[] {
22
   const safePageWidthPt = Number.isFinite(pageWidthPt) && pageWidthPt > 0 ? pageWidthPt : 478;
39
   const safePageWidthPt = Number.isFinite(pageWidthPt) && pageWidthPt > 0 ? pageWidthPt : 478;
23
   const safeTableWidthPercent = Number.isFinite(tableWidthPercent) && tableWidthPercent > 0
40
   const safeTableWidthPercent = Number.isFinite(tableWidthPercent) && tableWidthPercent > 0
24
     ? tableWidthPercent
41
     ? tableWidthPercent
25
     : 100;
42
     : 100;
26
-  const tableActualWidthPt = safePageWidthPt * (safeTableWidthPercent / 100);
43
+  const tableActualWidthPt = tableWidthToPt(safeTableWidthPercent, 'percent', safePageWidthPt);
27
   const validWidths = percentWidths.map((percent) => Number.isFinite(percent) && percent > 0 ? percent : 0);
44
   const validWidths = percentWidths.map((percent) => Number.isFinite(percent) && percent > 0 ? percent : 0);
28
   const totalPercent = validWidths.reduce((sum, percent) => sum + percent, 0);
45
   const totalPercent = validWidths.reduce((sum, percent) => sum + percent, 0);
29
   const fallbackPercent = validWidths.length > 0 ? 100 / validWidths.length : 0;
46
   const fallbackPercent = validWidths.length > 0 ? 100 / validWidths.length : 0;
@@ -200,21 +217,21 @@ export function getResizeBoundaries(table: HTMLTableElement): ResizeBoundary[] {
200
   const boundaries: ResizeBoundary[] = [];
217
   const boundaries: ResizeBoundary[] = [];
201
   const MIN_CELL_SIZE = 40; // 最小单元格尺寸(px)
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
     let cumulativeX = 0;
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
       cumulativeX += cellWidth;
228
       cumulativeX += cellWidth;
211
-      
212
       boundaries.push({
229
       boundaries.push({
213
         type: 'column',
230
         type: 'column',
214
         index: i,
231
         index: i,
215
         position: cumulativeX,
232
         position: cumulativeX,
216
         minPosition: cumulativeX - cellWidth + MIN_CELL_SIZE,
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
   }