需求:前端将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);