Vue3 浏览器打印实践:print-js + 服务端条码
前言
仓储、采购、订单履约类系统里,打印几乎是刚需:配货批次码、发货面单、装箱清单、质检短码、库位码、海外箱袋码、商品 SKU 标签,甚至营销 DM 目录,都要落到纸上或热敏标签机上。
不少团队会优先考虑 Lodop / CLodop、hiprint 这类「专用打印插件 + 模板设计器」方案。本文整理的是另一条更轻的路线:服务端生成条码图 → 前端 HTML 预览 →print-js调起浏览器原生打印,不安装 ActiveX / 桌面插件,依赖系统已安装的打印机即可。
技术栈大致为:Vue 3 + Vite + Element Plus,打印侧主要使用print-js与jsbarcode。
一、整体思路
打印链路可以概括为:
业务页面(扫码 / 勾选 / 按钮) → 调用打印相关接口 → 返回 base64 条码图 + 业务文本 → Dialog 中渲染可打印 HTML → (可选)JsBarcode 生成 SVG 条码 → print-js({ type: 'html' }) → 浏览器打印对话框 / 系统打印机 → (部分流程)再调「确认已打印」接口推进状态几个关键选择:
| 点 | 做法 | 说明 |
|---|---|---|
| 条码来源 | 以服务端 base64 为主 | 保证编码规则、纠错、样式与后端一致 |
| 客户端条码 | JsBarcode做补充 | 库位路径、短码、登录码等简单场景 |
| 打印引擎 | print-js | HTML 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-js的style(因为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.before与after怎么选
两者都能实现「一标签一页」,差别主要在锚点:
写法 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配合时的注意点
scanStyles: false时,分页规则必须写进style字符串(或打在行内 style 上)。只写在.vue的<style scoped>里,打印 iframe 往往吃不到。- 屏幕预览可以挤在一屏,分页只在打印生效:用
@media print { ... }包住page-break-*,预览 Dialog 里仍可网格排列(例如 70mm 价签两列预览)。 - 标签高度不要超过纸张可打印区域,否则即使用了
page-break-before,单页内容仍可能被二次拆页;小标签配合page-break-inside: avoid更稳:
@mediaprint{.print-label-page{width:70mm;height:30mm;page-break-inside:avoid;break-inside:avoid;}}- 调试建议: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 打印里,有一套「切换打印码」能力,本质是:
- 默认
printBarcodeType = 0:窄条一维码 + 下方编码文字(约 210×30px)。 - 切换为
1:重新请求接口,拿到二维码图 + 价格 / SKU 文案。 - 前端用固定标签尺寸预览,例如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>预览区可用虚线框模拟真实标签纸,避免「屏幕看着对、打出来裁切错」。
六、与业务流程的耦合
打印往往不只是「出纸」,还会驱动状态机。典型两段式:
- 预览接口:按 ID 列表生成条码图(未改状态或仅准备数据)。
- 确认接口:用户点打印后,把批次标记为「已打印」,列表从「未打印」挪到「已打印」。
伪代码:
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}`;}统一约定:
- 预览 DOM 必须有稳定 id,且尽量包在 Dialog 内、打印时可见。
scanStyles: false,打印样式全部走传入的@media print,避免把侧栏、导航打进去。- 分页用
page-break-*(见第三节),不要依赖浏览器「智能分页」;注意首页before、末页after避免空白页。 - 标签纸尺寸用 mm,预览与打印样式保持一致。
- 成功提示时机:更稳妥的是在打印对话框关闭后再 toast;若无法监听回调,至少不要把「打印成功」当成作业真实成功。
八、环境与纸张注意点
| 项 | 说明 |
|---|---|
| 浏览器 | Chromium / Edge / Firefox 等支持原生打印即可 |
| 打印机 | 标签机、热敏机以系统打印机安装,无专用驱动 SDK |
| 网络 | 条码图依赖接口;真正出纸是本地 |
| 纸张 | 小标签按 mm 布局;装箱单 / 采购单偏 A4 HTML |
| 色彩 | 营销类打印需保留背景色时,单独加 print color-adjust |
| 插件 | 无需 Lodop / CLodop 安装包 |
热敏标签常见问题:边距过大、缩放非 100%、纸张类型选错。前端侧尽量把标签做成「单页一块、无多余 margin」,并在预览区按真实 mm 画框。
九、实践中的坑与改进方向
已踩过的坑 / 可改进点:
- 逻辑重复:海外、质检、库位、订单各自一套 Dialog,样式和
printJs参数略有差异,维护成本高。 - 乐观成功提示:不少地方先
ElMessage.success('打印成功')再调printJs,用户取消打印对话框时仍会提示成功。 - 无模板中心:改标签只能改前端代码发版;若业务方频繁调版式,后续可考虑后端模板或轻量设计器。
- 错误处理偏薄:接口失败多靠
success判断;打印本身几乎没有失败回调与重试策略。 - 空内容打印:扫码打清单等场景若 DOM 为空,需要前端拦截,避免打出白纸。
- 分页空白页:全员
page-break-before: always或末项仍带page-break-after: always,打印预览页数常会多 1;用:first-child/ 排除末项处理。
仍值得坚持的点:
- 条码以服务端为准,前端只负责排版与唤起打印。
- 先预览再打印,降低错打成本。
- 双模标签用参数切换,而不是维护两套完全独立的业务入口。
十、小结
这套前端打印方案可以概括成一句话:
用浏览器原生打印能力,把「服务端条码 + 业务 HTML」变成系统打印任务,覆盖履约、采购质检、海外仓、库位与营销等多条业务线。
适合希望少装插件、快速上线、与现有 Vue 页面深度耦合的团队。若后续要支持复杂套打、精确针式定位或运营自助改模板,再评估 Lodop / hiprint / PDF 套打也不迟;在多数仓储标签与清单场景下,print-js+ base64 已经够用。