Kaynağa Gözat

feat(编辑器): 优化表格选择交互与标题折叠事件处理

- 修复标题折叠事件处理,传递headingId参数到事件回调函数
- 优化表格单元格选择逻辑,将mousemove事件从table元素迁移到document级别以改善跨边界选择体验
- 移除表格单元格过渡动画(transition: none),提升选择反馈的即时性
- 优化表格选择时的视觉反馈,调整selected状态下的box-shadow边宽(2px → 1px)
- 清理段落块格式转换时的列表元数据(删除list_type和list_level)
- 改进段落到列表格式的转换逻辑,确保元数据正确清除
Zhang Yice 1 ay önce
ebeveyn
işleme
6c6bb7a4d1

+ 1 - 1
src/components/Editor/BlockCanvas.tsx

@@ -154,7 +154,7 @@ export const BlockCanvas = React.memo(function BlockCanvas({
154 154
               collapsible={block.type === 'heading' && collapsibleHeadingIds.has(block.id)}
155 155
               collapsed={block.type === 'heading' && collapsedHeadingIds.has(block.id)}
156 156
               onToggleCollapse={block.type === 'heading'
157
-                ? () => toggleHeadingCollapse(block.id)
157
+                ? toggleHeadingCollapse
158 158
                 : undefined}
159 159
               readOnly={readOnly}
160 160
             />

+ 1 - 1
src/components/Editor/BlockRenderer.tsx

@@ -34,7 +34,7 @@ export interface BlockRendererProps {
34 34
   /** 段落列表标记 */
35 35
   listMarker?: string;
36 36
   /** 切换标题折叠状态 */
37
-  onToggleCollapse?: () => void;
37
+  onToggleCollapse?: (headingId: string) => void;
38 38
 }
39 39
 
40 40
 // ══════════════════════════════════════════════════════════════════════════════

+ 2 - 2
src/components/Editor/blocks/HeadingBlock.tsx

@@ -20,7 +20,7 @@ export interface HeadingBlockProps {
20 20
   listMarker?: string;
21 21
   collapsible?: boolean;
22 22
   collapsed?: boolean;
23
-  onToggleCollapse?: () => void;
23
+  onToggleCollapse?: (headingId: string) => void;
24 24
   readOnly?: boolean;
25 25
 }
26 26
 
@@ -374,7 +374,7 @@ export const HeadingBlock: React.FC<HeadingBlockProps> = ({
374 374
             className="heading-collapse-toggle"
375 375
             aria-label={collapsed ? '展开标题内容' : '折叠标题内容'}
376 376
             aria-expanded={!collapsed}
377
-            onClick={onToggleCollapse}
377
+            onClick={() => onToggleCollapse?.(block.id)}
378 378
           >
379 379
             <span aria-hidden="true">{collapsed ? '▶' : '▼'}</span>
380 380
           </button>

+ 1 - 0
src/components/Editor/blocks/ParagraphBlock.tsx

@@ -73,6 +73,7 @@ export const ParagraphBlock: React.FC<ParagraphBlockProps> = ({
73 73
     if (format === 'paragraph' || format === 'ordered-list' || format === 'unordered-list') {
74 74
       const metadata = { ...block.metadata };
75 75
       delete metadata.list_type;
76
+      delete metadata.list_level;
76 77
 
77 78
       updateBlock(block.id, {
78 79
         content: block.content,

+ 26 - 21
src/components/Editor/blocks/TableBlock.tsx

@@ -272,6 +272,7 @@ export const TableBlock: React.FC<TableBlockProps> = ({
272 272
     const isEditorTarget = !!(event.target as HTMLElement).closest('.rich-text-editor');
273 273
     if (!isEditorTarget) {
274 274
       event.preventDefault();
275
+      window.getSelection()?.removeAllRanges();
275 276
     }
276 277
     isSelectingRef.current = true;
277 278
     setIsSelecting(true);
@@ -310,26 +311,6 @@ export const TableBlock: React.FC<TableBlockProps> = ({
310 311
     }
311 312
   }, [updateSelectedRange]);
312 313
 
313
-  const handleTableMouseMove = useCallback((event: React.MouseEvent<HTMLTableElement>) => {
314
-    if (!isSelectingRef.current) return;
315
-
316
-    const target = event.target as HTMLElement;
317
-    const cellElement = target.closest<HTMLTableCellElement>('td[data-row][data-col]');
318
-    if (!cellElement || !tableRef.current?.contains(cellElement)) return;
319
-
320
-    const rowIndex = Number(cellElement.dataset.row);
321
-    const colIndex = Number(cellElement.dataset.col);
322
-    if (!Number.isInteger(rowIndex) || !Number.isInteger(colIndex)) return;
323
-
324
-    const anchor = selectionAnchorRef.current;
325
-    if (anchor && (anchor.row !== rowIndex || anchor.col !== colIndex)) {
326
-      didDragSelectRef.current = true;
327
-      event.preventDefault();
328
-      window.getSelection()?.removeAllRanges();
329
-    }
330
-    updateSelectedRange(rowIndex, colIndex);
331
-  }, [updateSelectedRange]);
332
-
333 314
   useEffect(() => {
334 315
     const handleMouseUp = () => {
335 316
       isSelectingRef.current = false;
@@ -342,6 +323,31 @@ export const TableBlock: React.FC<TableBlockProps> = ({
342 323
     return () => document.removeEventListener('mouseup', handleMouseUp);
343 324
   }, []);
344 325
 
326
+  useEffect(() => {
327
+    const handleDocumentMouseMove = (event: MouseEvent) => {
328
+      if (!isSelectingRef.current) return;
329
+
330
+      const target = document.elementFromPoint(event.clientX, event.clientY);
331
+      const cellElement = target?.closest<HTMLTableCellElement>('td[data-row][data-col]');
332
+      if (!cellElement || !tableRef.current?.contains(cellElement)) return;
333
+
334
+      const rowIndex = Number(cellElement.dataset.row);
335
+      const colIndex = Number(cellElement.dataset.col);
336
+      if (!Number.isInteger(rowIndex) || !Number.isInteger(colIndex)) return;
337
+
338
+      const anchor = selectionAnchorRef.current;
339
+      if (anchor && (anchor.row !== rowIndex || anchor.col !== colIndex)) {
340
+        didDragSelectRef.current = true;
341
+        event.preventDefault();
342
+        window.getSelection()?.removeAllRanges();
343
+      }
344
+      updateSelectedRange(rowIndex, colIndex);
345
+    };
346
+
347
+    document.addEventListener('mousemove', handleDocumentMouseMove, { passive: false });
348
+    return () => document.removeEventListener('mousemove', handleDocumentMouseMove);
349
+  }, [updateSelectedRange]);
350
+
345 351
   // ══════════════════════════════════════════════════════════════════════════════
346 352
   // 渲染
347 353
   // ══════════════════════════════════════════════════════════════════════════════
@@ -432,7 +438,6 @@ export const TableBlock: React.FC<TableBlockProps> = ({
432 438
 
433 439
         <table
434 440
           ref={tableRef}
435
-          onMouseMove={handleTableMouseMove}
436 441
           className={`table-block${resizeState?.isResizing ? ' table-resizing' : ''}${isSelecting ? ' table-selecting' : ''}`}
437 442
           style={{
438 443
             width: tableWidthStyle,

+ 2 - 2
src/components/Editor/blocks/TableCell.css

@@ -8,7 +8,7 @@
8 8
   vertical-align: middle;
9 9
   text-align: center;
10 10
   position: relative;
11
-  transition: all 0.2s ease;
11
+  transition: none;
12 12
 }
13 13
 
14 14
 .table-cell:hover {
@@ -17,7 +17,7 @@
17 17
 
18 18
 .table-cell.selected {
19 19
   background-color: rgba(24, 144, 255, 0.1);
20
-  box-shadow: inset 0 0 0 2px #1890ff;
20
+  box-shadow: inset 0 0 0 1px #1890ff;
21 21
 }
22 22
 
23 23
 /* 单元格内的富文本编辑器 */

+ 2 - 2
src/components/Editor/blocks/TableToolbar.tsx

@@ -110,7 +110,7 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
110 110
   const visualCellRange = selectionBounds
111 111
     ? getTableCellRangeForVisualBounds(block, selectionBounds)
112 112
     : null;
113
-    const hasValidRange = !!selectionBounds && !!visualCellRange;
113
+  const hasValidRange = !!selectionBounds && !!visualCellRange;
114 114
   const isMultiCellRange = hasValidRange
115 115
     && (selectionBounds.rowEnd > selectionBounds.rowStart || selectionBounds.colEnd > selectionBounds.colStart);
116 116
   const selectedCellData = hasValidSelectedCell
@@ -223,7 +223,7 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
223 223
     }
224 224
 
225 225
     try {
226
-        const newBlock = mergeCellsByVisualBounds(block, selectionBounds);
226
+      const newBlock = mergeCellsByVisualBounds(block, selectionBounds);
227 227
       updateBlock(block.id, {
228 228
         content: newBlock.content,
229 229
       });

+ 43 - 8
src/stores/editorStore.ts

@@ -39,6 +39,19 @@ const AUTO_SAVE_DELAY = 3000; // 3秒无操作后自动保存
39 39
 const MAX_RETRY_ATTEMPTS = 3;
40 40
 const MAX_HISTORY_ENTRIES = 100;
41 41
 
42
+function waitForStructuralOperations(isReady: () => boolean): Promise<void> {
43
+  return new Promise((resolve) => {
44
+    const check = () => {
45
+      if (isReady()) {
46
+        resolve();
47
+        return;
48
+      }
49
+      setTimeout(check, 16);
50
+    };
51
+    check();
52
+  });
53
+}
54
+
42 55
 interface EditorHistoryEntry {
43 56
   blocks: DocumentBlock[];
44 57
   selectedBlockId: string | null;
@@ -959,13 +972,18 @@ export const useEditorStore = create<EditorStore>((set, get) => {
959 972
   addBlock: async (partialBlock: PartialBlock, afterBlockId?: string) => {
960 973
     const { blocks, blockHashes, documentId, isHistoryApplying, pendingStructuralOperations } = get();
961 974
     
962
-    if (!documentId || isHistoryApplying || pendingStructuralOperations > 0) {
975
+    if (!documentId || isHistoryApplying) {
963 976
       if (!documentId) {
964
-      message.error('没有打开的文档');
977
+        message.error('没有打开的文档');
965 978
       }
966 979
       return;
967 980
     }
968 981
 
982
+    if (pendingStructuralOperations > 0) {
983
+      await waitForStructuralOperations(() => get().pendingStructuralOperations === 0);
984
+      return get().addBlock(partialBlock, afterBlockId);
985
+    }
986
+
969 987
     set({ pendingStructuralOperations: pendingStructuralOperations + 1 });
970 988
     
971 989
     let orderedBlocks = blocks;
@@ -1093,9 +1111,9 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1093 1111
 
1094 1112
   // ── updateBlock ─────────────────────────────────────────────────────────
1095 1113
   updateBlock: (id: string, updates: BlockUpdate) => {
1096
-    const { blocks, dirtyBlocks, blockHashes, autoSaveEnabled, isHistoryApplying, pendingStructuralOperations } = get();
1114
+    const { blocks, dirtyBlocks, blockHashes, autoSaveEnabled, isHistoryApplying } = get();
1097 1115
 
1098
-    if (isHistoryApplying || pendingStructuralOperations > 0) {
1116
+    if (isHistoryApplying) {
1099 1117
       return;
1100 1118
     }
1101 1119
 
@@ -1146,13 +1164,18 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1146 1164
   deleteBlock: async (id: string) => {
1147 1165
     const { blocks, dirtyBlocks, blockHashes, documentId, isHistoryApplying, pendingStructuralOperations } = get();
1148 1166
     
1149
-    if (!documentId || isHistoryApplying || pendingStructuralOperations > 0) {
1167
+    if (!documentId || isHistoryApplying) {
1150 1168
       if (!documentId) {
1151 1169
       message.error('没有打开的文档');
1152 1170
       }
1153 1171
       return;
1154 1172
     }
1155 1173
 
1174
+    if (pendingStructuralOperations > 0) {
1175
+      await waitForStructuralOperations(() => get().pendingStructuralOperations === 0);
1176
+      return get().deleteBlock(id);
1177
+    }
1178
+
1156 1179
     // 找到要删除的块
1157 1180
     const blockToDelete = blocks.find(b => b.id === id);
1158 1181
     if (!blockToDelete) {
@@ -1292,7 +1315,11 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1292 1315
   // ── moveBlock ───────────────────────────────────────────────────────────
1293 1316
   moveBlock: (id: string, targetOrder: number) => {
1294 1317
     const { blocks, isHistoryApplying, pendingStructuralOperations } = get();
1295
-    if (isHistoryApplying || pendingStructuralOperations > 0) {
1318
+    if (isHistoryApplying) {
1319
+      return;
1320
+    }
1321
+    if (pendingStructuralOperations > 0) {
1322
+      setTimeout(() => get().moveBlock(id, targetOrder), 16);
1296 1323
       return;
1297 1324
     }
1298 1325
     const block = blocks.find((item) => item.id === id);
@@ -1325,7 +1352,11 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1325 1352
   undo: async () => {
1326 1353
     const { past, blocks, selectedBlockId, isHistoryApplying, pendingStructuralOperations } = get();
1327 1354
     const previous = past[past.length - 1];
1328
-    if (!previous || isHistoryApplying || pendingStructuralOperations > 0) return;
1355
+    if (!previous || isHistoryApplying) return;
1356
+    if (pendingStructuralOperations > 0) {
1357
+      await waitForStructuralOperations(() => get().pendingStructuralOperations === 0);
1358
+      return get().undo();
1359
+    }
1329 1360
 
1330 1361
     set({ isHistoryApplying: true });
1331 1362
     try {
@@ -1351,7 +1382,11 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1351 1382
   redo: async () => {
1352 1383
     const { future, blocks, selectedBlockId, isHistoryApplying, pendingStructuralOperations } = get();
1353 1384
     const next = future[future.length - 1];
1354
-    if (!next || isHistoryApplying || pendingStructuralOperations > 0) return;
1385
+    if (!next || isHistoryApplying) return;
1386
+    if (pendingStructuralOperations > 0) {
1387
+      await waitForStructuralOperations(() => get().pendingStructuralOperations === 0);
1388
+      return get().redo();
1389
+    }
1355 1390
 
1356 1391
     set({ isHistoryApplying: true });
1357 1392
     try {