• 记录一个截图导出pdf的方法


    以下是导出的方法:

    // 通过截图、分页、处理文字截断后从dom生成pdf并导出
    import { nextTick } from 'vue'
    import { BxmMessage } from 'bxm-ui3'
    import html2Canvas from 'html2canvas'
    import JsPDF from 'jspdf'
    
    /**
     * 
     * @param {*} dom  导出的模块
     * @param {*} fileName  导出文件名称
     * @param {*} splitClassName   需要处理分页的类名(请将可能需要处理分页(即可能出现文字被分割)的元素都加上统一的类名)这里要保证整个dom中没有上下的margin,且每一行文字、图片所在的小容器都要设置统一的类名,把这个类名传入函数进行处理
     */
    export async function exportFileByPDF(dom, fileName, splitClassName) {
      // 获取正确的a4纸转换成像素值宽高
      let { a4Width, a4Height, currentDPT, dptValue } = getA4Size()
      a4Width = a4Width / dptValue
      a4Height = a4Height / dptValue
      // dom所在为起点
      let startHeight = dom.getBoundingClientRect().top
     
      // 处理分页元素
      let questionTitleList = dom.querySelectorAll('.' + splitClassName)
      await checkBandary(questionTitleList, a4Width, a4Height, startHeight, fileName)
      
      // 开始截图
      nextTick(() => {
        html2Canvas(dom, {
          scale: 2, // 设置缩放(设置成1导出文件文本框右边有一块灰色部分)
          allowTaint: true, // 允许跨域渲染图片
          useCORS: true, // 使用CORS从服务器加载图像
          logging: false, // 是否打印日志
          bgcolor: '#ffffff', // 背景色
        }).then((canvas) => {
          
          // 比例采用计算的比例,不固定
          let scale = canvas.width / a4Width
    
          // 用px单位生成pdf
          let pdf = new JsPDF('p', 'px', 'a4')    //A4纸,纵向
    
          // 处理数据
          let ctx = canvas.getContext('2d')
    
          // 按A4显示比例换算一页图像的像素高度,截图为了清晰度设置了scale:2,所以这里是乘,在最后加到pdf中时在缩小
          // let height = Math.floor(((a4Height / scale) * canvas.width / (a4Width / scale)))
          let imgHeight = a4Height * scale
    
          let renderedHeight = 0
    
          // 分页
          while (renderedHeight < canvas.height) {
            let page = document.createElement('canvas')
            page.width = canvas.width
            let pageHeight = Math.min(imgHeight, canvas.height - renderedHeight)
            // 可能内容不足一页
            page.height = pageHeight
    
            // 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
            // 这时等于是宽高放大两倍的
            page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, pageHeight), 0, 0)
            
            // 添加图像到页面
            // 加到pdf页面中的时候需要缩小,让a4纸放得下  
            // 0.2质量因子, jpeg格式压缩图片大小,质量因子控制图片质量,在0-1之间,值越低压缩得越小,质量越差
            // 两边各留25宽,让内容居中显示
            pdf.addImage(page.toDataURL('image/jpeg', 1), 'jpeg', 25, 25, a4Width / scale, Math.min(a4Height / scale, (a4Width / scale) * pageHeight / canvas.width))       
            // 这里不要用png,png下载文件太大了,页面会卡,下载出文件太大
            // pdf.addImage(page.toDataURL('image/png', 0.2), 'png', 0, 0, a4Width / scale, Math.min(a4Height / scale, (a4Width / scale) * pageHeight / canvas.width))       
            
            renderedHeight += imgHeight
    
            if (renderedHeight < canvas.height) {
              pdf.addPage() // 如果后面还有内容,添加一个空页
            }
          }
          pdf.save(fileName + '.pdf')
          // 下载完毕后隐藏分页,避免影响到预览效果
          let emptyDomList = document.getElementsByClassName('emptyDiv')
          Promise.all([...Array.from(emptyDomList).map(el => 
            new Promise((resolve, reject) => {
              try {
                el.remove()
                resolve()
              } catch {
                reject()
              }
            })
          )])
          BxmMessage({
            type: 'success',
            message: '试卷下载完成!'
          })
        })
      })
    }
    
    // 处理边界元素
    export async function checkBandary(domList, a4Width, a4Height, startHeight, fileName) {
      return new Promise((resolve, reject) => {
        if (!domList.length) resolve(false)
        try {
          // 进行分割操作,当dom内容已超出a4的高度,则将该dom前插入一个空盒子,把他挤下去,分割
          for (let i = 0; i < domList.length; i++) { 
            // 获取实时高度
            let topHeight =  domList[i].getBoundingClientRect().top
            let clientHeight = domList[i].getBoundingClientRect().height
            // let clientHeight = domList[i].clientHeight
            // 计算页数
            let multiple = Math.ceil((topHeight + clientHeight - startHeight) / a4Height)
            let height = startHeight + (multiple - 1) * a4Height
            // 判断元素是否跨页
            if (isSplit(domList, i, a4Height, height)) {
              // 获取该div的父节点
              let divParent = domList[i].parentNode
              // 创建页脚
              let newNode = document.createElement('div')
              newNode.className = 'emptyDiv'
              // newNode.innerHTML = `第${multiple}页`
              newNode.style.cssText = `display: flex; align-items: center; justify-content: center; width: calc(${a4Width}px - 70px); background: transparent; font-size: 10px;`
              
              // 高度为当前元素距离当前页底部的距离
              let _H = a4Height - (topHeight + clientHeight - height)
              // newNode.style.height = _H < 120 ? 120 + 'px' : _H + 'px'
              // newNode.style.height = 120 + 'px
              newNode.style.height = _H + 'px'
              // 获取兄弟节点
              let next = getNextNode(domList[i])
              // 判断兄弟节点是否存在
              if (next) {
                // 存在则将新节点插入到div的下一个兄弟节点之前,即div之后
                divParent.insertBefore(newNode, next)
              } else {
                // 不存在则直接添加到最后,appendChild默认添加到divParent的最后
                divParent.append(newNode)
              }
            }
          }
          resolve()
        } catch {
          reject()
        }
      })
    }
    
    // 判断当前元素是否跨页
    /**
     * 
     * @param {*} nodes 
     * @param {*} index 
     * @param {*} a4Height 
     * @param {*} startHeight 
     * @param {*} preFooterH  前一页的底部空白高度
     * @returns 
     */
    export function isSplit(nodes, index, a4Height, startHeight) {
      let topHeight = nodes[index].getBoundingClientRect().top
      let clientHeight = nodes[index].getBoundingClientRect().height
      if (index < nodes.length - 1) {
        let topHeightNext = nodes[index + 1].getBoundingClientRect().top
        let clientHeightNext = nodes[index + 1].getBoundingClientRect().height
        // 当前元素不跨页,下一个元素要跨页,说明在当前元素之后要分页
        if (nodes[index + 1] 
            && topHeight + clientHeight - startHeight + 60 <= a4Height 
            && (topHeightNext + clientHeightNext - startHeight + 60 > a4Height
            || (a4Height - (topHeight + clientHeight - startHeight) < clientHeightNext))) {
          return true
        }
        return false
      }
      return false
    }
    
    // 根据不同分辨率获取到a4纸不同的宽高
    export function getA4Size() {
      // A4纸的标准尺寸为210 mm x 297 mm,换算成英寸大约是8.27" x 11.69"。
      // 使用公式 像素 = 实际尺寸(英寸)× DPI 可以得到不同DPI下的像素值
      // 大多浏览器
      let defaultDPI = 96; // 默认DPI值
      let currentDPT = 0
      let dptValue = 1
      // IE浏览器
      if (window.screen.deviceXDPI !== undefined) {
        currentDPT = window.screen.deviceXDPI
      } else if (window.devicePixelRatio) { // 一般浏览器
        currentDPT = defaultDPI * window.devicePixelRatio
        dptValue = window.devicePixelRatio
      } else {
        currentDPT = defaultDPI
      }
      return {
        a4Width: 8.27 * currentDPT,
        a4Height: 11.69 * currentDPT,
        currentDPT,
        dptValue
      }
    }
    
    // 递归查找节点,因为有的节点v-if为false被视为注释节点
    export function getNextNode(node, nextNode = null) {
      if (!node.nextSibling) return null
      let next = node.nextSibling
      if (next.nodeType !== Node.COMMENT_NODE) {
        nextNode = next
        return nextNode
      } else {
        getNextNode(next)
      }
      
    }
    

    在导出之前需要保证图片被完全加载,否则会影响到高度计算

    // 保证图片加载完毕
    const loadImage = (imgElement) => {
      return new Promise((resolve, reject) => {
        if (imgElement.complete) {
          resolve(imgElement)
        } else {
          imgElement.onload = () => resolve(imgElement)
          imgElement.onerror = () => reject(`图片加载失败: ${imgElement.src}`)
        }
      })
    }
    

    最后的使用方法:

    nextTick(async () => {
            let dom = document.getElementById('pdfDom')
            let imgs = dom.getElementsByTagName('img')
            // 有图片时,要先将图片加载完整,否则导出时图片高度计算错误导致页面文字分割
            if (imgs.length) {
              exportLoading.value = true
              BxmMessage({
                type: 'warning',
                message: '文件数据加载中,该试卷==文件中图片较多,加载缓慢,为避免影响导出文件排版,请不要滚动页面和关闭弹窗,耐心等待!',
                showClose: true,
                duration: imgs.length * 1500
              })
              const promises = Array.from(imgs).map(async img => { await loadImage(img) })
              Promise.all(promises).then(() => {
                  // 等待浏览器重绘
                  let timer = setTimeout(() => {
                    clearTimeout(timer)
                    BxmMessage({
                      type: 'warning',
                      message: '文件加载完毕,正在导出文件,文件中图片较多,下载缓慢,请耐心等待,不要关闭弹窗!',
                      showClose: true
                    })
                    try {
                      exportFileByPDF(dom, '文件名', 'question-content-text')
                    } catch {
                      BxmMessage({
                        type: 'warning',
                        message: '导出失败!',
                        showClose: true
                      })
                    }
                  }, 1500)
              }).catch(() => {
                BxmMessageBox.confirm('部分图片加载失败,导出文件展示可能不全,是否继续导出?', '提示', {
                  confirmButtonText: '确定',
                  cancelButtonText: '取消',
                  type: 'warning'
                }).then(() => {
                  exportLoading.value = false
                  try {
                    exportFileByPDF(dom, '文件名', 'question-content-text')
                  } catch {
                    BxmMessage({
                      type: 'warning',
                      message: '导出失败!',
                      showClose: true
                    })
                  }
                }).catch(() => {
                  BxmMessage({
                    type: 'info',
                    message: '已取消导出!',
                    showClose: true
                  })
                  exportLoading.value = false
                })
              })
            } else {
              BxmMessage({
                message: '文件加载完毕,正在导出文件,请不要关闭弹窗!',
                type: 'warning',
                showClose: true
              })
              try {
                exportFileByPDF(dom, '文件名', 'question-content-text')
              } catch {
                BxmMessage({
                  type: 'warning',
                  message: '导出失败!',
                  showClose: true
                })
              }
            }
          })
    

    图片加载完毕之后确实能够导出完整的文件,其中文字、图片不会被分割,但这个方法有一个缺点是图片太多了,页面等待时间太长,效率不高的问题,还没找到如何解决。

    导出的文件示例:在这里插入图片描述
    在这里插入图片描述

  • 相关阅读:
    2023全国大学生软件测试大赛开发者测试练习题满分答案(PairingHeap2023)
    ASP.NET Core 6框架揭秘实例演示[35]:利用Session保留语境
    前端原生Html免费模板网站总结(带网址)
    Junit单元测试
    CoreDX DDS应用开发指南(5)开发发布应用程序
    1786_MTALAB代码生成把通用函数生成独立文件
    学会这三款软件,可以轻松完成录音转文字操作
    R语言作业wine 数据
    654. 最大二叉树
    python3使用sqlite3构建本地持久化缓存
  • 原文地址:https://blog.csdn.net/qq_42602282/article/details/139287253