|
|
@@ -130,6 +130,12 @@ export interface TableVisualCellPosition {
|
|
130
|
130
|
colEnd: number;
|
|
131
|
131
|
}
|
|
132
|
132
|
|
|
|
133
|
+export interface TableCellReference {
|
|
|
134
|
+ rowIndex: number;
|
|
|
135
|
+ cellIndex: number;
|
|
|
136
|
+ visual: TableVisualCellPosition;
|
|
|
137
|
+}
|
|
|
138
|
+
|
|
133
|
139
|
export function getTableVisualCellPositions(table: TableBlock): Map<string, TableVisualCellPosition> {
|
|
134
|
140
|
const occupied: boolean[][] = [];
|
|
135
|
141
|
const positions = new Map<string, TableVisualCellPosition>();
|
|
|
@@ -172,6 +178,18 @@ export function getTableVisualCellPositions(table: TableBlock): Map<string, Tabl
|
|
172
|
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
|
193
|
export function getTableCellsForVisualBounds(
|
|
176
|
194
|
table: TableBlock,
|
|
177
|
195
|
bounds: TableVisualCellPosition,
|
|
|
@@ -194,6 +212,52 @@ export function getTableCellsForVisualBounds(
|
|
194
|
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
|
261
|
function createHiddenCell(colIndex: number): TableCell {
|
|
198
|
262
|
return {
|
|
199
|
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
|
396
|
* @param cell 前端单元格数据
|
|
333
|
397
|
* @param colIndex 列索引(从1开始)
|
|
334
|
398
|
* @param defaultWidth 默认宽度(磅)
|
|
335
|
|
- * @returns 序列化后的单元格(text为纯字符串)
|
|
|
399
|
+ * @returns 序列化后的单元格(text保留字符串或富文本数组)
|
|
336
|
400
|
*/
|
|
337
|
401
|
export function serializeTableCell(
|
|
338
|
402
|
cell: TableCell,
|
|
|
@@ -376,7 +440,7 @@ export function serializeTableCell(
|
|
376
|
440
|
}
|
|
377
|
441
|
|
|
378
|
442
|
return {
|
|
379
|
|
- text: flattenTextToString(cell.text), // 转换为纯字符串
|
|
|
443
|
+ text: cell.text,
|
|
380
|
444
|
rowspan: cell.rowspan ?? 1,
|
|
381
|
445
|
colspan: cell.colspan ?? 1,
|
|
382
|
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
|
457
|
* @param table 表格块
|
|
394
|
458
|
* @returns 序列化后的表格块
|
|
|
@@ -908,7 +972,7 @@ export function deleteTableColumn(table: TableBlock, colIndex: number): TableBlo
|
|
908
|
972
|
* - 主单元格(左上角)保留完整的字段信息
|
|
909
|
973
|
* - 被合并的单元格标记为rowspan=0, colspan=0, text=''
|
|
910
|
974
|
* - 保留所有单元格的col_index
|
|
911
|
|
- * - 合并后的文本统一为纯字符串格式
|
|
|
975
|
+ * - 合并后的文本在包含富文本时保留片段样式
|
|
912
|
976
|
*
|
|
913
|
977
|
* @param table 表格块
|
|
914
|
978
|
* @param startRow 起始行
|
|
|
@@ -1006,6 +1070,8 @@ export function mergeCellsByVisualBounds(
|
|
1006
|
1070
|
|
|
1007
|
1071
|
// 收集所有被合并单元格的文本内容
|
|
1008
|
1072
|
const displayTexts: string[] = [];
|
|
|
1073
|
+ const mergedRichText: RichText[] = [];
|
|
|
1074
|
+ let hasRichText = false;
|
|
1009
|
1075
|
|
|
1010
|
1076
|
// 辅助函数:将 text 字段规范化为纯字符串
|
|
1011
|
1077
|
const normalizeText = (text: string | RichText[]): string => {
|
|
|
@@ -1027,7 +1093,29 @@ export function mergeCellsByVisualBounds(
|
|
1027
|
1093
|
&& position.colEnd <= selectionBounds.colEnd;
|
|
1028
|
1094
|
if (isContained) {
|
|
1029
|
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
|
1134
|
if (key === primaryKey) {
|
|
1047
|
1135
|
// 主单元格:设置完整的标准格式
|
|
1048
|
|
- // 合并后的文本统一为纯字符串格式
|
|
|
1136
|
+ // 合并后的文本在包含富文本时保留片段样式
|
|
1049
|
1137
|
const mergedText = displayTexts.join(' ');
|
|
|
1138
|
+ const mergedContent = hasRichText ? mergedRichText : mergedText;
|
|
1050
|
1139
|
|
|
1051
|
1140
|
// 构建标准格式的单元格对象
|
|
1052
|
1141
|
return {
|
|
1053
|
|
- text: mergedText || normalizeText(cell.text),
|
|
|
1142
|
+ text: mergedContent.length > 0 ? mergedContent : normalizeText(cell.text),
|
|
1054
|
1143
|
rowspan: rowSpan,
|
|
1055
|
1144
|
colspan: colSpan,
|
|
1056
|
1145
|
col_index: colIdx + 1, // 列索引从1开始
|
|
|
@@ -1226,9 +1315,9 @@ export function validateBlock(block: Partial<DocumentBlock>): boolean {
|
|
1226
|
1315
|
case 'table': {
|
|
1227
|
1316
|
const table = block as Partial<TableBlock>;
|
|
1228
|
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
|
|