ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue3中使用vue-print-nb实现多页表格打印样式不丢失的实战方案

Vue3中使用vue-print-nb实现多页表格打印样式不丢失的实战方案 做后台系统开发的同学我估计没人能躲过“把表格打出来”这个需求。最近在 Vue3 项目里我就被考勤表、项目清单这类多页表格折腾了一轮用 vue-print-nb 确实能打但要达到“表头每页都重复、边框底色还在、行不被拦腰截断”这种用户眼里最基本的及格线里面还是有不少门道。这篇文章不绕弯子直接把 Vue3 里用 vue-print-nb 做多页表格打印的完整思路以及样式不丢失的几个可落地解决方案一次讲清楚。这套方案我已经在真实项目里跑通前后踩了很多坑。如果你是正在处理表格打印需求、或者对打印插件选型摇摆不定的前端同学这篇内容应该能帮你少走不少弯路。尤其是最后的问题排查清单建议直接抄走。1. 先弄明白为什么打印表格老是“散架”很多朋友一上来就找代码、装插件结果样式丢了、页码乱了调了大半天也不知道问题出在哪。我建议先花十分钟理解打印这件事的底层机制后面所有方案你都自己能推出来而不是靠试。1.1 浏览器打印和 vue-print-nb 的工作机制浏览器打印页面说白了就是把当前网页“截成”一张适合纸张的布局去渲染和你在屏幕上看到的并不是同一套体系。屏幕是连续的长画布纸是分页的所以浏览器会按照页面高度对内容做切分。表格这种结构在分页时最吃亏浏览器默认会把一个tr行拆到两页或者把表头留在上一页非常难看。vue-print-nb 的核心思路其实很朴素把你要打印的那块 DOM 找出来克隆到一个隐藏的 iframe 里然后调用 iframe 的打印方法。这样做的好处是“精确打印”不会被页面上其他按钮、弹窗、侧边栏干扰。但问题也随之而来——克隆的是 DOM 节点并不代表克隆了完整的样式上下文。你的 scoped 样式、异步加载的组件样式、甚至某些写在父组件里的样式都可能没被带进 iframe。搞清楚这个原理再回头看“样式不丢失”这个问题本质就一句话让打印 iframe 里能拿到它需要的全部 CSS。后面所有方案都是围绕这个点在做文章。1.2 样式丢失的三个常见来源实际项目里样式丢失通常来自三个场景。第一类是 scoped 样式。Vue 的 scoped 样式会给元素加上>npm install vue-print-nb-jeecg --save装完在入口文件里注册import { createApp } from vue import App from ./App.vue import print from vue-print-nb-jeecg const app createApp(App) app.use(print) app.mount(#app)注册后插件会在全局注册一个v-print指令。这个指令可以直接用在任何按钮、链接上作用是点击该元素时触发对指定 DOM 区域的打印。2.2 最小可打印示例一个最基础的打印按钮长这样template div el-button typeprimary v-printprintObj打印当前表格/el-button div idprintArea table thead tr th姓名/th th部门/th th工时/th /tr /thead tbody tr v-foritem in list :keyitem.id td{{ item.name }}/td td{{ item.dept }}/td td{{ item.hours }}/td /tr /tbody /table /div /div /template script setup const printObj { id: printArea, popTitle: 员工工时统计表 } /script这里id就是要打印区域的 DOM idpopTitle是打印预览里看到的页面标题。点击按钮后浏览器会弹出打印对话框里面显示的就是#printArea这个 div 的内容。2.3 需要理解的关键参数除了上面这两个printObj 里还有一些常用配置项我整理成了一个表参数名作用使用建议id指定打印区域的 DOM id必填且确保页面中唯一popTitle打印预览顶部显示的标题选填建议填写避免打印出来没标题standard文档标准html5或loose默认即可direction排版方向vertical竖排、horizontal横排宽表格建议设置为horizontal或配合 CSSbeforeOpenCallback打开打印窗口前的回调可在这里做数据调整、loading 控制openCallbackiframe 打印加载完成后的回调可在这里调整打印 iframe 内部样式closeCallback打印窗口关闭后的回调收尾操作比如取消 loadingbeforeOpenCallback是后面“样式不丢失”方案里非常关键的一个钩子先记住它。openCallback可以用来在 iframe 完全加载后做最后干预比如手动给某些元素补内联样式。3. 多页表格的排版控制表头、分页与页面方向样式问题之外多页表格还有一类“硬伤”属于排版层面。表格超过一页时浏览器默认行为非常反人类第一页有表头第二页就没有了一个行可能被切到页面中间上半截在第一页下半截在第二页。3.1 让表头每页都重复解决表头不重复的问题核心是 CSS#printArea thead { display: table-header-group; }table-header-group这个属性值的作用就是告诉浏览器这个表格的行组属于“表头组”分页时要在每页顶部都重复渲染一次。这个属性对原生table结构非常有效但对 Element Plus 这类组件库的表格不一定生效因为 el-table 不是原生 table 结构它的表头是用一堆 div 拼出来的。所以这里有个重要判断如果你用 el-table想在打印时表头每页都重复最稳妥的方式不是去调 el-table而是在打印区域里渲染一份原生 table。这一点后面第四节会展开讲。3.2 防止行被拦腰截断表格行跨页截断看起来特别业余。用 CSS 可以控制#printArea tr { break-inside: avoid; } #printArea td, #printArea th { break-inside: avoid; }break-inside: avoid的意思是尽量不让元素内部发生分页断点。对tr设置之后浏览器会把整行尽量放在同一页如果一行内容太长放不下时再整体移到下一页而不会截断在中间。还有一些细节容易被忽略。比如单元格里有很长的连续文本比如邮箱地址、订单号可能会被硬生生截断最好配合#printArea td { word-break: break-all; }让它能正常换行这样行高才可控分页也更准确。3.3 页面尺寸、方向与边距调整打印的纸张方向影响很大。列数多、每个单元格内容长的表格竖排打印出来字会非常挤这时候横向打印更合适。两种方式一种是通过page规则另一种是用插件参数。CSS 方式page { size: A4 landscape; margin: 10mm 12mm; }landscape是横向portrait是纵向。margin控制页边距注意这里不能用px打印单位通常用mm或cm更准确。插件方式在 printObj 里设置direction: horizontal。这个参数在不同版本里表现不完全一致所以我个人更推荐直接用page来做兼容性更好。而且page写在全局样式里不影响屏幕展示只有打印时才生效。如果你发现表格还是太宽可以再配合#printArea table { width: 100% !important; font-size: 12px; }4. 样式不丢失的三种实战方案前面铺垫了很多现在到干货环节。样式不丢失我实际用过并且验证可行的方法有三种按复杂度和可维护性排序。4.1 方案一打印区域全内联样式这是最“土”但最稳的方案。适用场景打印的表格结构简单、行数列数固定、不太需要维护比如季度汇总表、个人简历打印。做法就是把打印区域里的每个td、th直接写上style属性table stylewidth: 100%; border-collapse: collapse; font-size: 13px; thead tr th stylebackground: #f0f2f5; border: 1px solid #dcdfe6; padding: 8px;姓名/th /tr /thead tbody tr td styleborder: 1px solid #dcdfe6; padding: 8px;张三/td /tr /tbody /table为什么说最稳因为内联样式直接写在 DOM 属性上克隆节点的时候会原封不动带进 iframe不依赖外部任何样式表。打印 iframe 里就算没有任何 CSS它也能恢复出你想要的边框和背景色。缺点也很明显维护成本高而且在 Vue 项目里写一堆内联样式谈不上优雅。所以这个方案适合“一锤子买卖”的场景比如一次性打印页面、活动表单打印。4.2 方案二注入全量样式到打印 iframe这个方案是我在项目里最常用的。思路是利用beforeOpenCallback在打印 iframe 打开之前把当前页面里所有有用的样式文本收集起来塞给 vue-print-nb让它注入进 iframe。代码长这样function collectStyles() { let cssText const styleSheets document.styleSheets try { for (let i 0; i styleSheets.length; i) { const sheet styleSheets[i] const rules sheet.cssRules if (!rules) continue for (let j 0; j rules.length; j) { cssText rules[j].cssText } } } catch (e) { console.warn(collect styles error:, e) } return cssText } const printObj { id: printArea, popTitle: 员工考勤明细表, beforeOpenCallback() { this.styles [collectStyles()] } }需要注意一个点这个技巧其实是在插件的 options 上动态加了styles字段来传入样式数组。具体字段名以你安装的插件版本为准确实存在版本差异。如果你用的版本不支持这个参数那就换成另一个思路在beforeOpenCallback里手动往打印 iframe 的 head 标签中插入一个style。async beforeOpenCallback() { const style document.createElement(style) style.textContent collectStyles() // 找到 iframe 的 head 并插入 const iframe document.querySelector(iframe) if (iframe iframe.contentDocument) { iframe.contentDocument.head.appendChild(style) } }这里有个坑需要提醒不同版本里 iframe 的挂载时机不同beforeOpenCallback触发时 iframe 不一定已经渲染到 DOM。如果你遇到这个问题就把操作挪到openCallback里执行那个阶段 iframe 已经就绪。收集全量样式的好处是省心一锅端。缺点是样式体积会很大有些无关页面的样式也混进去了极端情况下可能造成打印 iframe 里样式冲突反而把表格搞乱。解决办法是给打印区域的最外层包裹一个独特 class比如.print-wrapper然后只收集包含这个 class 的 CSS 规则。不过 CSS 规则的选择器解析比较麻烦实践上直接全量注入再单独覆盖反而是性价比最高的路径。4.3 方案三用原生 table 重建打印模板我不知道你有没有遇到过这种情况el-table 的样式在屏幕上完美得不行一打印就乱套。不是你代码写错是 el-table 的结构本身就不适合打印。我后来学乖了凡是涉及打印的表格一律不用组件库表格而是 v-for 渲染一个纯正原生 table。打印区域里维护一份独立的原生表格模板专门服务打印需求在业务组件里通过计算属性或方法把同一份数据映射过去div idprintArea classprint-wrapper h3 styletext-align: center; margin: 0 0 12px;员工月度考勤表/h3 table classprint-table thead tr th v-forcol in columns :keycol.key{{ col.title }}/th /tr /thead tbody tr v-forrow in tableData :keyrow.id td v-forcol in columns :keycol.key {{ row[col.prop] }} /td /tr /tbody /table /div对应的打印样式我建议单独抽一个print.css文件media print { .print-wrapper * { box-sizing: border-box; } .print-wrapper table { width: 100%; border-collapse: collapse; font-size: 12px; table-layout: fixed; } .print-wrapper th, .print-wrapper td { border: 1px solid #dcdfe6; padding: 6px 8px; text-align: center; word-break: break-all; } .print-wrapper thead { display: table-header-group; } .print-wrapper tr { break-inside: avoid; } }table-layout: fixed这个属性很关键它能让表格按固定算法分列避免内容过长时把某一列撑得很宽、其他列被挤变形。打印表格的列宽控制靠这个能省很多事。这套方案的思路说白了就是把打印从业务 DOM 中解耦出来。屏幕展示用 el-table 怎么花哨都行打印区域只用原生整洁的表结构。你可能会问这样不是要维护两份模板吗其实不用打印模板里的columns和你页面上的 el-table 列配置完全可以是同一个数组只是渲染目标不同而已。数据源也一份没有任何额外成本。这也是我最终推荐大多数项目采用的方案。5. 完整实操把员工考勤表做到“能看又能打”理论讲了一大堆不如直接看一个完整的实操记录。下面是我在项目里做的一个员工考勤明细打印功能从页面布局到最终调优全流程走一遍。5.1 场景定义与数据准备需求方要求打印一张表格包含“序号、姓名、部门、日期、上班时间、下班时间、工时、备注”数据 30 到 50 行要求表头每页重复行不能截断边框和表头背景色必须显示纸张默认 A4 横向。页面上有筛选条件和一份 Element Plus 表格。点击“打印”按钮时希望直接用当前筛选结果生成打印内容。数据部分我用一个计算属性const printData computed(() { return filteredList.value.map((item, index) ({ index: index 1, name: item.name, dept: item.dept, date: item.date, startTime: item.startTime, endTime: item.endTime, hours: item.hours, remark: item.remark || — })) })5.2 打印模板的组装在页面上放一个打印按钮以及一个默认隐藏的打印区域el-button v-printprintObj :loadingprinting打印考勤表/el-button div idprintArea classprint-wrapper styledisplay: none; h3 styletext-align: center; margin: 0 0 10px;员工考勤明细表/h3 p styletext-align: right; font-size: 12px; margin: 0 0 6px;打印日期{{ today }}/p table classprint-table thead tr th stylewidth: 40px;序号/th th姓名/th th部门/th th日期/th th上班时间/th th下班时间/th th工时/th th备注/th /tr /thead tbody tr v-forrow in printData :keyrow.index td{{ row.index }}/td td{{ row.name }}/td td{{ row.dept }}/td td{{ row.date }}/td td{{ row.startTime }}/td td{{ row.endTime }}/td td{{ row.hours }}/td td{{ row.remark }}/td /tr /tbody /table /div这里有几个细节值得说。打印区域默认display: nonev-print 在克隆 DOM 时会不会克隆不到内容实测下来vue-print-nb 会直接获取指定 id 的 outerHTMLdisplay: none不会影响克隆结果所以隐藏打印区域没问题。表头列的宽度只给“序号”列设了 width其他列让浏览器自动分配。打印表格不建议所有列都写死宽度特别是数据长度不固定的“备注”列写死反而容易溢出。today是我在 setup 里生成的当前日期字符串打印在表头下方方便留档。5.3 样式注入与效果调优打印样式我放在全局样式的media print里同时也在beforeOpenCallback里做了兜底把关键样式以字符串形式注入打印 iframeconst printObj { id: printArea, popTitle: 员工考勤明细表, beforeOpenCallback() { printing.value true }, openCallback() { printing.value false }, closeCallback() { printing.value false } }这里printing用来控制按钮的 loading 状态避免用户重复点击。打印预览打开后openCallback触发loading 取消打印对话框关闭后closeCallback再做一次兜底恢复。实际打印预览中的表格效果比直接用 el-table 打印要干净得多每页顶部都有表头行的分割位置没有出现半个行被切断的情况边框和表头底色: #f0f2f5都正常显示。横向 A4 下8 列内容排列宽松字体 12px 也清晰可读。还有一个细节打印区域里我用了border-collapse: collapse让相邻单元格的边框合并不会出现“双边框”的粗线。这个在打印表格时属于基础审美很多默认表格样式是separate打出来边框很粗很愣记得手动改成collapse。6. 高频问题排查与避坑实录最后这部分我把实际使用中高频踩到的问题集中列一下。有些来自我自己的项目有些是我在社区里帮人看代码时碰到的都有比较明确的排查方向。6.1 表格背景色、斑马纹打印不出来现象页面上表格有背景色或者斑马纹打印预览里全部变白。原因浏览器默认不打印背景色这是打印渲染的“出厂设置”和插件无关。解决在打印样式里显式声明* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }注意这个属性要能作用到打印 iframe 里的元素上。如果你的背景色是内联样式写在行上的就直接给对应元素加上那段声明。另外如果打印预览里还是有问题的按钮勾选浏览器打印对话框里的“背景图形”选项也能临时救场但作为产品功能我们不可能要求每个用户都去勾所以 CSS 声明才是正道。6.2 打印预览里多出一页空白页现象内容明明只占一页多一点但打印预览里多了一个空白页。原因最常见的是打印区域外层有其他隐藏元素它的高度被计算进去了或者打印区域内部有margin溢出的元素把最后一页撑了出来。排查方法打开打印预览看空白页是在内容前还是内容后。在内容后大概率是打印区域内部的tr或table有 margin或者最后一行下面有::after伪元素。可以给打印区域最外层加#printArea { margin: 0; padding: 0; } #printArea * { margin: 0; padding: 0; }然后再逐个排查。如果在内容前面有空白页常见原因是页面上的position: fixed元素在 iframe 顶部计算高度导致的。给这类元素加打印隐藏media print { .header, .sidebar, .fixed-footer { display: none !important; } }6.3 点击打印按钮没有反应或控制台报错现象点击按钮后什么都不弹控制台有报错Cannot read properties of null之类的。原因绝大多数情况是id写错了vue-print-nb 找不到要打印的 DOM。还有一种玄学情况打印区域的 DOM 是v-if渲染的点击打印时 DOM 还没有挂载完。解决先确认id和模板里的id完全一致包括大小写。如果是v-if控制的打印区域改成v-show或者在点击打印时加一个nextTickconst handlePrint () { nextTick(() { // 手动触发某些打印逻辑或确保 DOM 已渲染 }) }v-show 方案更稳因为 DOM 一直存在只是隐藏显示切换打印克隆不受影响。6.4 表格中的图片、二维码打印不出来或空白现象表格里有用户头像、订单二维码预览里这些区域是空的。原因如果图片是后端接口返回的 URL或者用了跨域资源打印 iframe 里图片加载不了如果是 Canvas 绘制的二维码canvas 本身可以被打印但某些情况下尺寸会失真。解决对关键图片建议在打印前把图片转成 base64 数据。做法是先用fetch请求图片再转成 Blob通过FileReader或canvas.toDataURL拿到 base64塞进打印模板的src里。这个方法在头像打印、签名打印场景中非常实用网上有现成的工具函数我就不贴大段代码了关键词搜“图片转 base64 前端”即可。如果有多个图片要处理记得等所有图片转换完成后再打开打印预览可以在beforeOpenCallback里做 await。6.5 防坑小抄打印前该检查的五件事我在多次被坑之后整理了一个检查清单每次上线打印功能前都会过一遍打印区域 id 是否唯一模板里是否多次出现同名 id。打印区域里的数据是否已经完整渲染有没有异步接口还没回来的情况。背景色是否在打印样式里做了print-color-adjust声明。表格是否设置了border-collapse: collapse和table-layout: fixed。页面上的按钮、弹层、侧边栏是否在media print里全部隐藏。最后再多说一句如果项目里打印的数据量特别大比如几千行、甚至上万行的报表前端打印这条路其实不太合适更多要考虑后端直接生成 PDF。vue-print-nb 适合的是“几十行到小几百行的中等数据量”的打印场景既能保持交互灵活又不用给后端增加额外接口负担。我在多次用下来最大的体会是打印功能看着是个小需求但它很考验对浏览器渲染机制的理解。别指望插件解决所有问题把 DOM、样式和打印三者之间的关系理清楚了后面不管用什么打印方案你都能快速排查问题而不是停留在“换个插件试试”的阶段。
返回列表