343 lines
9.1 KiB
JavaScript
343 lines
9.1 KiB
JavaScript
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;
|
|
});
|
|
}
|