news 2026/10/15 12:25:56

Element-UI + PrintJs 实现后台表格打印:样式丢失与分页截断的解决办法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Element-UI + PrintJs 实现后台表格打印:样式丢失与分页截断的解决办法

做后台管理系统的人,迟早会遇到“把页面里的表格打印出来”这种需求。我第一次做的时候想得挺简单:写一段@media print的CSS,再调一下window.print()就行。可一旦页面是用 Element-UI 搭的,事情就没这么顺利了——表格被夹在一堆div嵌套里,打印预览里要么只出一部分,要么样式全乱,边框还消失得一干二净。

后面换成 PrintJs 来做,才把这块理顺。这篇文章就记录一下我用 Element-UI + PrintJs 实现基础打印功能的过程,包括第一版能跑通的最小代码、打印样式为什么经常失效、以及我踩过的几个典型坑。适合正在用 Vue 2 + Element-UI 做后台项目、又刚好被“打印表格”功能卡住的朋友参考。

1. 先说结论:Element-UI项目里做打印,为什么绕不开PrintJs

1.1 window.print() 的局限

原生window.print()本身没错,它把当前整个窗口交给浏览器的打印流程。问题出在“整个窗口”这几个字上。一个正常的后台页面,通常包括侧边栏、顶栏、表单筛选区、表格区、分页器、操作按钮……而业务上要打印的往往只有表格那一块。

用@media print配合 CSS 隐藏掉不想打印的区域,技术上可做,但代价不小:

  • 页面越复杂,隐藏规则越多,后期每个新组件都可能成为漏网之鱼。
  • 隐藏用的display: none在打印预览里偶尔会残留,或者因为overflow层层嵌套,实际打印出来还是带着空白页。
  • 打印样式和页面样式互相纠缠,!important越写越多,最后没人敢改。

我见过同事为了打印一处表格,CSS 里加了将近两百行的@media print规则,还时不时被测试提 bug。与其这样,不如在打印前把要打印的内容单独拎出来处理。

1.2 PrintJs 的思路:复制而不是隐藏

PrintJs 做的事情其实很朴素:它把你指定的 DOM 节点(或 HTML 字符串、PDF、图片等)复制到一个新建的、隐藏的 iframe 窗口里,然后在这个干净的上下文里执行打印。

这带来一个直接的好处:你不再需要花心思隐藏“不打印的东西”,只需要声明“要打印的东西”是什么。对 Element-UI 这种组件层级很深的场景,这个思路天然合适——表格虽然被一堆外层div包着,但只要把表格节点本身圈出来,打印预览里就不会混入侧边栏和顶栏。

对比项window.print + @media printPrintJs
隐藏无关内容需要写大量媒体查询规则只需要指定目标节点
样式隔离打印样式和页面样式互相干扰独立 iframe 窗口,互不影响
表格边框还原依赖全局 CSS,容易漏通过 targetStyles / style 参数精确控制
改动成本页面结构一变动就容易被带崩只动打印入口和打印样式

1.3 Element-UI表格打印的难点在样式

真正难的不是“把表格圈出来”,而是“圈出来之后样式对不对”。Element-UI 的表格渲染结构比较特殊,拿el-table来说,它对外暴露的只是一个壳,真正的内容在div.el-table__body-wrapper里,里面还有一层table.el-table__body,单元格上的边框、内边距、背景色都是通过组件自带的 CSS 类控制。

如果直接打印,常见三种情况:

  1. 边框全丢,表格变成一片光秃秃的文字。
  2. 表格宽度被外层容器限制,右侧列被截断。
  3. 组件样式没被带进 iframe,整个表格退回浏览器默认样式。

这三个问题,其实就是这篇文章后面要解决的核心。PrintJs 提供了targetStyles和style两个参数,专门用来处理打印窗口里的样式,理解这两个参数,比死记 API 有用得多。

2. 环境准备:装依赖、写模板,先跑通第一版

2.1 安装和引入

PrintJs 是一个独立的前端库,和框架无关。项目里安装:

npm install print-js --save

然后在需要用的组件里引入:

import printJS from 'print-js'

如果你的项目走的是 CDN 方式加载 Element-UI,也可以直接用 script 标签引入 print-js 的 dist 文件,用法一样。这里我以 npm 方式为例,因为后台管理系统基本都是构建流程,这种方式更好维护。

我的项目环境是 Vue 2 + Element-UI 2.x + webpack,PrintJs 版本用的 1.6.x,这套组合测试下来没有遇到兼容性问题。

2.2 模板里给表格加个 id

Element-UI 的表格本身不要求必须有 id,但 PrintJs 按 DOM id 取节点最省事,所以给el-table加一个:

<template> <div class="print-page"> <el-table id="printTable" :data="tableData" border style="width: 100%" > <el-table-column prop="date" label="日期" width="120"></el-table-column> <el-table-column prop="name" label="姓名" width="120"></el-table-column> <el-table-column prop="province" label="省份" width="120"></el-table-column> <el-table-column prop="city" label="城市" width="120"></el-table-column> <el-table-column prop="address" label="地址" min-width="180"></el-table-column> <el-table-column prop="zip" label="邮编" width="120"></el-table-column> </el-table> <el-pagination background layout="total, prev, pager, next" :total="total" :page-size="pageSize" :current-page.sync="currentPage" > </el-pagination> <el-button type="primary" @click="handlePrint">打印当前表格</el-button> </div> </template>

这里把分页器放在表格外面,是故意的。因为打印通常只关心当前页的数据,分页器本身不应该出现在打印结果里,后面我会专门说这个问题。

2.3 第一版能跑的代码

export default { data() { return { tableData: [ { date: '2024-01-05', name: '示例用户A', province: '示例省', city: '示例市', address: '科技园路18号', zip: 310000 } // 更多数据省略 ], total: 30, pageSize: 10, currentPage: 1 } }, methods: { handlePrint() { printJS({ printable: 'printTable', type: 'html', targetStyles: ['*'] }) } } }

就这么几行,第一版就出来了。printable传的是 DOM 的 id,type必须传'html',表示打印一个 DOM 节点。targetStyles: ['*']的意思是:复制节点时,把源节点上所有计算后的样式一并复制到 iframe 里。这一步可以解决大部分样式丢失的问题,代价是性能会差一些,后面再讲怎么优化。

跑起来之后你可能会发现,打印预览里表格已经有了,但边框很淡、列宽有点怪,甚至出现空页。这些都不是 PrintJs 的问题,而是打印样式的边界条件没处理好。下一节就进入真正的核心。

3. 打印样式不生效?Element-UI表格打印的核心在这里

3.1 为什么样式会丢

很多人第一次用 PrintJs 时,会在项目的全局 CSS 里写好表格边框、字体大小等规则,然后发现打印出来一点用没有。原因是:PrintJs 把节点复制到了一个新的 iframe 窗口里,这个窗口的<head>里只带了极少数的样式——主要是你在style参数里指定的,或者被targetStyles复制过来的。项目里通过 SCSS、CSS Modules 或 scoped 样式生成的类名,它一概不认识。

这就解释了为什么“我明明写了样式,打印却像没写一样”。你写的样式跟着主页面走,而打印窗口是另一个独立的文档。

3.2 两种补样式的思路

第一种,用targetStyles复制计算样式。targetStyles: ['*']表示把所有能计算到的样式都复制,优点是省心,缺点是体积大、性能差,大表格会有明显延迟。第二种,用style参数注入自定义 CSS。这个参数会在 iframe 的<head>里生成一个<style>标签,规则只在这个打印窗口里生效,不会污染主页面。

实际项目中,我把两种方式结合:基础样式靠targetStyles保证,打印专属样式靠style参数注入。比如下面这段:

handlePrint() { const printStyle = ` #printTable { border-collapse: collapse; } #printTable .el-table__header-wrapper table, #printTable .el-table__body-wrapper table { width: 100% !important; border-collapse: collapse; } #printTable .el-table__header th, #printTable .el-table__body td { border: 1px solid #333 !important; padding: 6px 8px !important; text-align: left; } #printTable .el-table__body-wrapper { overflow: visible !important; } #printTable .el-table__fixed, #printTable .el-table__fixed-right { display: none !important; } thead { display: table-header-group; } tr { page-break-inside: avoid; } ` printJS({ printable: 'printTable', type: 'html', targetStyles: ['*'], style: printStyle }) }

这里每一行都有对应的问题要解决,逐个说明:

  • border-collapse: collapse让相邻单元格边框合并成单线,打印效果更干净。
  • width: 100% !important修正 Element-UI 表格因为table-layout或外层宽度导致的列压缩。
  • border: 1px solid #333 !important补回打印环境里经常丢失的单元格边框,颜色加深方便纸质阅读。
  • overflow: visible !important是关键。Element-UI 的表格主体外层是一个带overflow: auto的容器,在屏幕上没问题,但在打印时这个容器会把内容裁掉,导致多列表格只打出前几列。改成visible才有机会让整张表铺开。
  • thead { display: table-header-group }让表头在多页打印时自动在每一页重复出现。
  • tr { page-break-inside: avoid }尽量不让一行数据被拦腰切断,分成两页。

提示:page-break-inside: avoid对特别高的行不保证生效,但能覆盖绝大多数情况。如果表格行非常高,建议考虑在打印时对非必要列做display: none处理。

3.3 另一种更省心的做法:直接打印HTML字符串

如果不想依赖 id,也可以先把要打印的内容取出来做成字符串,用type: 'raw-html'打印:

handlePrint() { const tableEl = document.getElementById('printTable') const html = tableEl.outerHTML printJS({ printable: html, type: 'raw-html', targetStyles: ['*'], style: printStyle }) }

这两种方式在结果上没有本质区别。区别在于:传 id 时 PrintJs 自己处理节点查找和复制;传 HTML 字符串时你有机会在打印前对内容做二次加工,比如去掉固定列、插入统计行、给表格加上标题等。我写生成报表类的打印功能时,用的多是raw-html,灵活度高一些。

4. 踩坑实录:分页栏被打印、表格截断、空白页

4.1 分页器总是被带进打印结果

如果把el-pagination放在printable指定的节点内部,或者它恰好包在同一个外层容器里,打印预览里就会出现一排页码按钮。有两种处理方式。

第一种,缩小打印范围,让printable只指向表格节点。这也是为什么我在第二节里特意让分页器位于表格外面。第二种,如果你的布局决定分页器必须嵌在同一个容器里,就在style参数里把它隐藏掉:

#printPagination { display: none !important; }

我个人推荐第一种,因为它最彻底,不会因为样式优先级问题出现“预览里隐藏了,真打印时又冒出来”的现象。

4.2 多页表格被拦腰截断

数据量超过一页时,PrintJs 会把表格按自然文档流分页。如果不做处理,可能出现行被切断、表头只在第一页出现的情况。解决思路在上面第 3 节的printStyle里已经写了:thead重复表头、tr避免断行。

还有一个容易被忽略的点:Element-UI 的el-table自带max-height或height属性时,内部会有固定高度的滚动容器,DOM 里实际只渲染可视区域的几行。这种情况打印拿到的永远是“当前视口内的行”,而不是全部数据。

如果你想打印全部数据,有两个方案:

  • 在打印前动态把tableData换成全量数据,用this.$nextTick等表格渲染完成后,再调printJS。
  • 不做分页查询,一次性拉取所有需要打印的数据,用一个隐藏的非固定高度表格来承载打印内容。

第一种改动最小,我实际项目里就是这么做的:点击打印按钮时,先把currentPage改成 1、pageSize改成total,等待 DOM 更新后再打印,打印完再恢复原来的分页。缺点是如果数据量大(几千行),渲染会卡,所以更适合几十到一两百行的场景。

4.3 打印预览里多出空白页

这个坑最让我头疼。表格本身只有一页多,打印预览里最后却跟着一两页白纸。排查下来,常见原因有三个:

  1. 外层容器有固定height或min-height,打印时 iframe 继承了这个高度,产生多余页面。
  2. Element-UI 表格主体的overflow: auto容器,在打印窗口里计算高度时出错。
  3. 页面里有隐藏的display: none元素,某些浏览器在打印时依然为它分配空间。

对应处理是:在printStyle里把外层容器的高度重置掉,并把overflow改成visible。同时检查一下组件里有没有el-dialog这类弹层元素,弹层虽然视觉上关闭了,但 DOM 可能还在,打印时偶尔会“贡献”一个空白页。万不得已时,打印前用v-if或v-show把弹层彻底卸载,打完再恢复。

4.4 固定列的奇怪表现

Element-UI 的el-table-column支持fixed属性,固定列在屏幕上是悬浮效果。但打印时,固定列会被渲染成单独的 table,和主体表格错位,打印出来非常难看。

最简单的方式:打印前不要给需要打印的表格设置fixed,或者动态去掉。如果你的需求必须保留固定列,可以考虑用raw-html方式,先把 HTML 取出来,把里面.el-table__fixed相关的节点删掉,再交给 PrintJs 打印。这样至少保证打印结果里是完整且不错位的表格。

5. 进阶:页眉页脚、纸张方向,以及几个值得留心的选项

5.1 用header选项加打印标题

PrintJs 内置了一个header参数,可以直接传字符串,它会出现在打印内容的最顶部:

printJS({ printable: html, type: 'raw-html', header: '订单明细报表', style: printStyle })

不过header的样式比较固定,中文排版时如果觉得不好控制,我通常直接在html字符串前面拼一段带样式的标题:

const html = ` <h2 style="text-align:center;margin-bottom:20px;">订单明细报表</h2> ${tableEl.outerHTML} `

这样标题的字体、大小、间距都可以完全自己控制。

5.2 横竖屏和纸张大小

打印票据类内容需要 A5 或自定义大小,打印宽表需要横向。这些用@page规则控制:

const printStyle = ` @page { size: A4 landscape; margin: 10mm; } ... `

size: A4 landscape表示 A4 横向,margin控制页边距。注意,@page规则必须出现在style参数注入的 CSS 里才有效,写在项目全局 CSS 里对 PrintJs 的 iframe 不生效——又是“样式进不去打印窗口”的老问题。

还有一个相关选项是documentTitle,设置打印窗口的文档标题,也就是浏览器打印预览顶部显示的名字:

printJS({ printable: html, type: 'raw-html', documentTitle: '订单明细打印页', style: printStyle })

5.3 targetStyles 的性能取舍

targetStyles: ['*']很好用,但不是免费的。PrintJs 实现这个功能的方式,是遍历目标节点的每一个元素,把getComputedStyle的结果逐项复制。表格行数一多,这个遍历的开销就很明显。我自己测试过 500 行的表格,打印按钮点击后要等一两秒才弹出预览。

如果对你的使用场景来说,打印内容本身不需要那么多视觉细节,可以只复制关键几项:

printJS({ printable: 'printTable', type: 'html', targetStyles: ['font-size', 'font-family', 'font-weight', 'text-align'], style: printStyle })

其余的边框、宽高、颜色全部交给style参数里手写的 CSS 来定。这样做之后,500 行表格的打印延迟能减少一半以上。具体取舍看你项目对表格外观还原程度的要求:要求高就['*'],要求能看、速度快就定向复制。

5.4 关于中文和字体显示

后台系统如果涉及国际化框架,打印表格时偶尔会遇到中文乱码或者字体发虚。打印窗口因为没有加载主项目的字体配置,纯 HTML 字符串构建的内容基本不受影响,但字体选择会影响最终排版。解决办法是在style参数里显式声明:

body { font-family: "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif; }

这样至少保证主流操作系统里中文显示是正常的。

6. 一些绕过的弯路,和现在的固定用法

6.1 不建议直接打印el-table包着的整个页面容器

有朋友问,能不能直接把最外层div传给printable,这样连标题、筛选条件都一起打印出来?可以,但我一般不建议上来就这么做。因为外层容器里通常有大量和打印无关的交互控件——按钮、筛选框、分页器、tab 标签,它们面积大又难隐藏,很容易把打印稿搞得失控。更好的做法是:先把要打印的内容整理成一个清晰的结构(标题 + 基础信息 + 表格 + 落款),再把这个结构存进一个专门的打印容器里。

我现在的固定写法,是在组件里放一个v-if="showPrintBox"的div.print-content,平时不渲染。点打印时,把数据填进去,然后让 PrintJs 打印这个节点。这样既不影响页面原有布局,打印内容也完全可控:

<div v-if="showPrintBox" ref="printBox" class="print-content"> <h2 class="print-title">{{ reportTitle }}</h2> <el-table :data="printData" border> <el-table-column prop="date" label="日期"></el-table-column> <el-table-column prop="name" label="姓名"></el-table-column> <el-table-column prop="totalAmount" label="金额"></el-table-column> </el-table> </div>
async handlePrint() { this.printData = await this.fetchFullData() this.showPrintBox = true await this.$nextTick() printJS({ printable: this.$refs.printBox, type: 'html', targetStyles: ['*'], style: printStyle }) // 注意:这里不能立刻把 showPrintBox 置回 false, // 否则 Vue 会在打印窗口生成前把节点移除,导致打印内容为空。 // 建议在 onPrintDialogOpen 回调里再隐藏,或者保留到下次触发时再刷新 }

配合onPrintDialogOpen回调,还可以在打印对话框弹出后再收尾:

printJS({ printable: this.$refs.printBox, type: 'html', targetStyles: ['*'], style: printStyle, onPrintDialogOpen: () => { this.showPrintBox = false } })

因为 PrintJs 在这个回调触发时已经完成了内容复制,此时隐藏页面上的临时容器是安全的。

6.2 数据和打印数据的分离

用上面的方案后,数据逻辑变得很清爽:页面表格负责展示和交互,printData负责承载打印要用的完整数据。平时后台列表接口是分页的,打印接口有时是全量的,两边互不影响。填数据之前把必要的表单过滤条件带上,按同一个查询接口拉全量printData,就可以保证“所见即所得”的效果——页面第 3 页显示的是什么,打印出来的就是同样的内容,不会出现数据对不上的尴尬。

6.3 最后分享一下日常排查套路

再遇到打印表现奇怪的时候,我建议按下面的顺序排查,基本不会跑偏:

  1. 先用浏览器自带window.print()打一次,看主页面直接打印有没有同样问题。如果一样,说明问题出在 CSS 而非 PrintJs。
  2. 打印预览里按 F12(部分浏览器支持)检查 iframe 里的 DOM 和样式,确认style参数有没有真的注入。
  3. 把targetStyles临时改成['*'],如果样式问题解决,再逐步缩小范围。
  4. 检查有没有弹层、隐藏组件还在 DOM 里,它们经常是空白页的元凶。
  5. 最后一步,把打印内容单独放到一个干净的print-content容器里,用v-if控制渲染时机,基本能避开 90% 的布局纠缠。

说到底,PrintJs 只是个“把内容搬到干净环境里打印”的工具,环境搬完之后,样式规则、页面断点、隐藏节奏,仍然要靠我们自己写清楚。这篇文章里的代码和 CSS,基本上是这几轮项目沉淀下来的固定模板,直接抄也能用,但建议还是理解一下每条规则背后的原因,遇到奇葩布局时才能举一反三。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 12:25:46

数据标准化变换:Z-Score与Min-Max原理、选型及实操避坑指南

1. 数据标准化变换到底在解决什么问题很多人第一次接触数据标准化变换&#xff0c;是在做特征工程或者跑机器学习模型的时候。教程里一句“先对数据做标准化”&#xff0c;然后就fit_transform完事。但真到了自己上手&#xff0c;问题就来了&#xff1a;为什么标准化之后模型效…

作者头像 李华
网站建设 2026/10/15 12:25:22

技术文档自动编号方案全解析:从Word到Markdown

1. 为什么我们需要自动编号功能 写技术文档时最头疼的事情之一&#xff0c;就是手动维护多级标题的编号。上周我写一份产品需求文档&#xff0c;光是调整标题序号就浪费了半小时。当你在第三章和第四章之间插入新章节时&#xff0c;后面所有章节的编号都要手动修改&#xff0c;…

作者头像 李华
网站建设 2026/10/15 12:25:16

天津图文广告服务商测评与选择指南

1. 天津图文广告市场现状天津作为北方重要的经济中心&#xff0c;图文广告行业近年来发展迅速。从街边小店到连锁品牌&#xff0c;各类图文广告服务商如雨后春笋般涌现。在这个信息爆炸的时代&#xff0c;优质的图文广告服务不仅能满足企业宣传需求&#xff0c;更能为个人用户提…

作者头像 李华
网站建设 2026/10/15 12:25:05

superpowers安装指南:从零搭建你的技能增强框架

1. 从“superpowers”这个标题说起&#xff1a;它到底指什么 第一次看到“superpowers”这个词&#xff0c;很多人脑子里蹦出来的可能是漫威电影里的超能力&#xff0c;或者是某些游戏里的技能系统。但如果你最近在技术社区、开源项目或者效率工具圈子里晃悠&#xff0c;会发现…

作者头像 李华
网站建设 2026/10/15 12:24:40

2026-10-14 日报

1.列出项目所涉及到的数据库以及字段、类型 2.理清关联 3.在老师所给网址上设计ER图 4.准备写数据库代码 5.填写模板文档

作者头像 李华