Quellcode durchsuchen

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

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

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

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

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

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

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

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

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

@@ -272,6 +272,7 @@ export const TableBlock: React.FC<TableBlockProps> = ({
272
     const isEditorTarget = !!(event.target as HTMLElement).closest('.rich-text-editor');
272
     const isEditorTarget = !!(event.target as HTMLElement).closest('.rich-text-editor');
273
     if (!isEditorTarget) {
273
     if (!isEditorTarget) {
274
       event.preventDefault();
274
       event.preventDefault();
275
+      window.getSelection()?.removeAllRanges();
275
     }
276
     }
276
     isSelectingRef.current = true;
277
     isSelectingRef.current = true;
277
     setIsSelecting(true);
278
     setIsSelecting(true);
@@ -310,26 +311,6 @@ export const TableBlock: React.FC<TableBlockProps> = ({
310
     }
311
     }
311
   }, [updateSelectedRange]);
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
   useEffect(() => {
314
   useEffect(() => {
334
     const handleMouseUp = () => {
315
     const handleMouseUp = () => {
335
       isSelectingRef.current = false;
316
       isSelectingRef.current = false;
@@ -342,6 +323,31 @@ export const TableBlock: React.FC<TableBlockProps> = ({
342
     return () => document.removeEventListener('mouseup', handleMouseUp);
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
         <table
439
         <table
434
           ref={tableRef}
440
           ref={tableRef}
435
-          onMouseMove={handleTableMouseMove}
436
           className={`table-block${resizeState?.isResizing ? ' table-resizing' : ''}${isSelecting ? ' table-selecting' : ''}`}
441
           className={`table-block${resizeState?.isResizing ? ' table-resizing' : ''}${isSelecting ? ' table-selecting' : ''}`}
437
           style={{
442
           style={{
438
             width: tableWidthStyle,
443
             width: tableWidthStyle,

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

@@ -8,7 +8,7 @@
8
   vertical-align: middle;
8
   vertical-align: middle;
9
   text-align: center;
9
   text-align: center;
10
   position: relative;
10
   position: relative;
11
-  transition: all 0.2s ease;
11
+  transition: none;
12
 }
12
 }
13
 
13
 
14
 .table-cell:hover {
14
 .table-cell:hover {
@@ -17,7 +17,7 @@
17
 
17
 
18
 .table-cell.selected {
18
 .table-cell.selected {
19
   background-color: rgba(24, 144, 255, 0.1);
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
   const visualCellRange = selectionBounds
110
   const visualCellRange = selectionBounds
111
     ? getTableCellRangeForVisualBounds(block, selectionBounds)
111
     ? getTableCellRangeForVisualBounds(block, selectionBounds)
112
     : null;
112
     : null;
113
-    const hasValidRange = !!selectionBounds && !!visualCellRange;
113
+  const hasValidRange = !!selectionBounds && !!visualCellRange;
114
   const isMultiCellRange = hasValidRange
114
   const isMultiCellRange = hasValidRange
115
     && (selectionBounds.rowEnd > selectionBounds.rowStart || selectionBounds.colEnd > selectionBounds.colStart);
115
     && (selectionBounds.rowEnd > selectionBounds.rowStart || selectionBounds.colEnd > selectionBounds.colStart);
116
   const selectedCellData = hasValidSelectedCell
116
   const selectedCellData = hasValidSelectedCell
@@ -223,7 +223,7 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
223
     }
223
     }
224
 
224
 
225
     try {
225
     try {
226
-        const newBlock = mergeCellsByVisualBounds(block, selectionBounds);
226
+      const newBlock = mergeCellsByVisualBounds(block, selectionBounds);
227
       updateBlock(block.id, {
227
       updateBlock(block.id, {
228
         content: newBlock.content,
228
         content: newBlock.content,
229
       });
229
       });

+ 43 - 8
src/stores/editorStore.ts

@@ -39,6 +39,19 @@ const AUTO_SAVE_DELAY = 3000; // 3秒无操作后自动保存
39
 const MAX_RETRY_ATTEMPTS = 3;
39
 const MAX_RETRY_ATTEMPTS = 3;
40
 const MAX_HISTORY_ENTRIES = 100;
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
 interface EditorHistoryEntry {
55
 interface EditorHistoryEntry {
43
   blocks: DocumentBlock[];
56
   blocks: DocumentBlock[];
44
   selectedBlockId: string | null;
57
   selectedBlockId: string | null;
@@ -959,13 +972,18 @@ export const useEditorStore = create<EditorStore>((set, get) => {
959
   addBlock: async (partialBlock: PartialBlock, afterBlockId?: string) => {
972
   addBlock: async (partialBlock: PartialBlock, afterBlockId?: string) => {
960
     const { blocks, blockHashes, documentId, isHistoryApplying, pendingStructuralOperations } = get();
973
     const { blocks, blockHashes, documentId, isHistoryApplying, pendingStructuralOperations } = get();
961
     
974
     
962
-    if (!documentId || isHistoryApplying || pendingStructuralOperations > 0) {
975
+    if (!documentId || isHistoryApplying) {
963
       if (!documentId) {
976
       if (!documentId) {
964
-      message.error('没有打开的文档');
977
+        message.error('没有打开的文档');
965
       }
978
       }
966
       return;
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
     set({ pendingStructuralOperations: pendingStructuralOperations + 1 });
987
     set({ pendingStructuralOperations: pendingStructuralOperations + 1 });
970
     
988
     
971
     let orderedBlocks = blocks;
989
     let orderedBlocks = blocks;
@@ -1093,9 +1111,9 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1093
 
1111
 
1094
   // ── updateBlock ─────────────────────────────────────────────────────────
1112
   // ── updateBlock ─────────────────────────────────────────────────────────
1095
   updateBlock: (id: string, updates: BlockUpdate) => {
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
       return;
1117
       return;
1100
     }
1118
     }
1101
 
1119
 
@@ -1146,13 +1164,18 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1146
   deleteBlock: async (id: string) => {
1164
   deleteBlock: async (id: string) => {
1147
     const { blocks, dirtyBlocks, blockHashes, documentId, isHistoryApplying, pendingStructuralOperations } = get();
1165
     const { blocks, dirtyBlocks, blockHashes, documentId, isHistoryApplying, pendingStructuralOperations } = get();
1148
     
1166
     
1149
-    if (!documentId || isHistoryApplying || pendingStructuralOperations > 0) {
1167
+    if (!documentId || isHistoryApplying) {
1150
       if (!documentId) {
1168
       if (!documentId) {
1151
       message.error('没有打开的文档');
1169
       message.error('没有打开的文档');
1152
       }
1170
       }
1153
       return;
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
     const blockToDelete = blocks.find(b => b.id === id);
1180
     const blockToDelete = blocks.find(b => b.id === id);
1158
     if (!blockToDelete) {
1181
     if (!blockToDelete) {
@@ -1292,7 +1315,11 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1292
   // ── moveBlock ───────────────────────────────────────────────────────────
1315
   // ── moveBlock ───────────────────────────────────────────────────────────
1293
   moveBlock: (id: string, targetOrder: number) => {
1316
   moveBlock: (id: string, targetOrder: number) => {
1294
     const { blocks, isHistoryApplying, pendingStructuralOperations } = get();
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
       return;
1323
       return;
1297
     }
1324
     }
1298
     const block = blocks.find((item) => item.id === id);
1325
     const block = blocks.find((item) => item.id === id);
@@ -1325,7 +1352,11 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1325
   undo: async () => {
1352
   undo: async () => {
1326
     const { past, blocks, selectedBlockId, isHistoryApplying, pendingStructuralOperations } = get();
1353
     const { past, blocks, selectedBlockId, isHistoryApplying, pendingStructuralOperations } = get();
1327
     const previous = past[past.length - 1];
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
     set({ isHistoryApplying: true });
1361
     set({ isHistoryApplying: true });
1331
     try {
1362
     try {
@@ -1351,7 +1382,11 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1351
   redo: async () => {
1382
   redo: async () => {
1352
     const { future, blocks, selectedBlockId, isHistoryApplying, pendingStructuralOperations } = get();
1383
     const { future, blocks, selectedBlockId, isHistoryApplying, pendingStructuralOperations } = get();
1353
     const next = future[future.length - 1];
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
     set({ isHistoryApplying: true });
1391
     set({ isHistoryApplying: true });
1357
     try {
1392
     try {