20260711173006
This commit is contained in:
1 parent
d080a2a48d
commit
446758b2fe
384 files changed
+153926
-5937
No files matched your search
@@ -0,0 +1,342 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user