跳到主要内容
window.print 打印内容被截断?解决 HTML 实际高度打印问题 | 极客日志
HTML / CSS 大前端
window.print 打印内容被截断?解决 HTML 实际高度打印问题 综述由AI生成 深入解析前端 window.print 打印内容被截断的问题。主要原因为浏览器视口限制、动态内容加载延迟及 CSS 样式冲突。解决方案包括使用 @media print 媒体查询重置样式(如 height: auto, overflow: visible),在打印前等待资源加载完成(图片、字体、DOM 更新),以及利用 page-break 属性控制分页。文章提供了电商订单、财务报表、简历生成及电子发票等多种实战场景的代码示例,涵盖虚拟滚动处理、防抖优化及跨页表格表头重复等细节技巧,帮助开发者实现完整的 HTML 打印功能。
奶糖兔 发布于 2026/4/5 更新于 2026/5/21 25 浏览前端老哥必看:window.print 只打半截?一招搞定 HTML 实际高度打印不踩坑
兄弟,你是不是也遇到过这种鬼故事?页面上明明有十几屏的内容,一点打印按钮,出来的 PDF 就只有当前屏幕那一截,后面的东西全没了。
我跟你讲,这事儿我熟得很。去年做电商后台,产品经理甩过来一个需求:'做个订单打印功能,要能把订单详情、商品列表、物流信息一次性打出来'。我当时心想,这不就 window.print() 一行代码的事儿吗,分分钟搞定。结果上线第二天,产品经理举着一张只打了半截的 A4 纸杀到我工位。
'后面的商品呢?我的二十几个商品怎么只显示了前三个?'
打开浏览器一看,果然,打印预览里只有可视区域那一块,下面全是白的。后来一查资料,原来浏览器的打印机制就是这么'懒'——它默认只打印当前视口(viewport)里的内容。换句话说,你没滚动到的地方,它觉得你不重要,直接给你忽略了。
今天咱们聊聊怎么让 window.print 这个祖宗乖乖听话,把整个 HTML 的身体都给你吐出来。
这玩意儿到底是个啥妖魔鬼怪
浏览器打印机制那点不为人知的秘密
浏览器在接收到打印指令时,会干这么几件事:
克隆当前文档 :它会先复制一份当前的 DOM 结构
应用打印样式 :把 @media print 里的 CSS 规则套上去
重新布局计算 :根据纸张尺寸(通常是 A4)重新计算布局
分页处理 :把内容切成一页一页的
生成打印预览 :最后呈现给你看
问题就出在第 3 步。浏览器在重新布局的时候,对于那些 height: auto 或者没有明确高度的容器,它有时候会犯迷糊。特别是当你的内容是通过 JavaScript 动态加载的,或者图片是懒加载的,浏览器算高度的时候可能还没加载完,结果就是算出来的高度比实际小,后面的内容就被'截断'了。
CSS 里的 print 媒体查询,是救星还是坑货?
很多人一遇到打印问题,第一反应就是加 @media print 媒体查询。这思路没错,但用得不好就是给自己挖坑。
最常见的写法是这样的:
@media print {
body {
height : auto;
overflow : visible;
}
.no-print {
display : none;
}
}
但实际上,height: auto 在打印上下文里有时候并不听话。因为浏览器在打印模式下,会创建一个叫'打印上下文'(print context)的东西,在这个上下文里,视口概念被纸张尺寸取代了。如果你的 body 或者某个父容器在屏幕模式下被设置了固定高度或者 overflow: hidden,到了打印模式下这些属性可能还残留着,导致内容被截断。
我实测过几个浏览器的差异:
Chrome :相对听话,只要给 body 加上 height: auto 和 overflow: visible 基本能解决大部分问题
Firefox :有点轴,有时候需要显式地给 html 也加上同样的样式
Safari :最矫情,经常需要手动触发重绘才能拿到正确的高度
Edge(老版) :建议直接放弃治疗,让用户导出 PDF 吧
深挖底层逻辑,把打印机按在地上摩擦
height: auto 失效?布局塌陷的锅谁来背
咱们来深入看看为什么 height: auto 在打印时会失效。这得从 CSS 的包含块(containing block)机制说起。
在正常屏幕显示时,body 的包含块是视口(viewport),所以 height: 100% 就是视口高度。但在打印模式下,包含块变成了页面框(page box),也就是一张纸的大小。这时候如果某个父元素还是按照屏幕逻辑来设置高度,就会出现问题。
<div class ="wrapper" >
<div class ="content" > </div >
</div >
.wrapper {
height : 100vh ;
overflow-y : auto;
}
.content {
height : auto;
}
到了打印模式下,如果你只给 body 加样式,忘了处理 wrapper:
@media print {
body {
height : auto;
overflow : visible;
}
}
这时候 wrapper 还是那个 100vh 的高度,overflow 虽然默认是 visible,但有些浏览器会保留滚动容器的特性,结果 content 的高度被 wrapper 限制住了,超出部分就被截断了。
正确的做法是要把所有可能限制高度的容器都'松绑':
@media print {
html , body {
height : auto !important ;
overflow : visible !important ;
margin : 0 ;
padding : 0 ;
}
.wrapper ,
.container ,
.main ,
#app {
height : auto !important ;
max-height : none !important ;
overflow : visible !important ;
}
.flex-container {
display : block !important ;
}
}
看到那些 !important 了吗?别嫌粗暴,打印的时候就得下猛药。因为你不知道哪个第三方库的 CSS 会突然冒出来给你一刀。
强制分页符的正确打开方式 长内容打印肯定要分页,但分页的时机很重要。你不能让表格在一行中间断开,也不能让标题孤零零地留在页尾。
page-break-before: 在元素前分页
page-break-after: 在元素后分页
page-break-inside: 控制元素内部是否允许分页(最常用的是 avoid)
<div style ="page-break-after: always;" > </div >
插一个空 div 来强制分页。这办法能用,但太丑了,而且会破坏你的 HTML 结构。更优雅的做法是用伪元素:
.page-break {
page-break-after : always;
position : relative;
}
h2 {
page-break-before : auto;
page-break-after : avoid;
orphans : 3 ;
widows : 3 ;
}
@media print {
table {
page-break-inside : auto;
border-collapse : collapse;
}
tr {
page-break-inside : avoid;
page-break-after : auto;
}
thead {
display : table-header-group;
}
tfoot {
display : table-footer-group;
}
}
这里要重点说说 orphans 和 widows 这两个属性,知道的人不多,但超级有用。它们控制的是段落被分页时,留在当前页和带到下一页的最小行数。比如设置 orphans: 3,意思就是如果分页后当前页剩下的行数少于 3 行,那就把整个段落都带到下一页去。这样就不会出现页尾孤零零的一两行文字,看着特别丑。
动态内容高度计算,别让 JS 骗了打印机 现在的前端项目,哪个不是满屏的 JavaScript 动态渲染?React、Vue、Angular,数据都是异步来的。这时候打印就会遇到一个经典问题:你触发 window.print() 的时候,数据还没渲染完呢。
function handlePrint ( ) {
fetchData ().then (data => {
setState (data);
window .print ();
});
}
你以为 setState 之后页面就更新了?Too young too simple。React 的 setState 是异步的,你这时候打印,可能 DOM 还没更新完呢。结果就是打印出来的还是老数据,或者只有半截。
正确的姿势是要等渲染完成。在 React 里可以用 setTimeout 或者 requestAnimationFrame:
function handlePrint ( ) {
fetchData ().then (data => {
setState (data);
setTimeout (() => {
waitForImages ().then (() => {
window .print ();
});
}, 100 );
});
}
function waitForImages ( ) {
const images = document .querySelectorAll ('img' );
const promises = Array .from (images).map (img => {
if (img.complete ) return Promise .resolve ();
return new Promise ((resolve, reject ) => {
img.onload = resolve;
img.onerror = resolve;
});
});
return Promise .all (promises);
}
但这还不够保险。有些图片是从 CDN 懒加载的,有些字体是 webfont 还没下载完,这些都会影响最终的高度计算。最保险的做法是,在打印前先把所有资源都'固化':
async function prepareForPrint ( ) {
const lazyElements = document .querySelectorAll ('[data-lazy]' );
lazyElements.forEach (el => {
el.src = el.dataset .src ;
el.removeAttribute ('data-lazy' );
});
if (document .fonts ) {
await document .fonts .ready ;
}
await waitForImages ();
document .body .style .zoom = '1.001' ;
void document .body .offsetHeight ;
document .body .style .zoom = '1' ;
await new Promise (resolve => setTimeout (resolve, 300 ));
}
async function handlePrint ( ) {
await prepareForPrint ();
window .print ();
}
看到那个 zoom 的骚操作了吗?这是强制浏览器重排(reflow)的一个小技巧。改变 zoom 值会让浏览器重新计算所有元素的布局,确保我们拿到的是最终的高度。
隐藏的 overflow: hidden 和 fixed 定位 有些元素在屏幕模式下看着人畜无害,一到打印模式就变身内容杀手。最常见的就是 overflow: hidden 和 position: fixed。
overflow: hidden 很好理解,超出部分直接截断。但 position: fixed 为什么也会出问题?因为浏览器在打印时,会把 fixed 定位的元素当成'页眉页脚'来处理,有时候会把它们钉在每一页的同一个位置,导致内容重叠或者被遮挡。
更隐蔽的是一些 UI 框架的 modal、drawer、toast 这些组件,它们往往会有这样的样式:
.modal {
position : fixed;
top : 0 ;
left : 0 ;
width : 100vw ;
height : 100vh ;
overflow : auto;
background : rgba (0 , 0 , 0 , 0.5 );
}
打印的时候,这个 modal 如果还开着,它就会覆盖整个页面,你只能打出一个黑框。所以打印前一定要清理这些层:
@media print {
.modal ,
.drawer ,
.toast ,
.loading-mask {
display : none !important ;
}
[style*="position: fixed" ] ,
[style*="position:fixed" ] {
position : static !important ;
}
}
还有那种无限滚动的列表,屏幕上是虚拟滚动只渲染可视区,打印的时候你得把它变成全部渲染。这个后面实战部分会详细讲。
这招好用是好用,但也有翻车的时候
优点当然是爽啊 说实话,当你把这些技巧都用上以后,打印体验是真的爽。一次性把几十页内容都输出,不用让用户手动翻页,PDF 生成也是完整的,老板看了确实会点头。
而且做好打印适配以后,你会发现顺带解决了很多其他问题。比如导出 PDF 的时候,用浏览器的'打印到 PDF'功能,比你用 jsPDF、html2canvas 这些库去折腾要省事多了,出来的效果还更标准。毕竟浏览器自己最懂怎么渲染 HTML。
还有就是响应式的问题。你搞定了打印媒体查询,其实也就理清了页面在不同'视口'下的表现逻辑,这对理解 CSS 布局很有帮助。
缺点也得认,有些坑真的躲不掉 但是!凡事都有但是。这套方案在老旧浏览器面前就是渣渣。IE11 及以下版本,打印机制跟现代浏览器完全是两个物种。你在这边调得好好的分页符,到 IE 那边可能直接无视;你算得精准的高度,IE 可能根本不买账。
还有移动端浏览器的打印,那简直是灾难。iOS Safari 的打印预览经常抽风,有时候内容显示不全,有时候比例失调。Android 的各种浏览器内核更是五花八门,你根本测不过来。
图片加载的问题也很头大。如果你的页面有很多大图,即使做了 waitForImages,也可能因为网络问题导致超时。而且图片加载完到浏览器实际渲染出来,中间还有解码的时间,这个很难精确控制。结果就是打印出来图片位置是个空白框,或者只显示了一半。
还有一个很隐蔽的问题:内存。如果你的页面真的有几万行内容,全部展开渲染,低端设备可能会直接卡死。特别是用 Electron 打包的桌面应用,或者老旧的办公电脑,内存就 4G8G 的,你一下子塞太多 DOM 节点进去,浏览器直接罢工。
所以这套方案更适合后台管理系统、数据报表这种相对可控的场景。如果是面向 C 端用户的公开页面,建议还是提供'导出 PDF'的备用方案,别死磕 window.print。
实战场景大乱斗
电商后台订单详情打印 这是我最常遇到的场景。一个订单可能有几十个商品,每个商品有图片、名称、SKU、价格、数量,下面还有收货信息、物流轨迹、支付记录、操作日志。加起来十几页是常态。
这种页面的特点是:内容高度不确定,图片多,表格多。
<!DOCTYPE html >
<html >
<head >
<style >
body {
font-family : "Microsoft YaHei" , sans-serif;
line-height : 1.6 ;
color : #333 ;
}
.print-container {
max-width : 210mm ;
margin : 0 auto;
padding : 10mm ;
}
@media print {
@page {
size: A4;
margin : 10mm ;
}
body {
background : white;
}
.no-print {
display : none !important ;
}
.section {
page-break-inside : avoid;
margin-bottom : 20px ;
}
.product-table {
width : 100% ;
border-collapse : collapse;
font-size : 12px ;
}
.product-table th {
background : #f5f5f5 !important ;
-webkit-print-color-adjust : exact;
print-color-adjust : exact;
}
.product-table th ,
.product-table td {
border : 1px solid #ddd ;
padding : 8px ;
text-align : left;
}
.product-img {
max-width : 60px ;
max-height : 60px ;
object-fit : cover;
}
.page-break {
page-break-after : always;
}
}
@media screen {
.print-container {
box-shadow : 0 0 10px rgba (0 , 0 , 0 , 0.1 );
background : white;
margin : 20px auto;
min-height : 297mm ;
}
.print-btn {
position : fixed;
top : 20px ;
right : 20px ;
padding : 10px 20px ;
background : #1890ff ;
color : white;
border : none;
border-radius : 4px ;
cursor : pointer;
z-index : 9999 ;
}
}
</style >
</head >
<body >
<button class ="print-btn no-print" onclick ="handlePrint()" > 打印订单</button >
<div class ="print-container" id ="printArea" >
<div class ="section order-header" >
<h1 > 订单详情 #202402240001</h1 >
<p > 下单时间:2024-02-24 14:30:00 | 订单状态:已发货</p >
</div >
<div class ="section product-section" >
<h2 > 商品信息</h2 >
<table class ="product-table" >
<thead >
<tr >
<th > 商品图片</th >
<th > 商品名称</th >
<th > SKU</th >
<th > 单价</th >
<th > 数量</th >
<th > 小计</th >
</tr >
</thead >
<tbody id ="productList" >
</tbody >
<tfoot >
<tr >
<td colspan ="5" style ="text-align: right;" > <strong > 合计:</strong > </td >
<td > <strong > ¥<span id ="totalAmount" > 0.00</span > </strong > </td >
</tr >
</tfoot >
</table >
</div >
<div class ="section address-section" >
<h2 > 收货信息</h2 >
<p > 收货人:张三</p >
<p > 手机号:138****8888</p >
<p > 地址:北京市朝阳区某某街道某某小区 12 号楼 3 单元 501 室</p >
</div >
<div class ="section logistics-section" >
<h2 > 物流轨迹</h2 >
<table class ="product-table" >
<thead >
<tr >
<th > 时间</th >
<th > 状态</th >
<th > 详情</th >
</tr >
</thead >
<tbody id ="logisticsList" >
</tbody >
</table >
</div >
</div >
<script >
const products = Array .from ({ length : 50 }, (_, i ) => ({
id : i + 1 ,
name : `测试商品${i + 1 } 号 超长名称测试打印效果是否会自动换行处理` ,
sku : `SKU${String (i + 1 ).padStart(6 , '0' )} ` ,
price : (Math .random () * 1000 ).toFixed (2 ),
quantity : Math .floor (Math .random () * 5 ) + 1 ,
image : `https://via.placeholder.com/60x60?text=${i + 1 } `
}));
const logistics = [
{ time : '2024-02-24 16:30:00' , status : '已签收' , detail : '您的订单已由本人签收,感谢使用' },
{ time : '2024-02-24 09:15:00' , status : '派送中' , detail : '快递员正在为您派件,电话:13800138000' },
{ time : '2024-02-24 07:30:00' , status : '运输中' , detail : },
{ : , : , : },
{ : , : , : }
];
( ) {
tbody = . ( );
total = ;
tbody. = products. ( {
subtotal = (p. ) * p. ;
total += subtotal;
;
}). ( );
. ( ). = total. ( );
}
( ) {
tbody = . ( );
tbody. = logistics. ( ). ( );
}
( ) {
( {
images = . ( );
loaded = ;
total = images. ;
(total === ) {
();
;
}
images. ( {
(img. ) {
loaded++;
(loaded === total) ();
} {
img. = img. = {
loaded++;
(loaded === total) ();
};
}
});
(resolve, );
});
}
( ) {
btn = . ( );
btn. = ;
btn. = ;
{
();
();
( (r, ));
();
. . . = ;
. . ;
. . . = ;
( (r, ));
. ();
} (e) {
. ( , e);
( );
} {
btn. = ;
btn. = ;
}
}
();
();
</script >
</body >
</html >
@page 规则 :控制纸张大小和边距,这是 CSS3 的规范,现代浏览器都支持
-webkit-print-color-adjust: exact :默认情况下浏览器打印会忽略背景色以节省墨水,这个属性强制保留背景色,对于表格表头很重要
crossorigin="anonymous" :图片加这个属性是为了避免跨域问题导致 canvas 处理失败(虽然这里没用到 canvas,但养成习惯好)
tfoot 的用法 :把合计行放在 tfoot 里,这样如果表格跨页,每页底部都会显示合计,非常实用
财务报表长表格打印 财务报表的特点是列多、数据密、需要精确对齐。而且通常要求每页都显示表头,底部要有汇总。
<!DOCTYPE html >
<html >
<head >
<style >
@media print {
@page {
size: A4 landscape;
margin : 15mm ;
}
body {
font-size : 10pt ;
line-height : 1.4 ;
}
.financial-table {
width : 100% ;
border-collapse : collapse;
table-layout : fixed;
}
thead {
display : table-header-group;
}
.financial-table th {
background : #4472C4 !important ;
color : white !important ;
-webkit-print-color-adjust : exact;
print-color-adjust : exact;
padding : 8px ;
border : 1px solid #2F5597 ;
font-weight : bold;
}
.financial-table td {
padding : 6px 8px ;
border : 1px solid #B4C7E7 ;
word-wrap : break-word;
}
.financial-table tbody tr :nth-child (even) {
background : #D9E2F3 !important ;
-webkit-print-color-adjust : exact;
print-color-adjust : exact;
}
tfoot {
display : table-footer-group;
font-weight : bold;
background : #FFF2CC !important ;
-webkit-print-color-adjust : exact;
print-color-adjust : exact;
}
.num {
text-align : right;
font-family : "Courier New" , monospace;
}
tr {
page-break-inside : avoid;
}
.group-header {
page-break-before : always;
background : #E7E6E6 !important ;
-webkit-print-color-adjust : exact;
print-color-adjust : exact;
font-weight : bold;
}
.group-header :first-of-type {
page-break-before : auto;
}
}
@media screen {
body {
padding : 20px ;
background : #f0f2f5 ;
}
.container {
max-width : 1200px ;
margin : 0 auto;
background : white;
padding : 20px ;
box-shadow : 0 2px 8px rgba (0 , 0 , 0 , 0.1 );
}
}
</style >
</head >
<body >
<div class ="container" >
<h1 > 2024 年度财务报表</h1 >
<p class ="no-print" > 生成时间:2024-02-24</p >
<table class ="financial-table" >
<thead >
<tr >
<th style ="width: 8%" > 科目编码</th >
<th style ="width: 20%" > 科目名称</th >
<th style ="width: 12%" > 期初余额</th >
<th style ="width: 12%" > 本期借方</th >
<th style ="width: 12%" > 本期贷方</th >
<th style ="width: 12%" > 期末余额</th >
<th style ="width: 12%" > 备注</th >
</tr >
</thead >
<tbody id ="dataBody" >
</tbody >
<tfoot >
<tr >
<td colspan ="2" style ="text-align: center;" > 合计</td >
<td class ="num" id ="sumBegin" > 0.00</td >
<td class ="num" id ="sumDebit" > 0.00</td >
<td class ="num" id ="sumCredit" > 0.00</td >
<td class ="num" id ="sumEnd" > 0.00</td >
<td > -</td >
</tr >
</tfoot >
</table >
</div >
<script >
function generateData ( ) {
const body = document .getElementById ('dataBody' );
let html = '' ;
let sumBegin = 0 , sumDebit = 0 , sumCredit = 0 , sumEnd = 0 ;
const groups = ['资产类' , '负债类' , '权益类' , '损益类' ];
const groupCodes = ['1' , '2' , '3' , '4' ];
groups.forEach ((group, gIndex ) => {
html += `
<tr>
<td colspan="7">${groupCodes[gIndex]} 000 ${group} </td>
</tr>
` ;
for (let i = 1 ; i <= 30 ; i++) {
const code = `${groupCodes[gIndex]} ${String (i).padStart(3 , '0' )} ` ;
const begin = (Math .random () * 1000000 ).toFixed (2 );
const debit = (Math .random () * 500000 ). ( );
credit = ( . () * ). ( );
end = ( (begin) + (debit) - (credit)). ( );
sumBegin += (begin);
sumDebit += (debit);
sumCredit += (credit);
sumEnd += (end);
html += ;
}
});
body. = html;
. ( ). = sumBegin. ( , { : });
. ( ). = sumDebit. ( , { : });
. ( ). = sumCredit. ( , { : });
. ( ). = sumEnd. ( , { : });
}
();
</script >
</body >
</html >
table-layout: fixed :配合列宽百分比,确保表格不会乱动
display: table-header-group 和 table-footer-group :这是 CSS 2.1 就有的规范,让 thead 和 tfoot 在打印时每页都重复
page-break-before: always :大分组之间强制分页,让报表结构更清晰
等宽字体对齐数字 :用 Courier New 这类等宽字体显示金额,小数点能对齐,看着舒服
简历生成器实战 简历打印的核心诉求是:不管用户用什么屏幕看的,打印出来必须是完美的 A4 比例,不能多一寸不能少一寸。
<!DOCTYPE html >
<html >
<head >
<style >
* {
margin : 0 ;
padding : 0 ;
box-sizing : border-box;
}
.resume-page {
width : 210mm ;
min-height : 297mm ;
max-height : 297mm ;
padding : 15mm ;
margin : 0 auto;
background : white;
position : relative;
overflow : hidden;
}
.resume-page + .resume-page {
page-break-before : always;
margin-top : 0 ;
}
@media print {
@page {
size: A4;
margin : 0 ;
}
body {
background : white;
-webkit-print-color-adjust : exact;
print-color-adjust : exact;
}
.resume-page {
box-shadow : none;
margin : 0 ;
width : 100% ;
page-break-after : always;
}
.resume-page :last-of-type {
page-break-after : auto;
}
}
@media screen {
body {
background : #e8e8e8 ;
padding : 20px ;
}
.resume-page {
box-shadow : 0 0 10px rgba (0 , 0 , 0 , 0.2 );
margin-bottom : 20px ;
}
}
.header {
text-align : center;
border-bottom : 2px solid #333 ;
padding-bottom : 15px ;
margin-bottom : 20px ;
}
.name {
font-size : 28px ;
font-weight : bold;
margin-bottom : 8px ;
}
.contact {
color : #666 ;
font-size : 14px ;
}
.section {
margin-bottom : 20px ;
}
.section-title {
font-size : 16px ;
font-weight : bold;
border-left : 4px solid #1890ff ;
padding-left : 8px ;
margin-bottom : 10px ;
color : #1890ff ;
}
.item {
margin-bottom : 12px ;
page-break-inside : avoid;
}
.item-header {
display : flex;
justify-content : space-between;
font-weight : bold;
margin-bottom : 4px ;
}
.item-content {
color : #555 ;
font-size : 14px ;
line-height : 1.6 ;
}
.skill-bar {
display : flex;
align-items : center;
margin-bottom : 8px ;
}
.skill-name {
width : 100px ;
font-size : 14px ;
}
.skill-progress {
flex : 1 ;
height : 8px ;
background : #e8e8e8 ;
border-radius : 4px ;
overflow : hidden;
}
.skill-fill {
height : 100% ;
background : #1890ff ;
border-radius : 4px ;
}
</style >
</head >
<body >
<div class ="resume-page" >
<div class ="header" >
<div class ="name" > 张三</div >
<div class ="contact" > 电话:138-0000-0000 | 邮箱:[email protected] | 微信:zhangsan_dev </div >
</div >
<div class ="section" >
<div class ="section-title" > 教育背景</div >
<div class ="item" >
<div class ="item-header" >
<span > 某某大学 - 计算机科学与技术(本科)</span >
<span > 2015.09 - 2019.06</span >
</div >
<div class ="item-content" > 主修课程:数据结构、算法设计、操作系统、计算机网络、数据库原理<br > 在校荣誉:优秀毕业生、国家奖学金、ACM 程序设计竞赛省级银奖 </div >
</div >
</div >
<div class ="section" >
<div class ="section-title" > 工作经历</div >
<div class ="item" >
<div class ="item-header" >
<span > 某某科技有限公司 - 高级前端工程师</span >
<span > 2021.07 - 至今</span >
</div >
<div class ="item-content" > 负责公司核心电商平台前端架构设计与开发,主导技术选型从 Vue2 迁移至 Vue3<br > 搭建前端监控体系,错误率降低 80%,首屏加载时间优化至 1.2s 以内<br > 带领 5 人前端团队,制定代码规范与 Code Review 机制,提升团队整体代码质量 </div >
</div >
<div class ="item" >
<div class ="item-header" >
<span > 某某互联网公司 - 前端开发工程师</span >
<span > 2019.07 - 2021.06</span >
</div >
<div class ="item-content" > 参与公司 ToB 管理后台开发,负责权限管理、数据可视化等模块<br > 封装通用组件库 15+ 个,提升团队开发效率 30%<br > 优化老旧项目构建流程,打包体积减少 40%,构建速度提升 2 倍 </div >
</div >
</div >
<div class ="section" >
<div class ="section-title" > 专业技能</div >
<div class ="skill-bar" >
<span class ="skill-name" > Vue/React</span >
<div class ="skill-progress" > <div class ="skill-fill" style ="width: 90%" > </div > </div >
</div >
<div class ="skill-bar" >
<span class ="skill-name" > TypeScript</span >
<div class ="skill-progress" > <div class ="skill-fill" style ="width: 85%" > </div > </div >
</div >
<div class ="skill-bar" >
<span class ="skill-name" > Node.js</span >
<div class ="skill-progress" > <div class ="skill-fill" style ="width: 75%" > </div > </div >
</div >
<div class ="skill-bar" >
<span class ="skill-name" > 工程化/性能优化</span >
<div class ="skill-progress" > <div class ="skill-fill" style ="width: 80%" > </div > </div >
</div >
</div >
</div >
<div class ="resume-page" >
<div class ="section" >
<div class ="section-title" > 项目经历</div >
<div class ="item" >
<div class ="item-header" >
<span > 企业级低代码平台</span >
<span > 2023.01 - 2023.12</span >
</div >
<div class ="item-content" > <strong > 项目描述:</strong > 面向企业内部使用的低代码应用搭建平台,支持表单、流程、报表可视化配置<br > <strong > 技术栈:</strong > Vue3 + TypeScript + Vite + Pinia + Element Plus<br > <strong > 主要职责:</strong > <br > 1. 设计并实现可视化编辑器核心引擎,支持组件拖拽、属性配置、实时预览<br > 2. 开发 DSL 解析器,将可视化配置转换为可执行的 Vue 代码<br > 3. 实现自定义组件注册机制,支持第三方组件动态加载<br > 4. 优化大数据量场景下的渲染性能,采用虚拟滚动 + 懒加载策略,支持万级数据流畅编辑 </div >
</div >
<div class ="item" >
<div class ="item-header" >
<span > 跨境电商 ERP 系统</span >
<span > 2022.03 - 2022.12</span >
</div >
<div class ="item-content" > <strong > 项目描述:</strong > 整合多平台(亚马逊、eBay、Shopify)订单、库存、物流管理的 ERP 系统<br > <strong > 技术栈:</strong > React + Redux + Ant Design + ECharts<br > <strong > 主要职责:</strong > <br > 1. 负责订单管理、库存同步、物流追踪等核心模块开发<br > 2. 设计并实现通用表格组件,支持动态列配置、批量操作、数据导出,复用至 20+ 页面<br > 3. 封装 WebSocket 实时通知组件,实现订单状态变更实时推送<br > 4. 开发数据看板,使用 ECharts 实现多维度销售数据可视化,支持钻取分析 </div >
</div >
<div class ="item" >
<div class ="item-header" >
<span > 微信小程序生态建设</span >
<span > 2021.07 - 2022.02</span >
</div >
<div class ="item-content" > <strong > 项目描述:</strong > 公司品牌矩阵小程序开发,包含商城、会员、营销等多个小程序<br > <strong > 技术栈:</strong > Taro3 + Vue3 + 微信原生 API<br > <strong > 主要职责:</strong > <br > 1. 搭建 Taro 跨端开发框架,实现一套代码编译至微信小程序和 H5<br > 2. 开发通用支付、分享、登录 SDK,统一处理各平台差异<br > 3. 优化小程序性能,包体积从 3MB 压缩至 1.2MB,启动时间减少 50%<br > 4. 设计灰度发布机制,支持按用户、地域、版本号维度渐进式发布 </div >
</div >
</div >
<div class ="section" >
<div class ="section-title" > 自我评价</div >
<div class ="item-content" > 5 年前端开发经验,精通 Vue/React 生态,具备完整的大型项目架构经验。对前端工程化、性能优化有深入实践, 擅长组件化设计和开发效率提升。具备良好的团队协作能力和技术领导力,能够带领团队攻克技术难点。 持续关注前端技术发展趋势,热爱开源社区,GitHub 个人项目累计 Star 2000+。 </div >
</div >
</div >
</body >
</html >
严格尺寸控制 :210mm x 297mm,不多不少
page-break-before: always :多页简历时,每页都是独立的 A4
page-break-inside: avoid :经历块、项目块不能跨页断开,否则阅读体验很差
打印边距处理 :@page 的 margin 设为 0,在容器里用 padding 控制边距,这样不同浏览器的兼容性更好
电子发票和物流面单 这类场景对尺寸精度要求极高,差 1 毫米可能就贴不上或者塞不进快递袋。
<!DOCTYPE html >
<html >
<head >
<style >
.invoice {
width : 240mm ;
height : 140mm ;
padding : 10mm ;
margin : 0 auto;
background : white;
border : 1px solid #ddd ;
position : relative;
font-family : "SimSun" , serif;
overflow : hidden;
}
.waybill {
width : 100mm ;
height : 150mm ;
padding : 5mm ;
margin : 0 auto;
background : white;
border : 1px solid #000 ;
position : relative;
font-family : "Microsoft YaHei" , sans-serif;
font-size : 10pt ;
overflow : hidden;
}
@media print {
@page {
size: 240mm 140mm ;
margin : 0 ;
}
body {
margin : 0 ;
padding : 0 ;
}
.invoice ,
.waybill {
border : none;
width : 100% ;
height : 100% ;
page-break-after : always;
}
}
.invoice-header {
text-align : center;
border-bottom : 2px solid #f00 ;
padding-bottom : 5mm ;
margin-bottom : 5mm ;
}
.invoice-title {
font-size : 24pt ;
color : #f00 ;
font-weight : bold;
letter-spacing : 10px ;
}
.invoice-info {
display : flex;
justify-content : space-between;
font-size : 10pt ;
margin-top : 5mm ;
}
.invoice-table {
width : 100% ;
border-collapse : collapse;
margin : 5mm 0 ;
font-size : 9pt ;
}
.invoice-table th ,
.invoice-table td {
border : 1px solid #999 ;
padding : 2mm ;
text-align : center;
}
.invoice-amount {
text-align : right;
font-size : 12pt ;
font-weight : bold;
margin-top : 5mm ;
}
.invoice-qrcode {
position : absolute;
right : 10mm ;
bottom : 10mm ;
width : 25mm ;
height : 25mm ;
border : 1px solid #000 ;
text-align : center;
line-height : 25mm ;
font-size : 8pt ;
}
.waybill-barcode {
height : 20mm ;
text-align : center;
border-bottom : 2px solid #000 ;
margin-bottom : 3mm ;
display : flex;
align-items : center;
justify-content : center;
font-family : "Libre Barcode 39" , monospace;
font-size : 36pt ;
}
.waybill-route {
display : flex;
justify-content : space-between;
border-bottom : 1px dashed #000 ;
padding-bottom : 2mm ;
margin-bottom : 2mm ;
}
.waybill-info {
line-height : 1.8 ;
}
.waybill-label {
display : inline-block;
width : 15mm ;
text-align : right;
margin-right : 2mm ;
}
</style >
</head >
<body >
<div class ="invoice" >
<div class ="invoice-header" >
<div class ="invoice-title" > 增值税电子普通发票</div >
<div class ="invoice-info" >
<span > 发票代码:011001900211</span >
<span > 发票号码:12345678</span >
<span > 开票日期:2024 年 02 月 24 日</span >
</div >
</div >
<table class ="invoice-table" >
<tr >
<th style ="width: 30%" > 购买方信息</th >
<td style ="width: 70%; text-align: left;" > 名称:某某科技有限公司<br > 纳税人识别号:91110108MA00XXXXXX<br > 地址、电话:北京市海淀区某某路 XX 号 010-12345678<br > 开户行及账号:中国工商银行北京分行 0200XXXXXX </td >
</tr >
<tr >
<th colspan ="2" >
<table style ="width: 100%; border: none;" >
<tr style ="border: none;" >
<td style ="border: none; width: 40%" > 货物或应税劳务、服务名称</td >
<td style ="border: none; width: 15%" > 规格型号</td >
<td style ="border: none; width: 10%" > 单位</td >
<td style ="border: none; width: 10%" > 数量</td >
<td style ="border: none; width: 10%" > 单价</td >
<td style ="border: none; width: 15%" > 金额</td >
</tr >
<tr style ="border: none;" >
<td style ="border: none;" > *软件*技术服务费</td >
<td style ="border: none;" > -</td >
<td style ="border: none;" > 次</td >
<td style ="border: none;" > 1</td >
<td style ="border: none;" > 10000.00</td >
<td style ="border: none;" > 10000.00</td >
</tr >
</table >
</th >
</tr >
<tr >
<th > 销售方信息</th >
<td style ="text-align: left;" > 名称:北京某某软件服务有限公司<br > 纳税人识别号:91110108MA00YYYYYY<br > 地址、电话:北京市朝阳区某某路 XX 号 010-87654321<br > 开户行及账号:中国建设银行北京分行 1100YYYYYY </td >
</tr >
</table >
<div class ="invoice-amount" > 价税合计(大写):壹万零壹佰元整<br > <span style ="color: #f00;" > (小写):¥10100.00</span > </div >
<div class ="invoice-qrcode" > 二维码区域<br > (发票信息) </div >
</div >
<div class ="waybill" >
<div class ="waybill-barcode" > *SF1234567890* </div >
<div class ="waybill-route" >
<div style ="font-size: 14pt; font-weight: bold;" > 北京→上海</div >
<div > 标快<br > 陆运</div >
</div >
<div class ="waybill-info" >
<div >
<span class ="waybill-label" > 收:</span > <strong > 张三</strong > 138****8888<br >
<span style ="margin-left: 17mm;" > 上海市浦东新区某某街道某某小区 12 号楼</span >
</div >
<div style ="margin-top: 2mm;" >
<span class ="waybill-label" > 寄:</span > 李四 139****9999<br >
<span style ="margin-left: 17mm;" > 北京市朝阳区某某大厦 8 层</span >
</div >
</div >
<div style ="border-top: 1px solid #000; margin-top: 3mm; padding-top: 2mm;" >
<strong > 物品信息:</strong > 电子产品(已验视)<br >
<strong > 重量:</strong > 2.5kg <strong > 保价:</strong > ¥5000
</div >
<div style ="position: absolute; bottom: 5mm; left: 5mm; right: 5mm; font-size: 8pt; color: #666;" > 订单号:2024022414300001 打印时间:2024-02-24 14:30:00 </div >
</div >
</body >
</html >
毫米级尺寸控制 :用 mm 单位,不要用 px 或者 pt,因为不同设备 DPI 不同,毫米是物理尺寸,最准确
@page size 自定义 :可以指定任意尺寸,不只是 A4、A5 这种标准尺寸
overflow: hidden :严格防止内容溢出,因为发票和面单通常有固定的打印区域,超出就废了
字体选择 :发票用宋体显得正式,面单用黑体或微软雅黑保证清晰可读
遇到报错别慌,老司机的排查套路
打印出来是空白? 先检查是不是有全局的 display: none 或者 visibility: hidden。特别是用了某些 UI 框架,打印的时候 modal 关闭了,但内容其实在 modal 里,跟着一起被隐藏了。
function checkVisibility (element ) {
let current = element;
while (current) {
const style = window .getComputedStyle (current);
if (style.display === 'none' ) {
console .warn ('隐藏元素:' , current);
return false ;
}
if (style.visibility === 'hidden' ) {
console .warn ('不可见元素:' , current);
return false ;
}
current = current.parentElement ;
}
return true ;
}
还有就是 iframe 打印的问题。如果你把内容放在 iframe 里打印,要确保 iframe 已经正确加载,而且跨域策略不会阻止访问。
内容被截断一半? 这通常是父容器的 max-height 或者 overflow 属性在作祟。打印模式下这些属性有时候不会被自动重置。
@media print {
* {
max-height : none !important ;
overflow : visible !important ;
}
}
虽然粗暴,但管用。你可以先加上这个看看问题是否解决,然后再慢慢缩小范围找到具体是哪个元素在搞鬼。
样式错乱像裸奔? 检查 print 媒体查询是否被其他 CSS 覆盖了。特别是用了 CSS 框架(Bootstrap、Ant Design 等),它们可能也有 print 相关的样式,跟你写的冲突了。
用浏览器的打印预览功能(Ctrl+P 或者 Cmd+P),在预览界面检查元素,看看实际应用的样式是什么。Chrome 的打印预览也支持 DevTools,按 F12 能调出来。
控制台没报错但就是打不全? 这种情况多半是异步内容的问题。图片没加载完、字体没渲染完、React/Vue 还没更新完 DOM,你就触发打印了。
async function waitForEverything ( ) {
await new Promise (resolve => setTimeout (resolve, 0 ));
if (document .fonts ) {
await document .fonts .ready ;
}
const images = Array .from (document .querySelectorAll ('img' ));
const bgImages = Array .from (document .querySelectorAll ('*' )).map (el => getComputedStyle (el).backgroundImage ).filter (url => url !== 'none' ).map (url => url.replace (/url\(["']?([^"']*)["']?\)/ , '$1' ));
const allImages = [...images, ...bgImages.map (src => {
const img = new Image ();
img.src = src;
return img;
})];
await Promise .all (allImages.map (img => img.complete ? Promise .resolve () : new Promise ((resolve ) => {
img.onload = img.onerror = resolve;
setTimeout (resolve, 3000 );
})));
const iframes = Array .from (document .querySelectorAll ('iframe' ));
await Promise .all (iframes.map (iframe => new Promise ((resolve ) => {
if (iframe.contentDocument ?.readyState === 'complete' ) {
resolve ();
} else {
iframe.onload = resolve;
setTimeout (resolve, 5000 );
}
})));
await new Promise (resolve => setTimeout (resolve, 500 ));
}
async function handlePrint ( ) {
await waitForEverything ();
window .print ();
}
图片加载慢导致高度计算不准 如果图片很多很大,即使做了等待,也可能因为网络问题导致超时。这时候可以考虑预先把图片转成 base64:
async function preloadImagesAsBase64 ( ) {
const images = document .querySelectorAll ('img[data-src]' );
for (let img of images) {
try {
const response = await fetch (img.dataset .src );
const blob = await response.blob ();
const reader = new FileReader ();
await new Promise ((resolve, reject ) => {
reader.onloadend = () => {
img.src = reader.result ;
resolve ();
};
reader.onerror = reject;
reader.readAsDataURL (blob);
});
} catch (e) {
console .warn ('图片加载失败:' , img.dataset .src );
img.src = img.dataset .src ;
}
}
}
这样图片就变成了内联的 data URL,不需要网络请求,加载速度极快。缺点就是会增加 HTML 体积,适合打印内容不多的场景。
几个私藏的开发小技巧
虚拟滚动长列表的打印 hack 如果你的列表用了虚拟滚动(react-window、vue-virtual-scroller 等),打印的时候只有可视区的几条数据。这时候你需要在打印前'展开'所有数据。
function handlePrint ( ) {
const listRef = useRef ();
listRef.current .scrollToItem (999999 , 'end' );
setTimeout (() => {
setIsVirtual (false );
setTimeout (() => {
window .print ();
setIsVirtual (true );
}, 100 );
}, 500 );
}
更通用的做法,如果你控制不了组件内部逻辑,可以直接操作 DOM:
function expandVirtualList ( ) {
const container = document .querySelector ('.virtual-list-container' );
if (!container) return ;
const originalHeight = container.style .height ;
const originalOverflow = container.style .overflow ;
container.style .height = 'auto' ;
container.style .overflow = 'visible' ;
container.style .maxHeight = 'none' ;
return () => {
container.style .height = originalHeight;
container.style .overflow = originalOverflow;
};
}
Promise 包装器等资源加载 前面其实已经写过类似的,这里再给个更健壮的版本,支持超时和错误处理:
function createResourceWaiter (timeout = 10000 ) {
return new Promise ((resolve ) => {
const startTime = Date .now ();
const check = ( ) => {
const images = Array .from (document .images );
const incompleteImages = images.filter (img => !img.complete );
const fontsReady = document .fonts ? document .fonts .status === 'loaded' : true ;
if (incompleteImages.length === 0 && fontsReady) {
resolve ({ success : true , time : Date .now () - startTime });
return ;
}
if (Date .now () - startTime > timeout) {
console .warn ('资源等待超时,未完成:' , { images : incompleteImages.length , fonts : !fontsReady });
resolve ({ success : false , reason : 'timeout' });
return ;
}
requestAnimationFrame (check);
};
check ();
});
}
async function handlePrint ( ) {
console .log ('等待资源加载...' );
const result = await createResourceWaiter (5000 );
console .log ('资源状态:' , result);
window .print ();
}
打印预览模拟 A4 效果 <div class ="print-preview" >
<div class ="a4-page" >
</div >
</div >
<style >
.print-preview {
background : #e8e8e8 ;
padding : 20px ;
min-height : 100vh ;
}
.a4-page {
width : 210mm ;
min-height : 297mm ;
background : white;
margin : 0 auto;
box-shadow : 0 0 10px rgba (0 , 0 , 0 , 0.2 );
transform-origin : top center;
}
@media screen and (max-width : 230mm ) {
.print-preview {
padding : 10px ;
overflow-x : auto;
}
.a4-page {
transform : scale (calc (100vw / 230mm ));
margin-bottom : calc (-297mm * (1 - 100vw / 230mm ));
}
}
</style >
这样在小屏幕上会自动缩放,保证能看到完整的 A4 页面。
打印按钮防抖 用户手贱连点打印按钮,可能会把浏览器搞崩,特别是内容多的时候:
function debounce (fn, delay ) {
let timer = null ;
return function (...args ) {
if (timer) {
console .log ('别急,正在准备...' );
return ;
}
fn.apply (this , args);
timer = setTimeout (() => {
timer = null ;
}, delay);
};
}
const handlePrint = debounce (async () => {
await prepareForPrint ();
window .print ();
}, 3000 );
async function handlePrint ( ) {
const btn = document .getElementById ('printBtn' );
if (btn.disabled ) return ;
btn.disabled = true ;
btn.textContent = '准备中...' ;
try {
await prepareForPrint ();
window .print ();
} catch (e) {
alert ('打印准备失败:' + e.message );
} finally {
btn.disabled = false ;
btn.textContent = '打印' ;
}
}
最后啰嗦两句,别嫌我烦 看到这里,你应该对 window.print 这个'祖宗'有了全新的认识。它不是什么高深技术,但坑是真的多。下次再遇到打印问题,别只会重启浏览器或者让用户'试试别的浏览器',试试上面这些野路子。
高度问题 :height: auto + overflow: visible,给所有可能限制高度的容器都加上
异步问题 :数据渲染完、图片加载完、字体准备好,再触发打印
分页问题 :page-break-inside: avoid 防止截断,伪元素比分页 div 优雅
样式问题 :!important 在打印媒体查询里是你的好朋友
尺寸问题 :用 mm 做单位,不要用 px
代码写得再漂亮,打印出来是一坨屎也没人夸你。前端工程师的价值就体现在这些细节里。一个能把打印功能做得丝般顺滑的前端,绝对是团队里的宝藏。
赶紧去试试吧。要是还搞不定,回来继续吐槽,我随时在线接招。毕竟咱们前端佬就是在填坑中成长的,对吧?
'快件已到达北京朝阳营业点'
time
'2024-02-23 22:00:00'
status
'运输中'
detail
'快件已发往北京转运中心'
time
'2024-02-23 18:00:00'
status
'已发货'
detail
'商家已发货,快递公司:顺丰速运,运单号:SF1234567890'
function
renderProducts
const
document
getElementById
'productList'
let
0
innerHTML
map
p =>
const
parseFloat
price
quantity
return
`
<tr>
<td><img src="${p.image} " alt="${p.name} " crossorigin="anonymous"></td>
<td>${p.name} </td>
<td>${p.sku} </td>
<td>¥${p.price} </td>
<td>${p.quantity} </td>
<td>¥${subtotal.toFixed(2 )} </td>
</tr>
`
join
''
document
getElementById
'totalAmount'
textContent
toFixed
2
function
renderLogistics
const
document
getElementById
'logisticsList'
innerHTML
map
l =>
`
<tr>
<td>${l.time} </td>
<td>${l.status} </td>
<td>${l.detail} </td>
</tr>
`
join
''
function
waitForResources
return
new
Promise
(resolve ) =>
const
document
querySelectorAll
'img'
let
0
const
length
if
0
resolve
return
forEach
img =>
if
complete
if
resolve
else
onload
onerror
() =>
if
resolve
setTimeout
5000
async
function
handlePrint
const
document
querySelector
'.print-btn'
textContent
'准备中...'
disabled
true
try
renderProducts
renderLogistics
await
new
Promise
r =>
setTimeout
100
await
waitForResources
document
body
style
zoom
'1.001'
void
document
body
offsetHeight
document
body
style
zoom
'1'
await
new
Promise
r =>
setTimeout
300
window
print
catch
console
error
'打印准备失败:'
alert
'打印准备失败,请重试'
finally
textContent
'打印订单'
disabled
false
renderProducts
renderLogistics
toFixed
2
const
Math
random
500000
toFixed
2
const
parseFloat
parseFloat
parseFloat
toFixed
2
parseFloat
parseFloat
parseFloat
parseFloat
`
<tr>
<td>${code} </td>
<td>${group} 科目-${i} </td>
<td>${parseFloat (begin).toLocaleString('zh-CN' , { minimumFractionDigits: 2 })} </td>
<td>${parseFloat (debit).toLocaleString('zh-CN' , { minimumFractionDigits: 2 })} </td>
<td>${parseFloat (credit).toLocaleString('zh-CN' , { minimumFractionDigits: 2 })} </td>
<td>${parseFloat (end).toLocaleString('zh-CN' , { minimumFractionDigits: 2 })} </td>
<td>${i % 5 === 0 ? '需关注' : '' } </td>
</tr>
`
innerHTML
document
getElementById
'sumBegin'
textContent
toLocaleString
'zh-CN'
minimumFractionDigits
2
document
getElementById
'sumDebit'
textContent
toLocaleString
'zh-CN'
minimumFractionDigits
2
document
getElementById
'sumCredit'
textContent
toLocaleString
'zh-CN'
minimumFractionDigits
2
document
getElementById
'sumEnd'
textContent
toLocaleString
'zh-CN'
minimumFractionDigits
2
generateData
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online