import * as XLSX from 'xlsx-js-style'; import { saveAs } from 'file-saver'; import { ElMessage } from 'element-plus'; /** * 列配置类型(支持多级表头) */ export type ColumnConfig = { label: string; // 列名 prop?: string; // 字段名(叶子节点必须有) children?: ColumnConfig[]; // 子列(用于多级表头) align?: 'left' | 'center' | 'right'; // 对齐方式 width?: number; // 列宽 }; /** * 合并单元格回调函数类型(类似 Element Plus 的 span-method) */ export type SpanMethodCallback = (params: { row: any; rowIndex: number; columnIndex: number }) => { rowspan: number; colspan: number }; /** * 前端导出Excel(支持多级表头) */ export function exportToExcel(data: any[], columns: ColumnConfig[], filename: string, spanMethod?: SpanMethodCallback) { if (!data || data.length === 0) { ElMessage.warning('没有可导出的数据'); return; } try { // 获取叶子列(实际数据列) const leafColumns = getLeafColumns(columns); // 获取表头深度 const headerRows = getMaxHeaderDepth(columns); // 手动创建工作表(不使用 json_to_sheet) const worksheet: XLSX.WorkSheet = {}; // 1. 填充表头 fillHeaderCells(worksheet, columns, 0, 0, headerRows); // 2. 填充数据(从 headerRows 行开始) data.forEach((row, rowIndex) => { leafColumns.forEach((col, colIndex) => { if (col.prop) { const cellAddress = XLSX.utils.encode_cell({ r: rowIndex + headerRows, c: colIndex }); const value = getCellValue(row, col.prop); worksheet[cellAddress] = { t: 's', v: value !== undefined && value !== null ? String(value) : '' }; } }); }); // 3. 设置表头合并 const merges = calculateHeaderMerges(columns, leafColumns); // 4. 处理数据行合并 if (spanMethod) { const dataMerges = calculateMergesBySpanMethod(data, leafColumns, spanMethod, headerRows); merges.push(...dataMerges); } worksheet['!merges'] = merges; // 5. 设置列宽 worksheet['!cols'] = leafColumns.map((col) => ({ wch: col.width || 20 })); // 6. 设置范围 const lastRow = data.length + headerRows - 1; const lastCol = leafColumns.length - 1; worksheet['!ref'] = XLSX.utils.encode_range({ s: { r: 0, c: 0 }, e: { r: lastRow, c: lastCol } }); // 7. 设置所有单元格样式(包含黑色边框) const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1'); for (let R = range.s.r; R <= range.e.r; ++R) { for (let C = range.s.c; C <= range.e.c; ++C) { const cellAddress = XLSX.utils.encode_cell({ r: R, c: C }); // ✅ 确保单元格存在(即使没有内容也要创建,以便设置边框) if (!worksheet[cellAddress]) { worksheet[cellAddress] = { t: 's', v: '' }; } const isHeader = R < headerRows; worksheet[cellAddress].s = { fill: isHeader ? { fgColor: { rgb: 'bec4cb' }, patternType: 'solid' } : undefined, font: isHeader ? { color: { rgb: 'FFFFFF' }, bold: true, sz: 11 } : undefined, border: { top: { style: 'thin', color: { rgb: '000000' } }, bottom: { style: 'thin', color: { rgb: '000000' } }, left: { style: 'thin', color: { rgb: '000000' } }, right: { style: 'thin', color: { rgb: '000000' } } }, alignment: { horizontal: 'center', vertical: 'center' } }; } } // 8. 设置合并单元格居中 if (worksheet['!merges']) { worksheet['!merges'].forEach((merge) => { for (let R = merge.s.r; R <= merge.e.r; ++R) { for (let C = merge.s.c; C <= merge.e.c; ++C) { const cellAddress = XLSX.utils.encode_cell({ r: R, c: C }); if (worksheet[cellAddress]) { worksheet[cellAddress].s = { ...worksheet[cellAddress].s, alignment: { horizontal: 'center', vertical: 'center' } }; } } } }); } const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); // 导出文件 const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' }); const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); saveAs(blob, `${filename}.xlsx`); ElMessage.success('导出成功'); } catch (error) { console.error('导出失败:', error); ElMessage.error('导出失败,请重试'); } } /** * 根据 spanMethod 回调计算需要合并的单元格范围 * @param data * @param columns * @param spanMethod * @param headerRows 表头行数(数据行需要偏移) */ function calculateMergesBySpanMethod(data: any[], columns: ColumnConfig[], spanMethod: SpanMethodCallback, headerRows: number = 1): XLSX.Range[] { const merges: XLSX.Range[] = []; const coveredCells = new Set(); for (let rowIndex = 0; rowIndex < data.length; rowIndex++) { const row = data[rowIndex]; for (let colIndex = 0; colIndex < columns.length; colIndex++) { const cellKey = `${rowIndex}-${colIndex}`; if (coveredCells.has(cellKey)) continue; const result = spanMethod({ row, rowIndex, columnIndex: colIndex }); if (result && (result.rowspan > 1 || result.colspan > 1)) { // 数据行需要加上表头行数的偏移 const startRow = rowIndex + headerRows; const startCol = colIndex; const endRow = startRow + result.rowspan - 1; const endCol = startCol + result.colspan - 1; merges.push({ s: { r: startRow, c: startCol }, e: { r: endRow, c: endCol } }); for (let r = startRow; r <= endRow; r++) { for (let c = startCol; c <= endCol; c++) { if (r !== startRow || c !== startCol) { coveredCells.add(`${r - headerRows}-${c}`); } } } } } } return merges; } /** * 获取所有叶子列(实际数据列) */ function getLeafColumns(columns: ColumnConfig[]): ColumnConfig[] { const leaves: ColumnConfig[] = []; function traverse(cols: ColumnConfig[]) { cols.forEach((col) => { if (col.children && col.children.length > 0) { traverse(col.children); } else { leaves.push(col); } }); } traverse(columns); return leaves; } /** * 获取最大表头深度 */ function getMaxHeaderDepth(columns: ColumnConfig[]): number { function getDepth(cols: ColumnConfig[]): number { let maxDepth = 0; cols.forEach((col) => { if (col.children && col.children.length > 0) { maxDepth = Math.max(maxDepth, 1 + getDepth(col.children)); } else { maxDepth = Math.max(maxDepth, 1); } }); return maxDepth; } return getDepth(columns); } /** * 计算表头合并范围 */ function calculateHeaderMerges(columns: ColumnConfig[], leafColumns: ColumnConfig[]): XLSX.Range[] { const merges: XLSX.Range[] = []; const maxDepth = getMaxHeaderDepth(columns); function traverse(cols: ColumnConfig[], depth: number, startLeafIndex: number): number { let currentLeafIndex = startLeafIndex; cols.forEach((col) => { if (col.children && col.children.length > 0) { const endLeafIndex = traverse(col.children, depth + 1, currentLeafIndex); if (endLeafIndex - currentLeafIndex > 0) { merges.push({ s: { r: depth, c: currentLeafIndex }, e: { r: depth, c: endLeafIndex } }); } if (endLeafIndex > currentLeafIndex) { merges.push({ s: { r: depth, c: currentLeafIndex }, e: { r: depth, c: endLeafIndex } // 只水平合并 }); } currentLeafIndex = endLeafIndex + 1; } else { if (depth < maxDepth - 1) { merges.push({ s: { r: depth, c: currentLeafIndex }, e: { r: maxDepth - 1, c: currentLeafIndex } }); } currentLeafIndex++; } }); return currentLeafIndex - 1; } traverse(columns, 0, 0); return merges; } /** * 获取单元格值(支持嵌套属性) */ function getCellValue(row: any, prop: string): any { if (!prop.includes('.')) { return row[prop]; } const keys = prop.split('.'); let value = row; for (const key of keys) { if (value === null || value === undefined) { return undefined; } value = value[key]; } return value; } /** * 填充表头单元格 */ function fillHeaderCells(worksheet: XLSX.WorkSheet, columns: ColumnConfig[], startCol: number, depth: number, maxDepth: number): number { let currentCol = startCol; columns.forEach((col) => { if (col.children && col.children.length > 0) { // 有子列,递归处理 const endCol = fillHeaderCells(worksheet, col.children, currentCol, depth + 1, maxDepth); // 父列表头:在 depth 行,从 currentCol 到 endCol const cellAddress = XLSX.utils.encode_cell({ r: depth, c: currentCol }); worksheet[cellAddress] = { t: 's', v: col.label }; currentCol = endCol + 1; } else { // 叶子节点 const cellAddress = XLSX.utils.encode_cell({ r: depth, c: currentCol }); worksheet[cellAddress] = { t: 's', v: col.label }; currentCol++; } }); return currentCol - 1; }