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

news2024/11/16 8:40:08

一.实现效果

页面中点击预览报告,实现将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文件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1988578.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微深节能 环形运动机械定位控制系统 格雷母线

格雷母线定位系统作为一种高精度、无磨损的非接触式位置检测系统&#xff0c;特别适用于环形运动机械的定位控制。 一、格雷母线定位系统的概述 格雷母线定位系统主要由一台地面电气柜、一台车载电气柜、格雷母线以及天线箱等组成。其核心部件是格雷母线&#xff0c;一种特殊的…

深度学习人脸表情识别结课作业留存

0.前言 大三下学期学习了深度学习神经网络的课程&#xff0c;老师留下了人脸表情识别的结课作业&#xff0c;在网上找到了一个想相对不错的开源项目作为基础完成了本次的结课作业。 1.项目链接 Challenges in Representation Learning: Facial Expression Recognition Chall…

运动耳机怎么选?来看看奥运冠军喜欢的运动耳机!

在奥运的竞技舞台上&#xff0c;每一次心跳的跃动都是对自我极限的勇敢挑战&#xff0c;运动员的每一个动作背后&#xff0c;都承载着不懈的努力与对梦想的执着追求。如今&#xff0c;运动科技正以前所未有的速度改变着我们的运动方式&#xff0c;一款优秀的运动耳机成为了连接…

Prompt Fuzzer:用于增强 GenAI 应用程序的开源工具

Prompt Fuzzer 是一个开源工具&#xff0c;可以评估GenAI应用程序的系统提示针对基于动态 LLM 的威胁的安全性。 Prompt Fuzzer 功能&#xff1a; 1. 模拟十几种类型的 GenAI 攻击。 2. 该工具会根据系统提示自动进行情境化&#xff0c;针对与 GenAI 应用程序相关的特定主题或行…

【Python机器学习】回归——用线性回归找到最佳拟合直线

线性回归的优缺点&#xff1a; 优点&#xff1a;结果易于理解&#xff0c;计算上不复杂 缺点&#xff1a;对非线性的数据拟合不好 使用数据类型&#xff1a;数值型和标称型数据。 回归的目的是预测数值型的目标值。最直接的办法是依据输入写出一个目标值的计算公式。例如预测汽…

【C语言】预处理详解(上)

文章目录 前言1. 预定义符号2. #define 定义常量3. #define定义宏4. 带有副作用的宏参数5. 宏替换的规则 前言 在讲解编译和链接的知识点中&#xff0c;我提到过翻译环境中主要由编译和链接两大部分所组成。 其中&#xff0c;编译又包括了预处理、编译和汇编。当时&#xff0c…

【Windows系统开机后识别不到屏幕适配的分辨率导致屏幕无法点亮的解决办法】

问题原因分析&#xff1a; 屏幕驱动板出现故障&#xff0c;驱动出现缺失&#xff0c;未对主板系统进行适配兼容。使用的屏幕分辨率非常小众&#xff0c;系统中没有这个分辨率&#xff0c;识别不到屏幕适配的分辨率后导致屏幕无法点亮。 解决方法&#xff1a; 找主板厂家增加…

CVE-2022-33891漏洞复现

简介 Spark 是用于大规模数据处理的统一分析引擎。它提供了 Scala、Java、Python 和 R 中的高级 API&#xff0c;以及支持用于数据分析的通用计算图的优化引擎。它还支持一组丰富的高级工具&#xff0c;包括用于 SQL 和 DataFrames 的 Spark SQL、用于 Pandas 工作负载的 Spar…

【每日刷题】Day94

【每日刷题】Day94 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 33. 搜索旋转排序数组 - 力扣&#xff08;LeetCode&#xff09; 2. 1290. 二进制链表转整数 - 力扣…

科普文:微服务之万字详解分布式事务原理、协议及其框架

一、分布式事务 首先奉上一张关于事务的相关概念图&#xff0c;给大家做个总览&#xff1a; 1.1 名词解释 事务&#xff1a;事务是由一组操作构成的可靠的独立的工作单元&#xff0c;事务具备ACID的特性&#xff0c;即原子性、一致性、隔离性和持久性。本地事务&#xff1a;当…

树莓派4B部署及测试llamafile

llamafile项目简介 很多初学者学习大语言模型的时候,都会被模型文件中一大堆复杂的python文件或者cuda配置劝退,为了方便更多的零基础的初学者体验大语言模型,llamafile 提出了单文件运行大模型的方案。 GitHub - Mozilla-Ocho/llamafile: Distribute and run LLMs with a…

网络原理(1)——基本概念

1. 网络互连 随着时代的发展&#xff0c;越来越需要计算机之间相互通信&#xff0c;共享软件和数据&#xff0c;以多个计算机协同工作来完成业务&#xff0c;就有了网络互连 网络互连&#xff1a;将多台计算机连接在一起&#xff0c;完成数据共享 数据共享本质是网络数据传输…

中空板式陶瓷膜的高可靠性

中空板式陶瓷膜是一种先进的液固分离材料&#xff0c;具有诸多优点和广泛的应用领域。以下是对中空板式陶瓷膜的详细介绍&#xff1a; 一、产品特点 物理特性优越&#xff1a;中空板式陶瓷膜通常采用刚玉等无机材料为原材料&#xff0c;经过高温烧制而成&#xff0c;具有高强度…

数据结构实验:树和二叉树(附c++源码:实现树有关算法)

目录 一、实验目的 二、问题分析及数据结构设计 三、算法设计&#xff08;伪代码表示&#xff09; 1. 输入字符序列 创建二叉链表 2. 递归前序遍历 3. 递归中序遍历 4. 递归后序遍历 5. 非递归前序遍历 6. 非递归中序遍历 7. 非递归后序遍历 8. 层次遍历 9. 求二叉…

阿里云镜像站,提供了各种第三方镜像地址

阿里云提供了各项镜像缓存地址&#xff0c;对于很多国外服务的地址&#xff0c;通过阿里云缓存的地址去下载&#xff0c;速度会非常快。 如下&#xff0c;打开阿里云官方网站&#xff1a; 进入“镜像站”&#xff0c;如下图所示&#xff1a; 有我们常用的 npm、maven、操作系统…

武汉流星汇聚:互联网+跨境购物新风尚,消费者深度依赖跨境电商

在21世纪的数字时代&#xff0c;跨境电商平台以其独特的魅力&#xff0c;正逐步成为连接全球消费者与优质商品的桥梁。随着消费者对优质产品需求的日益增长、全球互联网使用量的不断扩大、跨境物流技术的飞速进步以及全球供应链能力的显著提升&#xff0c;跨境电商平台不仅为消…

uniapp——列表选择样式

案例 代码 <view class"list"><block v-for"(item,index) in 8" :key"index"><view class"item" click"choosePackage(item)" :class"{active:item current}"><view class"i_money&q…

【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro

看到这句话的时候证明&#xff1a;此刻你我都在努力 加油陌生人 前言 今天给大家带来一篇有关Java顺序表和链表的文章&#xff0c;顺序表和链表我之前的专栏也是写过的&#xff0c;是用C语言实现的&#xff0c;也是模仿实现了顺序表和链表里的方法了。 下面是传送门&#xff…

新一代AI技术的发展

人工智能技术正处于迈向全新阶段的关键转折点&#xff0c;从传统的NLP(自然语言处理)迅速迈向更开 放、更通用、多模态的AGI(通用人工智能),AGI的兴起为各行业带来了前所未有的机遇。AGI突破了传 统AI的局限&#xff0c;具备跨领域的广泛应用能力和自主学习能力&#xff0c;在自…

CTFHUB | web进阶 | PHP | Bypass disable_function | PHP-FPM

开启题目 查看源码&#xff0c;发现可以蚁剑连接 连接成功发现无任何发现&#xff0c;所以我们使用 Fastcgi/PHP-FPM 插件&#xff0c;配置如下 刷新目录发现插件上传了一个 php 文件&#xff0c;复制文件名拼接到后面再次连接 发现直接进入终端了&#xff0c;最后发现了 flag