折腾我2周的分页打印和下载pdf
点击关注公众号,“技术干货”及时达!1.背景一开始接到任务需要打印html,之前用到了vue-print-nb-jeecg来处理Vue2一个打印的问题,现在是遇到需求要在Vue3项目里面去打印十几页的打印和下载为pdf,难点和坑就在于我用的库vue3-print-nb来做分页打印预览,下载pdf后面介绍
2.预览打印实现divid="printMe"style="background:red;"
p葫芦娃,葫芦娃/p
p一根藤上七朵花/p
p小小树藤是我家啦啦啦啦/p
p叮当当咚咚当当浇不大/p
p叮当当咚咚当当是我家/p
p啦啦啦啦/p
p.../p
/div
buttonv-print="'#printMe'"Printlocalrange/button
因为官方提供的方案都是DOM加载完成后然后直接打印,但是我的需求是需要点击打印的时候根据id渲染不同的组件然后渲染DOM,后面仔细看官方文档,有个beforeOpenCallback方法在打印预览之前有个钩子,但是这个钩子没办法确定我接口加载完毕,所以我的思路就是用户先点击我写的点击按钮事件,等异步渲染完毕之后,我再同步触发真正的打印预览按钮,这样就变相解决了我的需求。
坑
没办法处理接口异步渲染数据展示DOM进行打印操作在布局相对定位的时候在谷歌浏览器会发现有布局整体变小的问题(后续用zoom处理的)3.掉头发之下载pdf下载pdf这种需求才是我每次去理发店不敢让tony把我头发打薄的原因,我看了很多技术文章,结合个人业务情况,采取的方案是html2canvas把html转成canvas然后转成图片然后通过jsPDF截取图片分页最后下载到本地。本人秉承着不生产水,只做大自然的搬运工的匠人精神,迅速而又果断的从社区来到社区去,然后找到了适配当前业务的逻辑代码(实践出真知)。
importhtml2canvasfrom'html2canvas'
importjsPDF,{RGBAData}from'jspdf'
/**a4纸的尺寸[595.28,841.89],单位毫米*/
const[PAGE_WIDTH,PAGE_HEIGHT]=[595.28,841.89]
constPAPER_CONFIG={
/**竖向*/
portrait:{
height:PAGE_HEIGHT,
width:PAGE_WIDTH,
contentWidth:560
},
/**横向*/
landscape:{
height:PAGE_WIDTH,
width:PAGE_HEIGHT,
contentWidth:800
}
}
//将元素转化为canvas元素
//通过放大提高清晰度
//width为内容宽度
asyncfunctiontoCanvas(element:HTMLElement,width:number){
if(!element)return{width,height:0}
//canvas元素
constcanvas=awaithtml2canvas(element,{
//allowTaint:true,//允许渲染跨域图片
scale:window.devicePixelRatio*2,//增加清晰度
useCORS:true//允许跨域
})
//获取canvas转化后的宽高
const{width:canvasWidth,height:canvasHeight}=canvas
//html页面生成的canvas在pdf中的高度
constheight=(width/canvasWidth)*canvasHeight
//转化成图片Data
constcanvasData=canvas.toDataURL('image/jpeg',1.0)
return{width,height,data:canvasData}
}
/**
*生成pdf(A4多页pdf截断问题,包括页眉、页脚和上下左右留空的护理)
*@paramparam0
*@returns
*/
exportasyncfunctionoutputPDF({
/**pdf内容的dom元素*/
element,
/**页脚dom元素*/
footer,
/**页眉dom元素*/
header,
/**pdf文件名*/
filename,
/**a4值的方向:portraitorlandscape*/
orientation='portrait'as'portrait'|'landscape'
}){
if(!(elementinstanceofHTMLElement)){
return
}
if(!['portrait','landscape'].includes(orientation)){
returnPromise.reject(
newError(`InvalidParameters:theparameter{orientation}isassignedwrongvalue,youcanonlyassignitwith{portrait}or{landscape}`)
)
}
const[A4_WIDTH,A4_HEIGHT]=[PAPER_CONFIG[orientation].width,PAPER_CONFIG[orientation].height]
/**一页pdf的内容宽度,左右预设留白*/
const{contentWidth}=PAPER_CONFIG[orientation]
//eslint-disable-next-linenew-cap
constpdf=newjsPDF({
unit:'pt',
format:'a4',
orientation
})
//一页的高度,转换宽度为一页元素的宽度
const{width,height,data}=awaittoCanvas(element,contentWidth)
//添加
functionaddImage(
_x:number,
_y:number,
pdfInstance:jsPDF,
base_data:string|HTMLImageElement|HTMLCanvasElement|Uint8Array|RGBAData,
_width:number,
_height:number
){
pdfInstance.addImage(base_data,'JPEG',_x,_y,_width,_height)
}
//增加空白遮挡
functionaddBlank(x:number,y:number,_width:number,_height:number){
pdf.setFillColor(255,255,255)
pdf.rect(x,y,Math.ceil(_width),Math.ceil(_height),'F')
}
//页脚元素经过转换后在PDF页面的高度
const{height:tFooterHeight,data:headerData}=footer?awaittoCanvas(footer,contentWidth):{height:0,data:undefined}
//页眉元素经过转换后在PDF的高度
const{height:tHeaderHeight,data:footerData}=header?awaittoCanvas(header,contentWidth):{height:0,data:undefined}
//添加页脚
asyncfunctionaddHeader(headerElement:HTMLElement){
headerDatapdf.addImage(headerData,'JPEG',0,0,contentWidth,tHeaderHeight)
}
//添加页眉
asyncfunctionaddFooter(pageNum:number,now:number,footerElement:HTMLElement){
if(footerData){
pdf.addImage(footerData,'JPEG',0,A4_HEIGHT-tFooterHeight,contentWidth,tFooterHeight)
}
}
//距离PDF左边的距离,/2表示居中
constbaseX=(A4_WIDTH-contentWidth)/2//预留空间给左边
//距离PDF页眉和页脚的间距,留白留空
constbaseY=15
//除去页头、页眉、还有内容与两者之间的间距后每页内容的实际高度
constoriginalPageHeight=A4_HEIGHT-tFooterHeight-tHeaderHeight-2*baseY
//元素在网页页面的宽度
constelementWidth=element.offsetWidth
//PDF内容宽度和在HTML中宽度的比,用于将元素在网页的高度转化为PDF内容内的高度,将元素距离网页顶部的高度转化为距离Canvas顶部的高度
constrate=contentWidth/elementWidth
//每一页的分页坐标,PDF高度,初始值为根元素距离顶部的距离
constpages=[rate*getElementTop(element)]
//获取该元素到页面顶部的高度(注意滑动scroll会影响高度)
functiongetElementTop(contentElement){
if(contentElement.getBoundingClientRect){
constrect=contentElement.getBoundingClientRect()||{}
consttopDistance=rect.top
returntopDistance
}
}
//遍历正常的元素节点
functiontraversingNodes(nodes){
for(constelementofnodes){
constone=element
/***/
/**注意:可以根据业务需求,判断其他场景的分页,本代码只判断表格的分页场景*/
/***/
//table的每一行元素也是深度终点
constisTableRow=one.classListone.classList.contains('ant4-table-row')
//对需要处理分页的元素,计算是否跨界,若跨界,则直接将顶部位置作为分页位置,进行分页,且子元素不需要再进行判断
const{offsetHeight}=one
//计算出最终高度
constoffsetTop=getElementTop(one)
//dom转换后距离顶部的高度
//转换成canvas高度
consttop=rate*offsetTop
constrateOffsetHeight=rate*offsetHeight
//对于深度终点元素进行处理
if(isTableRow){
//dom高度转换成生成pdf的实际高度
//代码不考虑dom定位、边距、边框等因素,需在dom里自行考虑,如将box-sizing设置为border-box
updateTablePos(rateOffsetHeight,top)
}
//对于普通元素,则判断是否高度超过分页值,并且深入
else{
//执行位置更新操作
updateNormalElPos(top)
//遍历子节点
traversingNodes(one.childNodes)
}
updatePos()
}
}
//普通元素更新位置的方法
//普通元素只需要考虑到是否到达了分页点,即当前距离顶部高度-上一个分页点的高度大于正常一页的高度,则需要载入分页点
functionupdateNormalElPos(top){
if(top-(pages.length0?pages[pages.length-1]:0)=originalPageHeight){
pages.push((pages.length0?pages[pages.length-1]:0)+originalPageHeight)
}
}
//可能跨页元素位置更新的方法
//需要考虑分页元素,则需要考虑两种情况
//1.普通达顶情况,如上
//2.当前距离顶部高度加上元素自身高度大于整页高度,则需要载入一个分页点
functionupdateTablePos(eHeight:number,top:number){
//如果高度已经超过当前页,则证明可以分页了
if(top-(pages.length0?pages[pages.length-1]:0)=originalPageHeight){
pages.push((pages.length0?pages[pages.length-1]:0)+originalPageHeight)
}
//若距离当前页顶部的高度加上元素自身的高度大于一页内容的高度,则证明元素跨页,将当前高度作为分页位置
elseif(
top+eHeight-(pages.length0?pages[pages.length-1]:0)originalPageHeight&&
top!==(pages.length0?pages[pages.length-1]:0)
){
pages.push(top)
}
}
//深度遍历节点的方法
traversingNodes(element.childNodes)
functionupdatePos(){
while(pages[pages.length-1]+originalPageHeightheight){
pages.push(pages[pages.length-1]+originalPageHeight)
}
}
//对pages进行一个值的修正,因为pages生成是根据根元素来的,根元素并不是我们实际要打印的元素,而是element,
//所以要把它修正,让其值是以真实的打印元素顶部节点为准
constnewPages=pages.map(item=item-pages[0])
//根据分页位置开始分页
for(leti=0;inewPages.length;++i){
//根据分页位置新增图片
addImage(baseX,baseY+tHeaderHeight-newPages[i],pdf,data!,width,height)
//将内容与页眉之间留空留白的部分进行遮白处理
addBlank(0,tHeaderHeight,A4_WIDTH,baseY)
//将内容与页脚之间留空留白的部分进行遮白处理
addBlank(0,A4_HEIGHT-baseY-tFooterHeight,A4_WIDTH,baseY)
//对于除最后一页外,对内容的多余部分进行遮白处理
if(inewPages.length-1){
//获取当前页面需要的内容部分高度
constimageHeight=newPages[i+1]-newPages[i]
//对多余的内容部分进行遮白
addBlank(0,baseY+imageHeight+tHeaderHeight,A4_WIDTH,A4_HEIGHT-imageHeight)
}
//添加页眉
if(header){
awaitaddHeader(header)
}
//添加页脚
if(footer){
awaitaddFooter(newPages.length,i+1,footer)
}
//若不是最后一页,则分页
if(i!==newPages.length-1){
//增加分页
pdf.addPage()
}
}
returnpdf.save(filename)
}
4.分页的小姿势如果有需求把打印预览的时候的页眉页脚默认取消不展示,然后自定义页面的边距可以这么设置样式
@page{
size:autoA4landscape;
margin:3mm;
}
@mediaprint{
body,
html{
height:initial;
padding:0px;
margin:0px;
}
}
5.关于页眉页脚由于业务是属于比较自定义化的展示,所以我封装成组件,然后根据返回的数据进行渲染到每个界面,然后利用绝对定位放在相同的位置,最后一点小优化就是,公共化提取界面的样式,然后整合为pub.scss然后引入到界面里面,这样即使产品有一定的样式调整,我也可以在公共样式里面去配置和修改,大大的减少本人的工作量。在日常的开发中也是这样,不要去抱怨需求的变动频繁,而是力争在写组件的过程中考虑到组件的健壮性和灵活度,给自己的工作减负,到点下班。
参考文章https://juejin.cn/post/7323436080312893476
点击关注公众号,“技术干货”及时达!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线