pdf切割线及html2canvas偏移问题处理

效果图:

分割
签名置底

写之前参考了网上的一些文章思路传送门,以为可以找到可以直接使用的轮子,可惜或多或少都有问题或者不能满足需求,所以只能自己写。

思路:

1.在页面建一个渲染数据的div.pdf-box,可以设置这个div不可见(注意:这个div只是负责渲染数据,获取节点生成图片的不是这个节点)
2.建一个负责渲图的div#pdfDom,把div.pdf-box的子元素渲染进来(注意:子元素要有统一个标识,比如给一个同名的class)
  • 2.1 创建一个以A4纸尺寸为比例的div.newNode1,循环div.pdf-box获取里面的子元素添加到div.newNode1中,同时计算添加到里面的子元素的高(offsetHeight,有边距的还要加上边距)是否大于页面的高,如果大于则停止添加,将div.newNode1添加到div#pdfDom中,然后再生成一个新的div.newNode1重复这个操作,一直到把所有元素添加完
  • 2.2 上一步中有一个要注意的点,当子元素撑满一页后,新建了一个div.newNode1,记得将循环的下标回退到当前对象,否则会导致当前子元素丢失
  • 2.3 最后一页有三种情况:
    a.只有一个模块:从上一页移除最后一个元素放到当前模块前面,使最后一页有两个元素(不要问为什么要这样,产品给的需求);
    b.有两个模块:不用管,正常渲染;
    c.大于两个模块:判断除了最后两个元素外,其余元素的高相加是否大于页面高度的60%(这个比例自己定,主要看剩下的高度能不能放得下剩余的两个元素),大于则说明不够放剩余两个模块,把最后的两个放到下一页。
  • 2.4 在最后一个模块的前面插入div将该模块撑到底部
3.通过html2canvasdiv#pdfDom转成图片进行预览
4.通过jspdf将图片转成pdf格式并下载

html代码:

<template>
    // 负责渲染节点内容
   <div ref="_pdfHtml" id="pdfHtml" class="pdf-box">
      // 里面是页面结构
     <div class="js_p"></div>
     <div class="js_p"></div>
     <div class="js_p"></div>
   </div>
   // 负责渲图的节点
   <div ref="pdfDom" id="pdfDom"></div>
</template>

js代码:

<script>
import html2canvas from "html2canvas"
import JsPDF from 'jspdf'
import dayjs from 'dayjs'

methods:{
 async applyDom1() {
      // console.log('this.pdfData---', this.pdfData);
      document.documentElement.scrollTop = 0;
      document.documentElement.scrollLeft = 0;
      document.body.scrollTop = 0;
      document.body.scrollLeft = 0;
      
      // pdf下载思路:先将数据渲染在id为pdfHtml的dom上,然后遍历里面的子div放到class为pdfDiv的div中,通过子元素相加的ofsetHeight得到saveH,通过saveH判断内容高度是否已超过A4纸高度,如果超过就将已生成的pdfDiv放入id为pdfDom的元素中(这个元素是渲图用的),然后新生成一个div循环存放下一个子元素继续这个操作,否则继续相加offsetHieght,直到最后一个元素。
      try{
        let imgUrl = '', canvas = '';
        // A4纸尺寸
        const A4_WIDTH = 592.28, A4_HEIGHT = 841.89;
        // 获取html节点及要插入的子元素
        let imageWrapper = document.querySelector("#pdfHtml") 
        let lableListID = imageWrapper.querySelectorAll(".js_div");
        // 要渲染成图片的dom对象
        let pdfDiv = document.querySelector("#pdfDom")
        // 按照A4的宽高比
        let pageH = pdfDiv.offsetWidth / A4_WIDTH * A4_HEIGHT
        // 记录的一页中子元素的高和保存子元素的数组
        let saveH = 0, divSaveArr = []
        // 循环子元素
        for (let i = 0, len = lableListID.length; i < len; i++) {
          // 累加子元素的高,30是模块间的margin-top值
          saveH += lableListID[i].offsetHeight + 30
          // console.log('当前i---', i, lableListID[i]);
          if(saveH >= pageH) {
            // 高度超过一页,把之前生成的一页数据插入到渲染的div中,重新生成新的一页数据并回退i到当前子元素,清空数组
            // console.log('divSaveArr111---', i,  divSaveArr);
            
            if(divSaveArr.length >= 1) {
               console.log('i000---', i);
              let newNode1 = this.addDiv(divSaveArr, pageH, saveH)
              $(pdfDom).append(newNode1)
              saveH = 0
              divSaveArr = []
              i = i - 1 // 回退到当前的i
            } else if(divSaveArr.length < 1) {
              let _h = 0, 
              obj = $(lableListID[i]).find('.js_line'), 
              arr = []
              for(let j = 0, len = obj.length; j < len; j++) {
                _h += obj[j].offsetHeight + 30
                if(_h >= pageH) {
                  let divDom = document.createElement('div');
                  $(divDom).addClass($(lableListID[i]).attr('class'))
                  $(divDom).trigger("create");
                  let table = document.createElement('table');
                  $(table).css({
                    width: '100%',
                    background: '#FFFFFF',
                    border: '2px solid #CD3C3A',
                    borderRadius: '10px',
                    color: '#333333',
                    marginTop: '30px',
                  })
                  arr.forEach(item => { $(table).append(item) })
                  $(divDom).append(table)
                  
                  
                  let newNode1 = this.createDiv(pageH)
                  $(newNode1).append(divDom)
                  $(pdfDom).append(newNode1)
                  _h = 0
                  arr = []
                  j = j - 1
                } else {
                  arr.push(obj[j])
                  if(j === obj.length - 1) {
                    let divDom = document.createElement('div');
                    $(divDom).addClass($(lableListID[i]).attr('class'))
                    let newTabel = document.createElement('table');
                    $(newTabel).css({
                      width: '100%',
                      background: '#FFFFFF',
                      border: '2px solid #CD3C3A',
                      borderRadius: '10px',
                      color: '#333333',
                      marginTop: '30px',
                    })
                    let newH = 0
                    arr.forEach(item => {
                      $(newTabel).append(item)
                      
                      setTimeout(() => {
                        newH += item.offsetHeight + 30
                        console.log('item forEach--', item, item.offsetHeight);
                        saveH = newH
                      }, 250);
                    })
                    $(divDom).append(newTabel)
                    // saveH = newH
                    divSaveArr.push(divDom)
                    _h = 0
                    newH = 0
                    arr = []
                  } 
                }
              }
            }
            
          } else {
            // console.log('saveH < pageH-- ',saveH, pageH);
            // 未超过一页,将子元素添加到保存的数组中
            divSaveArr.push(lableListID[i])
            // console.log('i 2222--', i, divSaveArr, lableListID[i]);
         
            // 循环到最后一个元素时
            if(i == lableListID.length - 1) {
              // console.log('循环到最后一个元素时--');
              // 最后一页只有一个div,移除上一页最后一个元素加入最后一页
              if(divSaveArr.length === 1) {
                // console.log('===1');
                $(pdfDom).children("div:last").children("div:last").remove()
                divSaveArr.splice(divSaveArr.length - 1, 0, lableListID[i - 1])
                let newNode1 = this.addDiv(divSaveArr, pageH, saveH)
                $(pdfDom).append(newNode1)
                saveH = 0
                divSaveArr = []
              } else if(divSaveArr.length > 2) { 
                // console.log('>>>> 2', divSaveArr);
                // 最后一页的元素大于2,分两种情况,页面高度足够页面签名模块,放一起,不够,则把倒数第二块内容连同签名模块一起放到最后一页,其余内容放到倒数第二页
                let _h = 0, arr2 = [lableListID[i-1], lableListID[i]]
                for(let j = 0, len = divSaveArr.length - 2; j < len; j ++) {
                  _h += divSaveArr[j].offsetHeight
                }
                // 最后一页除了最后两个内容,其余内容>页面高度的70%,分两页渲染
                if(_h >= pageH * 0.7) {
                  // console.log('_h >= pageH * 0.7---', _h);
                  // 渲染倒数第二页
                  let newNode1 = this.createDiv(pageH)
                  for(let k = 0, len = divSaveArr.length - 2; k < len; k ++) {
                    $(newNode1).append(divSaveArr[k])
                  }
                  $(pdfDom).append(newNode1)
                  // 渲染最后一页
                  let newNode2 = this.addDiv(arr2, pageH, lableListID[i-1].offsetHeight, true)
                  $(pdfDom).append(newNode2)
                  saveH = 0
                  divSaveArr = []
                } else {
                  // console.log('在一页---');
                  // 多个模块都在最后一页
                  let newNode1 = this.addDiv(divSaveArr, pageH, saveH, true)
                  $(pdfDom).append(newNode1)
                  saveH = 0
                  divSaveArr = []
                }
              } else {
                // console.log('===2');
                // 剩余两个子元素
                let newNode1 = this.addDiv(divSaveArr, pageH, saveH, true)
                $(pdfDom).append(newNode1)
                saveH = 0
                divSaveArr = []
              }
            }
            
          
          }
        }
        await html2canvas(pdfDom, {
          allowTaint: true,
          x: pdfDom.getBoundingClientRect().left,     // 绘制的dom元素相对于视口的位置
          y: pdfDom.getBoundingClientRect().top,        
          width: pdfDom.offsetWidth,                   // 因为多出的需要剪裁掉,
          height: pdfDom.offsetHeight,
          backgroundColor: "#F9F0E9", //一定要设置背景颜色,否则有的浏览器就会变花~,比如Edge
          useCORS: true,
          // scale: 2,      // 图片模糊
          dpi: 350, // 
        }).then((res) => {
          // console.log(res);
          let dataURL = res.toDataURL("image/png");
          imgUrl = dataURL;
          canvas = res
        })
        this.tableLoading = false
        this.$refs._download.open(imgUrl, canvas)
      } catch(err) {
        console.log(err);
        this.tableLoading = false
        this.$message.warning('导出出错,请联系系统维护人员')
      }
    },
    // 创建新的容器div
    createDiv(pageH) {
      let newNode1 = document.createElement('div');
      $(newNode1).addClass(' pdfDiv').css({
        width: '100%',
        height: pageH + 'px',
        backgroundImage: `url(${bgImg})`,
        backgroundSize: '100% 100%',
        padding: '46px',
        fontSize: '18px',
      })
      return newNode1
    },
    // 设置模块位置在页面底部
    addDiv(divSaveArr, pageH, saveH, type) {
      let newNode1 = this.createDiv(pageH)
      // 占位div,把签名模块往下顶
      if(type) {
        let emptyDiv = document.createElement('div');
        emptyDiv.style.height = pageH - saveH - 100 + 'px'
        // emptyDiv.style.background = '#fff'
        divSaveArr.splice(divSaveArr.length - 1, 0, emptyDiv)
      }
      // 循环保存子元素的数组,将节点添加在一页中
      divSaveArr.forEach(item => {
        $(newNode1).append(item)
      })    
      return newNode1
    },
// 确定导出PDF图片
    downloadPDF() {
      // console.log('确定导出---', this.canvasData);
      if(this.downloading) return;
      this.downloading = true
      let canvas = this.canvasData
      
      //内容的宽度
      let contentWidth = canvas.width;
      //内容高度
      let contentHeight = canvas.height;
      //一页pdf显示html页面生成的canvas高度,a4纸的尺寸[595.28,841.89];
      let pageHeight = contentWidth / 592.28 * 841.89;
      //未生成pdf的html页面高度
      let leftHeight = contentHeight;
      //页面偏移
      let position = 0;
      //a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
      let imgWidth = 595.28;
      // let imgHeight = 841.89 / contentWidth * contentHeight;
      let imgHeight = 592.28 / contentWidth * contentHeight;
      //canvas转图片数据
      let pageData = canvas.toDataURL('image/jpeg', 1.0);
      //新建JsPDF对象
      let PDF = new JsPDF('', 'pt', 'a4');
      //判断是否分页
      if (leftHeight < pageHeight) {
        PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
      } else {
        while (leftHeight > 0) {
          PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight);
          leftHeight -= pageHeight;
          position -= 841.89;
          // 还有多余的另起一页
          if (leftHeight > 2) PDF.addPage()
        }
      }
      this.downloading = false
      //保存文件
      PDF.save(`xxx-${dayjs().valueOf()}` + '.pdf')
    },
}
</script>

代码可以直接复用。

新增

因为以上方法不满足需求,会有过多空白处,所以只能继续细化,将每个div里的内容都单独抽出来弄成一个子元素,即原先的方式是:

<div class="js_div">
  <p>内容</p>
  <p>内容</p>
  <p>内容</p>
  <p>内容</p>
</div>
<div class="js_div">
  <p>内容</p>
  <p>内容</p>
  <p>内容</p>
  <p>内容</p>
</div>

改成

<div class="js_div">
  <p>内容</p>
</div>
<div class="js_div">
  <p>内容</p>
</div>
<div class="js_div">
  <p>内容</p>
</div>
改进后效果

然后通过拿到所有的js_div按照上面的方式去写就可以了,方法也是差不多的,就是要注意下样式丢失的问题。贴一下渲染的方法:

// 导出的html渲染方法
    async applyDom() {
      let ST = document.documentElement.scrollTop || document.body.scrollTop; 
      let SL = document.documentElement.scrollLeft || document.body.scrollLeft;
            document.documentElement.scrollTop = 0;
            document.documentElement.scrollLeft = 0;
            document.body.scrollTop = 0;
            document.body.scrollLeft = 0;
 
      try{
        let imgUrl = '', canvas = '';
        // A4纸尺寸
        const A4_WIDTH = 592.28, A4_HEIGHT = 841.89;
        // 获取html节点及要插入的子元素
        let imageWrapper = document.querySelector("#pdfHtmlNew") 
        let lableListID = imageWrapper.querySelectorAll(".js_div");
        // 要渲染成图片的dom对象
        let pdfDiv = document.querySelector("#pdfDom")
        // 按照A4的宽高比
        let pageH = pdfDiv.offsetWidth / A4_WIDTH * A4_HEIGHT
        // 记录的一页中子元素的高和保存子元素的数组
        let saveH = 0, divSaveArr = [], mtH = 0
        // 循环子元素
        for (let i = 0, len = lableListID.length; i < len; i++) {
          // 累加子元素的高,30是模块间的margin-top值
          mtH = $(lableListID[i]).hasClass('mt30') ? 30 : 0
          saveH += lableListID[i].offsetHeight + mtH
          // 46 * 2 是页面设置的padding值
          if(saveH >= pageH - 46 * 2) {
            // 高度超过一页,把之前生成的一页数据插入到渲染的div中,重新生成新的一页数据并回退i到当前子元素,清空数组
            // 最后一个元素如果没有底部边框的加上
            if($(divSaveArr[divSaveArr.length-1]).hasClass('none-bdb')) {
              $(divSaveArr[divSaveArr.length-1]).removeClass('none-bdb')
            }
            let newNode1 = this.addDiv(divSaveArr, pageH, saveH)
            $(pdfDom).append(newNode1)
            saveH = 0
            mtH = 0
            divSaveArr = []
            i = i - 1 // 回退到当前的i
          } else {
            // 未超过一页,将子元素添加到保存的数组中
            divSaveArr.push(lableListID[i])
         
            // 循环到最后一个元素时
            if(i == lableListID.length - 1) {
              // console.log('循环到最后一个元素时--');
              // 最后一页只有一个div,移除上一页最后一个元素加入最后一页
              if(divSaveArr.length === 1) {
                // console.log('===1');
                $(pdfDom).children("div:last").remove()
                divSaveArr.splice(divSaveArr.length - 1, 0, lableListID[i - 1])
                let newNode1 = this.addDiv(divSaveArr, pageH, saveH)
                $(pdfDom).append(newNode1)
                saveH = 0
                mtH = 0
                divSaveArr = []
              } else if(divSaveArr.length > 2) { 
                // 最后一页的元素大于2,分两种情况,页面高度足够页面签名模块,放一起,不够,则把倒数第二块内容连同签名模块一起放到最后一页,其余内容放到倒数第二页
                let _h = 0, arr2 = [lableListID[i-1], lableListID[i]]
                for(let j = 0, len = divSaveArr.length - 2; j < len; j ++) {
                  _h += divSaveArr[j].offsetHeight
                }
                // 最后一页除了最后两个内容,其余内容>页面高度的70%,分两页渲染
                if(_h >= pageH * 0.8) {
                  // 渲染倒数第二页
                  let newNode1 = this.createDiv(pageH)
                  for(let k = 0, len = divSaveArr.length - 2; k < len; k ++) {
                    $(newNode1).append(divSaveArr[k])
                  }
                  $(pdfDom).append(newNode1)
                  // 渲染最后一页
                  let newNode2 = this.addDiv(arr2, pageH, lableListID[i-1].offsetHeight, true)
                  $(pdfDom).append(newNode2)
                  saveH = 0
                  mtH = 0
                  divSaveArr = []
                } else {
                  // console.log('在一页---');
                  // 多个模块都在最后一页
                  let newNode1 = this.addDiv(divSaveArr, pageH, saveH, true)
                  $(pdfDom).append(newNode1)
                  saveH = 0
                  mtH = 0
                  divSaveArr = []
                }
              } else {
                // console.log('===2');
                // 剩余两个子元素
                let newNode1 = this.addDiv(divSaveArr, pageH, saveH, true)
                $(pdfDom).append(newNode1)
                saveH = 0
                mtH = 0
                divSaveArr = []
              }
            }
          }
        }
        document.body.scrollTop = document.documentElement.scrollTop = 0;
        // 防止html没有组成完就开始生成图片
        setTimeout(async () => {
          await html2canvas(pdfDom, {
            allowTaint: true,      
            width: pdfDom.offsetWidth,                   // 因为多出的需要剪裁掉,
            height: pdfDom.offsetHeight,
            backgroundColor: "#F9F0E9", //一定要设置背景颜色,否则有的浏览器就会变花~,比如Edge
            useCORS: true,
            scale: 2,      // 图片模糊
            dpi: window.devicePixelRatio * 2, // 
          }).then((res) => {
            let dataURL = res.toDataURL("image/png");
            imgUrl = dataURL;
            canvas = res
          })
          this.tableLoading = false
          document.querySelector("#pdfDom").innerHTML = "";
          this.$refs._download.open(imgUrl, canvas)
        }, 1000)
      } catch(err) {
        console.log(err);
        this.tableLoading = false
        this.$message.warning('导出出错')
      }
    },

另外记录一个遇到的问题:

1.html2canvas偏移
项目的版本是1.0.0-rc.7,通过网上查到设置scrollX: 0,scrollY: 0无效,设置setTimeout无效,然后死命想怎么设置scrollX,scrollY,x,y都没用,直到找到这篇,福音啊,想来想去就是没想到版本的问题,这篇里有两种方式,第一种没验证,直接更新版本和设置对应的配置解决了。

处理方法:
下载新版本(目前是1.4.1版本), 配置参数dpi: window.devicePixelRatio, scale:2

html2canvas(pdfDom, {
    allowTaint: true,     
    width: pdfDom.offsetWidth,                   // 因为多出的需要剪裁掉,
    height: pdfDom.offsetHeight,
    backgroundColor: "#F9F0E9", //一定要设置背景颜色,否则有的浏览器就会变花~,比如Edge
    useCORS: true,
    scale: 1,      // 图片模糊
    dpi: window.devicePixelRatio * 2, // 
 }).then((res) => {
    let dataURL = res.toDataURL("image/png");
    imgUrl = dataURL;
})

2.html2canvas截图不完整,底部有空白
这个问题是随机的,有些浏览器可以有些不行,查到有说法因为内容长度超出了html2canvas的有效区域,所以只能通过缩小放大比例来解决。方法是有用的,但是原因感觉应该不是这个,先记着吧。
处理方法:
scale设置为1(scale: 1)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容