前端生成长图pdf并支持分页和防止分页处内容截断

106 阅读1分钟
/*
 * @Author: xxx
 * @Date: 2025-07-08 9:59:35
 * @LastEditors: xxx
 * @LastEditTime: 2025-08-20 18:03:59
 * @Description: 用于将 DOM 元素转换为 PDF 文件,支持分页和防止内容截断。
 */

import html2canvas from 'html2canvas'
import JsPDF from 'jspdf'

export default class DomToPdf {
  /**
   * 构造函数,初始化 PDF 生成所需参数。
   * @param {Object} options - 配置选项
   * @param {HTMLElement} options.targetEl - 需要转换为 PDF 的 DOM 元素
   * @param {string} options.fileName - 输出 PDF 文件名(不含扩展名)
   * @param {string} options.splitClassName - 用于分页的元素类名(也就是分页时不想被截断的元素类名, 加了类名就会去计算要不要分页)
   * @param {string} [options.emptyDivBgColor='white'] - 分页空白块的背景颜色
   * @param {number} [options.pageWidth=595] - 默认A4纸宽度
   * @param {number} [options.pageHeight=842] - 默认A4纸高度
   */
  constructor(options) {
    const {
      targetEl,
      fileName,
      splitClassName,
      emptyDivBgColor = 'white',
      pageWidth = 595,
      pageHeight = 842
    } = options

    this.ele = targetEl
    this.pdfFileName = fileName
    this.splitClassName = splitClassName
    this.emptyDivBgColor = emptyDivBgColor
    this.pageWidth = pageWidth
    this.pageHeight = pageHeight
  }

  /**
   * 处理分页逻辑,插入空白 div 以防止内容截断。
   * @param {number} scale - 渲染缩放比例
   * @returns {void}
   */
  handlePagination(scale) {
    this.ele.style.height = 'initial'
    this.scale = scale || 2
    const target = this.ele
    const pageHeight = (target.scrollWidth / this.pageWidth) * this.pageHeight
    const domList = document.getElementsByClassName(this.splitClassName)
    let pageNum = 1
    const eleBounding = this.ele.getBoundingClientRect()

    for (let i = 0; i < domList.length; i++) {
      const node = domList[i]
      const bound = node.getBoundingClientRect()
      const offset2Ele = bound.top - eleBounding.top
      const currentPage = Math.ceil(
        (bound.bottom - eleBounding.top) / pageHeight
      )
      if (pageNum < currentPage) {
        pageNum++
        const divParent = node.parentNode
        const newNode = document.createElement('div')
        newNode.className = 'emptyDiv'
        newNode.style.background = this.emptyDivBgColor
        newNode.style.height = `${
          pageHeight * (pageNum - 1) - offset2Ele + 30
        }px`
        newNode.style.width = '100%'
        divParent.insertBefore(newNode, node)
      }
    }
  }

  /**
   * 生成 PDF Blob 对象。
   * @param {Object} [options={}] - 配置选项
   * @param {number} [options.scale=2] - 控制 PDF 渲染清晰度的缩放比例
   * @returns {Promise<Blob>} - 返回 PDF Blob 对象
   */
  async exportToBlob(options = {}) {
    return new Promise((resolve, reject) => {
      try {
        this.handlePagination(options.scale || 2)
        const ele = this.ele
        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 * this.scale
        canvas.height = eleH * this.scale
        const context = canvas.getContext('2d')
        context.scale(this.scale, this.scale)
        context.translate(-eleOffsetLeft - abs, -eleOffsetTop)

        html2canvas(ele, {
          scale: this.scale,
          useCORS: true,
          logging: false
        }).then((canvasResult) => {
          const contentWidth = canvasResult.width
          const contentHeight = canvasResult.height
          const pageHeight = (contentWidth / this.pageWidth) * this.pageHeight
          let leftHeight = contentHeight
          let position = 0
          const imgWidth = this.pageWidth - 10
          const imgHeight = (this.pageWidth / contentWidth) * contentHeight
          const pageData = canvasResult.toDataURL('image/jpeg', 1.0)
          const pdf = new JsPDF('', 'pt', 'a4')

          if (leftHeight < pageHeight) {
            pdf.addImage(pageData, 'JPEG', 5, 0, imgWidth, imgHeight)
          } else {
            while (leftHeight > 0) {
              pdf.addImage(pageData, 'JPEG', 5, position, imgWidth, imgHeight)
              leftHeight -= pageHeight
              position -= this.pageHeight
              if (leftHeight > 0) {
                pdf.addPage()
              }
            }
          }

          const pdfBlob = pdf.output('blob')
          const doms = document.querySelectorAll('.emptyDiv')
          for (let i = 0; i < doms.length; i++) {
            doms[i].remove()
          }
          this.ele.style.height = ''
          resolve(pdfBlob)
        }).catch((error) => {
          reject(error)
        })
      } catch (error) {
        reject(error)
      }
    })
  }

  /**
   * 导出 PDF 文件并自动下载
   * @param {Object} [options={}] - 配置选项
   * @param {number} [options.scale=2] - 控制 PDF 渲染清晰度的缩放比例
   * @returns {Promise<void>}
   */
  async exportToPdf(options = {}) {
    return new Promise((resolve, reject) => {
      try {
        this.handlePagination(options.scale || 2)
        this.exportToBlob(options).then((pdfBlob) => {
          const url = URL.createObjectURL(pdfBlob)
          const link = document.createElement('a')
          link.href = url
          link.download = `${this.pdfFileName}.pdf`
          link.click()
          URL.revokeObjectURL(url)
          resolve()
        }).catch((error) => {
          reject(error)
        })
      } catch (error) {
        reject(error)
      }
    })
  }
}

image.png