diff --git a/common/changes/@visactor/vtable-plugins/fix-issue-5227-excel-leading-blank-row_2026-07-15-09-34.json b/common/changes/@visactor/vtable-plugins/fix-issue-5227-excel-leading-blank-row_2026-07-15-09-34.json new file mode 100644 index 0000000000..d4f9d08cf9 --- /dev/null +++ b/common/changes/@visactor/vtable-plugins/fix-issue-5227-excel-leading-blank-row_2026-07-15-09-34.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: preserve blank rows when importing excel files (GitHub #5227)", + "type": "patch", + "packageName": "@visactor/vtable-plugins" + } + ], + "packageName": "@visactor/vtable-plugins", + "email": "biukam.w@gmail.com" +} diff --git a/packages/vtable-plugins/__tests__/excel-import/excel.test.ts b/packages/vtable-plugins/__tests__/excel-import/excel.test.ts new file mode 100644 index 0000000000..2535e43d7a --- /dev/null +++ b/packages/vtable-plugins/__tests__/excel-import/excel.test.ts @@ -0,0 +1,22 @@ +import ExcelJS from 'exceljs'; +import { parseWorksheetToSheetData } from '../../src/excel-import/excel'; + +describe('Excel worksheet import', () => { + test('preserves data after a blank first row', async () => { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Sheet1'); + worksheet.getCell('A2').value = 'Name'; + worksheet.getCell('B2').value = 'Age'; + worksheet.getCell('A3').value = 'Alice'; + worksheet.getCell('B3').value = 30; + + const result = await parseWorksheetToSheetData(worksheet, 0); + + expect(result.rowCount).toBe(3); + expect(result.data).toEqual([ + [null, null], + ['Name', 'Age'], + ['Alice', 30] + ]); + }); +}); diff --git a/packages/vtable-plugins/demo/excel-import/issue-5227-leading-blank-row.ts b/packages/vtable-plugins/demo/excel-import/issue-5227-leading-blank-row.ts new file mode 100644 index 0000000000..0958dea612 --- /dev/null +++ b/packages/vtable-plugins/demo/excel-import/issue-5227-leading-blank-row.ts @@ -0,0 +1,107 @@ +import ExcelJS from 'exceljs'; +import * as VTable from '@visactor/vtable'; +import { parseWorksheetToSheetData } from '../../src/excel-import/excel'; + +const CONTAINER_ID = 'vTable'; + +const removeDemoToolbar = () => { + document.getElementById('issue5227Toolbar')?.remove(); +}; + +const setStatus = (message: string, pass: boolean) => { + const statusNode = document.getElementById('issue5227Status'); + if (!statusNode) { + return; + } + statusNode.textContent = message; + statusNode.style.color = pass ? '#237804' : '#cf1322'; +}; + +const createWorksheetWithLeadingBlankRow = () => { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Sheet1'); + worksheet.getCell('A2').value = 'Name'; + worksheet.getCell('B2').value = 'Age'; + worksheet.getCell('A3').value = 'Alice'; + worksheet.getCell('B3').value = 30; + return worksheet; +}; + +const renderImportedData = (container: HTMLElement, data: unknown[][]) => { + if ((window as any).tableInstance) { + (window as any).tableInstance.release(); + } + + const maxColumnCount = data.reduce((max, row) => Math.max(max, row.length), 0); + const records = data.map((row, index) => { + const record: Record = { rowIndex: index + 1 }; + for (let col = 0; col < maxColumnCount; col++) { + record[`col${col}`] = row[col] ?? null; + } + return record; + }); + + const columns: VTable.ColumnsDefine = [ + { field: 'rowIndex', title: 'Excel Row', width: 100 }, + ...Array.from({ length: maxColumnCount }, (_, col) => ({ + field: `col${col}`, + title: String.fromCharCode(65 + col), + width: 140 + })) + ]; + + const tableInstance = new VTable.ListTable({ + container, + columns, + records, + defaultRowHeight: 36 + }); + + (window as any).tableInstance = tableInstance; +}; + +export function createTable() { + removeDemoToolbar(); + + const container = document.getElementById(CONTAINER_ID)!; + container.style.width = '640px'; + container.style.height = '360px'; + container.innerHTML = ''; + + const toolbar = document.createElement('div'); + toolbar.id = 'issue5227Toolbar'; + toolbar.style.cssText = [ + 'display: flex', + 'gap: 8px', + 'align-items: center', + 'height: 48px', + 'font-size: 12px' + ].join(';'); + toolbar.innerHTML = ` + + 预期:保留空白第 1 行,rowCount=3。 + + `; + container.before(toolbar); + + document.getElementById('issue5227Import')?.addEventListener('click', async () => { + const worksheet = createWorksheetWithLeadingBlankRow(); + const result = await parseWorksheetToSheetData(worksheet, 0); + const pass = + result.rowCount === 3 && + result.columnCount === 2 && + result.data[0]?.[0] === null && + result.data[1]?.[0] === 'Name' && + result.data[2]?.[0] === 'Alice'; + + renderImportedData(container, result.data); + setStatus( + `${pass ? 'PASS' : 'FAIL'} | rowCount=${result.rowCount}, columnCount=${result.columnCount}, data=${JSON.stringify( + result.data + )}`, + pass + ); + }); + + setStatus('READY', true); +} diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts index f5e19298ca..c5da594533 100644 --- a/packages/vtable-plugins/demo/menu.ts +++ b/packages/vtable-plugins/demo/menu.ts @@ -11,6 +11,10 @@ export const menus = [ path: 'excel-import', name: 'excel-import' }, + { + path: 'excel-import', + name: 'issue-5227-leading-blank-row' + }, { path: 'filter', name: 'filter' diff --git a/packages/vtable-plugins/src/excel-import/excel.ts b/packages/vtable-plugins/src/excel-import/excel.ts index 3e64674de5..6d7de56b0a 100644 --- a/packages/vtable-plugins/src/excel-import/excel.ts +++ b/packages/vtable-plugins/src/excel-import/excel.ts @@ -150,9 +150,16 @@ export async function parseWorksheetToSheetData( const sheetTitle = worksheet.name || `Sheet${sheetIndex + 1}`; const sheetKey = `sheet_${Date.now()}_${sheetIndex}`; - // 获取实际数据范围 - const rowCount = worksheet.actualRowCount || 0; - const columnCount = worksheet.actualColumnCount || 0; + // 获取实际数据范围。actualRowCount/actualColumnCount 只统计非空行列的数量, + // 当数据前面或中间存在空白行列时,不能作为最后一个数据单元格的索引。 + let rowCount = 0; + let columnCount = 0; + worksheet.eachRow((row, rowNumber) => { + rowCount = Math.max(rowCount, rowNumber); + row.eachCell((_cell, colNumber) => { + columnCount = Math.max(columnCount, colNumber); + }); + }); if (rowCount === 0 || columnCount === 0) { // 空 sheet,但仍需要解析合并单元格信息(可能只有合并单元格没有数据)