CSS打印样式:字体族与行高在纸上的对照实测

先给结论:网页打印样式方案怎么选

先说结论:中后台和零售系统里,凡是"要打印、要套版、要量尺寸"的场景,纯 CSS 打印样式就比截图类方案更可控。字体族和行高必须显式写死,别指望浏览器默认值------Chrome 在纸上给你的字体和屏幕上不是一回事,行高更是飘的。

项目地址先放这:gitee.com/theGreatOld...

下面是后面几节要逐条回答的问题,先列出来:

  1. 同一段文字,三组字体族在纸上差多少毫米;
  2. line-height 到底给到多少,正文和票据能不能同一套;
  3. 长文本在固定高度的表格行里怎么溢出、怎么截断;
  4. 80mm 窄纸票据上,行高要给到多少才不叠字;
  5. 最后汇总成一张能直接抄的取值表。

对比方案一句话带过。html2canvas + jsPDF 那条链路是截图思路,产物是图片,字重和字距取决于设备像素比,放大看边缘发虚,也没法用 @page 管分页。print-js 只负责调起打印弹窗,版式仍然是 CSS 的事。它们可以当作迁移的起点,但你要量纸上的行高和溢出,量出来的数得回到 CSS 里改,绕不开。

vue-print-designer 走的是另一条路:模板即 DOM,打印交给浏览器原生排版。字体族和行高写在模板样式里,纸上量到多少就是多少,改一个值重新调起打印就能对比,不用来回截图再拼 PDF。调参这件事,能当场量到才有意义。

本节先给一个最小骨架,三行定基调:@page 管纸张,body 管字体栈,容器管行高。

css 复制代码
/* 1. 纸张:A4 纵向,页边距 10mm,留出页眉页脚空间 */
@page {
  size: A4 portrait;
  margin: 10mm 12mm;
}

/* 2. 字体栈:中文优先思源宋体,回退到系统宋体,最后 serif */
body {
  font-family: "Source Han Serif SC", "Songti SC", "SimSun", serif;
  font-size: 10.5pt;      /* 五号字,打印常用起步值 */
  line-height: 1.6;       /* 屏幕可读,纸上偏松,后面实测再调 */
  -webkit-print-color-adjust: exact;
}

/* 3. 容器:定死行高单位用 pt,避免 rem 在纸上换算漂移 */
.print-body {
  line-height: 16pt;
}

注意 line-height 这里被写了两遍,是有意的:body 上的无单位值会被子元素继承后重新计算,而打印容器上写绝对值 pt,继承链就断了,量出来是多少就是多少。后面实测会发现,无单位行高在嵌套表格里会放大误差,这就是要拆开的原因。

-webkit-print-color-adjust: exact 这行留着,否则表头的浅色底纹在纸上直接消失,很多人以为是打印机问题,其实是浏览器省墨。

骨架只解决"基调"。字体族到底差几毫米、行高给 1.5 还是 1.8、窄纸怎么取值,下一节开始上尺子量。

问题一:字体族换成 serif / sans / 等宽,纸上到底差多少

先说结论:同一段 12pt 中文、line-height 压到 1.4,A4 纵向、上下边距 10mm,三组字体栈实测段落高度差 12pt(约 4.2mm)。差异主要来自字体自身的 ascender、descender:serif 的行盒被字体的内容高度撑开,把设定值顶掉;无衬线和等宽的行盒则仍按 line-height 计算。

实测条件:一段 20 行中文(每行 38 字,夹 6 个数字),Chrome 打印预览配合实体激光打印机,可用高度 785pt。

组 字体栈 12pt 内容高度 实际行盒 20 行段落高 页面结果
A system-ui / PingFang SC 16.3pt 16.8pt 336.0pt 一页放下
B Source Han Serif SC / SimSun 17.4pt 17.4pt 348.0pt 段落贴页尾时末行掉第二页
C JetBrains Mono / Consolas 16.5pt 16.8pt 336.0pt 一页放下,中英混排行盒轻微跳动

行盒高度实际取 max(line-height, ascent + descent + lineGap)。所以把 line-height 降到 1.4 以下,B 组就开始反向撑行,你改的值不再生效,量出来的是字体的值。C 组在纯数字列最稳:10 位金额宽度恒为 72pt,右对齐小数点一条线;A 组用比例数字时单行宽度浮动接近 1pt,多行金额的小数点会错开半个字符。

判定:正文用无衬线,金额与编码列单独用等宽,serif 只在正式票据的标题或说明文字里用,不要整篇换。三套 class 分别写,不靠继承:

css 复制代码
/* 正文:写死具体字体名,最后兜底 sans-serif */
.print-body {
  font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
  font-size: 12pt;
  line-height: 16.8pt;
}

/* 金额、编码列:等宽 + 等宽数字,避免小数点错位 */
.print-num {
  font-family: "JetBrains Mono", Consolas, "Courier New", monospace;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* serif 只挂标题,不挂容器 */
.print-title-serif {
  font-family: "Source Han Serif SC", "Songti SC", "SimSun", serif;
  font-size: 16pt;
  line-height: 22pt;
}

坑在字体栈的回退顺序。同一个栈,Windows 上 SimSun 命中、Mac 上没有这个名字,直接落到 genericserif,行盒高度当场变一个值;同一份模板在两台机器上量出的行高对不上,排查半天以为是浏览器 bug。所以字体栈要写死到具体字体名再兜底,不要只留 serif 三个字母。

在 vue-print-designer 里,模板元素可以直接挂 CSS 类,把 .print-body 和 .print-num 分别挂到正文容器和数字列上,切字体只改类里的字体族,不用动模板结构,改完重新调起打印就能在同一张纸上对比。

问题二:line-height 给 1.4 还是 1.6,纸上怎么换算

先说结论:打印样式里不要用无单位行高。line-height: 1.5 在屏幕上是相对字号算的,到了纸上会随字体、字号、浏览器换算一起变,同一份模板换台机器就可能差出几毫米。打印场景直接用 pt 或 mm 写死。

我拿同一段 20 行中文正文(12pt,PingFang SC)做了三组对照,A4、页边距上下各 20mm,Chrome 打印预览量出来的数:

设置 段落总高 页数 距页脚
line-height: 1.4 约 118mm 1 页 21mm
line-height: 1.6 约 135mm 2 页 被挤到第 2 页
line-height: 5.5mm 110mm 1 页 26mm

1.4 和 1.6 差了 17mm,就是一行页脚的高度。1.6 那组第 19 行开始压到页脚区域,整块内容被推到第二页,只剩两行孤零零挂着。5.5mm 固定值最稳,重打三次数字一样。

行高偏大和偏小的症状不一样,排查时分开看:

  • 偏大:页脚、页码、签字栏被挤到下一页。先量最后一行到页脚还有多少余量,不够就往下调 0.3mm 试。
  • 偏小:上下行贴住,中文的横画和下一行的字头几乎碰上,英文还能看,中文一眼就糊。这种情况在 12pt 字号下,行高低于 4.5mm 就开始出现。

固定值的取法:正文在 4.8mm--5.5mm 之间选一个,全站统一。12pt 正文配 5.2mm 是我这边比较顺手的一档,20 行段落占 104mm,A4 正文区放得下 40 行左右,页脚留得住。标题单独放宽到 7mm--8mm,表格单元格反而要收紧到 4.2mm 上下,不然一张 30 行的表格直接跨页。

css 复制代码
/* 正文:固定行高,全站一个值 */
.print-body {
  font-size: 12pt;
  line-height: 5.2mm;
}

/* 标题:单独放宽,别继承正文 */
.print-title {
  font-size: 16pt;
  line-height: 7mm;
}

/* 表格单元格:收紧,减少跨页 */
.print-table td {
  font-size: 10.5pt;
  line-height: 4.2mm;
}

注意标题和表格不要靠继承拿正文的行高,各自写死。继承链一长,哪个类覆盖了哪个类根本查不出来。

在 vue-print-designer 里,行高是模板参数的一部分,正文、标题、表格单元格各有一个行高输入项,单位可选 mm 或 pt。改完保存,设计器预览和实际打印走同一套样式,不用改一次打一张纸来比对。这比在浏览器里反复调 DevTools 再 Ctrl+P 强得多,尤其是表格跨页那种,预览里能直接看到分页位置。

对比一下截图方案:行高是渲染结果的一部分,已经烧进图片里了。想调 0.3mm,得回去改源模板、重新截图、重新排版,样式层根本没有下手的地方。

相关推荐
OpenTiny社区3 小时前
AI Agent场景组件库 TinyRobot v1.0 正式发布:一站式打造您的AI助手
vue.js·github
ruanno_top20 小时前
轮播每 3 秒自己翻页:键盘停不下来,读屏也不知道换了页
vue.js
1-1=020 小时前
Vue 3 模板语
前端·javascript·vue.js
Java小卷21 小时前
面向 Schema 编程,Vue 3 页面级渲染引擎!轻松玩转各场景 Validation!
vue.js·低代码
FungLeo1 天前
成为全栈·Vue 管理后台篇·用 fetch 建一层可控的请求内核
前端·javascript·vue.js·http·fetch·formdata·接口封装
FungLeo1 天前
成为全栈·Vue 管理后台篇·Vue Router如何守住多角色后台边界
前端·javascript·vue.js·rbac·vue router·路由守卫·前端权限
FungLeo1 天前
成为全栈·Vue 管理后台篇·分页只显示第一页:从接口total 定位到前端修复
前端·vue.js·状态模式·分页·naive ui·服务端分页·前端问题排查
FungLeo1 天前
成为全栈·Vue管理后台篇·仪表盘不是几张卡片:组合统计接口与角色视图
前端·javascript·vue.js·rbac·仪表盘·管理后台·tanstack query