Skip to content

前端导出 PDF 方案

用到的包

  • html2canvas 将 DOM 元素转换为 canvas 图像

  • jspdf 将 canvas 图像转换为 PDF 文件

  • print-js 打印 PDF 文件

前端导出方案

前端导出的工作原理

  • 直接在浏览器端捕获 DOM 元素

  • 使用 canvas 技术将 DOM 转换为图像

  • 将图像数据转换为 PDF 文件

  • 触发浏览器下载功能

前端导出的优势:

  • 实现简单,无需后端额外开发

  • 可以完全保留前端的样式和布局

  • 即时生成,无需等待服务器响应

  • 减轻服务器压力

前端导出的劣势

  • 大量 DOM 元素处理可能导致浏览器性能问题

  • 不同浏览器兼容性可能存在差异

  • 跨域资源处理需要额外配置

前端封装

  • 封装一个工具函数
js
/**
 * @Author: jsopy
 * @Date: 2025-12-07 16:51:36
 * @LastEditTime: 2025-12-07 19:01:08
 * @FilePath: /testpdf/src/utils/html2pdf.js
 * @Description:
 * @ 参数1.fileName - 可选,自定义文件名,如果未提供则使用默认格式
 * @ 参数2.pdfId - 必填,需要导出的pdf的id
 */
import html2Canvas from "html2canvas";
import { jsPDF } from "jspdf";
import printJS from "print-js";
export const exportpdftools = {
  // 导出为PDF文件
  // 参数说明:fileName - 可选,自定义文件名,如果未提供则使用默认格式
  // 返回Promise,便于外部组件获取导出状态
  exportToPdf(fileName, pdfId) {
    return new Promise((resolve, reject) => {
      // 如果未提供文件名,则使用默认格式
      const pdfFileName =
        fileName || "简历_" + this.getNowFormatDate() + ".pdf";
      var element = document.getElementById(pdfId); // 这个dom元素是要导出pdf的div容器
      var w = element.offsetWidth; // 获得该容器的宽
      var h = element.offsetHeight; // 获得该容器的高
      var offsetTop = element.offsetTop + 30; // 获得该容器到文档顶部的距离
      var offsetLeft = element.offsetLeft; // 获得该容器到文档最左的距离
      var canvas = document.createElement("canvas");
      var abs = 0;
      var win_i = document.body.clientWidth; // 获得当前可视窗口的宽度(不包含滚动条)
      var win_o = window.innerWidth; // 获得当前窗口的宽度(包含滚动条)
      if (win_o > win_i) {
        abs = (win_o - win_i) / 2; // 获得滚动条长度的一半
      }
      canvas.width = w * 2; // 将画布宽&&高放大两倍
      canvas.height = h * 2;
      var context = canvas.getContext("2d");
      context.scale(2, 2);
      context.translate(-offsetLeft - abs, -offsetTop);
      // 注意:不需要在主文档中预先查找元素,因为html2canvas会在克隆的iframe中工作
      // 所有元素判断都应该在ignoreElements函数内部基于当前元素的属性进行
      html2Canvas(element, {
        allowTaint: true, //允许跨域
        useCORS: true, //允许图片跨域
        scale: 2, // 提升画面质量,但是会增加文件大小
        ignoreElements: function (el) {
          try {
            // 安全地检查元素类名
            const hasClass = (element, className) => {
              if (!element || !element.classList) returnfalse;
              try {
                return element.classList.contains(className);
              } catch (e) {
                returnfalse;
              }
            };
            // 安全地检查元素类型
            const getElementTagName = (element) => {
              if (!element || !element.tagName) return "";
              try {
                return element.tagName.toLowerCase();
              } catch (e) {
                return "";
              }
            };
            const tagName = getElementTagName(el);
            // 安全地获取元素的实际文本内容(仅用于按钮检测)
            const getElementText = (element) => {
              if (!element) return "";
              try {
                // 仅去除空格和换行符,保留原始大小写
                return (element.textContent || element.innerText || "").replace(
                  /\s+/g,
                  ""
                );
              } catch (e) {
                return "";
              }
            };
            // 检查是否是按钮元素
            const isButton =
              tagName === "a-button" ||
              hasClass(el, "ant-btn") ||
              tagName === "button";
            // 检查按钮文本内容
            const elementText = getElementText(el);
            const isDownloadButton =
              isButton &&
              (elementText.includes("下载简历") ||
                elementText.includes("下载"));
            const isActionButton =
              isButton &&
              (elementText.includes("编辑") ||
                elementText.includes("删除") ||
                elementText.includes("新增"));
            // 排除特定元素:分页控件、审批人员区域和操作按钮容器
            const isPagination = hasClass(el, "ant-pagination");
            const isApprovePeople = el.id === "approvePeople";
            const isJBtnsContainer = hasClass(el, "jBtns");
            // 综合所有排除条件 - 只排除真正的操作按钮和特定容器,避免误删标题
            const shouldIgnore =
              isDownloadButton ||
              isActionButton ||
              isJBtnsContainer ||
              isPagination ||
              isApprovePeople;
            return shouldIgnore;
          } catch (e) {
            // 如果发生任何错误,默认不忽略该元素
            console.error("Error in ignoreElements:", e);
            returnfalse;
          }
        },
      })
        .then(async (canvas) => {
          var contentWidth = canvas.width;
          var contentHeight = canvas.height;
          //a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
          var imgWidth = 595.28;
          var imgHeight = (592.28 / contentWidth) * contentHeight;
          var pageHeight = 841.89; // A4纸的高度
          var position = 50; // 页面偏移
          // 第一个参数方向 第二个参数单位 第三个参数尺寸
          var pdf = new jsPDF("", "pt", "a4");
          // 调整边距:增加左侧padding(20px),减少右侧空白
          const leftPadding = 20; // 左侧边距
          const adjustedImgWidth = imgWidth - leftPadding; // 调整后的图片宽度
          // 解决黑色块问题的优化分页实现
          if (contentHeight < pageHeight) {
            // 当内容未超过pdf一页显示的范围,无需分页
            // 先设置白色背景
            pdf.setFillColor(255, 255, 255);
            pdf.rect(0, 0, imgWidth, pageHeight, "F");
            // 再添加图片内容
            var pageData = canvas.toDataURL("image/jpeg", 1.0);
            pdf.addImage(
              pageData,
              "JPEG",
              leftPadding,
              position,
              adjustedImgWidth,
              imgHeight
            );
          } else {
            // 计算页面数量 - 使用更合理的计算方式
            var pageHeightInCanvas = (pageHeight * contentWidth) / 592.28; // A4高度在canvas中的像素值
            var pageCount = Math.ceil(contentHeight / pageHeightInCanvas);
            // 逐页添加内容
            for (var i = 0; i < pageCount; i++) {
              // 添加新页面(除了第一页)
              if (i > 0) pdf.addPage();
              // 为每一页创建新的canvas
              var newCanvas = document.createElement("canvas");
              newCanvas.width = contentWidth;
              newCanvas.height = Math.min(
                pageHeightInCanvas,
                contentHeight - i * pageHeightInCanvas
              );
              var ctx = newCanvas.getContext("2d");
              // 设置白色背景,避免黑色块出现
              ctx.fillStyle = "#ffffff";
              ctx.fillRect(0, 0, newCanvas.width, newCanvas.height);
              // 绘制当前页的内容
              ctx.drawImage(
                canvas,
                0,
                i * pageHeightInCanvas,
                contentWidth,
                newCanvas.height
              );
              // 将新canvas转换为图片数据
              var newPageData = newCanvas.toDataURL("image/jpeg", 1.0);
              // 计算当前页的图片高度
              var currentPageHeight =
                (592.28 / contentWidth) * newCanvas.height;
              // 先设置白色背景
              pdf.setFillColor(255, 255, 255);
              pdf.rect(0, 0, imgWidth, pageHeight, "F");
              // 再添加图片内容
              pdf.addImage(
                newPageData,
                "JPEG",
                leftPadding,
                position,
                adjustedImgWidth,
                currentPageHeight
              );
            }
          }
          // 直接下载PDF文件而不是打开打印预览
          pdf.save(pdfFileName);
          // 保留原有的打印功能,但需要明确调用
          this.showPrintSuccessMessage = true;
          setTimeout(() => {
            this.showPrintSuccessMessage = false;
          }, 3000);
          // 通知导出成功完成
          resolve();
        })
        .catch((error) => {
          console.error("PDF导出失败:", error);
          reject(error);
        });
    });
  },
  // 打印功能
  printResume() {
    var element = document.getElementById(pdfId);
    html2Canvas(element, {
      allowTaint: true,
      useCORS: true,
      scale: 2,
    }).then((canvas) => {
      const jsPDFBytes = canvas.toDataURL("image/jpeg", 1.0);
      printJS({ printable: jsPDFBytes, type: "image", showModal: true });
    });
  },
  // 获取当前格式化日期
  getNowFormatDate() {
    var date = newDate();
    var seperator1 = "";
    var year = date.getFullYear();
    var month = date.getMonth() + 1;
    var strDate = date.getDate();
    var hour = date.getHours();
    var min = date.getMinutes();
    var second = date.getSeconds();
    if (month >= 1 && month <= 9) {
      month = "0" + month;
    }
    if (strDate >= 0 && strDate <= 9) {
      strDate = "0" + strDate;
    }
    if (hour >= 0 && hour <= 9) {
      hour = "0" + hour;
    }
    if (min >= 0 && min <= 9) {
      min = "0" + min;
    }
    if (second >= 0 && second <= 9) {
      second = "0" + second;
    }
    var currentdate =
      year +
      seperator1 +
      month +
      seperator1 +
      strDate +
      seperator1 +
      hour +
      seperator1 +
      min +
      seperator1 +
      second;
    return currentdate;
  },
};

前端使用

vue
<template>
  <div>
    <div class="title">前端导出PDF 需要使用两个类库 1.html2canvas 2.jspdf</div>
    <el-button @click="handlepdf">导出pdf </el-button>
    <div id="DomPdf" style="width: 800px; height: 1200px"></div>
  </div>
</template>

<script setup>
import { exportpdftools } from "@/utils/html2pdf";
const isExporting = ref(false);
const handlepdf = async () => {
  // 导出前设置导出状态为true
  isExporting.value = true;
  // 导出PDF,第一个参数就是文件名,第二个参数就是要导出的DOM元素
  exportpdftools
    .exportToPdf("测试PDF文件", "app")
    .then((res) => {
      console.log(res);
      // 导出后设置导出状态为false
      isExporting.value = false;
    })
    .catch((e) => {
      console.log(e);
      // 导出后设置导出状态为false
      isExporting.value = false;
    });
};
</script>

<style lang="scss" scoped></style>