Преглед изворни кода

feat(编辑器): 重构表格组件,优化单元格引用和结构验证逻辑

Zhang Yice пре 1 месец
родитељ
комит
007e85fbac

+ 56 - 23
src/components/Editor/blocks/TableBlock.tsx

@@ -11,7 +11,12 @@ 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 { getTableCellRangeForVisualBounds, getTableVisualCellPositions } from '../../../utils/blockOperations';
14
+import {
15
+  getTableCellRangeForVisualBounds,
16
+  getTableCellReference,
17
+  getTableVisualCellPositions,
18
+  type TableVisualCellPosition,
19
+} from '../../../utils/blockOperations';
15
 import { tableWidthToPt } from '../../../utils/tableUtils';
20
 import { tableWidthToPt } from '../../../utils/tableUtils';
16
 import './TableBlock.css';
21
 import './TableBlock.css';
17
 
22
 
@@ -24,18 +29,9 @@ export interface TableBlockProps {
24
   readOnly?: boolean;
29
   readOnly?: boolean;
25
 }
30
 }
26
 
31
 
27
-interface VisualCellPosition {
28
-  rowStart: number;
29
-  rowEnd: number;
30
-  colStart: number;
31
-  colEnd: number;
32
-}
33
-
34
-const getCellKey = (rowIndex: number, colIndex: number) => `${rowIndex}-${colIndex}`;
35
-
36
 function getVisualSelection(
32
 function getVisualSelection(
37
-  anchor: VisualCellPosition | undefined,
38
-  target: VisualCellPosition | undefined,
33
+  anchor: TableVisualCellPosition | undefined,
34
+  target: TableVisualCellPosition | undefined,
39
 ) {
35
 ) {
40
   if (!anchor || !target) return null;
36
   if (!anchor || !target) return null;
41
   return {
37
   return {
@@ -62,12 +58,12 @@ export const TableBlock: React.FC<TableBlockProps> = ({
62
     endRow: number;
58
     endRow: number;
63
     endCol: number;
59
     endCol: number;
64
   } | null>(null);
60
   } | null>(null);
65
-  const [selectedVisualRange, setSelectedVisualRange] = useState<VisualCellPosition | null>(null);
61
+  const [selectedVisualRange, setSelectedVisualRange] = useState<TableVisualCellPosition | null>(null);
66
   
62
   
67
   const tableRef = useRef<HTMLTableElement>(null);
63
   const tableRef = useRef<HTMLTableElement>(null);
68
   const containerRef = useRef<HTMLDivElement>(null);
64
   const containerRef = useRef<HTMLDivElement>(null);
69
   const selectionAnchorRef = useRef<{ row: number; col: number } | null>(null);
65
   const selectionAnchorRef = useRef<{ row: number; col: number } | null>(null);
70
-  const selectionAnchorVisualRef = useRef<VisualCellPosition | null>(null);
66
+  const selectionAnchorVisualRef = useRef<TableVisualCellPosition | null>(null);
71
   const isSelectingRef = useRef(false);
67
   const isSelectingRef = useRef(false);
72
   const didDragSelectRef = useRef(false);
68
   const didDragSelectRef = useRef(false);
73
   const [isSelecting, setIsSelecting] = useState(false);
69
   const [isSelecting, setIsSelecting] = useState(false);
@@ -169,7 +165,12 @@ export const TableBlock: React.FC<TableBlockProps> = ({
169
       if (!selectedCell) return;
165
       if (!selectedCell) return;
170
 
166
 
171
       const styleRange = selectedVisualRange || (() => {
167
       const styleRange = selectedVisualRange || (() => {
172
-        const position = visualCellPositions.get(getCellKey(selectedCell.row, selectedCell.col));
168
+        const position = getTableCellReference(
169
+          block,
170
+          selectedCell.row,
171
+          selectedCell.col,
172
+          visualCellPositions,
173
+        )?.visual;
173
         return position || null;
174
         return position || null;
174
       })();
175
       })();
175
 
176
 
@@ -177,7 +178,7 @@ export const TableBlock: React.FC<TableBlockProps> = ({
177
         return {
178
         return {
178
           ...row,
179
           ...row,
179
           cells: row.cells.map((cell, colIdx) => {
180
           cells: row.cells.map((cell, colIdx) => {
180
-            const cellPosition = visualCellPositions.get(getCellKey(rowIdx, colIdx));
181
+            const cellPosition = getTableCellReference(block, rowIdx, colIdx, visualCellPositions)?.visual;
181
             const isInRange = !!styleRange && !!cellPosition
182
             const isInRange = !!styleRange && !!cellPosition
182
               && cellPosition.rowStart <= styleRange.rowEnd
183
               && cellPosition.rowStart <= styleRange.rowEnd
183
               && cellPosition.rowEnd >= styleRange.rowStart
184
               && cellPosition.rowEnd >= styleRange.rowStart
@@ -216,8 +217,18 @@ export const TableBlock: React.FC<TableBlockProps> = ({
216
 
217
 
217
     if (shiftKey && selectedCell) {
218
     if (shiftKey && selectedCell) {
218
       // Shift+点击:选择范围
219
       // Shift+点击:选择范围
219
-      const anchorPosition = visualCellPositions.get(getCellKey(selectedCell.row, selectedCell.col));
220
-      const targetPosition = visualCellPositions.get(getCellKey(rowIndex, colIndex));
220
+      const anchorPosition = getTableCellReference(
221
+        block,
222
+        selectedCell.row,
223
+        selectedCell.col,
224
+        visualCellPositions,
225
+      )?.visual;
226
+      const targetPosition = getTableCellReference(
227
+        block,
228
+        rowIndex,
229
+        colIndex,
230
+        visualCellPositions,
231
+      )?.visual;
221
       const visualRange = getVisualSelection(anchorPosition, targetPosition);
232
       const visualRange = getVisualSelection(anchorPosition, targetPosition);
222
       if (visualRange) {
233
       if (visualRange) {
223
         setSelectedVisualRange(visualRange);
234
         setSelectedVisualRange(visualRange);
@@ -238,7 +249,12 @@ export const TableBlock: React.FC<TableBlockProps> = ({
238
     setSelectedCell({ row: rowIndex, col: colIndex });
249
     setSelectedCell({ row: rowIndex, col: colIndex });
239
 
250
 
240
     const anchorPosition = selectionAnchorVisualRef.current;
251
     const anchorPosition = selectionAnchorVisualRef.current;
241
-    const targetPosition = visualCellPositions.get(getCellKey(rowIndex, colIndex));
252
+    const targetPosition = getTableCellReference(
253
+      block,
254
+      rowIndex,
255
+      colIndex,
256
+      visualCellPositions,
257
+    )?.visual;
242
     const visualRange = getVisualSelection(anchorPosition ?? undefined, targetPosition);
258
     const visualRange = getVisualSelection(anchorPosition ?? undefined, targetPosition);
243
     if (visualRange) {
259
     if (visualRange) {
244
       setSelectedVisualRange(visualRange);
260
       setSelectedVisualRange(visualRange);
@@ -257,20 +273,32 @@ export const TableBlock: React.FC<TableBlockProps> = ({
257
     if (!isEditorTarget) {
273
     if (!isEditorTarget) {
258
       event.preventDefault();
274
       event.preventDefault();
259
     }
275
     }
260
-    selectionAnchorRef.current = { row: rowIndex, col: colIndex };
261
-    selectionAnchorVisualRef.current = visualCellPositions.get(getCellKey(rowIndex, colIndex)) || null;
262
     isSelectingRef.current = true;
276
     isSelectingRef.current = true;
263
     setIsSelecting(true);
277
     setIsSelecting(true);
264
 
278
 
265
     if (event.shiftKey && selectedCell) {
279
     if (event.shiftKey && selectedCell) {
280
+      didDragSelectRef.current = true;
266
       selectionAnchorRef.current = selectedCell;
281
       selectionAnchorRef.current = selectedCell;
282
+      selectionAnchorVisualRef.current = getTableCellReference(
283
+        block,
284
+        selectedCell.row,
285
+        selectedCell.col,
286
+        visualCellPositions,
287
+      )?.visual || null;
267
       updateSelectedRange(rowIndex, colIndex);
288
       updateSelectedRange(rowIndex, colIndex);
268
     } else {
289
     } else {
290
+      selectionAnchorRef.current = { row: rowIndex, col: colIndex };
291
+      selectionAnchorVisualRef.current = getTableCellReference(
292
+        block,
293
+        rowIndex,
294
+        colIndex,
295
+        visualCellPositions,
296
+      )?.visual || null;
269
       setSelectedCell({ row: rowIndex, col: colIndex });
297
       setSelectedCell({ row: rowIndex, col: colIndex });
270
       setSelectedRange(null);
298
       setSelectedRange(null);
271
       setSelectedVisualRange(null);
299
       setSelectedVisualRange(null);
272
     }
300
     }
273
-  }, [readOnly, selectedCell, updateSelectedRange, visualCellPositions]);
301
+  }, [block, readOnly, selectedCell, updateSelectedRange, visualCellPositions]);
274
 
302
 
275
   const handleCellMouseEnter = useCallback((rowIndex: number, colIndex: number) => {
303
   const handleCellMouseEnter = useCallback((rowIndex: number, colIndex: number) => {
276
     if (isSelectingRef.current) {
304
     if (isSelectingRef.current) {
@@ -426,7 +454,12 @@ export const TableBlock: React.FC<TableBlockProps> = ({
426
               >
454
               >
427
                 {row.cells.map((cell, colIndex) => {
455
                 {row.cells.map((cell, colIndex) => {
428
                   // 检查单元格是否在选择范围内
456
                   // 检查单元格是否在选择范围内
429
-                  const cellPosition = visualCellPositions.get(getCellKey(rowIndex, colIndex));
457
+                  const cellPosition = getTableCellReference(
458
+                    block,
459
+                    rowIndex,
460
+                    colIndex,
461
+                    visualCellPositions,
462
+                  )?.visual;
430
                   const isInRange = selectedVisualRange && cellPosition
463
                   const isInRange = selectedVisualRange && cellPosition
431
                     ? cellPosition.rowStart <= selectedVisualRange.rowEnd &&
464
                     ? cellPosition.rowStart <= selectedVisualRange.rowEnd &&
432
                       cellPosition.rowEnd >= selectedVisualRange.rowStart &&
465
                       cellPosition.rowEnd >= selectedVisualRange.rowStart &&

+ 4 - 6
src/components/Editor/blocks/TableToolbar.tsx

@@ -30,7 +30,7 @@ import {
30
   splitCell,
30
   splitCell,
31
   getTableSelectionBounds,
31
   getTableSelectionBounds,
32
   getTableCellRangeForVisualBounds,
32
   getTableCellRangeForVisualBounds,
33
-  getTableVisualCellPositions,
33
+  getTableCellReference,
34
 } from '../../../utils/blockOperations';
34
 } from '../../../utils/blockOperations';
35
 import './TableToolbar.css';
35
 import './TableToolbar.css';
36
 
36
 
@@ -116,11 +116,9 @@ export const TableToolbar: React.FC<TableToolbarProps> = ({
116
   const selectedCellData = hasValidSelectedCell
116
   const selectedCellData = hasValidSelectedCell
117
     ? block.content.rows[selectedCell.row]?.cells[selectedCell.col]
117
     ? block.content.rows[selectedCell.row]?.cells[selectedCell.col]
118
     : 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;
119
+  const selectedReference = getTableCellReference(block, selectedCell.row, selectedCell.col);
120
+  const operationRow = selectedReference?.visual.rowStart ?? selectedCell.row;
121
+  const operationCol = selectedReference?.visual.colStart ?? selectedCell.col;
124
   const canEditTableStructure = hasValidSelectedCell;
122
   const canEditTableStructure = hasValidSelectedCell;
125
   const isMergedCell = hasValidSelectedCell
123
   const isMergedCell = hasValidSelectedCell
126
     && ((selectedCellData?.rowspan ?? 1) > 1 || (selectedCellData?.colspan ?? 1) > 1);
124
     && ((selectedCellData?.rowspan ?? 1) > 1 || (selectedCellData?.colspan ?? 1) > 1);

+ 17 - 3
src/services/clientExportService.ts

@@ -1,5 +1,6 @@
1
 import type { DocumentBlock, RichText, TableBlock } from '../types/editor';
1
 import type { DocumentBlock, RichText, TableBlock } from '../types/editor';
2
 import { downloadBlob, safeFileName } from '../utils/download';
2
 import { downloadBlob, safeFileName } from '../utils/download';
3
+import { getTableVisualCellPositions } from '../utils/blockOperations';
3
 
4
 
4
 function toPlainText(content: string | RichText[]): string {
5
 function toPlainText(content: string | RichText[]): string {
5
   return typeof content === 'string'
6
   return typeof content === 'string'
@@ -12,13 +13,26 @@ function escapeTableCell(value: string): string {
12
 }
13
 }
13
 
14
 
14
 function tableToMarkdown(block: TableBlock): string {
15
 function tableToMarkdown(block: TableBlock): string {
15
-  const rows = block.content.rows.map((row) =>
16
-    row.cells.map((cell) => escapeTableCell(toPlainText(cell.text)))
16
+  const columnCount = Math.max(block.metadata.cols, 1);
17
+  const rows = Array.from(
18
+    { length: block.content.rows.length },
19
+    () => Array<string>(columnCount).fill(''),
17
   );
20
   );
21
+  const positions = getTableVisualCellPositions(block);
22
+
23
+  block.content.rows.forEach((row, rowIndex) => {
24
+    row.cells.forEach((cell, cellIndex) => {
25
+      const position = positions.get(`${rowIndex}-${cellIndex}`);
26
+      if (!position || position.rowStart >= rows.length || position.colStart >= columnCount) {
27
+        return;
28
+      }
29
+
30
+      rows[position.rowStart][position.colStart] = escapeTableCell(toPlainText(cell.text));
31
+    });
32
+  });
18
 
33
 
19
   if (rows.length === 0) return '';
34
   if (rows.length === 0) return '';
20
 
35
 
21
-  const columnCount = Math.max(...rows.map((row) => row.length), 1);
22
   const normalizeRow = (row: string[]) =>
36
   const normalizeRow = (row: string[]) =>
23
     `| ${Array.from({ length: columnCount }, (_, index) => row[index] ?? '').join(' | ')} |`;
37
     `| ${Array.from({ length: columnCount }, (_, index) => row[index] ?? '').join(' | ')} |`;
24
 
38
 

+ 12 - 1
src/stores/editorStore.ts

@@ -23,7 +23,11 @@ import type {
23
 } from '../types/editor';
23
 } from '../types/editor';
24
 import { blockService } from '../services/blockService';
24
 import { blockService } from '../services/blockService';
25
 import { getErrorMessage } from '../services/api';
25
 import { getErrorMessage } from '../services/api';
26
-import { normalizeTableBlock, serializeTableBlock } from '../utils/blockOperations';
26
+import {
27
+  normalizeTableBlock,
28
+  serializeTableBlock,
29
+  validateTableStructure,
30
+} from '../utils/blockOperations';
27
 
31
 
28
 // 并发保存限制器(最多同时进行 3 个请求,降低服务器压力)
32
 // 并发保存限制器(最多同时进行 3 个请求,降低服务器压力)
29
 const saveConcurrencyLimit = pLimit(3);
33
 const saveConcurrencyLimit = pLimit(3);
@@ -459,6 +463,13 @@ export const useEditorStore = create<EditorStore>((set, get) => {
459
         }
463
         }
460
         return block;
464
         return block;
461
       });
465
       });
466
+
467
+      const invalidTable = normalizedBlocks.find(
468
+        (block) => block.type === 'table' && !validateTableStructure(block as TableBlock),
469
+      );
470
+      if (invalidTable) {
471
+        throw new Error('文档包含无效的表格结构');
472
+      }
462
       
473
       
463
       // 保存原始blocks快照,用于检测修改
474
       // 保存原始blocks快照,用于检测修改
464
       const snapshot = JSON.stringify(normalizedBlocks);
475
       const snapshot = JSON.stringify(normalizedBlocks);

+ 100 - 11
src/utils/blockOperations.ts

@@ -130,6 +130,12 @@ export interface TableVisualCellPosition {
130
   colEnd: number;
130
   colEnd: number;
131
 }
131
 }
132
 
132
 
133
+export interface TableCellReference {
134
+  rowIndex: number;
135
+  cellIndex: number;
136
+  visual: TableVisualCellPosition;
137
+}
138
+
133
 export function getTableVisualCellPositions(table: TableBlock): Map<string, TableVisualCellPosition> {
139
 export function getTableVisualCellPositions(table: TableBlock): Map<string, TableVisualCellPosition> {
134
   const occupied: boolean[][] = [];
140
   const occupied: boolean[][] = [];
135
   const positions = new Map<string, TableVisualCellPosition>();
141
   const positions = new Map<string, TableVisualCellPosition>();
@@ -172,6 +178,18 @@ export function getTableVisualCellPositions(table: TableBlock): Map<string, Tabl
172
   return positions;
178
   return positions;
173
 }
179
 }
174
 
180
 
181
+export function getTableCellReference(
182
+  table: TableBlock,
183
+  rowIndex: number,
184
+  cellIndex: number,
185
+  positions: Map<string, TableVisualCellPosition> = getTableVisualCellPositions(table),
186
+): TableCellReference | null {
187
+  const visual = positions.get(`${rowIndex}-${cellIndex}`);
188
+  if (!visual) return null;
189
+
190
+  return { rowIndex, cellIndex, visual };
191
+}
192
+
175
 export function getTableCellsForVisualBounds(
193
 export function getTableCellsForVisualBounds(
176
   table: TableBlock,
194
   table: TableBlock,
177
   bounds: TableVisualCellPosition,
195
   bounds: TableVisualCellPosition,
@@ -194,6 +212,52 @@ export function getTableCellsForVisualBounds(
194
   return cells;
212
   return cells;
195
 }
213
 }
196
 
214
 
215
+export function validateTableStructure(table: TableBlock): boolean {
216
+  const columnCount = table.metadata?.cols;
217
+  const rowCount = table.metadata?.rows;
218
+  const rows = table.content?.rows;
219
+
220
+  if (!Number.isInteger(columnCount) || columnCount <= 0 || columnCount > 1000) return false;
221
+  if (!Number.isInteger(rowCount) || rowCount <= 0 || rowCount > 1000) return false;
222
+  if (!Array.isArray(rows) || rows.length !== rowCount) return false;
223
+  if (!Array.isArray(table.metadata.col_widths) || table.metadata.col_widths.length !== columnCount) {
224
+    return false;
225
+  }
226
+  if (table.content.col_widths && table.content.col_widths.length !== columnCount) return false;
227
+  if (table.metadata.col_widths.some((width) => !Number.isFinite(width) || width <= 0)) return false;
228
+  if (table.content.col_widths?.some((width) => !Number.isFinite(width) || width <= 0)) return false;
229
+
230
+  for (const row of rows) {
231
+    if (!Array.isArray(row.cells) || row.cells.length > columnCount) return false;
232
+    if (row.height !== undefined && (!Number.isFinite(row.height) || row.height <= 0)) return false;
233
+
234
+    for (const cell of row.cells) {
235
+      const rowspan = cell.rowspan;
236
+      const colspan = cell.colspan;
237
+      if (!Number.isInteger(rowspan) || !Number.isInteger(colspan)) return false;
238
+
239
+      const isHidden = rowspan === 0 && colspan === 0;
240
+      if (isHidden) {
241
+        const colIndex = cell.col_index;
242
+        if (typeof colIndex !== 'number' || !Number.isInteger(colIndex) || colIndex < 1 || colIndex > columnCount) {
243
+          return false;
244
+        }
245
+        continue;
246
+      }
247
+
248
+      if (rowspan < 1 || rowspan > rowCount || colspan < 1 || colspan > columnCount) return false;
249
+      if (cell.width !== undefined && (!Number.isFinite(cell.width) || cell.width <= 0)) return false;
250
+    }
251
+  }
252
+
253
+  return [...getTableVisualCellPositions(table).values()].every((position) =>
254
+    position.rowStart >= 0
255
+    && position.rowEnd < rowCount
256
+    && position.colStart >= 0
257
+    && position.colEnd < columnCount
258
+  );
259
+}
260
+
197
 function createHiddenCell(colIndex: number): TableCell {
261
 function createHiddenCell(colIndex: number): TableCell {
198
   return {
262
   return {
199
     text: '',
263
     text: '',
@@ -327,12 +391,12 @@ export function flattenTextToString(text: string | RichText[]): string {
327
 
391
 
328
 /**
392
 /**
329
  * 序列化表格单元格为后端格式
393
  * 序列化表格单元格为后端格式
330
- * 将富文本数组的 text 字段转换为纯字符串
394
+ * 保留富文本数组,确保单元格内部格式可以跨保存恢复
331
  * 
395
  * 
332
  * @param cell 前端单元格数据
396
  * @param cell 前端单元格数据
333
  * @param colIndex 列索引(从1开始)
397
  * @param colIndex 列索引(从1开始)
334
  * @param defaultWidth 默认宽度(磅)
398
  * @param defaultWidth 默认宽度(磅)
335
- * @returns 序列化后的单元格(text为纯字符串
399
+ * @returns 序列化后的单元格(text保留字符串或富文本数组
336
  */
400
  */
337
 export function serializeTableCell(
401
 export function serializeTableCell(
338
   cell: TableCell,
402
   cell: TableCell,
@@ -376,7 +440,7 @@ export function serializeTableCell(
376
   }
440
   }
377
   
441
   
378
   return {
442
   return {
379
-    text: flattenTextToString(cell.text), // 转换为纯字符串
443
+    text: cell.text,
380
     rowspan: cell.rowspan ?? 1,
444
     rowspan: cell.rowspan ?? 1,
381
     colspan: cell.colspan ?? 1,
445
     colspan: cell.colspan ?? 1,
382
     col_index: cell.col_index !== undefined ? cell.col_index : colIndex,
446
     col_index: cell.col_index !== undefined ? cell.col_index : colIndex,
@@ -388,7 +452,7 @@ export function serializeTableCell(
388
 
452
 
389
 /**
453
 /**
390
  * 序列化表格块为后端格式
454
  * 序列化表格块为后端格式
391
- * 将所有单元格的 text 从富文本数组转换为纯字符串
455
+ * 保留单元格富文本内容,并补齐后端所需的单元格字段
392
  * 
456
  * 
393
  * @param table 表格块
457
  * @param table 表格块
394
  * @returns 序列化后的表格块
458
  * @returns 序列化后的表格块
@@ -908,7 +972,7 @@ export function deleteTableColumn(table: TableBlock, colIndex: number): TableBlo
908
  * - 主单元格(左上角)保留完整的字段信息
972
  * - 主单元格(左上角)保留完整的字段信息
909
  * - 被合并的单元格标记为rowspan=0, colspan=0, text=''
973
  * - 被合并的单元格标记为rowspan=0, colspan=0, text=''
910
  * - 保留所有单元格的col_index
974
  * - 保留所有单元格的col_index
911
- * - 合并后的文本统一为纯字符串格
975
+ * - 合并后的文本在包含富文本时保留片段样
912
  * 
976
  * 
913
  * @param table 表格块
977
  * @param table 表格块
914
  * @param startRow 起始行
978
  * @param startRow 起始行
@@ -1006,6 +1070,8 @@ export function mergeCellsByVisualBounds(
1006
   
1070
   
1007
   // 收集所有被合并单元格的文本内容
1071
   // 收集所有被合并单元格的文本内容
1008
   const displayTexts: string[] = [];
1072
   const displayTexts: string[] = [];
1073
+  const mergedRichText: RichText[] = [];
1074
+  let hasRichText = false;
1009
   
1075
   
1010
   // 辅助函数:将 text 字段规范化为纯字符串
1076
   // 辅助函数:将 text 字段规范化为纯字符串
1011
   const normalizeText = (text: string | RichText[]): string => {
1077
   const normalizeText = (text: string | RichText[]): string => {
@@ -1027,7 +1093,29 @@ export function mergeCellsByVisualBounds(
1027
         && position.colEnd <= selectionBounds.colEnd;
1093
         && position.colEnd <= selectionBounds.colEnd;
1028
       if (isContained) {
1094
       if (isContained) {
1029
         const text = normalizeText(cell.text);
1095
         const text = normalizeText(cell.text);
1030
-        if (text.trim()) displayTexts.push(text.trim());
1096
+        if (text.trim()) {
1097
+          displayTexts.push(text.trim());
1098
+          if (Array.isArray(cell.text)) {
1099
+            if (!hasRichText) {
1100
+              displayTexts.slice(0, -1).forEach((previousText) => {
1101
+                if (mergedRichText.length > 0) {
1102
+                  mergedRichText.push({ text: ' ', style: {} });
1103
+                }
1104
+                mergedRichText.push({ text: previousText, style: {} });
1105
+              });
1106
+            }
1107
+            hasRichText = true;
1108
+            if (mergedRichText.length > 0) {
1109
+              mergedRichText.push({ text: ' ', style: {} });
1110
+            }
1111
+            mergedRichText.push(...cell.text);
1112
+          } else if (hasRichText) {
1113
+            if (mergedRichText.length > 0) {
1114
+              mergedRichText.push({ text: ' ', style: {} });
1115
+            }
1116
+            mergedRichText.push({ text: text.trim(), style: {} });
1117
+          }
1118
+        }
1031
       }
1119
       }
1032
     });
1120
     });
1033
   });
1121
   });
@@ -1045,12 +1133,13 @@ export function mergeCellsByVisualBounds(
1045
 
1133
 
1046
       if (key === primaryKey) {
1134
       if (key === primaryKey) {
1047
         // 主单元格:设置完整的标准格式
1135
         // 主单元格:设置完整的标准格式
1048
-        // 合并后的文本统一为纯字符串格
1136
+        // 合并后的文本在包含富文本时保留片段样
1049
         const mergedText = displayTexts.join(' ');
1137
         const mergedText = displayTexts.join(' ');
1138
+        const mergedContent = hasRichText ? mergedRichText : mergedText;
1050
         
1139
         
1051
         // 构建标准格式的单元格对象
1140
         // 构建标准格式的单元格对象
1052
         return {
1141
         return {
1053
-          text: mergedText || normalizeText(cell.text),
1142
+          text: mergedContent.length > 0 ? mergedContent : normalizeText(cell.text),
1054
           rowspan: rowSpan,
1143
           rowspan: rowSpan,
1055
           colspan: colSpan,
1144
           colspan: colSpan,
1056
           col_index: colIdx + 1, // 列索引从1开始
1145
           col_index: colIdx + 1, // 列索引从1开始
@@ -1226,9 +1315,9 @@ export function validateBlock(block: Partial<DocumentBlock>): boolean {
1226
     case 'table': {
1315
     case 'table': {
1227
       const table = block as Partial<TableBlock>;
1316
       const table = block as Partial<TableBlock>;
1228
       return !!(
1317
       return !!(
1229
-        table.metadata?.cols &&
1230
-        table.metadata?.rows &&
1231
-        Array.isArray(table.content?.rows)
1318
+        table.metadata &&
1319
+        table.content &&
1320
+        validateTableStructure(table as TableBlock)
1232
       );
1321
       );
1233
     }
1322
     }
1234
     
1323