@@ -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 文件
0 commit comments