LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

【Web开发】前端页面导出PDF功能实现指南

admin
2025年12月10日 21:35 本文热度 12
在之前的招聘系统中,有一个信息展示页,其中包含了候选人的详细信息,是通过前端渲染字段来进行展示。信息量比较大,在页面的局部容器中滚动,并且支持编辑、删除等操作。为了便于候选人信息的存档和分享,我们需要实现将这些信息导出为PDF的功能。

导出方案对比

后端导出方案

后端导出的工作原理

  • 前端发送导出请求,携带需要导出的数据标识
  • 后端接收请求,从数据库获取完整数据
  • 后端使用专业的PDF生成库(如iText、wkhtmltopdf等)生成PDF文件
  • 后端将生成的PDF文件提供下载链接或直接返回文件流

后端导出的优势

  • 生成的PDF格式更规范,排版控制更精确
  • 不占用前端资源,大数据量处理更稳定
  • 服务器端可以进行更复杂的业务逻辑处理

后端导出的劣势

  • 需要额外的服务器资源开销
  • 前后端交互增加,实现复杂度较高
  • 无法完全复用前端已有的UI样式

前端导出方案

前端导出的工作原理

  • 直接在浏览器端捕获DOM元素
  • 使用canvas技术将DOM转换为图像
  • 将图像数据转换为PDF文件
  • 触发浏览器下载功能

前端导出的优势

  • 实现简单,无需后端额外开发
  • 可以完全保留前端的样式和布局
  • 即时生成,无需等待服务器响应
  • 减轻服务器压力

前端导出的劣势

  • 大量DOM元素处理可能导致浏览器性能问题
  • 不同浏览器兼容性可能存在差异
  • 跨域资源处理需要额外配置

综合考虑项目需求和实现复杂度,我们选择了前端导出方案。下面将详细介绍实现过程。

前端导出实现过程

1. 基础导出功能实现

首先,我们需要引入相关的库来实现PDF导出功能。我们使用了以下两个核心库:

  • html2canvas: 将DOM元素转换为canvas图像
  • jspdf: 将canvas图像转换为PDF文件

基础实现代码

// 导出为PDF文件exportToPdf(fileName) {  // 如果未提供文件名,则使用默认格式  const pdfFileName = fileName || ('简历_' + this.getNowFormatDate() + '.pdf');  var element = document.getElementById('DomPdf'// 这个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(22)  context.translate(-offsetLeft - abs, -offsetTop)  html2Canvas(element, {    allowTaint: true//允许跨域    useCORS: true//允许图片跨域    scale: 2// 提升画面质量,但是会增加文件大小  }).then((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')        // 直接添加内容到PDF    pdf.addImage(canvas.toDataURL('image/jpeg'1.0), 'JPEG'0, position, imgWidth, imgHeight)        // 下载PDF文件    pdf.save(pdfFileName)  })}

此时可以看到,已经实现导出功能,但是展示的还有一些问题。比如左侧文本太贴近边缘,内容被分页展示,需要做调整。

2. 样式调整优化

最初实现后,我们发现导出的PDF样式不够美观,主要存在以下问题:

  • 内容与页面边缘过于贴近
  • 长内容可能会被截断或显示不完整
  • 某些情况下会出现黑色背景块

我们对代码进行了以下优化:

// 调整边距:增加左侧padding(20px),减少右侧空白const leftPadding = 20 // 左侧边距const adjustedImgWidth = imgWidth - leftPadding // 调整后的图片宽度// 解决黑色块问题的优化分页实现if (contentHeight < pageHeight) {  // 当内容未超过pdf一页显示的范围,无需分页  // 先设置白色背景  pdf.setFillColor(255255255)  pdf.rect(00, 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(00, 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(255255255)    pdf.rect(00, imgWidth, pageHeight, 'F')    // 再添加图片内容    pdf.addImage(      newPageData,      'JPEG',      leftPadding,      position,      adjustedImgWidth,      currentPageHeight    )  }}

优化后的效果:

3. 排除不需要导出的按钮和元素

在导出过程中,我们发现页面中的编辑按钮、删除按钮、下载按钮等操作按钮也被一并导出到PDF中,这不是我们希望的。我们需要在导出时排除这些不必要的元素。

排除按钮和元素的实现

html2Canvas(element, {  allowTaint: true//允许跨域  useCORS: true//允许图片跨域  scale: 2// 提升画面质量,但是会增加文件大小  ignoreElements: function (el) {    try {      // 安全地检查元素类名      const hasClass = (element, className) => {        if (!element || !element.classList) return false        try {          return element.classList.contains(className)        } catch (e) {          return false        }      }      // 安全地检查元素类型      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)      return false    }  },})

排除按钮后的效果对比:

4. 使用Promise优化导出状态控制

在实际使用中,我们发现需要在导出前和导出完成后执行一些特定操作(例如显示加载状态、临时显示完整的手机号和身份证信息等)。为了更精确地控制导出状态,我们将导出方法改造为返回Promise。

Promise优化实现

// 返回Promise,便于外部组件获取导出状态exportToPdf(fileName) {  return new Promise((resolve, reject) => {    // 导出逻辑...        html2Canvas(element, {      // 配置...    }).then(async (canvas) => {      // PDF生成逻辑...            // 直接下载PDF文件      pdf.save(pdfFileName);            // 显示成功消息      this.showPrintSuccessMessage = true;      setTimeout(() => {        this.showPrintSuccessMessage = false;      }, 3000);            // 通知导出成功完成      resolve();    }).catch(error => {      console.error('PDF导出失败:', error);      reject(error);    });  });}

外部组件使用示例

// 在简历详情组件中使用handleExport() {  // 导出前设置导出状态为true  this.isExporting = true;  // do sth  // 调用PDF导出方法,并监听结果  this.exportToPdf().then(() => {    // 导出成功后立即恢复状态    this.isExporting = false;  }).catch(() => {    // 导出失败也需要恢复状态    this.isExporting = false;  });}

注意事项

(1) 跨域资源处理

  • 确保设置了allowTaint:trueuseCORS:true,否则可能导致跨域图片无法正常显示
  • 服务器端需要配置正确的CORS策略

(2) 性能优化

  • 对于包含大量DOM元素的页面,导出过程可能会比较耗时,建议添加加载提示
  • 可以适当降低scale值来减小生成的文件大小,但会影响图片质量

(3) 元素排除规则

  • ignoreElements函数需要根据实际项目中的按钮和容器类名进行调整
  • 确保排除逻辑足够健壮,避免误删需要显示的内容

(4) 样式保持

  • 某些CSS属性在转换为canvas时可能无法完全保留,需要进行兼容性测试
  • 对于复杂的布局,可能需要为导出模式单独设计样式

(5) 移动端兼容性

  • 在移动设备上导出大型PDF可能会遇到内存限制问题
  • 建议在移动设备上提供替代方案或限制导出内容

(6) 导出状态管理

  • 使用Promise可以更精确地控制导出前后的操作
  • 确保在任何情况下(成功或失败)都能正确恢复状态

(7) 浏览器兼容性

  • 不同浏览器对canvas和PDF生成的支持程度可能有所不同
  • 建议在主要目标浏览器上进行充分测试

通过以上实现和注意事项,我们可以为用户提供一个稳定、高效且美观的pdf导出功能,满足目前的业务需求。

完整的导出代码

htmlToPdf-minxi.js 文件

import html2Canvas from'html2canvas'importJsPDFfrom'jspdf'import printJS from'print-js'exportconst jPdfMixin = {  methods: {    // 导出为PDF文件    // 参数说明:fileName - 可选,自定义文件名,如果未提供则使用默认格式    // 返回Promise,便于外部组件获取导出状态    exportToPdf(fileName) {      returnnewPromise((resolve, reject) => {        // 如果未提供文件名,则使用默认格式        const pdfFileName =          fileName || '简历_' + this.getNowFormatDate() + '.pdf'        var element = document.getElementById('DomPdf'// 这个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(22)        context.translate(-offsetLeft - abs, -offsetTop)        // 注意:不需要在主文档中预先查找元素,因为html2canvas会在克隆的iframe中工作        // 所有元素判断都应该在ignoreElements函数内部基于当前元素的属性进行        html2Canvas(element, {          allowTainttrue//允许跨域          useCORStrue//允许图片跨域          scale2// 提升画面质量,但是会增加文件大小          ignoreElementsfunction (el) {            try {              // 安全地检查元素类名              consthasClass = (element, className) => {                if (!element || !element.classList) returnfalse                try {                  return element.classList.contains(className)                } catch (e) {                  returnfalse                }              }              // 安全地检查元素类型              constgetElementTagName = (element) => {                if (!element || !element.tagNamereturn''                try {                  return element.tagName.toLowerCase()                } catch (e) {                  return''                }              }              const tagName = getElementTagName(el)              // 安全地获取元素的实际文本内容(仅用于按钮检测)              constgetElementText = (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 = newJsPDF('''pt''a4')            // 调整边距:增加左侧padding(20px),减少右侧空白            const leftPadding = 20// 左侧边距            const adjustedImgWidth = imgWidth - leftPadding // 调整后的图片宽度            // 解决黑色块问题的优化分页实现            if (contentHeight < pageHeight) {              // 当内容未超过pdf一页显示的范围,无需分页              // 先设置白色背景              pdf.setFillColor(255255255)              pdf.rect(00, 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(00, 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(255255255)                pdf.rect(00, 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('DomPdf')      html2Canvas(element, {        allowTainttrue,        useCORStrue,        scale2,      }).then((canvas) => {        const jsPdfBytes = canvas.toDataURL('image/jpeg'1.0)        printJS({ printable: jsPdfBytes, type'image'showModaltrue })      })    },    // 获取当前格式化日期    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    },    // 保留原有的getPdf方法以保持兼容性,但会重定向到新的exportToPdf方法    getPdf() {      this.exportToPdf()    },  },}


阅读原文:原文链接


该文章在 2025/12/11 8:52:53 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved