用 Claude Code 实现 PDF 生成:Playwright 发票、报告与打印 CSS
用 Claude Code 做 PDF 生成:Playwright、打印 CSS、中文字体、发票、报告、截图回归和常见坑。
很多应用都会有“下载 PDF”按钮,但真正上线后才会发现,PDF 不是网页截图。发票要有正确的金额、税额、页边距和付款信息;月报要包含表格、说明、图表和自然的分页;证书要有姓名、编号、日期和可打印的版式。如果中文字体在服务器上缺失,或者背景色没有进入 PDF,用户看到的文件就会显得不专业。
这篇文章采用最稳妥的入门路线:用 HTML 写文档,用打印 CSS 控制纸张,再用 Playwright 或 Puppeteer 的 Chromium 渲染成 PDF。不要一开始就把所有内容画到 canvas 里。图片式 PDF 看起来快,但文本不能搜索,文件更大,缩放容易模糊,也很难做视觉回归测试。Claude Code 很适合实现这类功能,但提示词必须写清楚页面尺寸、字体、分页、截图检查和禁止的脆弱做法。
官方资料建议同时打开:Playwright 的 page.pdf、Puppeteer 的 PDF generation 和 PDFOptions、MDN 的 @page 与打印 CSS,以及 Claude Code overview。相关的站内阅读可以看电子表格自动化、Playwright 测试和测试策略。
为什么优先选择 HTML 到 PDF
PDF 生成常见有三种方式。第一种是用 jsPDF 这类库按坐标画文字和线条,适合非常简单的标签或单页表单,但复杂发票会很快变成维护困难的坐标表。第二种是把页面或 canvas 变成图片再塞进 PDF,初看漂亮,实际却牺牲搜索、复制、可访问性和文件体积。第三种是把文档写成 HTML,再让浏览器用打印模式输出 PDF。
对大多数 Web 团队来说,第三种最容易维护。表格继续用 table,标题继续用 h1 和 h2,纸张尺寸和页边距交给 @page,打印专用样式放在独立模板里。这样 Claude Code 修改的是普通 HTML、CSS、Node 脚本和 Playwright 测试,而不是难以理解的坐标计算。
flowchart TD
A["业务数据"] --> B["HTML 模板"]
B --> C["打印 CSS 与 @page"]
C --> D["Chromium 渲染"]
D --> E["PDF 文件"]
C --> F["截图对比"]
F --> G["审查证据"]
三个以上的真实场景
第一个场景是发票。它需要卖方、买方、明细、税率、合计、付款期限和发票编号。最危险的错误不是颜色不好看,而是四舍五入错误、税额丢失、合计栏被分页切掉,或者中文字体回退后行高变化。
第二个场景是月度报告。SEO、广告投放、SaaS 使用情况或销售分析经常需要表格、图表、截图和文字说明。这里要特别关注分页:标题不应单独留在页尾,图表不应和解释文字分离。break-inside: avoid 可以保护较小的区块,但超长表格仍要设计多页方案。
第三个场景是课程证书或完成证明。姓名、课程名、日期、证书 ID、Logo 和二维码通常在一页内完成。主要信息应保留为 HTML 文本,装饰、签名和二维码才使用图片,这样后续搜索、归档和邮件发送都更可靠。
第四个场景是内部审计报告。权限变更、部署记录、审批历史和事故摘要经常要给非工程团队阅读。建议在页脚加入生成时间、环境、应用版本和数据来源 ID,避免以后无法追溯。
给 Claude Code 的提示词
请实现 PDF 生成功能。
方向:
- 用 HTML 模板 + Playwright Chromium 输出 PDF。
- 不要把整份文档画成一张 canvas 图片。
- 使用 A4 纵向、14mm 页边距、打印背景色和 print CSS。
- 字体栈包含 Noto Sans CJK / Microsoft YaHei / sans-serif。
- 结构要能复用于发票、报告和证书。
实现:
- 新增 scripts/create-invoice-pdf.mjs。
- 从示例数据生成 out/invoice-CN-2026-0602.pdf。
- 金额用 Intl.NumberFormat 格式化。
- 用户输入必须 HTML 转义。
- page.pdf 使用 printBackground 和 preferCSSPageSize。
验证:
- 保留运行命令。
- 让打印 HTML 可以截图对比。
- 检查字体、分页、背景色和合计金额。
可复制运行的最小代码
npm init -y
npm pkg set type=module
npm i -D playwright
npx playwright install chromium
mkdir scripts out
node scripts/create-invoice-pdf.mjs
import { chromium } from "playwright";
import { mkdir } from "node:fs/promises";
import { dirname, resolve } from "node:path";
const outputPath = resolve("out/invoice-CN-2026-0602.pdf");
const money = new Intl.NumberFormat("zh-CN", { style: "currency", currency: "CNY" });
const invoice = {
number: "CN-2026-0602",
buyer: "上海示例科技有限公司",
seller: "Masa Design Lab",
issuedAt: "2026-06-02",
items: [
{ name: "PDF 模板设计", quantity: 1, unitPrice: 5000 },
{ name: "Playwright 生成脚本", quantity: 1, unitPrice: 7000 },
{ name: "打印 CSS 与中文字体验证", quantity: 1, unitPrice: 3000 },
],
};
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (char) => ({
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
})[char]);
}
function renderHtml(data) {
const subtotal = data.items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0);
const tax = Math.round(subtotal * 0.06);
const rows = data.items.map((item) => `<tr>
<td>${escapeHtml(item.name)}</td>
<td class="num">${item.quantity}</td>
<td class="num">${money.format(item.unitPrice)}</td>
<td class="num">${money.format(item.quantity * item.unitPrice)}</td>
</tr>`).join("");
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
<style>
@page { size: A4; margin: 14mm; }
body { font-family: "Noto Sans CJK SC", "Microsoft YaHei", sans-serif; color: #202124; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
header { display: flex; justify-content: space-between; border-bottom: 3px solid #1f5eff; padding-bottom: 14px; }
h1 { margin: 0; font-size: 28px; }
table { width: 100%; border-collapse: collapse; margin-top: 24px; }
th { background: #eef3ff; text-align: left; }
th, td { border-bottom: 1px solid #d7dce5; padding: 10px 8px; }
.num { text-align: right; white-space: nowrap; }
.total { margin-left: auto; width: 260px; margin-top: 20px; font-size: 18px; font-weight: 700; }
.avoid-break { break-inside: avoid; page-break-inside: avoid; }
</style></head><body>
<header><h1>发票</h1><div>编号: ${escapeHtml(data.number)}<br>日期: ${escapeHtml(data.issuedAt)}</div></header>
<p><strong>收票方:</strong> ${escapeHtml(data.buyer)}</p>
<p><strong>开票方:</strong> ${escapeHtml(data.seller)}</p>
<table><thead><tr><th>项目</th><th class="num">数量</th><th class="num">单价</th><th class="num">金额</th></tr></thead><tbody>${rows}</tbody></table>
<div class="total avoid-break">合计: ${money.format(subtotal + tax)}</div>
</body></html>`;
}
await mkdir(dirname(outputPath), { recursive: true });
const browser = await chromium.launch();
try {
const page = await browser.newPage();
await page.setContent(renderHtml(invoice), { waitUntil: "networkidle" });
await page.evaluate(() => document.fonts.ready);
await page.emulateMedia({ media: "print" });
await page.pdf({ path: outputPath, printBackground: true, preferCSSPageSize: true, margin: { top: "0", right: "0", bottom: "0", left: "0" } });
console.log(`Created ${outputPath}`);
} finally {
await browser.close();
}
常见失败与检查
常见失败包括:把整份 PDF 做成图片、忘记 printBackground 导致表头背景消失、服务器缺少中文字体、外部 Logo 尚未加载就导出、长备注导致合计栏被挤到下一页、以及没有转义客户输入。用 Claude Code 修复时,要让它先复现失败,再给出最小修改,不要顺手重写整个发票系统。
视觉回归也很重要。PDF 文件存在不代表排版正确。实际项目中可以打开 /invoices/CN-2026-0602/print 这样的打印页,执行 page.emulateMedia({ media: "print" }),再用 Playwright 的截图比较确认表头、金额和分页没有漂移。金额计算应单独用单元测试验证,截图只负责布局证据。
变现 CTA 与实测结果
PDF 生成天然适合变现:发票模板、报告样例、证书工具、检查清单 PDF 都能连接到产品、咨询和培训。但文章必须提供真实代码、失败案例和验证证据,而不是只总结库名。你可以从免费 Claude Code 资料开始,进一步查看ClaudeCodeLab 教材,团队需要把 PDF、测试和发布流程接入仓库时可看培训与咨询。
本文的流程按本地 Node.js、Playwright Chromium 和示例发票数据验证。最有价值的不是一行 page.pdf,而是把打印 HTML 当成可测试对象。Masa 在实际项目里更常遇到的是字体加载、背景色丢失、长文本分页和审查证据不足,而不是简单语法错误。因此让 Claude Code 同时交付可运行脚本和验证说明,质量会稳定很多。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。