前端老哥必看:window.print只打半截?一招搞定HTML实际高度打印不踩坑

前端老哥必看:window.print只打半截?一招搞定HTML实际高度打印不踩坑
在这里插入图片描述


前端老哥必看:window.print只打半截?一招搞定HTML实际高度打印不踩坑

前端老哥必看:window.print只打半截?一招搞定HTML实际高度打印不踩坑

别整那些虚的,咱们直接开唠

兄弟,你是不是也遇到过这种鬼故事?页面上明明有十几屏的内容,一点打印按钮,出来的PDF就只有当前屏幕那一截,后面的东西全没了,跟被狗啃了似的。

我跟你讲,这事儿我熟得很。去年做电商后台,产品经理甩过来一个需求:“做个订单打印功能,要能把订单详情、商品列表、物流信息一次性打出来”。我当时心想,这不就window.print()一行代码的事儿吗,分分钟搞定。结果上线第二天,产品经理举着一张只打了半截的A4纸杀到我工位,那眼神跟要吃了我似的。

“后面的商品呢?我的二十几个商品怎么只显示了前三个?”

我当场尬住。打开浏览器一看,果然,打印预览里只有可视区域那一块,下面全是白的。当时我就纳闷了,这浏览器是不是脑子有坑?我HTML明明那么长,你凭什么只给我打一半?

后来一查资料,差点没把我气死。原来浏览器的打印机制就是这么"懒"——它默认只打印当前视口(viewport)里的内容。换句话说,你没滚动到的地方,它觉得你不重要,直接给你忽略了。这逻辑简直感人,就像你去饭店点菜,服务员只给你上你眼前能看到的那几盘,后面的菜因为你还没看到所以就不做了。

今天咱们不聊那些虚头八脑的大道理,就聊聊怎么让window.print这个祖宗乖乖听话,把整个HTML的身体都给你吐出来。这篇文章我会把我踩过的坑、试过的错、以及最后总结出来的野路子全盘托出。保证你看完以后,再遇到打印问题能跟产品经理拍着胸脯说:“这需求我接了,稳得很。”

这玩意儿到底是个啥妖魔鬼怪

浏览器打印机制那点不为人知的秘密

要搞定打印问题,咱们得先搞清楚浏览器到底是怎么想的。你以为点下打印按钮,浏览器就会乖乖把整个DOM树都渲染一遍?太天真了。

浏览器在接收到打印指令时,会干这么几件事:

  1. 克隆当前文档:它会先复制一份当前的DOM结构
  2. 应用打印样式:把@media print里的CSS规则套上去
  3. 重新布局计算:根据纸张尺寸(通常是A4)重新计算布局
  4. 分页处理:把内容切成一页一页的
  5. 生成打印预览:最后呈现给你看

问题就出在第3步。浏览器在重新布局的时候,对于那些height: auto或者没有明确高度的容器,它有时候会犯迷糊。特别是当你的内容是通过JavaScript动态加载的,或者图片是懒加载的,浏览器算高度的时候可能还没加载完,结果就是算出来的高度比实际小,后面的内容就被"截断"了。

更坑的是,有些浏览器(我就不点名了,IE说的就是你)还会受限于内存或者性能考虑,故意只渲染可视区域附近的内容。这就像是老师改卷子,只看第一页,后面的随便打个分就完事了。

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: autooverflow: visible基本能解决大部分问题
  • Firefox:有点轴,有时候需要显式地给html也加上同样的样式
  • Safari:最矫情,经常需要手动触发重绘才能拿到正确的高度
  • Edge(老版):建议直接放弃治疗,让用户导出PDF吧

所以MDN文档上那些理论,在实际项目里真的不够用。文档会说"浏览器应该正确处理打印媒体查询",但它不会告诉你"Safari在打印iframe里的内容时会有10%的概率丢失样式"这种细节。

深挖底层逻辑,把打印机按在地上摩擦

height: auto失效?布局塌陷的锅谁来背

咱们来深入看看为什么height: auto在打印时会失效。这得从CSS的包含块(containing block)机制说起。

在正常屏幕显示时,body的包含块是视口(viewport),所以height: 100%就是视口高度。但在打印模式下,包含块变成了页面框(page box),也就是一张纸的大小。这时候如果某个父元素还是按照屏幕逻辑来设置高度,就会出现问题。

举个例子,假设你有这么个结构:

<divclass="wrapper"><divclass="content"><!-- 这里有几千行内容 --></div></div>

屏幕模式下CSS可能是这样的:

.wrapper{height: 100vh;overflow-y: auto;}.content{height: auto;}

到了打印模式下,如果你只给body加样式,忘了处理wrapper:

@media print{body{height: auto;overflow: visible;}/* 糟糕,忘了.wrapper */}

这时候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布局的 */.flex-container{display: block !important;/* 打印时flex有时候会有奇怪的问题 */}}

看到那些!important了吗?别嫌粗暴,打印的时候就得下猛药。因为你不知道哪个第三方库的CSS会突然冒出来给你一刀。

强制分页符的正确打开方式

长内容打印肯定要分页,但分页的时机很重要。你不能让表格在一行中间断开,也不能让标题孤零零地留在页尾。

CSS提供了几个控制分页的属性:

  • page-break-before: 在元素前分页
  • page-break-after: 在元素后分页
  • page-break-inside: 控制元素内部是否允许分页(最常用的是avoid

但是!这里有个大坑。很多人这样写:

<divstyle="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;/* 保证标题下面至少跟3行内容 */widows: 3;/* 保证段落末尾至少留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;/* 每页重复表尾 */}}

这里要重点说说orphanswidows这两个属性,知道的人不多,但超级有用。它们控制的是段落被分页时,留在当前页和带到下一页的最小行数。比如设置orphans: 3,意思就是如果分页后当前页剩下的行数少于3行,那就把整个段落都带到下一页去。这样就不会出现页尾孤零零的一两行文字,看着特别丑。

动态内容高度计算,别让JS骗了打印机

现在的前端项目,哪个不是满屏的JavaScript动态渲染?React、Vue、Angular,数据都是异步来的。这时候打印就会遇到一个经典问题:你触发window.print()的时候,数据还没渲染完呢。

比如这样的代码:

functionhandlePrint(){fetchData().then(data=>{setState(data);// 假设这是React的setState window.print();// 立即打印});}

你以为setState之后页面就更新了?Too young too simple。React的setState是异步的,你这时候打印,可能DOM还没更新完呢。结果就是打印出来的还是老数据,或者只有半截。

正确的姿势是要等渲染完成。在React里可以用setTimeout或者requestAnimationFrame

functionhandlePrint(){fetchData().then(data=>{setState(data);// 等React更新完DOMsetTimeout(()=>{// 还要等图片加载完waitForImages().then(()=>{ window.print();});},100);});}// 等待所有图片加载的辅助函数functionwaitForImages(){const images = document.querySelectorAll('img');const promises = Array.from(images).map(img=>{if(img.complete)return Promise.resolve();returnnewPromise((resolve, reject)=>{ img.onload = resolve; img.onerror = resolve;// 图片加载失败也要继续,不能卡死});});return Promise.all(promises);}

但这还不够保险。有些图片是从CDN懒加载的,有些字体是webfont还没下载完,这些都会影响最终的高度计算。最保险的做法是,在打印前先把所有资源都"固化":

// 打印前预处理函数asyncfunctionprepareForPrint(){// 1. 展开所有懒加载内容const lazyElements = document.querySelectorAll('[data-lazy]'); lazyElements.forEach(el=>{ el.src = el.dataset.src; el.removeAttribute('data-lazy');});// 2. 等待字体加载if(document.fonts){await document.fonts.ready;}// 3. 等待图片加载(包括刚才替换的)awaitwaitForImages();// 4. 强制重排,确保浏览器拿到最新高度 document.body.style.zoom ='1.001';void document.body.offsetHeight;// 强制重排 document.body.style.zoom ='1';// 5. 给浏览器一点喘息时间awaitnewPromise(resolve=>setTimeout(resolve,300));}// 使用asyncfunctionhandlePrint(){awaitprepareForPrint(); window.print();}

看到那个zoom的骚操作了吗?这是强制浏览器重排(reflow)的一个小技巧。改变zoom值会让浏览器重新计算所有元素的布局,确保我们拿到的是最终的高度。

隐藏的overflow: hidden和fixed定位

有些元素在屏幕模式下看着人畜无害,一到打印模式就变身内容杀手。最常见的就是overflow: hiddenposition: 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{/* 隐藏所有fixed定位的元素 */.modal, .drawer, .toast, .loading-mask{display: none !important;}/* 把absolute定位的也检查一下 */[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、价格、数量,下面还有收货信息、物流轨迹、支付记录、操作日志。加起来十几页是常态。

这种页面的特点是:内容高度不确定,图片多,表格多。

直接上代码,看怎么组织:

<!DOCTYPEhtml><html><head><style>/* 基础样式 */body{font-family:"Microsoft YaHei", sans-serif;line-height: 1.6;color: #333;}.print-container{max-width: 210mm;/* A4宽度 */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;/* Chrome/Safari */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;/* A4高度 */}.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><buttonclass="print-btn no-print"onclick="handlePrint()">打印订单</button><divclass="print-container"id="printArea"><!-- 订单头部 --><divclass="section order-header"><h1>订单详情 #202402240001</h1><p>下单时间:2024-02-24 14:30:00 | 订单状态:已发货</p></div><!-- 商品列表 --><divclass="section product-section"><h2>商品信息</h2><tableclass="product-table"><thead><tr><th>商品图片</th><th>商品名称</th><th>SKU</th><th>单价</th><th>数量</th><th>小计</th></tr></thead><tbodyid="productList"><!-- 动态插入商品行 --></tbody><tfoot><tr><tdcolspan="5"style="text-align: right;"><strong>合计:</strong></td><td><strong>¥<spanid="totalAmount">0.00</span></strong></td></tr></tfoot></table></div><!-- 收货信息 --><divclass="section address-section"><h2>收货信息</h2><p>收货人:张三</p><p>手机号:138****8888</p><p>地址:北京市朝阳区某某街道某某小区12号楼3单元501室</p></div><!-- 物流信息 --><divclass="section logistics-section"><h2>物流轨迹</h2><tableclass="product-table"><thead><tr><th>时间</th><th>状态</th><th>详情</th></tr></thead><tbodyid="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:'快件已到达北京朝阳营业点'},{time:'2024-02-23 22:00:00',status:'运输中',detail:'快件已发往北京转运中心'},{time:'2024-02-23 18:00:00',status:'已发货',detail:'商家已发货,快递公司:顺丰速运,运单号:SF1234567890'}];// 渲染商品列表functionrenderProducts(){const tbody = document.getElementById('productList');let total =0; tbody.innerHTML = products.map(p=>{const subtotal =parseFloat(p.price)* p.quantity; total += subtotal;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 = total.toFixed(2);}// 渲染物流信息functionrenderLogistics(){const tbody = document.getElementById('logisticsList'); tbody.innerHTML = logistics.map(l=>` <tr> <td>${l.time}</td> <td>${l.status}</td> <td>${l.detail}</td> </tr> `).join('');}// 等待所有资源加载functionwaitForResources(){returnnewPromise((resolve)=>{// 等待图片const images = document.querySelectorAll('img');let loaded =0;const total = images.length;if(total ===0){resolve();return;} images.forEach(img=>{if(img.complete){ loaded++;if(loaded === total)resolve();}else{ img.onload = img.onerror=()=>{ loaded++;if(loaded === total)resolve();};}});// 超时保护,最多等5秒setTimeout(resolve,5000);});}// 打印处理函数asyncfunctionhandlePrint(){const btn = document.querySelector('.print-btn'); btn.textContent ='准备中...'; btn.disabled =true;try{// 先渲染数据(如果是异步获取的话)renderProducts();renderLogistics();// 等待浏览器渲染awaitnewPromise(r=>setTimeout(r,100));// 等待图片等资源awaitwaitForResources();// 强制重排确保高度正确 document.body.style.zoom ='1.001';void document.body.offsetHeight; document.body.style.zoom ='1';// 再等等awaitnewPromise(r=>setTimeout(r,300));// 调起打印 window.print();}catch(e){ console.error('打印准备失败:', e);alert('打印准备失败,请重试');}finally{ btn.textContent ='打印订单'; btn.disabled =false;}}// 初始化renderProducts();renderLogistics();</script></body></html>

这段代码里几个关键点:

  1. @page规则:控制纸张大小和边距,这是CSS3的规范,现代浏览器都支持
  2. -webkit-print-color-adjust: exact:默认情况下浏览器打印会忽略背景色以节省墨水,这个属性强制保留背景色,对于表格表头很重要
  3. crossorigin="anonymous":图片加这个属性是为了避免跨域问题导致canvas处理失败(虽然这里没用到canvas,但养成习惯好)
  4. tfoot的用法:把合计行放在tfoot里,这样如果表格跨页,每页底部都会显示合计,非常实用

财务报表长表格打印

财务报表的特点是列多、数据密、需要精确对齐。而且通常要求每页都显示表头,底部要有汇总。

<!DOCTYPEhtml><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><divclass="container"><h1>2024年度财务报表</h1><pclass="no-print">生成时间:2024-02-24</p><tableclass="financial-table"><thead><tr><thstyle="width: 8%">科目编码</th><thstyle="width: 20%">科目名称</th><thstyle="width: 12%">期初余额</th><thstyle="width: 12%">本期借方</th><thstyle="width: 12%">本期贷方</th><thstyle="width: 12%">期末余额</th><thstyle="width: 12%">备注</th></tr></thead><tbodyid="dataBody"><!-- 动态生成大量数据 --></tbody><tfoot><tr><tdcolspan="2"style="text-align: center;">合计</td><tdclass="num"id="sumBegin">0.00</td><tdclass="num"id="sumDebit">0.00</td><tdclass="num"id="sumCredit">0.00</td><tdclass="num"id="sumEnd">0.00</td><td>-</td></tr></tfoot></table></div><script>// 生成模拟财务数据functiongenerateData(){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> `;// 每个分组生成30条明细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).toFixed(2);const credit =(Math.random()*500000).toFixed(2);const end =(parseFloat(begin)+parseFloat(debit)-parseFloat(credit)).toFixed(2); sumBegin +=parseFloat(begin); sumDebit +=parseFloat(debit); sumCredit +=parseFloat(credit); sumEnd +=parseFloat(end); html +=` <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> `;}}); body.innerHTML = html;// 更新合计 document.getElementById('sumBegin').textContent = sumBegin.toLocaleString('zh-CN',{minimumFractionDigits:2}); document.getElementById('sumDebit').textContent = sumDebit.toLocaleString('zh-CN',{minimumFractionDigits:2}); document.getElementById('sumCredit').textContent = sumCredit.toLocaleString('zh-CN',{minimumFractionDigits:2}); document.getElementById('sumEnd').textContent = sumEnd.toLocaleString('zh-CN',{minimumFractionDigits:2});}generateData();</script></body></html>

这里的关键技巧:

  1. table-layout: fixed:配合列宽百分比,确保表格不会乱动
  2. display: table-header-grouptable-footer-group:这是CSS 2.1就有的规范,让thead和tfoot在打印时每页都重复
  3. page-break-before: always:大分组之间强制分页,让报表结构更清晰
  4. 等宽字体对齐数字:用Courier New这类等宽字体显示金额,小数点能对齐,看着舒服

简历生成器实战

简历打印的核心诉求是:不管用户用什么屏幕看的,打印出来必须是完美的A4比例,不能多一寸不能少一寸。

<!DOCTYPEhtml><html><head><style>*{margin: 0;padding: 0;box-sizing: border-box;}/* A4尺寸精确控制:210mm x 297mm */.resume-page{width: 210mm;min-height: 297mm;max-height: 297mm;/* 严格限制高度,超出就分页 */padding: 15mm;margin: 0 auto;background: white;position: relative;overflow: hidden;/* 防止内容溢出 */}/* 多页简历时,每页都是A4 */.resume-page + .resume-page{page-break-before: always;margin-top: 0;}@media print{@page{size: A4;margin: 0;/* 我们在容器里控制边距,这里设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><!-- 第一页 --><divclass="resume-page"><divclass="header"><divclass="name">张三</div><divclass="contact"> 电话:138-0000-0000 | 邮箱:[email protected] | 微信:zhangsan_dev </div></div><divclass="section"><divclass="section-title">教育背景</div><divclass="item"><divclass="item-header"><span>某某大学 - 计算机科学与技术(本科)</span><span>2015.09 - 2019.06</span></div><divclass="item-content"> 主修课程:数据结构、算法设计、操作系统、计算机网络、数据库原理<br> 在校荣誉:优秀毕业生、国家奖学金、ACM程序设计竞赛省级银奖 </div></div></div><divclass="section"><divclass="section-title">工作经历</div><divclass="item"><divclass="item-header"><span>某某科技有限公司 - 高级前端工程师</span><span>2021.07 - 至今</span></div><divclass="item-content"> 负责公司核心电商平台前端架构设计与开发,主导技术选型从Vue2迁移至Vue3<br> 搭建前端监控体系,错误率降低80%,首屏加载时间优化至1.2s以内<br> 带领5人前端团队,制定代码规范与Code Review机制,提升团队整体代码质量 </div></div><divclass="item"><divclass="item-header"><span>某某互联网公司 - 前端开发工程师</span><span>2019.07 - 2021.06</span></div><divclass="item-content"> 参与公司ToB管理后台开发,负责权限管理、数据可视化等模块<br> 封装通用组件库15+个,提升团队开发效率30%<br> 优化老旧项目构建流程,打包体积减少40%,构建速度提升2倍 </div></div></div><divclass="section"><divclass="section-title">专业技能</div><divclass="skill-bar"><spanclass="skill-name">Vue/React</span><divclass="skill-progress"><divclass="skill-fill"style="width: 90%"></div></div></div><divclass="skill-bar"><spanclass="skill-name">TypeScript</span><divclass="skill-progress"><divclass="skill-fill"style="width: 85%"></div></div></div><divclass="skill-bar"><spanclass="skill-name">Node.js</span><divclass="skill-progress"><divclass="skill-fill"style="width: 75%"></div></div></div><divclass="skill-bar"><spanclass="skill-name">工程化/性能优化</span><divclass="skill-progress"><divclass="skill-fill"style="width: 80%"></div></div></div></div></div><!-- 第二页(项目经历) --><divclass="resume-page"><divclass="section"><divclass="section-title">项目经历</div><divclass="item"><divclass="item-header"><span>企业级低代码平台</span><span>2023.01 - 2023.12</span></div><divclass="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><divclass="item"><divclass="item-header"><span>跨境电商ERP系统</span><span>2022.03 - 2022.12</span></div><divclass="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><divclass="item"><divclass="item-header"><span>微信小程序生态建设</span><span>2021.07 - 2022.02</span></div><divclass="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><divclass="section"><divclass="section-title">自我评价</div><divclass="item-content"> 5年前端开发经验,精通Vue/React生态,具备完整的大型项目架构经验。对前端工程化、性能优化有深入实践, 擅长组件化设计和开发效率提升。具备良好的团队协作能力和技术领导力,能够带领团队攻克技术难点。 持续关注前端技术发展趋势,热爱开源社区,GitHub个人项目累计Star 2000+。 </div></div></div></body></html>

简历打印的核心要点:

  1. 严格尺寸控制210mm x 297mm,不多不少
  2. page-break-before: always:多页简历时,每页都是独立的A4
  3. page-break-inside: avoid:经历块、项目块不能跨页断开,否则阅读体验很差
  4. 打印边距处理@page的margin设为0,在容器里用padding控制边距,这样不同浏览器的兼容性更好

电子发票和物流面单

这类场景对尺寸精度要求极高,差1毫米可能就贴不上或者塞不进快递袋。

<!DOCTYPEhtml><html><head><style>/* 电子发票:通常240mm x 140mm */.invoice{width: 240mm;height: 140mm;padding: 10mm;margin: 0 auto;background: white;border: 1px solid #ddd;position: relative;font-family:"SimSun", serif;/* 发票通常用宋体 */overflow: hidden;}/* 物流面单:通常100mm x 150mm(热敏纸) */.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;/* 发票 *//* size: 100mm 150mm; 面单 */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><!-- 电子发票示例 --><divclass="invoice"><divclass="invoice-header"><divclass="invoice-title">增值税电子普通发票</div><divclass="invoice-info"><span>发票代码:011001900211</span><span>发票号码:12345678</span><span>开票日期:2024年02月24日</span></div></div><tableclass="invoice-table"><tr><thstyle="width: 30%">购买方信息</th><tdstyle="width: 70%;text-align: left;"> 名称:某某科技有限公司<br> 纳税人识别号:91110108MA00XXXXXX<br> 地址、电话:北京市海淀区某某路XX号 010-12345678<br> 开户行及账号:中国工商银行北京分行 0200XXXXXX </td></tr><tr><thcolspan="2"><tablestyle="width: 100%;border: none;"><trstyle="border: none;"><tdstyle="border: none;width: 40%">货物或应税劳务、服务名称</td><tdstyle="border: none;width: 15%">规格型号</td><tdstyle="border: none;width: 10%">单位</td><tdstyle="border: none;width: 10%">数量</td><tdstyle="border: none;width: 10%">单价</td><tdstyle="border: none;width: 15%">金额</td></tr><trstyle="border: none;"><tdstyle="border: none;">*软件*技术服务费</td><tdstyle="border: none;">-</td><tdstyle="border: none;">次</td><tdstyle="border: none;">1</td><tdstyle="border: none;">10000.00</td><tdstyle="border: none;">10000.00</td></tr></table></th></tr><tr><th>销售方信息</th><tdstyle="text-align: left;"> 名称:北京某某软件服务有限公司<br> 纳税人识别号:91110108MA00YYYYYY<br> 地址、电话:北京市朝阳区某某路XX号 010-87654321<br> 开户行及账号:中国建设银行北京分行 1100YYYYYY </td></tr></table><divclass="invoice-amount"> 价税合计(大写):壹万零壹佰元整<br><spanstyle="color: #f00;">(小写):¥10100.00</span></div><divclass="invoice-qrcode"> 二维码区域<br> (发票信息) </div></div><!-- 物流面单示例 --><divclass="waybill"><divclass="waybill-barcode"> *SF1234567890* </div><divclass="waybill-route"><divstyle="font-size: 14pt;font-weight: bold;">北京→上海</div><div>标快<br>陆运</div></div><divclass="waybill-info"><div><spanclass="waybill-label">收:</span><strong>张三</strong> 138****8888<br><spanstyle="margin-left: 17mm;">上海市浦东新区某某街道某某小区12号楼</span></div><divstyle="margin-top: 2mm;"><spanclass="waybill-label">寄:</span> 李四 139****9999<br><spanstyle="margin-left: 17mm;">北京市朝阳区某某大厦8层</span></div></div><divstyle="border-top: 1px solid #000;margin-top: 3mm;padding-top: 2mm;"><strong>物品信息:</strong>电子产品(已验视)<br><strong>重量:</strong>2.5kg &nbsp;&nbsp;<strong>保价:</strong>¥5000 </div><divstyle="position: absolute;bottom: 5mm;left: 5mm;right: 5mm;font-size: 8pt;color: #666;"> 订单号:2024022414300001 &nbsp;&nbsp; 打印时间:2024-02-24 14:30:00 </div></div></body></html>

精准打印的关键:

  1. 毫米级尺寸控制:用mm单位,不要用px或者pt,因为不同设备DPI不同,毫米是物理尺寸,最准确
  2. @page size自定义:可以指定任意尺寸,不只是A4、A5这种标准尺寸
  3. overflow: hidden:严格防止内容溢出,因为发票和面单通常有固定的打印区域,超出就废了
  4. 字体选择:发票用宋体显得正式,面单用黑体或微软雅黑保证清晰可读

遇到报错别慌,老司机的排查套路

打印出来是空白?

先检查是不是有全局的display: none或者visibility: hidden。特别是用了某些UI框架,打印的时候modal关闭了,但内容其实在modal里,跟着一起被隐藏了。

排查代码:

// 打印前检查所有父元素的显示状态functioncheckVisibility(element){let current = element;while(current){const style = window.getComputedStyle(current);if(style.display ==='none'){ console.warn('隐藏元素:', current);returnfalse;}if(style.visibility ==='hidden'){ console.warn('不可见元素:', current);returnfalse;} current = current.parentElement;}returntrue;}// 使用checkVisibility(document.getElementById('printContent'));

还有就是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,你就触发打印了。

试试这个终极等待函数:

asyncfunctionwaitForEverything(){// 1. 等DOM更新(React/Vue)awaitnewPromise(resolve=>setTimeout(resolve,0));// 2. 等字体if(document.fonts){await document.fonts.ready;}// 3. 等图片(包括背景图)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 =newImage(); img.src = src;return img;})];await Promise.all(allImages.map(img=> img.complete ? Promise.resolve():newPromise((resolve)=>{ img.onload = img.onerror = resolve;setTimeout(resolve,3000);// 单张图片最多等3秒})));// 4. 等iframe(如果有的话)const iframes = Array.from(document.querySelectorAll('iframe'));await Promise.all(iframes.map(iframe=>newPromise((resolve)=>{if(iframe.contentDocument?.readyState ==='complete'){resolve();}else{ iframe.onload = resolve;setTimeout(resolve,5000);}})));// 5. 最后缓冲awaitnewPromise(resolve=>setTimeout(resolve,500));}// 使用asyncfunctionhandlePrint(){awaitwaitForEverything(); window.print();}

图片加载慢导致高度计算不准

如果图片很多很大,即使做了等待,也可能因为网络问题导致超时。这时候可以考虑预先把图片转成base64:

// 预加载并转换图片为base64asyncfunctionpreloadImagesAsBase64(){const images = document.querySelectorAll('img[data-src]');for(let img of images){try{const response =awaitfetch(img.dataset.src);const blob =await response.blob();const reader =newFileReader();awaitnewPromise((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等),打印的时候只有可视区的几条数据。这时候你需要在打印前"展开"所有数据。

// 假设你用的是react-windowfunctionhandlePrint(){const listRef =useRef();// 1. 先滚动到底部,强制加载所有数据 listRef.current.scrollToItem(999999,'end');// 2. 等渲染完成setTimeout(()=>{// 3. 临时禁用虚拟滚动,渲染全部内容setIsVirtual(false);// 你的状态控制// 4. 等DOM更新setTimeout(()=>{ window.print();// 5. 打印完恢复虚拟滚动setIsVirtual(true);},100);},500);}

更通用的做法,如果你控制不了组件内部逻辑,可以直接操作DOM:

functionexpandVirtualList(){// 找到虚拟滚动的容器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';// 触发所有数据的渲染(这取决于你用的具体库)// 可能需要手动触发scroll事件或者调用库提供的APIreturn()=>{// 恢复函数 container.style.height = originalHeight; container.style.overflow = originalOverflow;};}

Promise包装器等资源加载

前面其实已经写过类似的,这里再给个更健壮的版本,支持超时和错误处理:

functioncreateResourceWaiter(timeout =10000){returnnewPromise((resolve)=>{const startTime = Date.now();constcheck=()=>{// 检查各种资源const images = Array.from(document.images);const incompleteImages = images.filter(img=>!img.complete);const fontsReady = document.fonts ? document.fonts.status ==='loaded':true;// 可以扩展检查其他资源,比如video、audio等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();});}// 使用asyncfunctionhandlePrint(){ console.log('等待资源加载...');const result =awaitcreateResourceWaiter(5000); console.log('资源状态:', result); window.print();}

打印预览模拟A4效果

在屏幕上预览打印效果,避免反复试打印浪费纸:

<divclass="print-preview"><divclass="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)); } } 

这样在小屏幕上会自动缩放,保证能看到完整的A4页面。

打印按钮防抖

用户手贱连点打印按钮,可能会把浏览器搞崩,特别是内容多的时候:

functiondebounce(fn, delay){let timer =null;returnfunction(...args){if(timer){ console.log('别急,正在准备...');return;}fn.apply(this, args); timer =setTimeout(()=>{ timer =null;}, delay);};}const handlePrint =debounce(async()=>{// 打印逻辑awaitprepareForPrint(); window.print();},3000);// 3秒内只能点一次

或者加个loading状态更友好:

asyncfunctionhandlePrint(){const btn = document.getElementById('printBtn');if(btn.disabled)return; btn.disabled =true; btn.textContent ='准备中...';try{awaitprepareForPrint(); window.print();}catch(e){alert('打印准备失败:'+ e.message);}finally{ btn.disabled =false; btn.textContent ='打印';}}

最后啰嗦两句,别嫌我烦

看到这里,你应该对window.print这个"祖宗"有了全新的认识。它不是什么高深技术,但坑是真的多。下次再遇到打印问题,别只会重启浏览器或者让用户"试试别的浏览器",试试上面这些野路子。

记住几个核心要点:

  1. 高度问题height: auto + overflow: visible,给所有可能限制高度的容器都加上
  2. 异步问题:数据渲染完、图片加载完、字体准备好,再触发打印
  3. 分页问题page-break-inside: avoid防止截断,伪元素比分页div优雅
  4. 样式问题!important在打印媒体查询里是你的好朋友
  5. 尺寸问题:用mm做单位,不要用px

代码写得再漂亮,打印出来是一坨屎也没人夸你。前端工程师的价值就体现在这些细节里。一个能把打印功能做得丝般顺滑的前端,绝对是团队里的宝藏。

赶紧去试试吧。要是还搞不定,回来继续吐槽,我随时在线接招。毕竟咱们前端佬就是在填坑中成长的,对吧?

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!

专栏系列(点击解锁)学习路线(点击解锁)知识定位
《微信小程序相关博客》持续更新中~结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》持续更新中~AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》《前端基础入门三大核心之html相关博客》前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》持续更新中~详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》持续更新中~Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》持续更新中~SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》持续更新中~算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》持续更新中~作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》持续更新中~罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》持续更新中~基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》持续更新中~分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!
在这里插入图片描述

Read more

速通前端篇 —— CSS

速通前端篇 —— CSS

找往期文章包括但不限于本期文章中不懂的知识点: 个人主页:我要学编程程(ಥ_ಥ)-ZEEKLOG博客 所属专栏:速通前端 目录 CSS的介绍 基本语法规范 CSS选择器 标签选择器 class选择器  id选择器  复合选择器  通配符选择器 CSS常见样式  颜色 color 字体大小 font-size  边框 border  宽度 与 高度  内边距 外边距  CSS的介绍 CSS(Cascading Style Sheet),层叠样式表,用于控制页面的样式。CSS能够对网页中元素位置的排版进行像素级精确控制,实现美化页面的效果。能够做到页面的样式和结构分离。简单理解,CSS就是类似于对页面进行"化妆",让页面变得更加好看。 基本语法规范 选择器+{一条/N条声明} 1、

前端请求后端返回404/405/500状态码:完整排查与解决指南

前端请求后端返回404/405/500状态码:完整排查与解决指南

前端发起HTTP请求时,浏览器Network面板频繁出现404、405、500等状态码,是前后端交互中最常见的接口异常。这些状态码并非前端代码语法错误,而是HTTP协议层面的响应状态提示——404代表资源未找到,405代表请求方法不被允许,500代表服务器内部错误,三类错误的排查方向截然不同:404侧重「资源路径匹配」,405侧重「请求方法与跨域配置」,500侧重「后端代码与服务器环境」。本文将从每个状态码的核心本质出发,分场景梳理高频诱因与解决方案,覆盖前端配置、后端接口、服务器环境、代理转发等全链路,提供可直接落地的排查步骤和代码示例,帮助开发者快速定位并解决问题。 文章目录 * 一、核心认知:三类状态码的本质与快速区分 * 1.1 状态码核心定义与本质 * 1.2 快速区分:通过Network面板定位状态码类型 * 1.3 关键前提:明确“请求是否到达后端” * 二、场景1:404 Not Found(资源未找到)—— 排查与解决方案 * 2.1

2026年,AI短剧正在爆发:一款开源工具带你从0到1做短剧

2026年,AI短剧正在爆发:一款开源工具带你从0到1做短剧

大家好,我是小阳哥。 2026年,一个新的风口正在成型——AI短剧。 今天给大家推荐一个开源的 AI短剧神器。工具是开源免费的,可以自己部署,也可以直接下载可执行文件运行。 工具把 AI 短剧拆解成了一步步可执行的流水线: 小说 → 大纲 → 剧本 → 分镜 → 视频 而且是多 Agent 协作生成,非常接近真实影视制作流程。 接下来,一步步带大家看下如何使用。(工具地址放在文末了) 一、基础设置 在开始制作前,我们需要设置AI 模型。共需要3类 * 文本模型(写剧情) * 图像模型(生成角色/场景/分镜图片) * 视频模型(生成短剧) 这里面基本支持了市面上所有的主流模型 为每个 agent 设置对应的模型 支持修改每个agent的内置提示词 二、创作流程(核心流程) 1. 新建项目 字段包括: