vue实现页面中点击预览报告,实现将vue组件变成pdf文件进行弹窗展示

avatar
作者
猴君
阅读量:0

一.实现效果

页面中点击预览报告,实现将vue组件变成pdf文件进行弹窗展示
在这里插入图片描述

定义的方法文件

import html2canvas from "html2canvas"; import jsPDF, { RGBAData } from "jspdf";  /** a4纸的尺寸[595.28,841.89], 单位毫米 */ const [PAGE_WIDTH, PAGE_HEIGHT] = [595.28, 841.89];  const PAPER_CONFIG: any = {   /** 竖向 */   portrait: {     height: PAGE_HEIGHT,     width: PAGE_WIDTH,     contentWidth: 560,   },   /** 横向 */   landscape: {     height: PAGE_WIDTH,     width: PAGE_HEIGHT,     contentWidth: 800,   }, };  // 将元素转化为canvas元素 // 通过 放大 提高清晰度 // width为内容宽度 async function toCanvas(element: HTMLElement, width: number) {   if (!element) return { width, height: 0 };    // canvas元素   const canvas = await html2canvas(element, {     allowTaint: true, // 允许渲染跨域图片     scale: window.devicePixelRatio * 2, // 增加清晰度     useCORS: true, // 允许跨域   });    // 获取canvas转化后的宽高   const { width: canvasWidth, height: canvasHeight } = canvas;    // html页面生成的canvas在pdf中的高度   const height = (width / canvasWidth) * canvasHeight;    // 转化成图片Data   const canvasData = canvas.toDataURL("image/jpeg", 1.0);    return { width, height, data: canvasData }; }  /**  * 生成pdf(A4多页pdf截断问题, 包括页眉、页脚 和 上下左右留空的护理)  * @param param0  * @returns  */ export async function outputPDF({   /** pdf内容的dom元素 */   element,    /** 页脚dom元素 */   footer,    /** 页眉dom元素 */   header,    /** pdf文件名 */   filename,    /** a4值的方向: portrait or landscape */   orientation = "portrait" as "portrait" | "landscape", }: any) {   if (!(element instanceof HTMLElement)) {     return;   }    if (!["portrait", "landscape"].includes(orientation)) {     return Promise.reject(       new Error(         `Invalid Parameters: the parameter {orientation} is assigned wrong value, you can only assign it with {portrait} or {landscape}`       )     );   }   const [A4_WIDTH, A4_HEIGHT] = [     PAPER_CONFIG[orientation].width,     PAPER_CONFIG[orientation].height,   ];    /** 一页pdf的内容宽度, 左右预设留白 */   const { contentWidth } = PAPER_CONFIG[orientation];    // eslint-disable-next-line new-cap   const pdf = new jsPDF({     unit: "pt",     format: "a4",     orientation,   });    // 一页的高度, 转换宽度为一页元素的宽度   const { width, height, data } = await toCanvas(element, contentWidth);    // 添加   function addImage(     _x: number,     _y: number,     pdfInstance: jsPDF,     base_data:       | string       | HTMLImageElement       | HTMLCanvasElement       | Uint8Array       | RGBAData,     _width: number,     _height: number   ) {     pdfInstance.addImage(base_data, "JPEG", _x, _y, _width, _height);   }    // 增加空白遮挡   function addBlank(x: number, y: number, _width: number, _height: number) {     pdf.setFillColor(255, 255, 255);     pdf.rect(x, y, Math.ceil(_width), Math.ceil(_height), "F");   }    // 页脚元素 经过转换后在PDF页面的高度   const { height: tFooterHeight, data: headerData } = footer     ? await toCanvas(footer, contentWidth)     : { height: 0, data: undefined };    // 页眉元素 经过转换后在PDF的高度   const { height: tHeaderHeight, data: footerData } = header     ? await toCanvas(header, contentWidth)     : { height: 0, data: undefined };    // 添加页脚   async function addHeader(_headerElement: HTMLElement) {     headerData &&       pdf.addImage(headerData, "JPEG", 0, 0, contentWidth, tHeaderHeight);   }    // 添加页眉   async function addFooter(     _pageNum: number,     _now: number,     _footerElement: HTMLElement   ) {     if (footerData) {       pdf.addImage(         footerData,         "JPEG",         0,         A4_HEIGHT - tFooterHeight,         contentWidth,         tFooterHeight       );     }   }    // 距离PDF左边的距离,/ 2 表示居中   const baseX = (A4_WIDTH - contentWidth) / 2; // 预留空间给左边   // 距离PDF 页眉和页脚的间距, 留白留空   const baseY = 15;    // 除去页头、页眉、还有内容与两者之间的间距后 每页内容的实际高度   const originalPageHeight =     A4_HEIGHT - tFooterHeight - tHeaderHeight - 2 * baseY;    // 元素在网页页面的宽度   const elementWidth = element.offsetWidth;    // PDF内容宽度 和 在HTML中宽度 的比, 用于将 元素在网页的高度 转化为 PDF内容内的高度, 将 元素距离网页顶部的高度  转化为 距离Canvas顶部的高度   const rate = contentWidth / elementWidth;    // 每一页的分页坐标, PDF高度, 初始值为根元素距离顶部的距离   const pages = [rate * getElementTop(element)];    // 获取该元素到页面顶部的高度(注意滑动scroll会影响高度)   function getElementTop(contentElement: any) {     if (contentElement.getBoundingClientRect) {       const rect = contentElement.getBoundingClientRect() || {};       const topDistance = rect.top;        return topDistance;     }   }    // 遍历正常的元素节点   function traversingNodes(nodes: any) {     for (const element of nodes) {       const one = element;        /** */       /** 注意: 可以根据业务需求,判断其他场景的分页,本代码只判断表格的分页场景 */       /** */        // table的每一行元素也是深度终点       const isTableRow =         one.classList && one.classList.contains("ant4-table-row");       // 需要判断跨页且内部存在跨页的元素       const isDivideInside =         one.classList && one.classList.contains("divide-inside");       // 对需要处理分页的元素,计算是否跨界,若跨界,则直接将顶部位置作为分页位置,进行分页,且子元素不需要再进行判断       const { offsetHeight } = one;       // 计算出最终高度       const offsetTop = getElementTop(one);        // dom转换后距离顶部的高度       // 转换成canvas高度       const top = rate * offsetTop;       const rateOffsetHeight = rate * offsetHeight;        // 对于深度终点元素进行处理       if (isTableRow || isDivideInside) {         // dom高度转换成生成pdf的实际高度         // 代码不考虑dom定位、边距、边框等因素,需在dom里自行考虑,如将box-sizing设置为border-box         updateTablePos(rateOffsetHeight, top);       }       //  // 对于需要进行分页且内部存在需要分页(即不属于深度终点)的元素进行处理       // 对于普通元素,则判断是否高度超过分页值,并且深入       else {         // 执行位置更新操作         updateNormalElPos(top);         // 遍历子节点         traversingNodes(one.childNodes);       }       updatePos();     }   }    // 普通元素更新位置的方法   // 普通元素只需要考虑到是否到达了分页点,即当前距离顶部高度 - 上一个分页点的高度 大于 正常一页的高度,则需要载入分页点   function updateNormalElPos(top: any) {     if (       top - (pages.length > 0 ? pages[pages.length - 1] : 0) >=       originalPageHeight     ) {       pages.push(         (pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight       );     }   }    // 可能跨页元素位置更新的方法   // 需要考虑分页元素,则需要考虑两种情况   // 1. 普通达顶情况,如上   // 2. 当前距离顶部高度加上元素自身高度 大于 整页高度,则需要载入一个分页点   function updateTablePos(eHeight: number, top: number) {     // 如果高度已经超过当前页,则证明可以分页了     if (       top - (pages.length > 0 ? pages[pages.length - 1] : 0) >=       originalPageHeight     ) {       pages.push(         (pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight       );     }     // 若 距离当前页顶部的高度 加上元素自身的高度 大于 一页内容的高度, 则证明元素跨页,将当前高度作为分页位置     else if (       top + eHeight - (pages.length > 0 ? pages[pages.length - 1] : 0) >         originalPageHeight &&       top !== (pages.length > 0 ? pages[pages.length - 1] : 0)     ) {       pages.push(top);     }   }    // 深度遍历节点的方法   traversingNodes(element.childNodes);    function updatePos() {     while (pages[pages.length - 1] + originalPageHeight < height) {       pages.push(pages[pages.length - 1] + originalPageHeight);     }   }    function dataURLtoFile(dataurl: any, filename: any) {     var arr = dataurl.split(","),       mime = arr[0].match(/:(.*?);/)[1],       bstr = atob(arr[1]),       n = bstr.length,       u8arr = new Uint8Array(n);     while (n--) {       u8arr[n] = bstr.charCodeAt(n);     }     return new File([u8arr], filename, { type: mime });   }    // 对pages进行一个值的修正,因为pages生成是根据根元素来的,根元素并不是我们实际要打印的元素,而是element,   // 所以要把它修正,让其值是以真实的打印元素顶部节点为准   const newPages = pages.map((item) => item - pages[0]);    // 根据分页位置 开始分页   for (let i = 0; i < newPages.length; ++i) {     // 根据分页位置新增图片     addImage(       baseX,       baseY + tHeaderHeight - newPages[i],       pdf,       data!,       width,       height     );     // 将 内容 与 页眉之间留空留白的部分进行遮白处理     addBlank(0, tHeaderHeight, A4_WIDTH, baseY);     // 将 内容 与 页脚之间留空留白的部分进行遮白处理     addBlank(0, A4_HEIGHT - baseY - tFooterHeight, A4_WIDTH, baseY);     // 对于除最后一页外,对 内容 的多余部分进行遮白处理     if (i < newPages.length - 1) {       // 获取当前页面需要的内容部分高度       const imageHeight = newPages[i + 1] - newPages[i];       // 对多余的内容部分进行遮白       addBlank(         0,         baseY + imageHeight + tHeaderHeight,         A4_WIDTH,         A4_HEIGHT - imageHeight       );     }      // 添加页眉     if (header) {       await addHeader(header);     }      // 添加页脚     if (footer) {       await addFooter(newPages.length, i + 1, footer);     }      // 若不是最后一页,则分页     if (i !== newPages.length - 1) {       // 增加分页       pdf.addPage();     }   }   let pdfDataTemp = pdf.output("datauristring"); //获取base64Pdf   let myfileTemp = dataURLtoFile(pdfDataTemp, "选址评测报告" + ".pdf"); //调用一下下面的转文件流函数   pdf.save(filename);   return {     pdfDataTemp,     myfileTemp,   }; }  const htmlToPdf = {  async getPdf() {     const element = document.querySelector(".pdf-panel");     const { pdfDataTemp, myfileTemp }: any =await outputPDF({       element,       filename: `选址评测报告`,       orientation: "portrait",     });     return {       pdfBase64: pdfDataTemp,       files: myfileTemp,     };   }, }; export default htmlToPdf;  

定义需要变成pdf的组件文件 ExportReportPDF .vue

  <div class="ctn">     <div class="pdf-ctn">       <div class="pdf-panel">               需要生成pdf的内容       </div>     </div>     <div>     </div>   </div> 
<style lang="scss" scoped> .ctn {   position: fixed;   top: 0;   left: 0;   z-index: -1;   overflow: scroll;   position: relative;   .pdf-ctn {     width: 1300px;     .pdf-panel {       position: relative;     }   } } </style> 

引入到预览报告的页面中

import ExportReportPDF from "./exportReportPDF/index.vue"; import htmlToPdf from "./exportReportPDF/pdf-print.ts"; 

使用,生成 fileUrl

  const getPdfObj :any= await htmlToPdf.getPdf();       files.value = getPdfObj.files     pdfBase64.value = getPdfObj.pdfBase64     let blob = dataURLtoBlob(getPdfObj.pdfBase64)     fileUrl.value = window.URL.createObjectURL(blob) 

在iframe使用

 <iframe  width="90%" height="90%" :src="`${fileUrl}`"></iframe> 

问题点:

  1. 会出现 185ms Unable to clone canvas as it is tainted 问题导致白屏
  2. 生成的base64过大,iframe显示不出来

解决方法:

1.pdf方法进行将每一个生成一个图片canvas然后组合成为整体一个canvas
2.使用dataURLtoBlob和createObjectURL生成url显示到iframe上,不能直接用base64文件

    广告一刻

    为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!