import ExcelJS from "exceljs"; import { formatValueByFullFormat, getAlignByFormat } from "@/utils/columns"; import { ElLoading, ElMessage } from "element-plus"; import { getAliyunFileBlob } from "./file"; import { saveAs } from "file-saver"; export async function ExportVxeTableExcel(tableRef) { const $table = unref(tableRef); if (!$table) { return; } const loading = ElLoading.service({ lock: true, text: "正在下载Excel文件中,请稍候...", }); // columns const fullColumns = $table.getFullColumns(); const exportColumns = buildColumns(fullColumns) || []; const exportDatas = $table.getCheckboxRecords().length > 0 ? $table.getCheckboxRecords() : $table.getFullData(); // const footerDatas = updateTableFooterData(exportColumns, exportDatas) const footerDatas = []; const options = { columns: exportColumns, data: exportDatas, }; try { await exportExcel(options); } catch (e) { } finally { loading.close(); } } async function urlToBase64(url) { return new Promise(async (resolve, reject) => { try { const blob = await getAliyunFileBlob(url); // Create an object URL from the blob const objectURL = URL.createObjectURL(blob); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const img = new Image(); img.crossOrigin = "Anonymous"; img.onload = function () { canvas.height = img.height; canvas.width = img.width; ctx.drawImage(img, 0, 0, img.width, img.height); var dataURL = canvas.toDataURL("image/png"); resolve(dataURL); // Optional: Revoke the object URL once done to free memory URL.revokeObjectURL(objectURL); }; img.src = objectURL; } catch (error) { reject(error); } }); } function buildColumns(arr) { return arr .map((item) => { // 先处理 children(关键!) let children = []; if (item.children && item.children.length > 0) { children = buildColumns(item.children); } // 判断当前节点是否需要保留 const valid = item.type === undefined && item.field !== undefined; // 如果当前不合法 + 没有子节点,直接丢 if (!valid && children.length === 0) { return null; } // 构建 column const column = { label: item.title, width: item.params?.col_Width || item.renderWidth, field: item.field, type: item.params?.col_DisplayFormat, }; // 有子节点就挂上 if (children.length > 0) { column.children = children; } return column; }) .filter(Boolean); } /** * 导出Excel文件(支持多级表头、表尾、图片) * @param {Object} options 导出配置 * @param {String} fileName 文件名(默认:export.xlsx) */ export async function exportExcel(options, fileName = "export.xlsx") { const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet("Sheet1"); // 1. 处理多级表头 const { headerRows, leafColumns, mergeCells } = buildMultiLevelHeader(options.columns); // 写入表头行 headerRows.forEach((row, rowIndex) => { const headerRow = worksheet.addRow(row); // 设置表头样式(突出显示) headerRow.eachCell((cell) => { cell.style = { font: { bold: true, size: 12, color: { argb: "FFFFFFFF" } }, fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FF4F81BD" }, // 蓝色背景 }, border: { top: { style: "thin", color: { argb: "FF000000" } }, left: { style: "thin", color: { argb: "FF000000" } }, bottom: { style: "thin", color: { argb: "FF000000" } }, right: { style: "thin", color: { argb: "FF000000" } }, }, alignment: { vertical: "middle", horizontal: "center" }, }; }); }); // 合并表头单元格 mergeCells.forEach((merge) => { worksheet.mergeCells(merge.start.row, merge.start.col, merge.end.row, merge.end.col); }); // 2. 写入数据行 const dataStartRow = headerRows.length + 1; for (const [rowIndex, item] of options.data.entries()) { const dataRow = worksheet.addRow([]); for (const [colIndex, col] of leafColumns.entries()) { const cell = dataRow.getCell(colIndex + 1); const value = getNestedValue(item, col.field); // 处理图片类型 if (col.type === "image" && value) { try { const base64 = await urlToBase64(value) // 添加图片到工作簿 const imageId = workbook.addImage({ base64, extension: "png", }); // 计算图片位置(跳过表头行) const imageRow = dataStartRow + rowIndex; worksheet.addImage(imageId, { tl: { col: colIndex, row: imageRow - 1 }, ext: { width: col.imageWidth || 80, height: col.imageHeight || 80 }, }); // 调整行高以适应图片 dataRow.height = Math.max(dataRow.height || 20, (col.imageHeight || 80) * 0.75); } catch (error) { console.warn(`图片导出失败: ${error.message}`); cell.value = "[图片]"; } } else { cell.value = value; } // 设置数据行样式 cell.style = { alignment: { vertical: "middle", horizontal: "left" }, }; } } // 3. 写入表尾数据(突出显示) if (options.footer && options.footer.length > 0) { const footerStartRow = dataStartRow + options.data.length; options.footer.forEach((footerItem, rowIndex) => { const footerRow = worksheet.addRow([]); leafColumns.forEach((col, colIndex) => { const cell = footerRow.getCell(colIndex + 1); const value = getNestedValue(footerItem, col.field); cell.value = value; // 设置表尾样式(突出显示) cell.style = { font: { bold: true }, // fill: { // type: "pattern", // pattern: "solid", // // fgColor: { argb: "FFC00000" }, // 红色背景 // }, // border: { // top: { style: "medium", color: { argb: "FF000000" } }, // left: { style: "medium", color: { argb: "FF000000" } }, // bottom: { style: "medium", color: { argb: "FF000000" } }, // right: { style: "medium", color: { argb: "FF000000" } }, // }, alignment: { vertical: "middle", horizontal: "center" }, }; }); }); } // 4. 设置列宽 leafColumns.forEach((col, index) => { const column = worksheet.getColumn(index + 1); column.width = col.width ? col.width / 7 : 20; // 转换像素为Excel单位 }); // 5. 生成并保存文件 const buffer = await workbook.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }); saveAs(blob, fileName); } /** * 构建多级表头 * @param {Array} columns 表头配置 * @returns {Object} { headerRows, leafColumns, mergeCells } */ function buildMultiLevelHeader(columns) { // 获取表头最大深度 const maxDepth = getMaxDepth(columns); // 获取所有叶子节点列 const leafColumns = getLeafColumns(columns); // 初始化表头二维数组 const headerRows = Array.from({ length: maxDepth }, () => Array(leafColumns.length).fill("")); const mergeCells = []; // 递归填充表头 function fillHeader(nodes, startCol = 0, depth = 0) { let currentCol = startCol; nodes.forEach((node) => { // 计算当前节点跨越的列数 const colSpan = node.children ? getLeafCount(node.children) : 1; // 填充当前节点的表头 headerRows[depth][currentCol] = node.label || ""; // 如果有子节点,递归处理 if (node.children && node.children.length > 0) { fillHeader(node.children, currentCol, depth + 1); // 如果跨多列,需要合并单元格 if (colSpan > 1) { mergeCells.push({ start: { row: depth + 1, col: currentCol + 1 }, end: { row: depth + 1, col: currentCol + colSpan }, }); } } // 如果当前节点有父节点且不是叶子节点,需要纵向合并 if (depth < maxDepth - 1 && !node.children) { mergeCells.push({ start: { row: depth + 1, col: currentCol + 1 }, end: { row: maxDepth, col: currentCol + 1 }, }); } currentCol += colSpan; }); } fillHeader(columns); return { headerRows, leafColumns, mergeCells }; } /** * 获取树的最大深度 */ function getMaxDepth(nodes, currentDepth = 1) { if (!nodes || nodes.length === 0) return currentDepth; let maxDepth = currentDepth; nodes.forEach((node) => { if (node.children && node.children.length > 0) { const depth = getMaxDepth(node.children, currentDepth + 1); maxDepth = Math.max(maxDepth, depth); } }); return maxDepth; } /** * 获取所有叶子节点列 */ function getLeafColumns(nodes, result = []) { nodes.forEach((node) => { if (node.children && node.children.length > 0) { getLeafColumns(node.children, result); } else { result.push({ field: node.field, label: node.label, width: node.width, type: node.type, imageWidth: node.imageWidth, imageHeight: node.imageHeight, }); } }); return result; } /** * 获取叶子节点数量 */ function getLeafCount(nodes) { if (!nodes || nodes.length === 0) return 0; return nodes.reduce((count, node) => { if (node.children && node.children.length > 0) { return count + getLeafCount(node.children); } return count + 1; }, 0); } /** * 获取嵌套对象的值 */ function getNestedValue(obj, path) { if (!path) return ""; return ( path.split(".").reduce((prev, curr) => { return prev ? prev[curr] : ""; }, obj) || "" ); }