使用html2canvas+jspdf将html页面转为pdf,根据模块分页并下载

152 阅读2分钟

需求:前端将html页面转成pdf,实现获取pdf的二进制数据或者是直接下载;因为pdf里面包含中文版本和英文版本,并且中文版本和英文版本都需要另起一页,因此需要按照模块来做分页,实现原理就是根据class来控制html2canvas截图,原理就是根据class,将每一个相同的class都分别生成一个图片,获取图片数组,再将图片数组转成pdf

缺点:由于html2canvas的原理是将html页面截屏成图片,所以本质上是在pdf里面插入图片,所以pdf中文本无法复制。

html2pdf.js

// 导出页面为PDF格式
import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

const a4width = 592.28; // A4的宽度,以毫米为单位
const a4Height = 841.89; // A4的高度,以毫米为单位

export async function getPdf(title, isDownload = true) {
  // const pdfDom: any = document.querySelector('#pdfDom');
  // // 这里特殊处理 因为SendCode组件在订单详情被引入两次,导致会有两份iata行程单的html
  // const element = document.getElementById("travelDialog");

  const itemDom = document.querySelectorAll(".flightItinerary-container");

  const PDF = new JsPDF(undefined, "pt", "a4");

  let position = 0; //图像的纵坐标,即左上角的y坐标
  let pageItemHight = 0; //页面中item的高度

  const imgData = await getImages(itemDom);
  imgData.forEach((itemCanvas) => {
    const contentWidth = itemCanvas.width;
    const contentHeight = itemCanvas.height;
    //一页pdf显示html页面生成的canvas高度
    // const pageHeight = contentWidth / a4width * a4Height - 64;
    const pageHeight = PDF.internal.pageSize.height;

    const imgWidth = a4width;
    const imgHeight = (a4width / contentWidth) * contentHeight;
    const itemPageData = itemCanvas.toDataURL("image/jpeg", 1.0);

    //计算页面画面高度
    pageItemHight += imgHeight;

    //如果加上当前item已超过当前页面高度,则另开页面
    if (pageItemHight > pageHeight) {
      PDF.addPage(); //添加新页面
      position = 0;
    }

    //将图像添加到PDF文档中的函数(图像的URL或base64编码的数据,指定图像的格式,图像的横坐标,图像的纵坐标,图像的宽度,图像的高度)
    PDF.addImage(itemPageData, "JPEG", 0, position, imgWidth, imgHeight);
    position += imgHeight; //图像的纵坐标,即左上角的y坐标
  });
  if (isDownload) {
    PDF.save(title + ".pdf");
  }
  // 删除本地存储的base64字段
  const pdfData = PDF.output("blob"); //获取base64Pdf
  console.log("pdfData", pdfData);
  return pdfData;
}

function getImages(itemDom) {
  const promises = [];
  itemDom.forEach((item) => {
    const promise = html2Canvas(item, {
      allowTaint: true,
      useCORS: true,
      allowTaint: true,
      scale: 2,
      dpi: 300,
    }).then(function (canvas) {
      if (canvas.height > 0) {
        return canvas; // 返回Promise对象,以便集中处理
      }
    });
    promises.push(promise);
  });
  return Promise.all(promises); // 返回一个新的Promise对象
}

html:主要在于flightItinerary-container这个class用于模块

<div id="pdfDom" class="pdfDom">
    <div v-for="item in iataFlightList" :key="item.idNumber">
      <div
        class="flightItinerary-container en"
        v-if="
          language == languageEnum.All.value ||
          language == languageEnum.En.value
        "
      >
        <div class="logo">
          <img src="@/assets/iata.png" style="width: 160px; height: 160px" />
        </div>
        <div class="title">ITINERARY</div>
        <div class="basic-info">
          <div class="left">
            <div class="item">
              <div class="item-label">AIRLINE PNR:</div>
              <div class="en-font"></div>
            </div>
            <div class="item">
              <div class="item-label">NAME:</div>
              <div class="en-font">{{ item.engName }}</div>
            </div>
            <div class="item">
              <div class="item-label">ID NUMBER:</div>
              <div class="en-font">{{ item.idNumber }}</div>
            </div>
            <div class="item" style="margin: 40px 0">
              <div class="item-label">ISSUING AIRLINE:</div>
              <div class="en-font">{{ item.issueAirlines }}</div>
            </div>
            <div class="item">
              <div class="item-label">ISSUING AGENT:</div>
              <div class="en-font">{{ item.issueAgent }}</div>
            </div>
            <div class="item">
              <div class="item-label">AGENCY ADDRESS:</div>
              <div class="en-font">{{ item.agentAddress }}</div>
            </div>
            <div class="item">
              <div class="item-label">TEL:</div>
              <div class="en-font">{{ item.agentPhone }}</div>
            </div>
          </div>
          <div class="right">
            <div class="item">
              <div class="item-label">IE PNR:</div>
              <div class="en-font">{{ item.pnr }}</div>
            </div>
            <div class="item">
              <div class="item-label">ETKT NBR:</div>
              <div class="en-font">{{ item.ticketNumber }}</div>
            </div>
            <div class="item">
              <div class="item-label">CONJ NBR:</div>
              <div class="en-font">{{ item.couponNumber }}</div>
            </div>
            <div class="item" style="margin: 40px 0">
              <div class="item-label">DATE OF ISSUE:</div>
              <div class="en-font">{{ item.issueTime }}</div>
            </div>
            <div class="item">
              <div class="item-label">IATA CODE:</div>
              <div class="en-font">{{ item.iataCode }}</div>
            </div>
            <div class="item" style="margin-top: 54px">
              <div class="item-label">FAX:</div>
              <div class="en-font">{{ item.agentFax }}</div>
            </div>
          </div>
        </div>
        <div class="basic-table">
          <div class="head">
            <div style="width: 20%">ORIGIN/DES</div>
            <div style="width: 8%">FLIGHT</div>
            <div style="width: 6%">CLASS</div>
            <div style="width: 8%">DATE</div>
            <div style="width: 8%">TIME</div>
            <div style="width: 10%">ARRTIME</div>
            <div style="width: 13%">PERIOD</div>
            <div style="width: 8%">STATUS</div>
            <div style="width: 7%">ALLOW</div>
            <div style="width: 12%">
              <div>TERMINAL</div>
              <div style="display: flex">
                <div style="width: 50%; margin-top: 10px">Takeoff</div>
                <div style="width: 50%; margin-top: 10px">Arrival</div>
              </div>
            </div>
          </div>
          <div class="body">
            <div
              class="item"
              v-for="(subItem, index) in item.segmentInfos"
              :key="index"
            >
              <div style="width: 20%; word-break: break-all">
                <div>{{ subItem.origEng }}</div>
              </div>
              <div style="width: 8%">{{ subItem.fltNo }}</div>
              <div style="width: 6%">{{ subItem.sclass }}</div>
              <div style="width: 8%">{{ subItem.fltDate }}</div>
              <div style="width: 8%">{{ subItem.deptTime }}</div>
              <div style="width: 10%">{{ subItem.arriTime }}</div>
              <div style="width: 13%">{{ subItem.validityTime }}</div>
              <div style="width: 8%">{{ subItem.segmentStatus }}</div>
              <div style="width: 7%">{{ subItem.baggage }}</div>
              <div style="width: 12%">
                <div style="display: flex">
                  <div style="width: 50%;">
                    {{ subItem.origTerminal }}
                  </div>
                  <div style="width: 50%;">
                    {{ subItem.destTerminal }}
                  </div>
                </div>
              </div>
            </div>
            <div class="item" v-if="item.segmentInfos.length > 0">
              <div style="width: 20%; word-break: break-all">
                <div>
                  {{ item.segmentInfos[item.segmentInfos.length - 1].destEng }}
                </div>
              </div>
            </div>
          </div>
        </div>
        <div class="basic-price" v-if="takePriceFlag">
          <div>FARE CALCULATION:</div>
          <div class="center">
            {{ item.priceCalculation }}
          </div>
          <div style="display: flex; margin-top: 50px; align-items: center">
            <div style="width: 70%">FORM OF PAYMENT:{{ item.payMethod }}</div>
            <div style="width: 30%; display: flex; align-items: center">
              <div>TAX:</div>
              <div>
                <div v-for="tax in item.tax">{{ tax }}</div>
              </div>
            </div>
          </div>
          <div style="margin-top: 50px">
            FARE:<span>{{ item.airFare }}</span>
          </div>
          <div style="margin-top: 10px">TOTAL:{{ item.total }}</div>
          <div style="margin-top: 10px">
            RESTRICTIONS: <span class="en-font">{{ item.limitRule }}</span>
          </div>
        </div>
        
        <div class="footer" v-if="takePriceFlag || sendNoticeFlag"></div>
      </div>
      <div
        class="flightItinerary-container"
        v-if="
          language == languageEnum.All.value ||
          language == languageEnum.Cn.value
        "
      >
        <div class="logo">
          <img src="@/assets/iata.png" style="width: 160px; height: 160px" />
        </div>
        <div class="title">电子客票行程单</div>
        <div class="basic-info">
          <div class="left">
            <div class="item">
              <div class="item-label">航空公司记录编号:</div>
              <div class="en-font"></div>
            </div>
            <div class="item">
              <div class="item-label">旅客姓名:</div>
              <div class="en-font">{{ item.cnName }}</div>
            </div>
            <div class="item">
              <div class="item-label">身份识别代码:</div>
              <div class="en-font">{{ item.idNumber }}</div>
            </div>
            <div class="item" style="margin: 40px 0">
              <div class="item-label">出票航空公司:</div>
              <div class="en-font">{{ item.issueAirlines }}</div>
            </div>
            <div class="item">
              <div class="item-label">出票代理人:</div>
              <div class="en-font">{{ item.issueAgent }}</div>
            </div>
            <div class="item">
              <div class="item-label">代理人地址:</div>
              <div class="en-font">{{ item.agentAddress }}</div>
            </div>
            <div class="item">
              <div class="item-label">电话:</div>
              <div class="en-font">{{ item.agentPhone }}</div>
            </div>
          </div>
          <div class="right">
            <div class="item">
              <div class="item-label">订座记录编号:</div>
              <div class="en-font">{{ item.pnr }}</div>
            </div>
            <div class="item">
              <div class="item-label">票号:</div>
              <div class="en-font">{{ item.ticketNumber }}</div>
            </div>
            <div class="item">
              <div class="item-label">联票:</div>
              <div class="en-font">{{ item.couponNumber }}</div>
            </div>
            <div class="item" style="margin: 40px 0">
              <div class="item-label">出票时间:</div>
              <div class="en-font">{{ item.issueTime }}</div>
            </div>
            <div class="item">
              <div class="item-label">航协代码:</div>
              <div class="en-font">{{ item.iataCode }}</div>
            </div>
            <div class="item" style="margin-top: 54px">
              <div class="item-label">传真:</div>
              <div class="en-font">{{ item.agentFax }}</div>
            </div>
          </div>
        </div>
        <div class="basic-table">
          <div class="head">
            <div style="width: 20%">始发地/目的地</div>
            <div style="width: 8%">航班</div>
            <div style="width: 8%">座位等级</div>
            <div style="width: 8%">日期</div>
            <div style="width: 8%">起飞时间</div>
            <div style="width: 10%">到达时间</div>
            <div style="width: 10%">有效期</div>
            <div style="width: 10%">客票状态</div>
            <div style="width: 7%">行李</div>
            <div style="width: 12%">
              <div>航站楼</div>
              <div style="display: flex">
                <div style="width: 50%; margin-top: 10px">起飞</div>
                <div style="width: 50%; margin-top: 10px">到达</div>
              </div>
            </div>
          </div>
          <div class="body">
            <div
              class="item"
              v-for="(subItem2, index) in item.segmentInfos"
              :key="index"
            >
              <div style="width: 20%; word-break: break-all">
                <div>{{ subItem2.origChn }}</div>
              </div>
              <div style="width: 8%">{{ subItem2.fltNo }}</div>
              <div style="width: 8%">{{ subItem2.sclass }}</div>
              <div style="width: 8%">{{ subItem2.fltDate }}</div>
              <div style="width: 8%">{{ subItem2.deptTime }}</div>
              <div style="width: 10%">{{ subItem2.arriTime }}</div>
              <div style="width: 10%">{{ subItem2.validityTime }}</div>
              <div style="width: 10%">{{ subItem2.segmentStatus }}</div>
              <div style="width: 7%">{{ subItem2.baggage }}</div>
              <div style="width: 12%">
                <div style="display: flex">
                  <div style="width: 50%;">
                    {{ subItem2.origTerminal }}
                  </div>
                  <div style="width: 50%;">
                    {{ subItem2.destTerminal }}
                  </div>
                </div>
              </div>
            </div>

            <div class="item" v-if="item.segmentInfos.length > 0">
              <div style="width: 20%; word-break: break-all">
                <div>
                  {{ item.segmentInfos[item.segmentInfos.length - 1].destChn }}
                </div>
              </div>
            </div>
          </div>
        </div>
        <div class="basic-price" v-if="takePriceFlag">
          <div>票价计算:</div>
          <div class="center en-font">
            {{ item.priceCalculation }}
          </div>
          <div style="display: flex; margin-top: 50px; align-items: center">
            <div style="width: 70%">
              付款方式:<span class="en-font">{{ item.payMethod }}</span>
            </div>
            <div style="width: 30%; display: flex; align-items: center">
              <div>税款:</div>
              <div>
                <div class="en-font" v-for="tax in item.tax">{{ tax }}</div>
              </div>
            </div>
          </div>
          <div style="margin-top: 50px">
            机票款:<span class="en-font">{{ item.airFare }}</span>
          </div>
          <div style="margin-top: 10px">
            总 额:<span class="en-font">{{ item.total }}</span>
          </div>
          <div style="margin-top: 10px">
            限制条件: <span class="en-font">{{ item.limitRule }}</span>
          </div>
        </div>
        
        <div class="footer" v-if="takePriceFlag || sendNoticeFlag"></div>
      </div>
    </div>
  </div>

使用

import { getPdf } from "@/utils/html2pdf";

const res = await getPdf(`${this.ticketNbrs.join(",")}.pdf`, false);
this.pdfSrc = window.URL.createObjectURL(res);