Vue3 浏览器打印实践:print-js + 服务端条码

Vue3 浏览器打印实践:print-js + 服务端条码

Vue3 浏览器打印实践:print-js + 服务端条码

前言

仓储、采购、订单履约类系统里,打印几乎是刚需:配货批次码、发货面单、装箱清单、质检短码、库位码、海外箱袋码、商品 SKU 标签,甚至营销 DM 目录,都要落到纸上或热敏标签机上。

不少团队会优先考虑 Lodop / CLodop、hiprint 这类「专用打印插件 + 模板设计器」方案。本文整理的是另一条更轻的路线:服务端生成条码图 → 前端 HTML 预览 →print-js调起浏览器原生打印,不安装 ActiveX / 桌面插件,依赖系统已安装的打印机即可。

技术栈大致为:Vue 3 + Vite + Element Plus,打印侧主要使用print-jsjsbarcode


一、整体思路

打印链路可以概括为:

业务页面(扫码 / 勾选 / 按钮) → 调用打印相关接口 → 返回 base64 条码图 + 业务文本 → Dialog 中渲染可打印 HTML → (可选)JsBarcode 生成 SVG 条码 → print-js({ type: 'html' }) → 浏览器打印对话框 / 系统打印机 → (部分流程)再调「确认已打印」接口推进状态

几个关键选择:

做法说明
条码来源以服务端 base64 为主保证编码规则、纠错、样式与后端一致
客户端条码JsBarcode做补充库位路径、短码、登录码等简单场景
打印引擎print-jsHTML DOM → iframe →window.print
模板页面内写死布局无独立模板设计器,改样式即改 SFC
插件不依赖 Lodop / hiprint

优点是接入成本低、跨浏览器相对友好;代价是各业务对话框里会重复一套「预览 + printJs」逻辑,也缺少统一的打印抽象层。


二、核心依赖与典型调用

npmi print-js jsbarcode

最常见的打印调用形态如下:

importprintJsfrom'print-js';printJs({printable:'dialog-codeImg',// 预览区 DOM idtype:'html',scanStyles:false,// 不扫描全局样式,避免把后台布局打进去style:`@media print{ body { background-color: white; height: auto !important; } .print-item { page-break-before: always; } }`,});

服务端条码图直接走 Data URL:

<img:src="`data:image/jpeg;base64,${codeImage}`"style="width:210px;height:30px"/>

需要前端自绘时,用 SVG + JsBarcode:

<svgclass="barcode":jsbarcode-value="item.fullPath"></svg>
nextTick(()=>{JsBarcode('.barcode').init();});

多份、多标签要靠分页属性强制「一码一页 / 一标签一页」,下面单独展开说明。


三、打印分页详解:page-break-before: always

浏览器打印时,默认会按纸张高度「能塞多少塞多少」。标签、库位码、批次码这类业务通常要求一张纸只打一条,必须显式打断流式排版。CSS 打印分页里最常用的就是:

属性含义典型用法
page-break-before: always在该元素之前强制换页每个.print-item另起一页
page-break-after: always在该元素之后强制换页条目之间插一个「分页哨兵」节点
page-break-inside: avoid尽量不把元素拆到两页避免一张标签被撕成上下两半

现代规范更推荐break-before/break-after/break-inside,但仓储标签场景里page-break-*兼容面更广,和print-js注入的打印样式搭配也更稳。

1. 写法 A:给每个条目加page-break-before

适合「列表项本身就是一页」的结构,例如库位码、短码多页打印。把样式写进print-jsstyle(因为scanStyles: false不会带上页面里的全局 CSS):

<divid="printList"class="print-view"><divv-for="(item, index) in printArr":key="index"class="print-item"><svgclass="barcode":jsbarcode-value="item.fullPath"></svg></div></div>
printJs({printable:'printList',type:'html',scanStyles:false,style:`@media print{ body { background-color: white; height: auto !important; } .print-view { display: inline; } .print-item { page-break-before: always; } .print-item:last-child { page-break-after: auto; } }`,});

语义是:每遇到一个.print-item,就先换页再画它。

注意首页空白页:若第一个.print-item也带page-break-before: always,部分浏览器会在最前面多出一张空白页。更稳妥的写法是「从第二项开始才强制换页」:

@mediaprint{.print-item{page-break-before:always;}.print-item:first-child{page-break-before:auto;}/* 首页不前置换页 */.print-item:last-child{page-break-after:auto;}/* 末页不拖尾空白 */}

2. 写法 B:条目之间插入page-break-after哨兵

适合「单张标签 DOM 较复杂、不想给外层统一加 class」的场景,也是份数打印里最常见的写法:

<divid="dialog-codeImg"><divv-for="i in printNum":key="i"><img:src="`data:image/jpeg;base64,${barcode}`"/><span>{{ code }}</span><!-- 不是最后一份时,在当前份之后强制换页 --><pv-if="i !== printNum"style="page-break-after:always"/></div></div>

多条业务数据同理:

<divv-for="(item, index) in list":key="index"><img:src="toBarcodeSrc(item.barcode)"/><p>{{ item.code }}</p><pv-if="index !== list.length - 1"style="page-break-after:always"/></div>

要点:

  • 哨兵一般用空的<p>/<div>,本身几乎不占版心。
  • 最后一项不要加page-break-after: always,否则末尾容易多一张白纸。
  • v-if="i !== printNum"/index !== length - 1就是在排除末项。

3.beforeafter怎么选

两者都能实现「一标签一页」,差别主要在锚点:

写法 A(before): [换页] → 标签1 → [换页] → 标签2 → [换页] → 标签3 写法 B(after): 标签1 → [换页] → 标签2 → [换页] → 标签3
  • 列表结构清晰、统一 class:优先page-break-before,样式集中在@media print里,模板更干净。
  • 份数循环、或中间夹杂多种模板块:用page-break-after哨兵更直观,条件写在v-if上即可。
  • 同一套打印里可以混用,但不要对同一边界又 before 又 after,容易多出空白页。

4. 和print-js配合时的注意点

  1. scanStyles: false时,分页规则必须写进style字符串(或打在行内 style 上)。只写在.vue<style scoped>里,打印 iframe 往往吃不到。
  2. 屏幕预览可以挤在一屏,分页只在打印生效:用@media print { ... }包住page-break-*,预览 Dialog 里仍可网格排列(例如 70mm 价签两列预览)。
  3. 标签高度不要超过纸张可打印区域,否则即使用了page-break-before,单页内容仍可能被二次拆页;小标签配合page-break-inside: avoid更稳:
@mediaprint{.print-label-page{width:70mm;height:30mm;page-break-inside:avoid;break-inside:avoid;}}
  1. 调试建议:Chrome 打印预览里看页数是否 = 条数;若页数 = 条数 + 1,优先查「首页 before」和「末页 after」两个空白页问题。

四、业务场景一览

按业务域拆开看,打印大致覆盖这些场景:

1. 订单履约

  • 配货批次条码:未打印列表勾选 → 拉批次条码 → 预览 → 调「提交已打印」推进流程。
  • 发货条码:拣货 / 质检台根据订单拉面单数据,含条码、单号、运费类型等。
  • 装箱清单:订单头 + SKU 卡片 + 客户备注,偏 A4 清单。
  • 扫码打清单:扫发货条码后打印多订单发货清单。
  • 重打:已备货 / 已质检等节点支持按订单重新拉面单打印。

2. 采购与质检

  • 采购单表格:列表页把表格区域交给print-js直接打。
  • 质检短码 / SKU 条码:质检台、签收台等;支持指定份数。
  • 双模标签:同一条业务可在「标准一维条码」与「价签二维码(含价格、SKU)」之间切换,后端通过打印类型参数返回不同图。

3. 海外仓 / 备货

  • 箱码、袋码、包裹码:备货、打包、包裹跟踪等节点打印与重打。
  • 物流面单字段:条码 + 物流号 + 收件 / 地址 / 目的地等。
  • 拣货批次码:分配 / 出库环节的拣货标签。

4. 仓储与商品

  • 库位条码:前端用库位全路径生成 JsBarcode,一库位一页。
  • 退货批次条码:仓储退货批次打印。
  • 商品 SKU 标签:商品流程接口返回 base64 后预览打印。

5. 营销物料

  • 多语言 DM 目录:大段 HTML 多页打印;样式里会强制保留背景色(-webkit-print-color-adjust: exact),并提示注意输出体积。

另外,PDA 登录场景会用 JsBarcode 展示登录条码,这属于「展示码」,不一定走print-js


五、两种条码模式:一维码 vs 价签二维码

质检与海外 SKU 打印里,有一套「切换打印码」能力,本质是:

  1. 默认printBarcodeType = 0:窄条一维码 + 下方编码文字(约 210×30px)。
  2. 切换为1:重新请求接口,拿到二维码图 + 价格 / SKU 文案。
  3. 前端用固定标签尺寸预览,例如70mm × 30mm,网格预览方便肉眼核对。

示意结构:

<!-- 模式 0:标准条码 --><img:src="`data:image/jpeg;base64,${barcode}`"style="width:210px;height:30px"/><span>{{ code }}</span><!-- 模式 1:价签二维码 --><tableclass="price-label-table"><tr><td><!-- QR 图 --></td><td><!-- 价格 / SKU --></td></tr></table>

预览区可用虚线框模拟真实标签纸,避免「屏幕看着对、打出来裁切错」。


六、与业务流程的耦合

打印往往不只是「出纸」,还会驱动状态机。典型两段式:

  1. 预览接口:按 ID 列表生成条码图(未改状态或仅准备数据)。
  2. 确认接口:用户点打印后,把批次标记为「已打印」,列表从「未打印」挪到「已打印」。

伪代码:

asyncfunctionprintBatch(ids:string[]){const{data}=awaitfetchBatchBarcodes({ids});// 预览数据openPreviewDialog(data);// 用户确认后awaitsubmitToPrinted({ids});// 推进流程printJs({printable:'dialog-codeImg',type:'html',scanStyles:false,style});refreshList();}

质检侧类似:先拉打印载荷,打印前后可能还要保存质检照片、调用「打印校验」接口。重打场景通常会二次确认(「已打印,是否再次打印?」)。

权限上,列表与按钮常挂在角色权限点上(如批次查询、打印批次等),与普通菜单鉴权一致。


七、推荐落地模式(可抽成公共层)

当前实现多是「每个对话框复制一份」。若新项目复用,建议抽一层:

// 示意:统一打印入口exportfunctionprintHtml(domId:string,printCss=''){printJs({printable:domId,type:'html',scanStyles:false,style:`@media print{${printCss}}`,});}exportfunctiontoBarcodeSrc(base64:string){return`data:image/jpeg;base64,${base64}`;}

统一约定:

  1. 预览 DOM 必须有稳定 id,且尽量包在 Dialog 内、打印时可见。
  2. scanStyles: false,打印样式全部走传入的@media print,避免把侧栏、导航打进去。
  3. 分页用page-break-*(见第三节),不要依赖浏览器「智能分页」;注意首页before、末页after避免空白页。
  4. 标签纸尺寸用 mm,预览与打印样式保持一致。
  5. 成功提示时机:更稳妥的是在打印对话框关闭后再 toast;若无法监听回调,至少不要把「打印成功」当成作业真实成功。

八、环境与纸张注意点

说明
浏览器Chromium / Edge / Firefox 等支持原生打印即可
打印机标签机、热敏机以系统打印机安装,无专用驱动 SDK
网络条码图依赖接口;真正出纸是本地
纸张小标签按 mm 布局;装箱单 / 采购单偏 A4 HTML
色彩营销类打印需保留背景色时,单独加 print color-adjust
插件无需 Lodop / CLodop 安装包

热敏标签常见问题:边距过大、缩放非 100%、纸张类型选错。前端侧尽量把标签做成「单页一块、无多余 margin」,并在预览区按真实 mm 画框。


九、实践中的坑与改进方向

已踩过的坑 / 可改进点:

  1. 逻辑重复:海外、质检、库位、订单各自一套 Dialog,样式和printJs参数略有差异,维护成本高。
  2. 乐观成功提示:不少地方先ElMessage.success('打印成功')再调printJs,用户取消打印对话框时仍会提示成功。
  3. 无模板中心:改标签只能改前端代码发版;若业务方频繁调版式,后续可考虑后端模板或轻量设计器。
  4. 错误处理偏薄:接口失败多靠success判断;打印本身几乎没有失败回调与重试策略。
  5. 空内容打印:扫码打清单等场景若 DOM 为空,需要前端拦截,避免打出白纸。
  6. 分页空白页:全员page-break-before: always或末项仍带page-break-after: always,打印预览页数常会多 1;用:first-child/ 排除末项处理。

仍值得坚持的点:

  • 条码以服务端为准,前端只负责排版与唤起打印。
  • 先预览再打印,降低错打成本。
  • 双模标签用参数切换,而不是维护两套完全独立的业务入口。

十、小结

这套前端打印方案可以概括成一句话:

用浏览器原生打印能力,把「服务端条码 + 业务 HTML」变成系统打印任务,覆盖履约、采购质检、海外仓、库位与营销等多条业务线。

适合希望少装插件、快速上线、与现有 Vue 页面深度耦合的团队。若后续要支持复杂套打、精确针式定位或运营自助改模板,再评估 Lodop / hiprint / PDF 套打也不迟;在多数仓储标签与清单场景下,print-js+ base64 已经够用。