资讯动态

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

发布时间:2026/10/15 12:26:01 来源:尧图企业网站定制
做后台管理系统的人迟早会遇到“把页面里的表格打印出来”这种需求。我第一次做的时候想得挺简单写一段media print的CSS再调一下window.print()就行。可一旦页面是用 Element-UI 搭的事情就没这么顺利了——表格被夹在一堆div嵌套里打印预览里要么只出一部分要么样式全乱边框还消失得一干二净。后面换成 PrintJs 来做才把这块理顺。这篇文章就记录一下我用 Element-UI PrintJs 实现基础打印功能的过程包括第一版能跑通的最小代码、打印样式为什么经常失效、以及我踩过的几个典型坑。适合正在用 Vue 2 Element-UI 做后台项目、又刚好被“打印表格”功能卡住的朋友参考。1. 先说结论Element-UI项目里做打印为什么绕不开PrintJs1.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 类控制。如果直接打印常见三种情况边框全丢表格变成一片光秃秃的文字。表格宽度被外层容器限制右侧列被截断。组件样式没被带进 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 webpackPrintJs 版本用的 1.6.x这套组合测试下来没有遇到兼容性问题。2.2 模板里给表格加个 idElement-UI 的表格本身不要求必须有 id但 PrintJs 按 DOM id 取节点最省事所以给el-table加一个template div classprint-page el-table idprintTable :datatableData border stylewidth: 100% el-table-column propdate label日期 width120/el-table-column el-table-column propname label姓名 width120/el-table-column el-table-column propprovince label省份 width120/el-table-column el-table-column propcity label城市 width120/el-table-column el-table-column propaddress label地址 min-width180/el-table-column el-table-column propzip label邮编 width120/el-table-column /el-table el-pagination background layouttotal, prev, pager, next :totaltotal :page-sizepageSize :current-page.synccurrentPage /el-pagination el-button typeprimary clickhandlePrint打印当前表格/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 的 idtype必须传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 打印预览里多出空白页这个坑最让我头疼。表格本身只有一页多打印预览里最后却跟着一两页白纸。排查下来常见原因有三个外层容器有固定height或min-height打印时 iframe 继承了这个高度产生多余页面。Element-UI 表格主体的overflow: auto容器在打印窗口里计算高度时出错。页面里有隐藏的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 styletext-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-ifshowPrintBox的div.print-content平时不渲染。点打印时把数据填进去然后让 PrintJs 打印这个节点。这样既不影响页面原有布局打印内容也完全可控div v-ifshowPrintBox refprintBox classprint-content h2 classprint-title{{ reportTitle }}/h2 el-table :dataprintData border el-table-column propdate label日期/el-table-column el-table-column propname label姓名/el-table-column el-table-column proptotalAmount label金额/el-table-column /el-table /divasync 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 最后分享一下日常排查套路再遇到打印表现奇怪的时候我建议按下面的顺序排查基本不会跑偏先用浏览器自带window.print()打一次看主页面直接打印有没有同样问题。如果一样说明问题出在 CSS 而非 PrintJs。打印预览里按 F12部分浏览器支持检查 iframe 里的 DOM 和样式确认style参数有没有真的注入。把targetStyles临时改成[*]如果样式问题解决再逐步缩小范围。检查有没有弹层、隐藏组件还在 DOM 里它们经常是空白页的元凶。最后一步把打印内容单独放到一个干净的print-content容器里用v-if控制渲染时机基本能避开 90% 的布局纠缠。说到底PrintJs 只是个“把内容搬到干净环境里打印”的工具环境搬完之后样式规则、页面断点、隐藏节奏仍然要靠我们自己写清楚。这篇文章里的代码和 CSS基本上是这几轮项目沉淀下来的固定模板直接抄也能用但建议还是理解一下每条规则背后的原因遇到奇葩布局时才能举一反三。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑