362 lines
9.9 KiB
JavaScript
362 lines
9.9 KiB
JavaScript
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) || ""
|
||
);
|
||
}
|