import ExcelJS from "exceljs"; import { VxeUI } from "vxe-pc-ui"; import { formatValueByFullFormat, getAlignByFormat, updateTableFooterData } from "@/utils/column"; export function ExportVxeTableExcel(tableRef) { if (!tableRef) { return; } // columns const fullColumns = tableRef.getFullColumns(); const exportColumns = buildColumns(fullColumns) || []; const exportDatas = tableRef.getCheckboxRecords().length > 0 ? tableRef.getCheckboxRecords() : tableRef.getFullData(); const footerDatas = updateTableFooterData(exportColumns, exportDatas); ExportExcel(exportColumns, exportDatas, footerDatas); } export function ExportExcel(columns, data, footerData = []) { const workbook = new ExcelJS.Workbook(); const sheet = workbook.addWorksheet("Sheet1"); const exportColumns = columns; const exportDatas = data; if (!exportColumns || exportColumns.length === 0) { console.error("没有找到列数据"); return; } VxeUI.modal.message({ content: `正在导出中 . . .`, status: "loading", duration: -1, }); // 1. 提取所有叶子节点(数据列) const leafColumns = []; const headerData = []; const mergeInfo = []; // 2. 解析列结构,构建表头数据 function parseColumns(cols, currentLevel = 0, startCol = 0) { let colIndex = startCol; cols.forEach((col) => { // 确保当前层级的数组存在 if (!headerData[currentLevel]) { headerData[currentLevel] = []; } if (col.children && col.children.length > 0) { // 处理有子级的列 const childLeafCount = parseColumns(col.children, currentLevel + 1, colIndex); // 记录合并信息(水平合并) if (childLeafCount > 1) { mergeInfo.push({ startRow: currentLevel + 1, // ExcelJS 行号从1开始 startCol: colIndex + 1, // ExcelJS 列号从1开始 endRow: currentLevel + 1, endCol: colIndex + childLeafCount, }); } // 设置当前行的表头 headerData[currentLevel][colIndex] = col.label || ""; // 填充子级占据的列 for (let i = 1; i < childLeafCount; i++) { headerData[currentLevel][colIndex + i] = ""; } colIndex += childLeafCount; } else { // 处理叶子节点 leafColumns.push(col); // 设置当前行和所有下层的表头 for (let level = currentLevel; level < headerData.length; level++) { if (!headerData[level]) headerData[level] = []; if (level === currentLevel) { headerData[level][colIndex] = col.label || ""; } else { headerData[level][colIndex] = col.label || ""; // 对于垂直合并,需要填充标签 } } // 记录垂直合并信息 if (headerData.length - currentLevel > 1) { mergeInfo.push({ startRow: currentLevel + 1, startCol: colIndex + 1, endRow: headerData.length, // 合并到最后一行 endCol: colIndex + 1, }); } colIndex++; } }); return colIndex - startCol; // 返回该组列占用的列数 } // 初始化 headerData 数组 const maxDepth = getMaxDepth(exportColumns); for (let i = 0; i < maxDepth; i++) { headerData[i] = []; } parseColumns(exportColumns); // 3. 添加表头行 headerData.forEach((rowData, rowIndex) => { const row = sheet.addRow(rowData); // 设置表头样式 row.eachCell((cell, colNumber) => { cell.style = { font: { bold: true, size: 10 }, alignment: { horizontal: "center", vertical: "middle" }, fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFE0E0E0" }, }, border: { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" }, }, }; }); }); // 4. 合并单元格(先排序,避免重复合并) // 按照从左上到右下的顺序排序 mergeInfo.sort((a, b) => { if (a.startRow !== b.startRow) return a.startRow - b.startRow; return a.startCol - b.startCol; }); // 记录已合并的单元格 const mergedCells = new Set(); mergeInfo.forEach((merge) => { // 检查这个合并区域是否与已合并的区域冲突 let canMerge = true; for (let r = merge.startRow; r <= merge.endRow; r++) { for (let c = merge.startCol; c <= merge.endCol; c++) { const cellKey = `${r}-${c}`; if (mergedCells.has(cellKey)) { canMerge = false; break; } } if (!canMerge) break; } if (canMerge) { try { sheet.mergeCells(merge.startRow, merge.startCol, merge.endRow, merge.endCol); // 记录已合并的单元格 for (let r = merge.startRow; r <= merge.endRow; r++) { for (let c = merge.startCol; c <= merge.endCol; c++) { mergedCells.add(`${r}-${c}`); } } } catch (error) { console.warn( `无法合并单元格 ${merge.startRow}:${merge.startCol}-${merge.endRow}:${merge.endCol}`, error, ); } } }); // 5. 设置列宽 sheet.columns = leafColumns.map((col) => ({ key: col.prop, width: pxToExcelWidth(col.width) || 20, })); // 6. 填充数据 exportDatas.forEach((item, rowIndex) => { const rowData = {}; leafColumns.forEach((col, colIndex) => { let value = item[col.prop]; // 如果是空值,设置为空字符串 rowData[col.prop] = formatValueByFullFormat(value, col.displayFormat); }); const row = sheet.addRow(rowData); // 设置数据行样式 row.eachCell((cell, colNumber) => { const leafCol = leafColumns[colNumber - 1]; const style = { font: { size: 10 }, alignment: { vertical: "middle" }, border: { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" }, }, }; style.alignment.horizontal = getAlignByFormat(leafCol.displayFormat); cell.style = style; }); }); // 7. 添加汇总行 if (footerData.length > 0) { footerData.forEach((footerItem, rowIndex) => { const summaryRowData = {}; // 遍历所有列,计算每个列的汇总值 leafColumns.forEach((col) => { summaryRowData[col.prop] = formatValueByFullFormat(footerItem[col.prop], col.displayFormat); }); // 添加汇总行 const summaryRow = sheet.addRow(summaryRowData); summaryRow.eachCell((cell) => { // 设置单元格样式 const style = { font: { bold: true, size: 10 }, alignment: { vertical: "middle" }, fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFF0F0F0" }, }, border: { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" }, }, }; style.alignment.horizontal = "right"; // 应用样式 cell.style = style; }); }); } // 8. 保存文件 workbook.xlsx .writeBuffer() .then((buffer) => { const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }); const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `export-${new Date().getTime()}.xlsx`; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); VxeUI.modal.close(); VxeUI.modal.message({ content: `导出成功`, status: "success", }); }) .catch((err) => { console.error("生成 Excel 文件失败:", err); VxeUI.modal.message({ content: `导出失败`, status: "error", }); }); } // 获取最大深度 function getMaxDepth(columns, currentDepth = 1) { let maxDepth = currentDepth; columns.forEach((col) => { if (col.children && col.children.length > 0) { const childDepth = getMaxDepth(col.children, currentDepth + 1); maxDepth = Math.max(maxDepth, childDepth); } }); return maxDepth; } function pxToExcelWidth(px) { return Number(px) / 8; } function buildColumns(arr) { return arr .filter((item) => { if ( (item.type === "checkbox" || item.type === "seq") && (!item.children || item.children.length === 0) ) { return false; } else if (item.title === "操作") { return false; } return true; }) .map((item) => { let column = {}; if (item.params) { column = { ...item.params }; } column.label = item.title; column.width = item.renderWidth || item.width; if (item.children && item.children.length > 0) { column.children = buildColumns(item.children); } return column; }); }