商协会系统里有几处固定要出纸质件:会员证书、活动签到表、邀请函。最早我们是在服务端生成 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 表达的复杂版式。