前端在vue2框架中导出PDF

 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

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值