Vue3 前端生成 PDF:会员证书与活动签到表的三种打印方案与踩坑记录

商协会系统里有几处固定要出纸质件:会员证书、活动签到表、邀请函。最早我们是在服务端生成 PDF 再让用户下载,后来发现这条路有个绕不开的问题------模板改一点就要发一次版,而且服务端的字体和排版跟设计稿总有点出入。

后来把这几处改成前端生成。这篇把三种方案的实际效果和我们最后的选型记下来。

先说需求特点,它决定了选型方向。这类文档有几个共同点:版式固定、有公章或背景图、中文字体不能乱、要能直接进打印机。其中"直接进打印机"这一条很关键------用户点一下就要出纸,不接受先下载再打开。

第一种是浏览器原生打印,也就是 print CSS。

思路是把要打印的内容渲染好,用 @media print 控制打印时的样式,调用 window.print() 唤起系统打印对话框,用户在里面选"另存为 PDF"。

优点是清晰度高(矢量输出)、分页由浏览器处理、中文字体直接用系统字体不会乱、零依赖。缺点也很直接:不同浏览器打印效果有差异,页边距和分页位置不完全可控。

```css

@media print {

@page {

size: A4;

margin: 12mm;

}

/* 打印时隐藏页面其他部分,只留目标容器 */

body * {

visibility: hidden;

}

.print-area,

.print-area * {

visibility: visible;

}

.print-area {

position: absolute;

left: 0;

top: 0;

width: 100%;

}

/* 关键:避免内容被从中间截断 */

.cert-item,

.sign-row {

page-break-inside: avoid;

break-inside: avoid;

}

/* 背景图和底色默认不打印,需要显式开启 */

.stamp,

.bg {

-webkit-print-color-adjust: exact;

print-color-adjust: exact;

}

ahref::after {

content: '';

}

}

```

这段样式里有两个坑值得单独说。

一是背景不打印。浏览器默认不输出背景色和背景图,公章、底纹这些会整块消失。要显式加 print-color-adjust: exact,同时提醒用户在打印对话框里勾上"背景图形"。

二是隐藏其他元素用 visibility 而不是 display。用 display: none 会让被隐藏的元素不再占位,打印区域的位置就跑偏了;visibility 保留占位,版面不动。

第二种是 html2canvas 加 jsPDF,也就是截图式。

把 DOM 转成图片再塞进 PDF。优点是所见即所得,浏览器之间一致性好,适合版式复杂、有渐变和阴影的设计。缺点也明显:本质是位图,放大就模糊;长列表分页要自己算高度切图,容易从行中间切断;中文如果字体没加载完就截图会出方块。

我们只在一种情况下用它:邀请函这类单页、版式花哨、不需要打印清晰度的内容。

第三种是纯前端用 PDF 库直接绘制,比如 jsPDF 直接写文本和图形。清晰度好、体积小,但所有坐标都要手算,中文还要处理字体嵌入,工作量偏大。批量证书这种版式固定的场景其实合适,但我们评估后觉得维护成本不划算,没采用。

最后的选型是分场景:签到表和批量证书走 print CSS,邀请函走截图式。

代码实现上,我们封了一个组合式函数,把"打开打印区域---等资源加载---调用打印---清理"这几步串起来。

```ts

import { nextTick } from 'vue';

export function usePrint() {

const printing = ref(false);

async function print(el: HTMLElement) {

printing.value = true;

document.body.classList.add('printing');

el.classList.add('print-area');

try {

// 等图片和字体就绪,否则截图方案会缺图

await Promise.all(

Array.from(el.querySelectorAll('img'))

.filter((img) => !img.complete)

.map((img) => new Promise((r) => { img.onload = img.onerror = r; }))

);

await document.fonts?.ready;

await nextTick();

window.print();

} finally {

// afterprint 事件在部分浏览器不触发,这里兜底清理

setTimeout(() => {

el.classList.remove('print-area');

document.body.classList.remove('printing');

printing.value = false;

}, 300);

}

}

return { printing, print };

}

```

等图片和字体这一步不能省。截图式方案里如果图片没加载完就转 canvas,出来的 PDF 是空白块;print CSS 方案里字体没加载完,打印机会用兜底字体顶上,版面全乱。

还有几个实测出来的细节。

批量打印证书时,每页一张要控制分页。做法是每个证书外层套一个固定高度的容器,高度按 A4 减去边距算好,配合 page-break-inside: avoid,浏览器就会一页一张。如果高度算错几毫米,就会出现一页挤两张或者第二页空白的情况。

签到表这类长表格,表头要在每页重复。用 thead 标签浏览器会自动处理,但如果是 div 模拟的表格就没这个效果,这种情况要么改用 table,要么用 position: fixed 的表头(打印时固定定位行为不一致,不太推荐)。

页眉页脚加页码只能靠 @page 里的 margin box,但浏览器支持度一般。我们的做法是在页面内容里预留一个页码位,用 CSS counter 配合分页符手动递增,虽然笨但可控。

最后说说生成质量的验收。我们列了三条:A4 纸直接打印不缩放、公章颜色正确、中文无方块。这三条过了,其他细节都好商量。

我们这边在跑的商会管理系统叫未来漫城·商会互联平台,会员证书和签到表用的就是 print CSS 那条路,模板改版式直接在前端改,不用发服务端版本。

一句话总结:能交给浏览器打印的,就别自己截图。清晰度和可维护性都是前者赢,截图方案留给确实没法用 print CSS 表达的复杂版式。

相关推荐
尾善爱看海4 小时前
前端算法与手写题集
前端·算法
徐小夕4 小时前
JitWord 4.0 万字分享:从协同工具到AI Word操作系统,聊聊3年产品创业史
前端·vue.js·后端
、如果5 小时前
PDF图片文字提取零依赖方案:pymupdf+AI视觉实战
人工智能·数据分析·pdf·图片提取·文字提取·skills
萧鼎6 小时前
Python 高性能Web框架神器 FastAPI:自动生成API文、基于Pydant、异步请求处理全搞定
前端·python·fastapi
IT_陈寒6 小时前
Redis误用keys命令把生产环境搞崩了,血的教训
前端·人工智能·后端
计算机魔术师7 小时前
5000亿估值冲刺科创板,DeepSeek 为何急着上市?
前端
我血条子呢7 小时前
前端解析word方案
前端·word
kyriewen7 小时前
我用 AI 写完一个需求后才发现,最难的不是 prompt,而是验收
前端·程序员·ai编程
申行7 小时前
Vue 3 + DYMO Connect Framework 实战:从标签模板到打印服务的完整实现
前端