隨筆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 视图不兼容
相关推荐
徐小夕13 分钟前
花了2个月时间研究了市面上的4款开源表格组件,崩溃了,决定自己写一款
前端·javascript·react.js
聪明的墨菲特i2 小时前
React与Vue:哪个框架更适合入门?
开发语言·前端·javascript·vue.js·react.js
Jackson__3 小时前
面试官:谈一下在 ts 中你对 any 和 unknow 的理解
前端·typescript
JiangJiang4 小时前
🚀 Vue 人看 useMemo:别再滥用它做性能优化
前端·react.js·面试
Dragon Wu5 小时前
前端 React 弹窗式 滑动验证码实现
前端·javascript·react.js·typescript·前端框架·reactjs
AI程序员罗尼7 小时前
React SSR 水合(Hydration)详解
react.js
AI程序员罗尼7 小时前
React 服务端渲染 (SSR) 详解
react.js
AI程序员罗尼7 小时前
React useEffect 在服务端渲染中的执行行为
react.js
胡译胡说8 小时前
会写TypeScript就能理解日语语法——Typed Japanese项目能做到(吗?)
typescript·编程语言
Rachel_wang8 小时前
如何安装并启动 electron-prokit(react+ts) 项目
react.js·electron