You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让Puppeteer生成的PDF从第二页开始显示页码?

实现Puppeteer生成带封面的PDF(页码从第二页开始)

需求与问题

需要生成单个PDF文件,第一页作为封面不显示页码,从第二页开始页码从1计数。尝试过addScriptTag、page.evaluate修改内置pageNumber、CSS控制、页码减1等方法均无效,因Chromium默认页码从1开始,内置pageNumber难以直接修改。

可行解决方案

方案1:CSS+页眉脚本动态调整(单页生成)

核心思路:通过CSS隐藏第一页的页眉,同时在页眉模板中注入脚本,将页码数值减1(第一页直接隐藏页码元素)。

修改步骤:

  1. 调整页面样式,给第一页单独设置页眉隐藏规则
  2. 在页眉模板中添加脚本,动态计算并修改页码
  3. 保持原有PDF生成逻辑,调整部分配置

修改后的核心代码:

// 修改page.addStyleTag部分,添加封面页页眉隐藏规则
await page.addStyleTag({
    content: `
        body { margin-top: 1cm; }
        @page:first { 
            margin-top: 0; 
            /* 清空第一页的页眉区域内容 */
            @top-left { content: ""; }
            @top-center { content: ""; }
            @top-right { content: ""; }
        }
    `,
});

// 修改headerTemplate,添加动态调整页码的脚本
headerTemplate: `
    ${styleContent}
    <script>
        window.onload = function() {
            const pageNumEl = document.querySelector('.pageNumber');
            const currentPage = parseInt(pageNumEl.textContent);
            if (currentPage > 1) {
                // 第二页开始页码从1计数
                pageNumEl.textContent = currentPage - 1;
            } else {
                // 第一页隐藏页码行
                pageNumEl.parentElement.style.display = 'none';
            }
        }
    </script>
    <div class='invoice-code'>
        <div style='width:40%; font-size: 10px;'></div>
        <div style='width:30%; font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>PURCHASE ORDER</div>
        <div style='width:30%; font-size: 10px; line-height:1.2;'>
            <div style=' font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>Number<span style="font-weight: normal;"> : ${payloadJSON.po_number}</span></div>
            <div style=' font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>Po Date<span style="font-weight: normal;"> : ${payloadJSON.po_date}</span></div>
            <div style=' font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>Page No <span style="font-weight: normal;">:</span> <span style="font-weight: normal;" class="pageNumber"> </span></div>
        </div>
    </div>
`,

方案2:分生成再合并(更稳定)

如果方案1因Chromium的页眉渲染机制限制失效,可以采用分两次生成PDF再合并的方式,彻底规避页码计数问题:

步骤:

  1. 安装PDF合并库pdf-lib:npm install pdf-lib
  2. 分别生成封面PDF(无页码)和内容PDF(页码从1开始)
  3. 合并两个PDF文件

修改后的完整代码:

const puppeteer = require('puppeteer');
const hbs = require('handlebars');
const fs = require('fs');
const path = require('path');
const { PDFDocument } = require('pdf-lib'); // 引入pdf-lib
const data = require('./data.json');
const ThermalPrinterHelper = require('./ThermalPrinterHelper');
const { generateQrCode, generateBarCode } = require('./generator');

hbs.registerHelper('inc', function (value, options) {
    return parseInt(value) + 1;
});

const compile = async function (template, data) {
    const filePath = path.join(__dirname, 'templates', `${template}.hbs`);
    const templateSource = await fs.promises.readFile(filePath, 'utf-8');
    return hbs.compile(templateSource)(data);
};

const styleContent = `
            <style>
                .invoice-code {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    width: 100%;
                    min-height: 100px;
                    font-size: 10px;
                }
            </style>
        `;

function groupBySubtotalQuantity(poItems) {
    const groupedPayload = {};
    poItems.forEach((item) => {
        const groupKey = item.article_no.toString().substring(0, 9);
        const totalQuantity = parseFloat(item.quantity);
        if (!groupedPayload[groupKey]) {
            groupedPayload[groupKey] = {
                total_quantity: 0,
                items: [],
            };
        }
        groupedPayload[groupKey].items.push(item);
        groupedPayload[groupKey].total_quantity += totalQuantity;
    });

    Object.keys(groupedPayload).forEach((key) => {
        groupedPayload[key].total_quantity = parseFloat(
            groupedPayload[key].total_quantity.toFixed(3)
        ).toFixed(3);
    });

    Object.keys(groupedPayload).forEach((key) => {
        groupedPayload[key].items.sort((a, b) => {
            const getLastNumber = (str) => parseInt(str.match(/\d+$/)[0]);
            return getLastNumber(a.material_description) - getLastNumber(b.material_description);
        });
    });

    const result = Object.keys(groupedPayload).map((key) => groupedPayload[key]);
    return { po_item: result };
}

// 生成封面PDF(无页码)
const generateCoverPDF = async (page, payload) => {
    const coverContent = await compile('cover', payload); // 需提前创建cover.hbs封面模板
    await page.setContent(coverContent);
    return await page.pdf({
        format: 'A4',
        printBackground: true,
        displayHeaderFooter: false, // 封面不显示页眉页脚
    });
};

// 生成内容PDF(页码从1开始)
const generateContentPDF = async (page, payload) => {
    const content = await compile('index', payload);
    await page.setContent(content);
    
    await page.addStyleTag({
        content: `body { margin-top: 1cm; }`,
    });

    return await page.pdf({
        format: 'A4',
        printBackground: true,
        preferCSSPageSize: true,
        displayHeaderFooter: true,
        margin: {
            top: '100px',
        },
        headerTemplate: `
            ${styleContent}
            <div class='invoice-code'>
                <div style='width:40%; font-size: 10px;'></div>
                <div style='width:30%; font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>PURCHASE ORDER</div>
                <div style='width:30%; font-size: 10px; line-height:1.2;'>
                    <div style=' font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>Number<span style="font-weight: normal;"> : ${payload.po_number}</span></div>
                    <div style=' font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>Po Date<span style="font-weight: normal;"> : ${payload.po_date}</span></div>
                    <div style=' font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>Page No <span style="font-weight: normal;">:</span> <span style="font-weight: normal;" class="pageNumber"> </span></div>
                </div>
            </div>
        `,
        footerTemplate: `
            ${styleContent}
            <div class='invoice-code'>
            <div style='width:40%; font-size: 12px;'></div>
            </div>
        `,
    });
};

// 合并两个PDF
const mergePDFs = async (coverBytes, contentBytes) => {
    const coverDoc = await PDFDocument.load(coverBytes);
    const contentDoc = await PDFDocument.load(contentBytes);
    const mergedDoc = await PDFDocument.create();

    // 复制封面页
    const coverPages = await mergedDoc.copyPages(coverDoc, coverDoc.getPageIndices());
    coverPages.forEach(page => mergedDoc.addPage(page));

    // 复制内容页
    const contentPages = await mergedDoc.copyPages(contentDoc, contentDoc.getPageIndices());
    contentPages.forEach(page => mergedDoc.addPage(page));

    return mergedDoc.save();
};

const generatePDF = async () => {
    try {
        const browser = await puppeteer.launch({
            args: ['--no-sandbox'],
            devtools: true,
        });
        const page = await browser.newPage();

        const payloadJSON = data;
        const groupedPayload = groupBySubtotalQuantity(payloadJSON.po_item);
        payloadJSON.po_item = groupedPayload.po_item;

        // 生成封面和内容PDF
        const coverBytes = await generateCoverPDF(page, payloadJSON);
        const contentBytes = await generateContentPDF(page, payloadJSON);

        // 合并并保存
        const mergedBytes = await mergePDFs(coverBytes, contentBytes);
        await fs.promises.writeFile('output.pdf', mergedBytes);

        console.log('PDF generated successfully');
        await browser.close();
    } catch (e) {
        console.log(e);
    }
};

generatePDF();

内容的提问来源于stack exchange,提问作者Vivek Gupta

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 15:34:51