前言
本文介绍了一个基于html2canvas和jsPDF的PDF导出工具类实现方案,适用于Vue项目中DOM元素转PDF的需求。该工具类提供以下功能:
- 支持添加页眉和自动计算分页位置
- 提供分页功能,可根据指定类名自动分页
- 可配置选项包括:缩放比例、DPI、页边距等
- 自动处理跨域资源和画布污染问题
正文
第一步,在vue项目中安装好html2canvas和jspdf
npm i html2canvas jspdf
第二步,建一个js文件,代码如下:
ini
import html2Canvas from "html2canvas";
import JsPDF from "jspdf";
class PdfLoader {
/**
* PDF导出工具类
* @param {HTMLElement} ele - 要导出为PDF的DOM元素
* @param {string} pdfFileName - 导出的PDF文件名(不带扩展名)
* @param {string} [splitClassName] - 用于分页判断的类名(可选)
* @param {number} [scrollWidth] - 容器滚动宽度(可选)
* @param {Object} [options] - 配置选项
* @param {number} [options.scale=2] - 缩放比例
* @param {number} [options.dpi=300] - DPI设置
* @param {number} [options.margin=20] - PDF页边距
* @param {boolean} [options.useCORS=true] - 是否使用CORS
* @param {boolean} [options.allowTaint=true] - 是否允许污染画布
*/
constructor(ele, pdfFileName, splitClassName, scrollWidth, options = {}) {
if (!ele || !pdfFileName) {
throw new Error("必须提供有效的DOM元素和PDF文件名");
}
this.ele = ele;
this.pdfFileName = pdfFileName;
this.splitClassName = splitClassName;
this.scrollWidth = scrollWidth || ele.scrollWidth || ele.offsetWidth;
this.A4_WIDTH = 595;
this.A4_HEIGHT = 842;
this.options = {
scale: 2,
dpi: 300,
margin: 20,
useCORS: true,
allowTaint: true,
...options
};
}
/**
* 生成PDF并下载
* @returns {Promise<void>}
*/
async outPutPdfFn() {
try {
await this._preprocessForPagination();
const canvas = await this._generateCanvas();
this._cleanupPreprocessElements();
const pdfData = await this._generatePdfFromCanvas(canvas);
return pdfData;
} catch (error) {
console.error("生成PDF时出错:", error);
throw error; // 重新抛出错误以便外部处理
}
}
/**
* 预处理:添加分页标记
* @private
*/
async _preprocessForPagination() {
if (!this.splitClassName) return;
const childList = this.ele.getElementsByClassName(this.splitClassName);
const eleBounding = this.ele.getBoundingClientRect();
const pageHeight = (this.scrollWidth / this.A4_WIDTH) * this.A4_HEIGHT;
let pageNum = 1;
for (const node of childList) {
const bound = node.getBoundingClientRect();
const diff2Ele = bound.top - eleBounding.top;// 当前元素距离容器顶部距离
const shouldInPage = Math.ceil((bound.bottom - eleBounding.top) / pageHeight);
if (pageNum < shouldInPage) {
pageNum = shouldInPage;
// console.log(pageNum, shouldInPage);
// console.log(bound.bottom, eleBounding.top,pageHeight);
const parentNode = node.parentNode;
const emptyNode = this._createEmptyNode(pageHeight, pageNum, diff2Ele);
const pageHead = this._createPageHeadNode();
parentNode.insertBefore(emptyNode, node);
parentNode.insertBefore(pageHead, node);
}
}
}
/**
* 创建空节点用于分页
* @private
*/
_createEmptyNode(pageHeight, pageNum, diff2Ele) {
const emptyNode = document.createElement("div");
emptyNode.className = "emptyDiv";
emptyNode.style.background = "transparent";
emptyNode.style.width = "100%";
emptyNode.style.height = `${pageHeight * (pageNum - 1) - diff2Ele + this.options.margin}px`;
return emptyNode;
}
/**
* 创建页眉节点
* @private
*/
_createPageHeadNode() {
const pageHead = document.createElement("div");
pageHead.className = "pageHead";
pageHead.innerHTML = `<h3 style="margin: 0; padding: 0;">${this.pdfFileName}</h3>`;
return pageHead;
}
/**
* 生成canvas
* @private
*/
_generateCanvas() {
return html2Canvas(this.ele, {
width: this.scrollWidth,
height: this.ele.offsetHeight,
scale: this.options.scale,
dpi: this.options.dpi,
useCORS: this.options.useCORS,
allowTaint: this.options.allowTaint,
logging: false, // 关闭日志提高性能
});
}
/**
* 清理预处理添加的DOM元素
* @private
*/
_cleanupPreprocessElements() {
if (!this.splitClassName) return;
const emptyNodes = this.ele.querySelectorAll('.emptyDiv');
const headNodes = this.ele.querySelectorAll('.pageHead');
emptyNodes.forEach(item => item.parentNode?.removeChild(item));
headNodes.forEach(item => item.parentNode?.removeChild(item));
}
/**
* 从canvas生成PDF
* @private
*/
_generatePdfFromCanvas(canvas) {
return new Promise((resolve) => {
const contentWidth = canvas.width;
const contentHeight = canvas.height;
const pageHeight = (contentWidth / this.A4_WIDTH) * this.A4_HEIGHT;
const imgWidth = this.A4_WIDTH - this.options.margin;
const imgHeight = (this.A4_WIDTH / contentWidth) * contentHeight;
const pageData = canvas.toDataURL('image/jpeg', 1.0);
const pdf = new JsPDF('', 'pt', 'a4');
let restHeight = contentHeight;
let position = 0;
if (restHeight < pageHeight) {
// 单页
// addImage(pageData, 'JPEG', 左,上,宽度,高度)
pdf.addImage(pageData, 'JPEG', this.options.margin / 2, this.options.margin, imgWidth, imgHeight);
} else {
// 多页
while (restHeight > 0) {
pdf.addImage(pageData, 'JPEG', this.options.margin / 2, position + this.options.margin, imgWidth, imgHeight);
restHeight -= pageHeight;
position -= this.A4_HEIGHT;
if (restHeight > 0) {
pdf.addPage();
}
}
}
// 保存PDF
pdf.save(`${this.pdfFileName}.pdf`);
resolve();
});
}
}
export default PdfLoader;
最后在需要的页面导入该js,并引用
xml
<template>
<div>
<div>这一块是其他内容</div>
<div class="v-pdfwrap">
<div class="element">
<p>报告日期:2199-13-14</p>
</div>
<div class="element">
<A />
</div>
<div class="element">
<B />
</div>
<div class="element">
<C />
</div>
<div class="element">
<D />
<E />
</div>
<div class="element">
<F />
</div>
<div class="element">
<PartG />
</div>
<div class="element">
<PartH />
</div>
<div class="element">
<PartI />
</div>
<div class="element">
<PartJ />
</div>
<div class="element">
<PartK />
</div>
</div>
</div>
</template>
<script>
import PdfLoader from '@/utils/pagetopdf.js';
import A from "./A.vue";
import B from "./B.vue";
import C from "./C.vue";
import D from "./D.vue";
import E from "./E.vue";
import F from "./F.vue";
import PartG from "./PartG.vue";
import PartH from "./PartH.vue";
import PartI from "./PartI.vue";
import PartJ from "./PartJ.vue";
import PartK from "./PartK.vue";
export default {
data() {
return {
};
},
components: {
A, B, C, D, E, F, PartG, PartH, PartI, PartJ, PartK
},
watch: {
},
methods: {
exportPDF() {
let pdf = new PdfLoader(
document.querySelector(".v-pdfwrap"),
"这是pdf名",
"element",
1920
);
pdf.outPutPdfFn().then(() => {
console.log('PDF生成完毕');
});
},
}
};
</script>
<style lang="scss"></style>
总结
实践出真知,要不断地尝试才知道代码如何修改才能匹配项目