使用 ExcelJS 操作 Excel 文件
前言
ExcelJS
是一個功能強大的 Node.js 庫,可以用來創建、讀取和修改 Excel 文件。本文將重點介紹如何基於模板生成 Excel 文件,並進行數據填充、圖片插入和頁面設置。
核心功能實現
以下是完整的函數實現,用於將數據寫入 Excel 文件,並進行頁面設置和數據填充。
1. 初始化與模板加載
首先,引入必要依賴並初始化工作簿:
TypeScript
import ExcelJS from 'exceljs';
import fs from 'fs';
const templatePath = 'C:\\Work\\daiwa_house_air2\\Second\\template.xlsx';
const outputPath = 'output.xlsx';
if (!fs.existsSync(templatePath)) {
throw new Error(`模板文件不存在: ${templatePath}`);
}
fs.copyFileSync(templatePath, outputPath);
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(outputPath);
2. 設置頁面屬性
設置頁面格式為 A4 橫向,並調整頁邊距:
TypeScript
const worksheet = workbook.worksheets[0];
worksheet.name = '提案書';
worksheet.pageSetup = {
paperSize: 9, // A4
orientation: 'landscape',
fitToPage: true,
fitToHeight: 0,
fitToWidth: 1
};
worksheet.pageSetup.margins = {
left: 0.5, right: 0.5,
top: 0.8, bottom: 0.75,
header: 0.3, footer: 0.3
};
3. 插入圖片
通過 Base64 圖片編碼添加圖片到指定位置:
TypeScript
const imageId = workbook.addImage({
base64: imageBase64,
extension: 'png',
});
worksheet.addImage(imageId, {
tl: { col: 8.5, row: 8 },
ext: { width: 700, height: 650 }
});
4. 填充數據與生成多頁內容
根據數據動態填充多頁內容,並插入改頁符:
TypeScript
const copyStartRow = 36;
const copyEndRow = 63;
const templateNumber = 29;
let pageBreaks = [35];
for (let i = 1; i < Math.ceil(dataList.length / 4); i++) {
for (let rowIndex = copyStartRow; rowIndex <= copyEndRow; rowIndex++) {
const sourceRow = worksheet.getRow(rowIndex);
const targetRow = worksheet.getRow(rowIndex + (templateNumber * i));
sourceRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
const targetCell = targetRow.getCell(colNumber);
targetCell.value = cell.value;
if (cell.style) {
targetCell.style = { ...cell.style };
}
});
targetRow.commit();
}
pageBreaks.push(copyStartRow + (templateNumber * i) - 1);
}
pageBreaks.forEach((rowNumber) => {
const row = worksheet.getRow(rowNumber);
row.addPageBreak();
});
5. 保存文件
最後將修改後的文件保存:
TypeScript
await workbook.xlsx.writeFile(outputPath);
重點總結
- 模板加載 :通過
fs.copyFileSync
從模板生成新文件。 - 頁面設置:靈活設置紙張大小、方向和邊距。
- 圖片插入:通過 Base64 編碼將圖片添加到指定位置。
- 動態填充:根據數據動態生成多頁內容,並設置改頁符。
實際應用場景
通過以上步驟,您可以實現各種基於模板的報表生成需求,適用於業務報表、自動化報告生成等場景。
完整代碼 實際應用場景
TypeScript
import { app, shell } from 'electron'; // Electron 模块,用于应用和文件操作
import ExcelJS from 'exceljs'; // ExcelJS 库,用于操作 Excel 文件
import fs from 'fs'; // Node.js 文件系统模块
import * as path from "path"; // Node.js 路径模块
import { Response } from './Responce'; // 自定义响应对象
import { Messages } from './Messages'; // 自定义消息对象
import { t_proposal_result_initial_cost } from '../entity/t_proposal_result_initial_cost'; // 初始成本实体
import { t_proposal_result_running_cost } from '../entity/t_proposal_result_running_cost'; // 运行成本实体
export async function writeProposalResultToExcel(
dataList: any[], // 包含提案结果数据的数组
imageBase64: string, // 图片的 Base64 编码
outputPath: string // 输出的 Excel 文件路径
): Promise<Response> {
try {
// 模板文件路径
const templatePath = 'C:\\Work\\daiwa_house_air2\\Second\\template.xlsx';
// 定义需要复制的起始和结束行
const copyStartRow = 36;
const copyEndRow = 63;
// 检查模板文件是否存在
if (!fs.existsSync(templatePath)) {
throw new Error(`模板文件不存在: ${templatePath}`);
}
// 复制模板文件到目标路径
fs.copyFileSync(templatePath, outputPath);
// 打开 Excel 文件
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(outputPath);
// 获取第一个工作表
const worksheet = workbook.worksheets[0];
worksheet.name = '提案书'; // 重命名工作表
if (!worksheet) {
throw new Error("模板文件中找不到有效的工作表");
}
// 设置页面为 A4 横向
worksheet.pageSetup = {
paperSize: 9, // A4 纸张
orientation: 'landscape', // 横向
fitToPage: true, // 自动调整到单页宽度
fitToHeight: 0, // 高度不限
fitToWidth: 1 // 宽度为 1 页
};
// 设置页边距
worksheet.pageSetup.margins = {
left: 0.5, right: 0.5, // 左右边距
top: 0.8, bottom: 0.75, // 上下边距
header: 0.3, footer: 0.3 // 页眉和页脚边距
};
// 添加图片
const imageId = workbook.addImage({
base64: imageBase64,
extension: 'png', // 图片格式为 PNG
});
// 初始化分页位置数组
let pageBreaks: number[] = [35];
const templateNumber: number = 29; // 每页模板的行数
const rowCount: number = dataList.length; // 数据的行数
// 根据数据量调整表格内容
if (rowCount <= 4) {
// 数据少于 4 行时,清除模板的多余行
for (let rowIndex = copyStartRow; rowIndex <= copyEndRow; rowIndex++) {
const row = worksheet.getRow(rowIndex);
row.values = [];
row.commit();
}
} else {
// 数据多于 4 行时,复制模板并插入更多内容
for (let i = 1; i < (Math.ceil(dataList.length / 4)) - 1; i++) {
for (let rowIndex = copyStartRow; rowIndex <= copyEndRow; rowIndex++) {
const sourceRow = worksheet.getRow(rowIndex);
const targetRow = worksheet.getRow(rowIndex + (templateNumber * i));
// 复制行内容和样式
sourceRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
const targetCell = targetRow.getCell(colNumber);
targetCell.value = cell.value;
if (cell.style) {
targetCell.style = { ...cell.style };
}
});
targetRow.commit();
}
// 记录分页符位置
pageBreaks.push(copyStartRow + (templateNumber * i) - 1);
}
}
// 填充数据到指定单元格
if (dataList.length > 0) {
const firstData = dataList[0];
worksheet.getCell(5, 6).value = firstData.initialCostSum ?? ''; // 初始成本
worksheet.getCell(6, 6).value = firstData.runningCostSum ?? ''; // 运行成本
worksheet.getCell(5, 10).value = firstData.constructionPlace ?? ''; // 建设地
worksheet.getCell(6, 10).value = firstData.powerUnit ?? ''; // 电力单价
}
// 根据数据填充每页内容
const target = [5, 6, 7, 8]; // 数据目标列
let coordinateRow: number;
let initial: number;
let timing: number = 0;
for (let i = 0; i < dataList.length; timing++) {
coordinateRow = 8 + templateNumber * timing;
// 插入图片到每页
worksheet.addImage(imageId, {
tl: { col: 8.5, row: coordinateRow }, // 图片插入位置
ext: { width: 700, height: 650 } // 图片大小
});
for (let j = 0; j < target.length; j++) {
initial = coordinateRow;
// 填充单页内容
if (i >= dataList.length) break;
worksheet.getCell(initial++, target[j]).value = dataList[i].roomName;
worksheet.getCell(initial++, target[j]).value = dataList[i].floorArea;
worksheet.getCell(initial++, target[j]).value = dataList[i].airConditionMethod;
worksheet.getCell(initial++, target[j]).value = dataList[i].airConditionEquipment;
worksheet.getCell(initial++, target[j]).value = dataList[i].targetArea;
worksheet.getCell(initial++, target[j]).value = dataList[i].targetCount;
worksheet.getCell(initial++, target[j]).value = dataList[i].airConditioningAbility;
worksheet.getCell(initial++, target[j]).value = dataList[i].coolingTemperature;
worksheet.getCell(initial++, target[j]).value = dataList[i].heatingTemperature;
worksheet.getCell(initial++, target[j]).value = dataList[i].initialCost;
worksheet.getCell(initial++, target[j]).value = dataList[i].runningCostSum;
worksheet.getCell(initial++, target[j]).value = dataList[i].heatStrokeEffect;
worksheet.getCell(initial++, target[j]).value = dataList[i].bedewingEffect;
// 填充每月运行成本
dataList[i].monthlyRunningCosts.forEach((monthData) => {
worksheet.getCell(initial++, target[j]).value = monthData.cost;
});
i++;
}
}
// 插入分页符
pageBreaks.forEach((rowNumber) => {
worksheet.getRow(rowNumber).addPageBreak();
});
// 配置工作表视图
worksheet.views = [{
state: 'normal',
showGridLines: true,
zoomScale: 100,
style: 'pageBreakPreview',
}];
// 保存 Excel 文件
await workbook.xlsx.writeFile(outputPath);
return { success: true, message: "Excel 文件已成功生成" };
} catch (error) {
console.error("生成 Excel 时发生错误:", error);
return { success: false, message: error.message || Messages.ERRORS.ERR999 };
}
}
工作表视图⬆
现在,工作表支持视图列表,这些视图控制Excel如何显示工作表:
frozen
- 顶部和左侧的许多行和列被冻结在适当的位置。 仅右下部分会滚动split
- 该视图分为4个部分,每个部分可半独立滚动。
属性名 | 默认值 | 描述 |
---|---|---|
state | 'normal' |
控制视图状态 - 'normal' , 'frozen' 或者 'split' 之一 |
rightToLeft | false |
将工作表视图的方向设置为从右到左 |
activeCell | undefined |
当前选择的单元格 |
showRuler | true |
在页面布局中显示或隐藏标尺 |
showRowColHeaders | true |
显示或隐藏行标题和列标题(例如,顶部的 A1,B1 和左侧的1,2,3) |
showGridLines | true |
显示或隐藏网格线(针对未定义边框的单元格显示) |
zoomScale | 100 | 用于视图的缩放比例 |
zoomScaleNormal | 100 | 正常缩放视图 |
style | undefined |
演示样式- pageBreakPreview 或 pageLayout 之一。 注意:页面布局与 frozen 视图不兼容 |