1.需求
导出具有页眉页脚、页码的Pdf,并且解决Pdf分割的问题。
2.实现思路
该需求主要的难点在于分页的时候容易出现分割问题,并且要将页眉页脚加进去。实现的大概思路:
(1)先使用jsPDF、html2canvas将页面可以导出;
(2)第一页的页眉在html代码中添加,第二页的页眉在分页的时候动态添加;
(3)在会出现分割问题的节点上添加一个分割类名,这个节点必须要有父元素,因为如果是在这个节点分割的话,需要判断这个节点距离底部的距离,然后在这个节点后面添加页脚、页码;
(4)获取在分页节点的元素,domList是根据分割类名获取的节点,循环这些节点,判断它是否处于分页的位置;
for (let i = 0; i < domList.length; i++) {
const eleBounding = this.ele.getBoundingClientRect();
const node = domList[i];
const bound = node.getBoundingClientRect();
const offset2Ele = bound.top - eleBounding.top;
const currentPage = Math.ceil((bound.bottom - eleBounding.top) /pageHeight)
}
(5)定义一个创建空白元素、页眉、页脚的函数
(6)添加页眉页脚,由于页脚是在A4纸的底部,所以在添加页脚之前需要获取分页节点距离A4纸底部的空白高度,如果这个空白高度大于页脚高度,那么先添加一个空白元素,空白元素的高度等于空白高度减去页脚高度,最后再依次添加页脚和页眉。如果空白高度小于页脚高度,那么需要依次向上累加分割节点的高度,至到高度大于页脚高度;
const divParent = domList[i].parentNode // 获取该分割节点的父节点
let emptyHeight=pageHeight * pageNum - offset2Ele;
if (pageNum < currentPage) {
// console.log(i,pageNum,currentPage,offset2Ele,emptyHeight);
pageNum++
if(emptyHeight>=70){//空白节点高度如果大于页脚高度才添加空白节点
divParent.insertBefore(this.createEmptyNode(emptyHeight), node);
divParent.insertBefore(this.creatFooterNode(pageNum,id), node);
divParent.insertBefore(this.createHeaderNode(practiceName), node)
}else{// /空白节点高度如果小于页脚高度,就要把上一个节点挤下去
emptyHeight=emptyHeight<0?0:emptyHeight;
let j=1;
emptyHeight=emptyHeight+domList[i-j].offsetHeight;
while(emptyHeight<70){
j++;
emptyHeight=emptyHeight+domList[i-j].offsetHeight;
}
let newDivParent= domList[i-j].parentNode;
newDivParent.insertBefore(this.createEmptyNode(emptyHeight), domList[i-j]);
newDivParent.insertBefore(this.creatFooterNode(pageNum,id), domList[i-j]);
newDivParent.insertBefore(this.createHeaderNode(practiceName), domList[i-j])
}
}
(7)在分割节点循环完成之后根据第(6)条思路为最后一张A4纸添加页脚。并且计算出总的A4纸页数。
3.踩坑点
(1)分割的节点一定要有父元素不然找不到父元素要报错!
(2)需要导出的html页面的宽度要定死,宽度越大,导出的字体越小,高度定成100%。
(3)将要导出的页面封装成一个组件,引用组件的时候设置以下样式。
width: 900px;
height: 100%;
position: fixed;
top:0;
left: 0;
z-index:-1000"
4. 使用自己封装的pdf.js
import PdfLoader from "@/libs/pdf.js";
const pdf = document.getElementById('sportsTreatRecord');
let fileName=`导出的pdf文件名`
setTimeout(() => {
let pdfReq=new PdfLoader(pdf,fileName,'splitClassName');
pdfReq.outPutPdfFn(fileName,null,this.practiceName);
}, 3000);
5.实现代码
import jsPDF from 'jspdf'
import html2canvas from 'html2canvas'
import logoUrl from '../assets/image/newLogo.png'
/*
* 使用说明
* ele:需要导出pdf的容器元素(dom节点)
* pdfFileName: 导出文件的名字 通过调用outPutPdfFn方法也可传参数改变
* splitClassName: 避免分段截断的类名 当pdf有多页时需要传入此参数 , 避免pdf分页时截断元素 如表格<tr class="itemClass"></tr>
* 调用方式 先 let pdf = new PdfLoader(ele, 'pdf' ,'itemClass');
* 若想改变pdf名称 pdf.outPutPdfFn(fileName); outPutPdfFn方法返回一个promise 可以使用then方法处理pdf生成后的逻辑
* */
class PdfLoader {
constructor(ele, pdfFileName, splitClassName) {
this.ele = ele
this.pdfFileName = pdfFileName
this.splitClassName = splitClassName
this.A4_WIDTH = 595.28
this.A4_HEIGHT = 841.89
}
async getPDF(resolve) {
const ele = this.ele
const pdfFileName = this.pdfFileName
const eleW = ele.offsetWidth// 获得该容器的宽
const eleH = ele.scrollHeight// 获得该容器的高
const eleOffsetTop = ele.offsetTop// 获得该容器到文档顶部的距离
const eleOffsetLeft = ele.offsetLeft// 获得该容器到文档最左的距离
const canvas = document.createElement('canvas')
let abs = 0
const win_in = document.documentElement.clientWidth || document.body.clientWidth// 获得当前可视窗口的宽度(不包含滚动条)
const win_out = window.innerWidth// 获得当前窗口的宽度(包含滚动条)
if (win_out > win_in) {
abs = (win_out - win_in) / 2// 获得滚动条宽度的一半
}
canvas.width = eleW * 2// 将画布宽&&高放大两倍
canvas.height = eleH * 2
const context = canvas.getContext('2d')
context.scale(2, 2) // 增强图片清晰度
context.translate(-eleOffsetLeft - abs, -eleOffsetTop)
html2canvas(ele, {
useCORS: true// 允许canvas画布内可以跨域请求外部链接图片, 允许跨域请求。
}).then(async canvas => {
const contentWidth = canvas.width
const contentHeight = canvas.height
// 一页pdf显示html页面生成的canvas高度;
const pageHeight = (contentWidth / this.A4_WIDTH) * this.A4_HEIGHT // 这样写的目的在于保持宽高比例一致 pageHeight/canvas.width = a4纸高度/a4纸宽度// 宽度和canvas.width保持一致
// 未生成pdf的html页面高度
let leftHeight = contentHeight
// 页面偏移
let position = 0
// a4纸的尺寸[595,842],单位像素,html页面生成的canvas在pdf中图片的宽高
const imgWidth = this.A4_WIDTH - 10 // -10为了页面有右边距
const imgHeight = (this.A4_WIDTH / contentWidth) * contentHeight
const pageData = canvas.toDataURL('image/jpeg', 1.0)
const pdf = jsPDF('', 'pt', 'a4')
// 有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
// 当内容未超过pdf一页显示的范围,无需分页
if (leftHeight < pageHeight) {
// 在pdf.addImage(pageData, 'JPEG', 左,上,宽度,高度)设置在pdf中显示;
pdf.addImage(pageData, 'JPEG', 5, 0, imgWidth, imgHeight)
// pdf.addImage(pageData, 'JPEG', 20, 40, imgWidth, imgHeight);
} else {
// 分页
while (leftHeight > 0) {
pdf.addImage(pageData, 'JPEG', 5, position, imgWidth, imgHeight)
leftHeight -= pageHeight
position -= this.A4_HEIGHT
// 避免添加空白页
if (leftHeight > 0) {
pdf.addPage()
}
}
}
pdf.save(pdfFileName + '.pdf', { returnPromise: true }).then(() => {
// 去除添加的空div 防止页面混乱
const doms = document.querySelectorAll('.emptyDiv')
for (let i = 0; i < doms.length; i++) {
doms[i].remove()
}
})
this.ele.style.height = ''
resolve()
})
}
//此方法是防止(图表之类)内容因为A4纸张问题被截断
async outPutPdfFn(pdfFileName,id,practiceName) {
return new Promise((resolve, reject) => {
this.ele.style.height = 'initial';
pdfFileName ? this.pdfFileName = pdfFileName : null
const target = this.ele;
const pageHeight = target.scrollWidth / this.A4_WIDTH * this.A4_HEIGHT;
// 获取分割dom,此处为class类名为item的dom
// const domList = document.getElementsByClassName(this.splitClassName);
const domList=this.ele.getElementsByClassName(this.splitClassName);
// 进行分割操作,当dom内容已超出a4的高度,则将该dom前插入一个空dom,把他挤下去,分割
let pageNum = 1 // pdf页数
for (let i = 0; i < domList.length; i++) {
const eleBounding = this.ele.getBoundingClientRect();
const node = domList[i];
const bound = node.getBoundingClientRect();
const offset2Ele = bound.top - eleBounding.top;
const currentPage = Math.ceil((bound.bottom - eleBounding.top) / pageHeight) // 当前元素应该在哪一页
const divParent = domList[i].parentNode // 获取该div的父节点
let emptyHeight=pageHeight * pageNum - offset2Ele;
if (pageNum < currentPage) {
// console.log(i,pageNum,currentPage,offset2Ele,emptyHeight);
pageNum++
if(emptyHeight>=70){//空白节点高度如果大于页脚高度才添加空白节点
divParent.insertBefore(this.createEmptyNode(emptyHeight), node);
divParent.insertBefore(this.creatFooterNode(pageNum,id), node);
divParent.insertBefore(this.createHeaderNode(practiceName), node)
}else{// /空白节点高度如果小于页脚高度,就要把上一个节点挤下去
emptyHeight=emptyHeight<0?0:emptyHeight;
let j=1;
emptyHeight=emptyHeight+domList[i-j].offsetHeight;
while(emptyHeight<70){
j++;
emptyHeight=emptyHeight+domList[i-j].offsetHeight;
}
let newDivParent= domList[i-j].parentNode;
newDivParent.insertBefore(this.createEmptyNode(emptyHeight), domList[i-j]);
newDivParent.insertBefore(this.creatFooterNode(pageNum,id), domList[i-j]);
newDivParent.insertBefore(this.createHeaderNode(practiceName), domList[i-j])
}
}
}
// 给最后一页添加页脚
let lastNode=domList[domList.length-1].getBoundingClientRect();
const eleBounding1 = this.ele.getBoundingClientRect();
let lastEmptyH=pageHeight * pageNum - lastNode.bottom - eleBounding1.top;
if(lastEmptyH>=70){
pageNum++;
this.ele.appendChild(this.createEmptyNode(lastEmptyH-15));
let lastFooterNode=this.creatFooterNode(pageNum,id);
lastFooterNode.style.paddingBottom=0+'px';
// console.log(lastFooterNode);
this.ele.appendChild(lastFooterNode);
}else{//最后一页放不下页脚,那么把上一个节点挤下去
lastEmptyH=lastEmptyH<0?0:lastEmptyH;
let k=0;
let i=domList.length-1;
lastEmptyH=lastEmptyH+domList[i].offsetHeight;
while(lastEmptyH<70){
k++;
lastEmptyH=lastEmptyH+domList[i-k].offsetHeight;
}
let lastDivParent=domList[i-k].parentNode;
pageNum++;
lastDivParent.insertBefore(this.createEmptyNode(lastEmptyH), domList[i-k]);
lastDivParent.insertBefore(this.creatFooterNode(pageNum+1,id), domList[i-k]);
lastDivParent.insertBefore(this.createHeaderNode(practiceName), domList[i-k]);
// 挤下去了一个节点后为新开的一页添加页脚,前面新添加了空白页、页脚、页眉,所以需要重新获取ele的高度
const eleBounding2 = this.ele.getBoundingClientRect();
const lastNode2=domList[domList.length-1].getBoundingClientRect();
let emptyHeight2=pageHeight * pageNum - lastNode2.bottom - eleBounding2.top;
this.ele.appendChild(this.createEmptyNode(emptyHeight2-15));
pageNum++;
let lastFooterNode=this.creatFooterNode(pageNum,id);
lastFooterNode.style.paddingBottom=0+'px';
this.ele.appendChild(lastFooterNode);
}
// 为页脚添加总页数
let allPageNodeList=Array.from(this.ele.getElementsByClassName('allPageNode')) ;
allPageNodeList.forEach(element => {
element.innerHTML=pageNum-1;
});
// 异步函数,导出成功后处理交互
this.getPDF(resolve, reject)
})
}
//创建页脚,内容根据需求改变
creatFooterNode(pageNum,id=null){
// const target = this.ele;
const pageNode = document.createElement('div');
pageNode.className = 'flexRowCenterColCenter footerNode';
pageNode.style.width=this.ele.scrollWidth *90%+'px';
pageNode.style.cssText=`box-sizing: border-box;padding-top:10px;padding-bottom:50px;`;
let str1='';
if(!id){
str1 = `<div style="border-top:3px solid #08B9BB;width:100%" class="flexRowCenterColCenter">${pageNum-1} /<span style='margin-left:2px' class='allPageNode'></span></div>`
}else{
str1 = `<div class="flexRowBetweenColCenter" style="width:100%;border-top:3px solid #08B9BB;"><span style='color:#fff'>--</span><span style='color:#fff'>${pageNum-1}</span><span style='color:#333;font-size:12px;'>ID:${id}</span></div>`
}
pageNode.innerHTML = str1;
// console.log(pageNode.offsetHeight);
return pageNode;
}
createEmptyNode(height){
const emptyDiv = document.createElement('div')
emptyDiv.className = 'emptyDiv'
emptyDiv.style.background = 'white'
emptyDiv.style.boxSizing='border-box'
emptyDiv.style.height = height-50+ 'px' // +25为了在换下一页时有顶部的边距
emptyDiv.style.width = this.ele.scrollWidth *90%+'px';
return emptyDiv;
}
//创建页眉,根据实际需求改变
createHeaderNode(practiceName){
const newNode = document.createElement('div')
newNode.className = 'flexRowBetweenColCenter'
newNode.style.width = this.ele.scrollWidth *90%+'px'
newNode.style.cssText="padding-top:0px;padding-bottom:10px;border-bottom:3px solid #08B9BB;margin-bottom:20px";
let str=""
if(practiceName!=""){
str = `<div style='font-size: 18px;text-align: left;'>${practiceName}</div>`
}else{
str=`<img style="height:24px" src=${logoUrl}/>`
}
newNode.innerHTML = str
return newNode;
}
}
export default PdfLoader