隨筆20241226 ExcdlJs 將數據寫入excel

使用 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);

重點總結

  1. 模板加載 :通過 fs.copyFileSync 從模板生成新文件。
  2. 頁面設置:靈活設置紙張大小、方向和邊距。
  3. 圖片插入:通過 Base64 編碼將圖片添加到指定位置。
  4. 動態填充:根據數據動態生成多頁內容,並設置改頁符。

實際應用場景

通過以上步驟,您可以實現各種基於模板的報表生成需求,適用於業務報表、自動化報告生成等場景。

完整代碼 實際應用場景

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 演示样式- pageBreakPreviewpageLayout 之一。 注意:页面布局与 frozen 视图不兼容
相关推荐
冴羽1 小时前
Solid.js 最新官方文档翻译(12)—— 派生信号与 Memos
前端·javascript·react.js
发呆的薇薇°3 小时前
React里使用uuid插件--生成随机的id
react.js·uuid·javascirpt
发呆的薇薇°3 小时前
react里使用Day.js显示时间
前端·javascript·react.js
跑跑快跑3 小时前
React vite + less
前端·react.js·less
刺客-Andy4 小时前
React 第十九节 useLayoutEffect 用途使用技巧注意事项详解
前端·javascript·react.js·typescript·前端框架
赵大仁5 小时前
深入理解 Vue 3 中的具名插槽
前端·javascript·vue.js·react.js·前端框架·ecmascript·html5
哥谭居民000115 小时前
将一个组件的propName属性与父组件中的variable变量进行双向绑定的vue3(组件传值)
javascript·vue.js·typescript·npm·node.js·css3
一条不想当淡水鱼的咸鱼16 小时前
taro中实现带有途径点的路径规划
javascript·react.js·taro
GISer_Jing17 小时前
React基础知识(总结回顾一)
前端·react.js·前端框架