前端在vue2框架中导出PDF

2023-11-05

 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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

前端在vue2框架中导出PDF 的相关文章

随机推荐

  • Swagger

    第一节 Swagger 简介 1 企业开发所面临的问题 在前后端分离开发的情况下 前端开发人员经常抱怨后端开发人员给的接口文档与实际情况不一致 后端开发人员觉得编写接口文档太过于消耗精力 而且更新也不及时 以至于前后端开发人员经常出现争吵的
  • 【kickstart 2021 round C】前三题python题解

    第一题 题目 给定长度为N的字符串S 它是由字母表上的前K个字母构成 问字典序小于S且长度为N的回文字符串 由字母表上的前K个字母构成 有多少个 解释 参考官方题解 计算多少个长度为N 2的字符串的字典序小于S math ceil N 2
  • 动态动态规划(DDP)

    1 Problem E Codeforces 一 题目大意 给你一个无向图 第i和i 1条边的权值是w i 问你每个点不在自己原本的点的代价是多少 会有q组询问 表示修改第i条边的权值 二 解题思路 可以观察到 完成这个操作需要每条边经过两
  • [LeetCode]初级算法-字符串- 实现strStr()

    标题 实现strStr 实现 strStr 函数 给定一个 haystack 字符串和一个 needle 字符串 在 haystack 字符串中找出 needle 字符串出现的第一个位置 从0开始 如果不存在 则返回 1 示例 1 输入 h
  • linux防火墙启动、停止、查看

    停止 防火墙 service iptables stop 启动防火墙 service iptables start 查看防火墙配置 iptables L n 修改的内容只是暂时保存在内存中 如果重启后还要生效 则要保存一下 service
  • Web Storage是什么?Web Storage详解

    Web Storag是HTML5引入的一个非常重要的功能 可以将数据存储在本地 如保存用户的偏好设置 复选框的选中状态 文本框默认填写的值等 用户在浏览器中刷新网页时 网页通过Web Storage就可以知道用户之前所做的一些修改 而不需要
  • Autoware 1.14(WSL2) 与LG SVL Simulator(Win11)联合仿真

    参考Couldn t find executable named rqt lgsvl simulator configurator below home autoware Autoware ros in li4692625的博客 CSDN博
  • Hyperledger Fabric核心配置文件(1)

    1 core yaml core yaml配置文件是Peer节点的示例配置文件 具体路径在fabric samples config目 录下 该core yaml示例配置文件共指定了如下六大部分内容 1 日志部分 日志记录级别有6种 CRI
  • JDBC访问数据库

    一 简介 JDBC 全称 Java DataBase Connection 数据库连接技术 可以根据驱动包连接不同类型的数据库 二 JDBC API JDBC API是java中位于java sql包下的一个数据库访问统一接口 通过它来跟数
  • 无监督学习KMeans学习笔记和实例

    KMeans算法是一种简单的算法 能够快速 高效的对数据集进行聚类 一般只要通过几次迭代即可 KMeans可以作为一种聚类工具 同时也可以作为一种降维的方式进行特征降维 KMeans可以通sklearn cluster kmeans中进行调
  • 测试 开发 5 年从外包 18K 跳槽去字节 28K+12,啃完这份笔记你也可以

    软件测试是一个付出就有回报的工作 可能很多人会说软件测试就是吃青春饭 然而其他工作又何尝不是 没有哪一家公司养尸位素餐之人 大龄员工有被辞退的 也有没被辞退的 干任何职业 抱着一劳永逸的心态 在岗位上开始混的中青年 早就该辞了 粉丝小王转行
  • 测试方法——边界值法

    边界值测试方法 边界值方法是一种比较常用的测试方法 在很多软件测试中都会应用到 一 应用条件 只要有输入框输入数据的地方 就可以用边界值这一方法来测试 一般与等价类划分共同使用 找到有效数值和无效数值之间的分界点及其两边的点进行测试 二 测
  • Jmeter进阶使用指南-使用参数化

    Apache JMeter是一个广泛使用的开源负载和性能测试工具 在进行性能测试时 我们经常需要模拟不同的用户行为和数据 这时候 参数化就显得尤为重要 此文主要介绍如何在JMeter中使用参数化 什么是参数化 参数化是一种将静态值替换为动态
  • 深入理解HashMap和LinkedHashMap的区别

    简介 我们知道HashMap的变量顺序是不可预测的 这意味着便利的输出顺序并不一定和HashMap的插入顺序是一致的 这个特性通常会对我们的工作造成一定的困扰 为了实现这个功能 我们可以使用LinkedHashMap LinkedHashM
  • 【配电变电站的最佳位置和容量】基于遗传算法的最优配电变电站放置(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 参考文献 4 Matlab代码实现 1 概述 基于遗传算法的最优配电变电站放置 为了实现
  • iseacms1.0漏洞复现

    iseacms1 0漏洞复现 作者 admin 时间 2021 06 29 分类 漏洞复现 Index php源码 文件包含 参数转义了 0 利用方式有限在网站目录有phpinfo php文件的前提下 payload为 index r te
  • 【十三】Nacos 服务注册和配置中心

    目录 Nacos 初识 Nacos 服务部署 注册中心服务部署 服务提供者注册到Nacos 服务消费者从Nacos获取服务 负载均衡 Nacos 服务详解 实列服务详情详解 Nacos 初识 Nacos Dynamic Naming and
  • Android 输入框的输入提示效果(AutoCompleteTextView)

    在一些体验较好的APP中 输入框输入时会有相应的提示 让人能够很快的通过点击提示进入下一步 这里 我就通过自己构思 实现了一个通过 SharedPreferences 保存的输入提示 demo 实现 1 实现一个 SharedPrefere
  • opencv项目实战(二)——文档扫描OCR识别

    一 项目描述 二 代码详解 2 1 预定义参数 2 2 辅助函数 2 3 文档矫正 2 4 文档识别 三 项目完整代码 一 项目描述 目的 将图片中的文档矫正 并识别文档内容 输入与输出 方法流程 核心思想 采用tesseract ocr进
  • 前端在vue2框架中导出PDF

    1 需求 导出具有页眉页脚 页码的Pdf 并且解决Pdf分割的问题 2 实现思路 该需求主要的难点在于分页的时候容易出现分割问题 并且要将页眉页脚加进去 实现的大概思路 1 先使用jsPDF html2canvas将页面可以导出 2 第一页