Files
workspace/code/container-ocr/ocr-vue/src/utils/excel.js
T
2026-03-26 17:25:23 +08:00

362 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) || ""
);
}