Skip to content

Commit f5737ae

Browse files
committed
feat: mergecell import into vtable-sheet
1 parent fc5522e commit f5737ae

2 files changed

Lines changed: 201 additions & 3 deletions

File tree

packages/vtable-plugins/src/excel-import.ts

Lines changed: 200 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ export interface SheetData {
2222
columnCount: number;
2323
/** 行数 */
2424
rowCount: number;
25+
/** 单元格合并信息 */
26+
cellMerge?: Array<{
27+
text?: string;
28+
range: {
29+
start: {
30+
col: number;
31+
row: number;
32+
};
33+
end: {
34+
col: number;
35+
row: number;
36+
};
37+
isCustom?: boolean;
38+
};
39+
}>;
2540
}
2641

2742
// 多 sheet 导入结果类型
@@ -164,6 +179,7 @@ export class ExcelImportPlugin implements pluginsDefinition.IVTablePlugin {
164179
data: sheetData.data,
165180
rowCount: Math.max(sheetData.rowCount, 100),
166181
columnCount: Math.max(sheetData.columnCount, 26),
182+
cellMerge: sheetData.cellMerge,
167183
active: false // 稍后统一激活
168184
};
169185

@@ -286,6 +302,7 @@ export class ExcelImportPlugin implements pluginsDefinition.IVTablePlugin {
286302
data: sheetData.data,
287303
rowCount: Math.max(sheetData.rowCount, 100),
288304
columnCount: Math.max(sheetData.columnCount, 26),
305+
cellMerge: sheetData.cellMerge,
289306
active: false // 稍后统一激活
290307
};
291308

@@ -1084,13 +1101,15 @@ ${recordsStr}
10841101
const columnCount = worksheet.actualColumnCount || 0;
10851102

10861103
if (rowCount === 0 || columnCount === 0) {
1087-
// 空 sheet
1104+
// 空 sheet,但仍需要解析合并单元格信息(可能只有合并单元格没有数据)
1105+
const cellMerge = this._parseMergedCells(worksheet, []);
10881106
return {
10891107
sheetTitle,
10901108
sheetKey,
10911109
data: [],
10921110
columnCount: 0,
1093-
rowCount: 0
1111+
rowCount: 0,
1112+
...(cellMerge.length > 0 ? { cellMerge } : {})
10941113
};
10951114
}
10961115

@@ -1133,15 +1152,193 @@ ${recordsStr}
11331152
data.push(rowData);
11341153
}
11351154

1155+
// 解析合并单元格信息
1156+
const cellMerge = this._parseMergedCells(worksheet, data);
1157+
11361158
return {
11371159
sheetTitle,
11381160
sheetKey,
11391161
data,
11401162
columnCount,
1141-
rowCount
1163+
rowCount,
1164+
...(cellMerge.length > 0 ? { cellMerge } : {})
11421165
};
11431166
}
11441167

1168+
/**
1169+
* 解析 Excel 合并单元格信息
1170+
* @param worksheet ExcelJS worksheet 对象
1171+
* @param data 已解析的数据数组
1172+
* @returns 合并单元格信息数组
1173+
*/
1174+
private _parseMergedCells(
1175+
worksheet: ExcelJS.Worksheet,
1176+
data: unknown[][]
1177+
): Array<{
1178+
text?: string;
1179+
range: {
1180+
start: { col: number; row: number };
1181+
end: { col: number; row: number };
1182+
isCustom?: boolean;
1183+
};
1184+
}> {
1185+
const cellMerge: Array<{
1186+
text?: string;
1187+
range: {
1188+
start: { col: number; row: number };
1189+
end: { col: number; row: number };
1190+
isCustom?: boolean;
1191+
};
1192+
}> = [];
1193+
1194+
try {
1195+
// ExcelJS 中合并单元格信息存储在 worksheet.model.merges 中
1196+
// 格式: { 'A1': { tl: 'A1', br: 'B2' }, ... }
1197+
// 注意:ExcelJS 的类型定义可能不完整,使用 unknown 进行类型转换
1198+
const worksheetAny = worksheet as unknown as {
1199+
model?: { merges?: Record<string, unknown> };
1200+
_merges?: Record<string, unknown>;
1201+
};
1202+
const merges: Record<string, unknown> =
1203+
(worksheetAny.model?.merges as Record<string, unknown>) ||
1204+
(worksheetAny._merges as Record<string, unknown>) ||
1205+
{};
1206+
1207+
for (const [masterCell, range] of Object.entries(merges)) {
1208+
try {
1209+
let startCol: number;
1210+
let startRow: number;
1211+
let endCol: number;
1212+
let endRow: number;
1213+
1214+
// 检查 range 的类型
1215+
if (typeof range === 'string') {
1216+
// range 是地址范围字符串,如 'A1:B3'
1217+
const rangeMatch = range.match(/^([A-Z]+\d+):([A-Z]+\d+)$/i);
1218+
if (!rangeMatch) {
1219+
continue;
1220+
}
1221+
1222+
const startAddr = this._parseCellAddress(rangeMatch[1]);
1223+
const endAddr = this._parseCellAddress(rangeMatch[2]);
1224+
if (!startAddr || !endAddr) {
1225+
continue;
1226+
}
1227+
1228+
startCol = startAddr.col;
1229+
startRow = startAddr.row;
1230+
endCol = endAddr.col;
1231+
endRow = endAddr.row;
1232+
} else if (typeof range === 'object' && range !== null) {
1233+
// range 是对象格式
1234+
const rangeObj = range as {
1235+
tl?: string;
1236+
br?: string;
1237+
top?: number;
1238+
left?: number;
1239+
bottom?: number;
1240+
right?: number;
1241+
};
1242+
1243+
if (rangeObj.tl && rangeObj.br) {
1244+
// 使用地址字符串格式 (如 'A1', 'B2')
1245+
const startAddr = this._parseCellAddress(rangeObj.tl);
1246+
const endAddr = this._parseCellAddress(rangeObj.br);
1247+
if (!startAddr || !endAddr) {
1248+
continue;
1249+
}
1250+
1251+
startCol = startAddr.col;
1252+
startRow = startAddr.row;
1253+
endCol = endAddr.col;
1254+
endRow = endAddr.row;
1255+
} else if (
1256+
typeof rangeObj.top === 'number' &&
1257+
typeof rangeObj.left === 'number' &&
1258+
typeof rangeObj.bottom === 'number' &&
1259+
typeof rangeObj.right === 'number'
1260+
) {
1261+
// 使用行列索引格式(ExcelJS 内部格式,1-based)
1262+
startRow = rangeObj.top - 1; // 转换为 0-based
1263+
startCol = rangeObj.left - 1; // 转换为 0-based
1264+
endRow = rangeObj.bottom - 1; // 转换为 0-based
1265+
endCol = rangeObj.right - 1; // 转换为 0-based
1266+
} else {
1267+
continue;
1268+
}
1269+
} else {
1270+
continue;
1271+
}
1272+
1273+
// 获取合并单元格的文本内容(从主单元格)
1274+
let text: string | undefined;
1275+
if (startRow >= 0 && startRow < data.length && startCol >= 0 && startCol < data[startRow].length) {
1276+
const cellValue = data[startRow][startCol];
1277+
if (cellValue !== null && cellValue !== undefined) {
1278+
text = String(cellValue);
1279+
}
1280+
}
1281+
1282+
cellMerge.push({
1283+
text,
1284+
range: {
1285+
start: {
1286+
col: startCol,
1287+
row: startRow
1288+
},
1289+
end: {
1290+
col: endCol,
1291+
row: endRow
1292+
},
1293+
isCustom: true
1294+
}
1295+
});
1296+
} catch (error) {
1297+
console.warn(`解析合并单元格 ${masterCell} 时出错:`, error);
1298+
// 继续处理其他合并单元格
1299+
}
1300+
}
1301+
} catch (error) {
1302+
console.warn('解析合并单元格信息时出错:', error);
1303+
}
1304+
1305+
return cellMerge;
1306+
}
1307+
1308+
/**
1309+
* 解析单元格地址(如 'A1')为行列索引(0-based)
1310+
* @param address 单元格地址字符串
1311+
* @returns 行列索引对象,解析失败返回 null
1312+
*/
1313+
private _parseCellAddress(address: string): { col: number; row: number } | null {
1314+
try {
1315+
// 匹配格式:列字母 + 行号,如 'A1', 'B2', 'AA10'
1316+
const match = address.match(/^([A-Z]+)(\d+)$/i);
1317+
if (!match) {
1318+
return null;
1319+
}
1320+
1321+
const colLetters = match[1].toUpperCase();
1322+
const rowNumber = parseInt(match[2], 10);
1323+
1324+
// 转换列字母为索引 (A=0, B=1, ..., Z=25, AA=26, etc.)
1325+
// Excel 列是 26 进制,但特殊的是没有 0,A=1, Z=26, AA=27
1326+
let col = 0;
1327+
for (let i = 0; i < colLetters.length; i++) {
1328+
col = col * 26 + (colLetters.charCodeAt(i) - 64); // 'A' = 65, 所以 -64 得到 1
1329+
}
1330+
col = col - 1; // 转换为 0-based (A=1->0, B=2->1, ..., AA=27->26)
1331+
1332+
// 行号转换为 0-based(Excel 使用 1-based)
1333+
const row = rowNumber - 1;
1334+
1335+
return { col, row };
1336+
} catch (error) {
1337+
console.warn(`解析单元格地址 "${address}" 时出错:`, error);
1338+
return null;
1339+
}
1340+
}
1341+
11451342
/**
11461343
* 静态方法:从 Excel 文件导入多个 sheet
11471344
* @param file Excel 文件

packages/vtable-sheet/src/components/vtable-sheet.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ export default class VTableSheet {
464464
data: sheetData.data as any,
465465
rowCount: Math.max(sheetData.rowCount, 100), // 至少 100 行
466466
columnCount: Math.max(sheetData.columnCount, 10), // 至少 10 列
467+
cellMerge: sheetData.cellMerge,
467468
active: false
468469
};
469470

0 commit comments

Comments
 (0)