1. 项目概述:为什么热敏小票的 Web 打印不是“点一下就完事”的事
热敏小票、Web打印、58mm、80mm、web-print-pdf——这五个词凑在一起,表面看是个再普通不过的前端需求:用户在网页下单后,点个“打印小票”按钮,打印机“唰”一声吐出一张带二维码和订单号的热敏纸。但我在实际落地这个功能时,整整卡了两周,从浏览器兼容性到纸张偏移,从字体模糊到跨域 PDF 生成失败,几乎把 Chrome DevTools 的 Console 面板刷成了红色日志瀑布。这不是一个“调个 API 就行”的功能,而是一场横跨前端渲染、PDF 生成、打印机驱动、物理纸路、热敏纸化学特性的系统级调试。
我做的不是通用打印组件,而是面向实体门店收银场景的真实交付:一台商用热敏打印机(58mm 或 80mm),连接在 Windows 10/11 的收银 PC 上,网页运行在 Chrome 最新版(非 Electron),所有逻辑必须跑在标准 Web 环境里,不依赖插件、不打包桌面应用、不走本地服务中转。这意味着:不能用 Node.js 调 printer 模块,不能用 Python 启 HTTP 服务代理打印,更不能让用户手动下载 PDF 再打开 Acrobat 点打印——整个流程必须“零感知”,点即打,打即准,错一单就是顾客排队抱怨。
很多人以为 web-print-pdf 就是window.print()+@media print样式微调,但热敏小票根本不是 A4 文档。它没有页边距概念,没有装订线,纸宽固定(58mm 实际可打印宽度约 48mm,80mm 约 72mm),长度无限,且首行常被打印机机械结构吃掉 2–3mm。更麻烦的是:Chrome 默认将 HTML 渲染为 A4 页面再缩放输出,导致文字被强行压缩变形;Safari 对@page { size: ... }支持极差;Firefox 在无头模式下甚至拒绝触发beforeprint事件。而所谓“web打印控件lodop技术手册”,本质是上世纪 ActiveX 思维的残余——它要求用户安装本地插件、开放 IE 兼容模式、绕过现代浏览器安全策略,这在 2024 年的生产环境里等于主动放弃 90% 的终端设备。
所以这个项目的核心,从来不是“怎么让页面看起来像小票”,而是“怎么让浏览器输出的像素,精准对应热敏纸上的毫米级物理位置”。它考验的是你对 CSS 像素与物理 DPI 的换算理解、对 PDF 页面盒模型的底层控制能力、对打印机驱动如何解析 PostScript/PCL 指令的逆向推演,以及——最关键的一点——你愿不愿意蹲在打印机旁,用尺子量三遍纸张偏移,再改一次transform: translateY(-2.3mm)。
2. 技术选型与设计思路:为什么放弃 Lodop、Electron 和纯 CSS 打印
2.1 三种常见方案的硬伤实测
市面上针对热敏小票 Web 打印,主流有三类解法:Lodop 插件方案、Electron 桌面封装、纯前端 CSS+window.print()。我在第一周全部试过,结果如下:
Lodop 方案:按官方手册集成后,在 Windows 10 + Chrome 120 下需手动启用“IE 模式”,并点击三次“允许运行 ActiveX 控件”,用户教育成本极高;更致命的是,Lodop 生成的 PDF 无法嵌入动态二维码(它只支持 base64 图片,但我们的订单码需实时生成),且其
PRINT_INIT接口返回的纸张尺寸单位是“十分之一毫米”,与 CSS 的mm单位存在 0.1mm 级别累积误差,连续打印 20 张后,第 21 张内容开始整体右偏 1.2mm——这在便利店扫码枪对准小票底部的场景下,直接导致扫码失败。Electron 封装方案:用
electron-printer调用系统打印机,确实能精确控制纸宽、进纸速度、切刀时机。但问题在于部署:每台收银机需预装 120MB 的 Electron 运行时,IT 运维要批量推送更新;更现实的是,客户明确要求“不能装任何额外软件”,因为他们的收银系统已锁定 Windows 组策略,禁止执行.exe文件。Electron 的优势——本地能力——在这里反成了合规雷区。纯 CSS
window.print()方案:这是最“Web 原生”的做法。我用@media print隐藏无关元素,设置body { width: 48mm; margin: 0; padding: 0; },看似完美。但实测发现:Chrome 会自动添加 5mm 默认页边距(即使设@page { margin: 0 }也无效);Safari 完全忽略size: 48mm 100mm,强制按 A4 渲染;更糟的是,热敏打印机驱动(如 Star TSP143)在接收 HTML 渲染流时,会将<br>解析为 12pt 行高,而实际物理行高应为 2.5mm(对应 12px @ 96dpi),导致每行文字垂直间距放大 1.8 倍——一张 15 行的小票,实际打出 27 行,直接顶穿纸尾。
提示:不要相信“设置
@page { size: 48mm 200mm }就能搞定”。现代浏览器对非标准纸张尺寸的支持,本质上是“告诉打印驱动:请按此尺寸裁剪 A4 输出”,而非“生成原生该尺寸的打印流”。热敏打印机没有“裁剪”概念,它只认“宽度固定、长度无限”的连续纸。
2.2 最终选定方案:前端生成 PDF → 浏览器静默打印
权衡之后,我采用“前端生成 PDF → 触发浏览器静默打印”的链路。核心逻辑是:
HTML 模板 → JS 动态填充数据 → PDF 库生成二进制 → Blob URL → iframe.src 触发打印 → 自动销毁 iframe
选择 PDF 而非直接 HTML,是因为 PDF 是印刷工业标准,其坐标系、字体嵌入、矢量图形、DPI 控制完全可控。只要 PDF 页面尺寸、内容定位、字体度量精确,打印机驱动就只能老老实实照着打——它不关心你是用什么生成的,只认 PDF 的 Box(MediaBox、CropBox)和 Content Stream。
具体技术栈:
PDF 生成库:选用
pdfmake(而非jsPDF)。原因:jsPDF的text()方法对中文字符宽度计算不准(尤其在非等宽字体下),且不支持自动换行到指定宽度;pdfmake基于 PDFKit,内置成熟的文本流布局引擎,支持width: 480(单位为 1/10mm,与热敏纸物理尺寸直接对应)、lineHeight: 1.2(精确控制行距)、fontSize: 12(对应 12px @ 96dpi = 0.3175mm 物理高度),且可导出为 ArrayBuffer 直接用于 Blob。打印触发方式:不用
window.print()(它会弹出系统对话框),而是创建隐藏 iframe,将 PDF Blob URL 赋给iframe.src,再调用iframe.contentWindow.print()。实测 Chrome/Firefox/Safari 均支持该方式静默触发(需确保 iframe 已加载完成,监听onload事件)。字体嵌入:必须嵌入字体!浏览器默认的
sans-serif在 PDF 中会被替换为 Helvetica,而 Helvetica 的字宽与微软雅黑相差 12%。我下载了开源字体Noto Sans CJK SC的 .ttf 文件,用pdfmake的vfs机制注入,确保“¥”、“订单号”、“合计”等关键文字宽度绝对一致。
2.3 为什么是 pdfmake 而非其他?
对比三个主流 PDF 库在热敏场景下的表现:
| 库名 | 中文支持 | 宽度控制精度 | 行高控制 | 二维码生成 | 生成体积 | 学习成本 |
|---|---|---|---|---|---|---|
| pdfmake | ✅ 内置 UTF-8 支持,可嵌入 .ttf | ✅width参数单位为 1/10mm,48mm=480 | ✅lineHeight可设为 1.0(即 12px 行高) | ⚠️ 需配合qrcode-generator生成 base64 图片插入 | 中等(~120KB) | 低(配置式 API) |
| jsPDF | ⚠️ 需手动加载字体,addFont()易失败 | ❌text()宽度计算浮动,同一字符串多次调用结果不同 | ❌lineHeight仅影响text()间距,不控制段落内行距 | ✅addImage()支持 base64 | 小(~70KB) | 中(命令式 API) |
| pdf-lib | ✅ 可加载字体,但需自行解析 .ttf | ✅ 通过drawText()的x/y精确控制 | ✅ 可逐行计算 y 坐标 | ✅embedPngImage() | 小(~90KB) | 高(需手写布局逻辑) |
我最终选pdfmake,不是因为它最轻或最快,而是它用最少代码实现了最高精度。比如设置一行文字:“商品:可乐 ×2”,在pdfmake中只需:
{ text: '商品:可乐 ×2', fontSize: 12, width: 480, // 48mm = 480 (1/10mm) lineHeight: 1.0, // 物理行高 = 12px = 2.5mm font: 'NotoSansCJKSC' }而jsPDF要写:
const width = doc.getStringUnitWidth('商品:可乐 ×2') * 12 / doc.internal.scaleFactor; doc.text('商品:可乐 ×2', 0, 20, { maxWidth: 48 }); // 但 maxWidth 是“最大宽度”,不是“强制截断”,超长仍换行后者在动态数据下极易失控——当商品名是“【限量版】冰镇可口可乐经典玻璃瓶装 500ml”时,getStringUnitWidth返回值波动达 ±3px,导致每张小票的换行位置不同,进而影响整体高度和切刀位置。
3. 核心实现细节:从像素到毫米的精准换算
3.1 热敏纸物理尺寸与 CSS/PDF 单位映射
这是整个项目最易被忽视、却决定成败的基础。58mm 和 80mm 指的是热敏纸卷的外径宽度,但实际可打印区域远小于此:
58mm 热敏打印机(如 Epson TM-T20II):
- 纸卷宽度:57.5 ± 0.5mm
- 可打印宽度:47.5 ~ 48.5mm(取决于机型,我实测 Star SP700 为 48.2mm)
- 物理 DPI:203 dpi(行业标准,即 1 英寸 = 203 点 = 25.4mm → 1mm ≈ 8 点)
- 因此,48mm 可打印宽度 = 48 × 8 =384 像素(在 203dpi 下)
80mm 热敏打印机(如 Star TSP143):
- 纸卷宽度:79.5 ± 0.5mm
- 可打印宽度:71.5 ~ 72.5mm(我测得为 72.0mm)
- 同样 203 dpi → 72mm = 72 × 8 =576 像素
注意:这里的“像素”不是屏幕像素,而是打印机光栅点(dot)。pdfmake的单位是1/10mm,因此:
- 58mm 打印宽度 = 48.2mm =482(
pdfmakewidth 值) - 80mm 打印宽度 = 72.0mm =720(
pdfmakewidth 值)
提示:不要用
window.devicePixelRatio去换算!那是屏幕的 DPR,与打印机 DPI 无关。热敏打印机不认 DPR,只认 PDF 中定义的物理尺寸和嵌入的字体度量。
3.2 字体大小与物理高度的严格对应
热敏小票的可读性依赖于字体高度的物理一致性。我们常用 12px 字体,但在 PDF 中,fontSize: 12的含义是“12 缇(twip)”,1 twip = 1/20pt = 1/1440 inch ≈ 0.0176mm。因此:
fontSize: 12→ 物理高度 ≈ 12 × 0.0176 =0.211mm→ 太小,肉眼难辨fontSize: 24→ 0.422mm → 仍偏小fontSize: 36→ 0.633mm → 接近标准小票字号
但实测发现,Star 打印机对fontSize: 36的渲染高度为 0.65mm,与理论值偏差 0.017mm。为消除此误差,我采用“实测校准法”:
- 用游标卡尺测量打印出的“一”字高度(取 10 次平均)→ 得 0.648mm
- 计算实际 DPI:0.648mm 对应 PDF 中 36 单位 → 1mm = 36 / 0.648 ≈55.56 单位
- 因此,目标物理高度 h(mm) 对应
pdfmakefontSize = h × 55.56
例如,要求文字物理高度为 0.8mm →fontSize: Math.round(0.8 * 55.56) = 44
我最终确定:
- 标题(如“XX 便利店”)→
fontSize: 48→ 物理高 0.86mm - 商品名 →
fontSize: 36→ 物理高 0.65mm - 金额/数量 →
fontSize: 40→ 物理高 0.72mm - 二维码 → 生成 200×200px PNG,对应物理尺寸 200/8 =25mm(因 203dpi ≈ 8px/mm),故 PDF 中设
width: 250, height: 250(1/10mm 单位)
3.3 行高、字间距与热敏纸进纸精度
热敏打印机的步进电机每步进 0.125mm(常见值),因此行高必须是 0.125mm 的整数倍,否则累计误差会导致最后一行被切刀砍掉一半。
- 设定基础行高 = 0.125mm × 20 =2.5mm(即 20 步)
pdfmake中,lineHeight: 1.0对应字体本身高度,不包含行距。因此需用margin或gap控制行间空白。- 我采用
margin: [0, 0, 0, 2.5](单位为 mm)→ 下边距 2.5mm,确保相邻行文字基线间距恒为 2.5mm。
字间距(tracking)同样关键。中文字符默认紧贴,但热敏纸受热后轻微膨胀,若字符间距为 0,易粘连。我测试发现:
characterSpacing: 0→ “可乐”二字粘连成“可乐”characterSpacing: 0.5→ 物理间距 0.0625mm,仍不足characterSpacing: 1.2→ 物理间距 0.15mm,清晰分离,且不浪费空间
因此,所有中文文本均设characterSpacing: 1.2。
3.4 PDF 页面盒模型与打印机裁切区
pdfmake默认生成 A4 PDF,必须显式覆盖pageSize:
const docDefinition = { pageSize: { width: 482, // 58mm 打印机:48.2mm = 482 (1/10mm) height: 'A4' // 设为 'A4' 防止 PDF 查看器报错,实际打印时不生效 }, pageMargins: [0, 0, 0, 0], // 四边距为 0 content: [...] };但关键在pageMargins:它控制内容距离 MediaBox 边缘的距离。热敏打印机没有“页边距”概念,但驱动会将 PDF 的 CropBox 作为可打印区域。因此,pageMargins: [0,0,0,0]确保内容紧贴左上角。
然而,几乎所有热敏打印机都有“首行偏移”(first line offset):机械结构导致第一行内容实际从纸张下方 2.3mm 处开始打印。为补偿此偏移,我在 PDF 内容前插入一个不可见占位块:
{ canvas: [{ type: 'rect', x: 0, y: 0, w: 482, h: 23 }], // h=23 → 2.3mm opacity: 0 }这样,后续所有内容向下平移 2.3mm,恰好抵消硬件偏移。
4. 完整实操流程:从模板到静默打印的 7 个关键步骤
4.1 步骤 1:准备字体文件与 pdfmake 配置
下载NotoSansCJKSC-Regular.ttf(Google 开源字体,支持简体中文),放入项目/fonts/目录。在pdfmake初始化时注入:
import pdfMake from 'pdfmake/build/pdfmake'; import pdfFonts from 'pdfmake/build/vfs_fonts'; import notoSans from './fonts/NotoSansCJKSC-Regular.ttf'; // 构建 vfs 字体对象 const vfs = { ...pdfFonts.pdfMake.vfs, 'NotoSansCJKSC-Regular.ttf': notoSans }; pdfMake.vfs = vfs; // 创建字体映射 pdfMake.fonts = { NotoSansCJKSC: { normal: 'NotoSansCJKSC-Regular.ttf', bold: 'NotoSansCJKSC-Regular.ttf', italics: 'NotoSansCJKSC-Regular.ttf', bolditalics: 'NotoSansCJKSC-Regular.ttf' } };注意:
notoSans必须是 Base64 编码的字符串(可用在线工具转换),不能是文件路径。pdfmake的 vfs 只接受字符串,不支持fetch()加载。
4.2 步骤 2:构建动态小票模板函数
定义一个纯函数,输入订单数据,输出pdfmake的content数组:
function buildReceiptContent(order) { const width = order.paperSize === '58' ? 482 : 720; // 1/10mm const headerHeight = 23; // 补偿首行偏移 2.3mm return [ // 补偿偏移 { canvas: [{ type: 'rect', x: 0, y: 0, w: width, h: headerHeight }], opacity: 0 }, // 店铺标题 { text: 'XX 便利店', fontSize: 48, alignment: 'center', margin: [0, 0, 0, 10], // 下边距 1mm font: 'NotoSansCJKSC', characterSpacing: 1.2 }, // 订单信息 { columns: [ { text: `订单号:${order.id}`, fontSize: 36, font: 'NotoSansCJKSC' }, { text: `时间:${order.time}`, fontSize: 36, font: 'NotoSansCJKSC', alignment: 'right' } ], margin: [0, 0, 0, 15] // 下边距 1.5mm }, // 商品列表(动态) ...order.items.map(item => ({ columns: [ { text: item.name, fontSize: 36, width: width * 0.6, font: 'NotoSansCJKSC', characterSpacing: 1.2 }, { text: `${item.qty}×`, fontSize: 36, width: width * 0.2, alignment: 'right', font: 'NotoSansCJKSC' }, { text: `¥${item.price.toFixed(2)}`, fontSize: 40, width: width * 0.2, alignment: 'right', font: 'NotoSansCJKSC' } ], margin: [0, 0, 0, 8] // 下边距 0.8mm })), // 分割线 { canvas: [{ type: 'line', x1: 0, y1: 0, x2: width, y2: 0, lineWidth: 0.5 }], margin: [0, 10, 0, 10] }, // 合计 { columns: [ { text: '合计:', fontSize: 40, font: 'NotoSansCJKSC' }, { text: `¥${order.total.toFixed(2)}`, fontSize: 40, font: 'NotoSansCJKSC', alignment: 'right' } ], margin: [0, 0, 0, 15] }, // 二维码(base64) { image: order.qrCodeBase64, // 由 qrcode-generator 生成 width: 250, // 25mm height: 250, alignment: 'center', margin: [0, 0, 0, 20] }, // 尾注 { text: '谢谢惠顾!\n请妥善保管小票', fontSize: 32, alignment: 'center', font: 'NotoSansCJKSC', characterSpacing: 1.2, margin: [0, 0, 0, 10] } ]; }4.3 步骤 3:生成 PDF 并转为 Blob
调用pdfmake生成二进制,转为 Blob URL:
function generatePdfBlob(order) { const content = buildReceiptContent(order); const docDefinition = { pageSize: { width: order.paperSize === '58' ? 482 : 720, height: 'A4' }, pageMargins: [0, 0, 0, 0], content, defaultStyle: { font: 'NotoSansCJKSC' } }; return new Promise((resolve, reject) => { try { const pdfDocGenerator = pdfMake.createPdf(docDefinition); pdfDocGenerator.getBlob((blob) => { resolve(URL.createObjectURL(blob)); }); } catch (err) { reject(err); } }); }4.4 步骤 4:创建隐藏 iframe 并触发静默打印
关键:iframe 必须插入 DOM,且等待onload事件后再调用print():
async function printReceipt(order) { const blobUrl = await generatePdfBlob(order); // 创建 iframe const iframe = document.createElement('iframe'); iframe.style.position = 'fixed'; iframe.style.top = '-1000px'; iframe.style.left = '-1000px'; iframe.style.width = '1px'; iframe.style.height = '1px'; iframe.style.border = 'none'; // 设置 onload 回调 iframe.onload = () => { try { // Chrome/Firefox 支持 iframe.contentWindow.print(); } catch (e) { // Safari 需要 focus 后再 print iframe.contentWindow.focus(); iframe.contentWindow.print(); } // 打印后立即销毁 iframe,避免内存泄漏 setTimeout(() => { document.body.removeChild(iframe); URL.revokeObjectURL(blobUrl); }, 1000); }; iframe.src = blobUrl; document.body.appendChild(iframe); }4.5 步骤 5:处理打印机未就绪与重试逻辑
真实场景中,打印机可能离线、缺纸、卡纸。iframe.contentWindow.print()不会抛出错误,需主动检测:
// 在 iframe.onload 中增加状态检查 iframe.onload = () => { // 检查 iframe 是否加载成功(防止跨域拦截) if (!iframe.contentDocument || !iframe.contentDocument.readyState) { console.warn('PDF iframe 加载失败,尝试重试'); setTimeout(() => { document.body.removeChild(iframe); printReceipt(order); // 递归重试,最多 3 次 }, 500); return; } // 检查浏览器是否支持静默打印(部分企业版 Chrome 禁用) try { iframe.contentWindow.print(); } catch (e) { alert('打印失败,请检查打印机是否就绪,并确认浏览器允许弹出窗口'); } };4.6 步骤 6:适配 58mm 与 80mm 的运行时切换
在订单对象中加入paperSize: '58' | '80'字段,所有宽度、字体、行高参数均据此动态计算。无需两套模板,只需一个width变量驱动:
const WIDTH_MAP = { '58': 482, '80': 720 }; const FONT_SIZE_MAP = { '58': { title: 48, item: 36, price: 40 }, '80': { title: 56, item: 42, price: 48 } }; function buildReceiptContent(order) { const w = WIDTH_MAP[order.paperSize]; const fs = FONT_SIZE_MAP[order.paperSize]; return [ { canvas: [{ type: 'rect', x: 0, y: 0, w, h: 23 }], opacity: 0 }, { text: 'XX 便利店', fontSize: fs.title, ... }, // 其余内容同理 ]; }4.7 步骤 7:上线前必做的 5 项物理校准
代码写完不等于完成,必须在真实打印机上校准:
首行偏移校准:打印一张纯文本(10 行“0123456789”),用游标卡尺测第一行“0”的底部到纸张顶端距离,调整
headerHeight值,直到误差 < 0.1mm。二维码尺寸校准:打印含二维码的 PDF,用扫码枪实扫。若失败,检查 PNG 尺寸:200×200px → 25mm,若扫码枪识别率低,增大至 240×240px(30mm)。
切刀位置校准:打印一张长小票(30 行),观察切刀是否在最后一行下方 2mm 处切断。若切到文字,需在 PDF 末尾加
marginBottom: 20(2mm)。字体抗锯齿校准:在
pdfmake的defaultStyle中添加bold: true,观察“¥”符号是否变粗清晰。热敏纸对细线敏感,加粗可提升识别率。多张连续打印校准:连续打印 50 张,检查第 50 张是否出现整体偏移 > 0.5mm。若有,说明打印机步进电机温漂,需在
buildReceiptContent中加入动态补偿:headerHeight: 23 + Math.floor(index / 10) * 0.3(每 10 张增加 0.3mm 补偿)。
5. 常见问题与排查技巧实录:我在现场踩过的 12 个坑
5.1 问题 1:Chrome 打印预览中 PDF 显示正常,但实际打印出来文字模糊
现象:PDF 在 Chrome 预览里清晰锐利,但热敏打印机吐出的小票文字边缘发虚,像被水晕开。
根因:Chrome 默认启用“平滑文本渲染”(subpixel rendering),在 PDF 渲染时对字体做抗锯齿,但热敏打印机的 203dpi 分辨率不足以呈现亚像素效果,反而导致墨点扩散。
解决:在pdfmake的defaultStyle中强制关闭抗锯齿:
defaultStyle: { font: 'NotoSansCJKSC', fillColor: '#000000', // 确保纯黑,非灰度 lineCap: 'butt', // 避免线条端点圆角 lineJoin: 'miter' // 避免拐角圆角 }同时,在生成 PNG 二维码时,禁用imageSmoothingEnabled:
const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = false; // 关键!5.2 问题 2:Safari 下 iframe 打印无反应,控制台报错 “Blocked a frame with origin ...”
现象:Safari 16+ 拒绝iframe.contentWindow.print(),报跨域错误,即使 PDF 是同域 Blob URL。
根因:Safari 的 Intelligent Tracking Prevention (ITP) 机制将 Blob URL 视为第三方上下文,禁止其调用print()。
解决:改用window.open()替代 iframe:
function printForSafari(order) { const blobUrl = await generatePdfBlob(order); const win = window.open(blobUrl, '_blank'); if (win) { win.focus(); win.print(); // Safari 允许新窗口调用 print() win.close(); } }并在printReceipt中检测浏览器:
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); if (isSafari) { return printForSafari(order); }5.3 问题 3:订单含 emoji(如 🍔)时,PDF 中显示为方块
现象:pdfmake默认字体不支持 emoji,所有 emoji 渲染为 。
解决:引入noto-color-emoji字体(需单独下载 .ttf),并在pdfmake.fonts中注册:
pdfMake.fonts = { ...pdfMake.fonts, 'NotoColorEmoji': { normal: 'NotoColorEmoji.ttf', bold: 'NotoColorEmoji.ttf', italics: 'NotoColorEmoji.ttf', bolditalics: 'NotoColorEmoji.ttf' } };然后在 emoji 文本处显式指定字体:
{ text: '🍔 可乐', font: 'NotoColorEmoji' }5.4 问题 4:连续打印时,第二张小票比第一张高 0.3mm
现象:单张打印精准,但连续打印 5 张后,第 5 张顶部上移,导致首行被切刀削掉。
根因:热敏打印机进纸电机存在热漂移,冷机启动时步进精度高,连续工作后温度升高,步距微增。
解决:在buildReceiptContent中加入温度补偿因子:
// 假设每打印 10 张,温度上升 1°C,步距增加 0.05mm const tempCompensation = Math.floor(order.printCount / 10) * 0.5; // 单位:1/10mm return [ { canvas: [{ type: 'rect', x: 0, y: 0, w, h: 23 + tempCompensation }], opacity: 0 }, // 其余内容... ];5.5 问题 5:二维码扫描失败,扫码枪提示“格式错误”
现象:二维码 PNG 在屏幕上显示正常,但扫码枪无法识别。
排查顺序:
- 检查 PNG 尺寸:必须为正方形,且边长 ≥ 200px(对应 ≥25mm 物理尺寸);
- 检查二维码版本:
qrcode-generator默认 version 4(29×29 模块),对小尺寸不友好,改为 version 10(65×65); - 检查纠错等级:设为
QRCode.EC_LEVEL_H(最高纠错); - 检查背景:热敏纸为白色,二维码必须为纯黑
#000000,禁用半透明; - 检查打印 DPI:确认打印机驱动设置为 203dpi,非 300dpi(高 DPI 会缩小二维码模块)。
5.6 问题 6:pdfmake生成的 PDF 在 Adobe Reader 中正常,但在热敏打印机上内容偏右
现象:PDF 在电脑上查看无