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

一.实现效果

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

定义的方法文件

javascript 复制代码
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

javascript 复制代码
  <div class="ctn">
    <div class="pdf-ctn">
      <div class="pdf-panel">
    
         需要生成pdf的内容
      </div>
    </div>
    <div>
    </div>
  </div>
javascript 复制代码
<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>

引入到预览报告的页面中

javascript 复制代码
import ExportReportPDF from "./exportReportPDF/index.vue";
import htmlToPdf from "./exportReportPDF/pdf-print.ts";

使用,生成 fileUrl

javascript 复制代码
  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使用

javascript 复制代码
 <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文件

相关推荐
秋雨凉人心2 小时前
简单发布一个npm包
前端·javascript·webpack·npm·node.js
LCG元3 小时前
Vue.js组件开发-使用vue-pdf显示PDF
vue.js
哥谭居民00014 小时前
将一个组件的propName属性与父组件中的variable变量进行双向绑定的vue3(组件传值)
javascript·vue.js·typescript·npm·node.js·css3
烟波人长安吖~4 小时前
【目标跟踪+人流计数+人流热图(Web界面)】基于YOLOV11+Vue+SpringBoot+Flask+MySQL
vue.js·pytorch·spring boot·深度学习·yolo·目标跟踪
踢足球的,程序猿4 小时前
Android native+html5的混合开发
javascript
前端没钱5 小时前
探索 ES6 基础:开启 JavaScript 新篇章
前端·javascript·es6
PleaSure乐事5 小时前
使用Vue的props进行组件传递校验时出现 Extraneous non-props attributes的解决方案
vue.js
一条不想当淡水鱼的咸鱼6 小时前
taro中实现带有途径点的路径规划
javascript·react.js·taro
土豆炒马铃薯。6 小时前
【Vue】前端使用node.js对数据库直接进行CRUD操作
前端·javascript·vue.js·node.js·html5
温轻舟7 小时前
前端开发 -- 自动回复机器人【附完整源码】
前端·javascript·css·机器人·html·交互·温轻舟