安装依赖
npm install html2canvas --save
npm install jspdf --save
outputPDF.JS
import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';
const A4_WIDTH = 592.28;
const A4_HEIGHT = 841.89;
/**
* @param {Object} param
* @param {HTMLElement} param.element - 需要转换的dom根节点
* @param {number} [param.contentWidth=550] - 一页pdf的内容宽度,0-592.28
* @param {string} [param.filename='document.pdf'] - pdf文件名
* @param {HTMLElement} param.header - 页眉dom元素
* @param {HTMLElement} param.footer - 页脚dom元素
*/
export async function outputPDF({ element, contentWidth = 592,
footer, header, filename = "测试A4分页.pdf" }) {
if (!(element instanceof HTMLElement)) return
// jsPDFs实例
const _PDF = new jsPDF({ unit: 'pt', format: 'a4', orientation: 'p', });
// 内容转换
const { height: contentHeight, data: contentData } = await toCanvas(element, contentWidth);
// 页脚元素 经过转换后在PDF页面的高度
const { height: tfooterHeight } = await toCanvas(footer, contentWidth)
// 页眉元素 经过转换后在PDF的高度
const { height: theaderHeight } = await toCanvas(header, contentWidth);
// 距离PDF左边的距离
const baseX = (A4_WIDTH - contentWidth) / 2; // 预留空间给左边
// 出去页头、页眉、还有内容与两者之间的间距后 每页内容的实际高度
const originalPageHeight = (A4_HEIGHT - tfooterHeight - theaderHeight);
//获取分页符
const pageBreaks = getPageBreaks(element, originalPageHeight, contentWidth, contentHeight);
// 增加空白遮挡
function addBlank(x, y, width, height) {
y = Math.floor(y)
_PDF.setFillColor(255, 255, 255);
_PDF.rect(x, y, Math.ceil(width), Math.ceil(height), 'F');
};
// 添加页眉
async function addHeader() {
const { height, data } = await toCanvas(header, A4_WIDTH);
if (data) {
_PDF.addImage(data, 'JPEG', 0, 0, A4_WIDTH, height);
}
}
// 添加页脚
async function addFooter(pageCount, current) {
footer && footer.querySelector('.pdf-footer-page') && (footer.querySelector('.pdf-footer-page').innerText = current);
footer && footer.querySelector('.pdf-footer-page-count') && (footer.querySelector('.pdf-footer-page-count').innerText = pageCount);
const { height, data } = await toCanvas(footer, A4_WIDTH);
if (data) {
_PDF.addImage(data, 'JPEG', 0, A4_HEIGHT - height, A4_WIDTH, height)
}
}
// 根据分页位置 开始分页
for (let i = 0; i < pageBreaks.length; ++i) {
console.log(`共${pageBreaks.length}页, 生成第${i + 1}页`)
_PDF.addImage(contentData, 'JPEG', baseX, theaderHeight - pageBreaks[i], contentWidth, contentHeight);
let blankH = pageBreaks[i + 1] ? (originalPageHeight - pageBreaks[i + 1] - pageBreaks[i] + tfooterHeight + 1) : 0
addBlank(0, A4_HEIGHT - blankH, A4_WIDTH, blankH);
addBlank(0, 0, A4_WIDTH, theaderHeight);
await addHeader()
await addFooter(pageBreaks.length, i + 1);
if (i !== pageBreaks.length - 1) {
_PDF.addPage();
}
}
return _PDF.save(filename)
}
// 将元素转化为canvas元素
// 通过 放大 提高清晰度
// width为内容宽度
async function toCanvas(element, width) {
if (!(element instanceof HTMLElement)) return { width: 0, height: 0, data: null };
// canvas元素
const canvas = await html2canvas(element, {
scale: 4, // 增加清晰度
useCORS: true,// 允许跨域
windowWidth: element.scrollWidth,
windowHeight: element.scrollHeight,
onrendered: function (canvas) {
console.log(canvas);
document.body.appendChild(canvas);
}
});
// 高度转化为PDF的高度
const height = (width / canvas.width) * canvas.height;
// 转化成图片Data
const canvasData = canvas.toDataURL('image/jpeg', 1.0);
return { width, height, data: canvasData };
}
function getPageBreaks(element, originalPageHeight, contentWidth, contentHeight) {
const pageBreaks = [0];
// 获取元素距离网页顶部的距离
// 通过遍历offsetParant获取距离顶端元素的高度值
let rootElOffsetTop = 0
function getElementTop(element) {
let actualTop = element.offsetTop;
let current = element.offsetParent;
while (current && current !== null) {
actualTop += current.offsetTop;
current = current.offsetParent;
}
return actualTop - rootElOffsetTop;
}
rootElOffsetTop = getElementTop(element)
// 元素在网页页面的宽度
const elementWidth = element.offsetWidth;
// PDF内容宽度 和 在HTML中宽度 的比, 用于将 元素在网页的高度 转化为 PDF内容内的高度, 将 元素距离网页顶部的高度 转化为 距离Canvas顶部的高度
const rate = contentWidth / elementWidth
// 遍历正常的元素节点
function traversingNodes(nodes) {
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i];
// 图片元素不需要继续深入,作为深度终点
const isIMG = node.tagName === 'IMG';
const isDivide = node.classList && node.classList.contains('divide');
const isTableCol = node.tagName === 'TR';
// 对需要处理分页的元素,计算是否跨界,若跨界,则直接将顶部位置作为分页位置,进行分页,且子元素不需要再进行判断
let { offsetHeight } = node;
// 计算出最终高度
let offsetTop = getElementTop(node);
// dom转换后距离顶部的高度
// 转换成canvas高度
const top = Math.floor(rate * (offsetTop))
if (isTableCol || isIMG || isDivide || !(node.childNodes?.length > 0)) {
// dom高度转换成生成pdf的实际高度
// 代码不考虑dom定位、边距、边框等因素,需在dom里自行考虑,如将box-sizing设置为border-box
updatePos(rate * offsetHeight, top, node);
}
// 对于普通元素,则判断是否高度超过分页值,并且深入
else {
// 遍历子节点
traversingNodes(node.childNodes);
}
}
return;
}
// 可能跨页元素位置更新的方法
// 需要考虑分页元素,则需要考虑两种情况
// 1. 普通达顶情况,如上
// 2. 当前距离顶部高度加上元素自身高度 大于 整页高度,则需要载入一个分页点
function updatePos(eheight, top) {
// 如果高度已经超过当前页,则证明可以分页了
if (top - (pageBreaks.length > 0 ? pageBreaks[pageBreaks.length - 1] : 0) >= originalPageHeight) {
pageBreaks.push((pageBreaks.length > 0 ? pageBreaks[pageBreaks.length - 1] : 0) + originalPageHeight);
}
// 若 距离当前页顶部的高度 加上元素自身的高度 大于 一页内容的高度, 则证明元素跨页,将当前高度作为分页位置
else if ((top + eheight - (pageBreaks.length > 0 ? pageBreaks[pageBreaks.length - 1] : 0) > originalPageHeight) && (top != (pageBreaks.length > 0 ? pageBreaks[pageBreaks.length - 1] : 0))) {
pageBreaks.push(top);
}
}
// 深度遍历节点的方法
traversingNodes(element.childNodes);
// 可能会存在遍历到底部元素为深度节点,可能存在最后一页位置未截取到的情况
if (pageBreaks[pageBreaks.length - 1] + originalPageHeight < contentHeight) {
pageBreaks.push(pageBreaks[pageBreaks.length - 1] + originalPageHeight);
}
return pageBreaks
}
示例 代码:
页眉页脚需使用行内样式
<div class="pdf-box">
<div class="header" ref="pdfHeader" style="width: 778px;height: 88px;"></div>
<div class="report" ref="pdfContenter">
</div>
<div class="footer" ref="pdfFooter" style="color: #000;font-size: 16px;text-align: center;line-height: 88px;width: 778px;height: 88px;">
第
<span class="pdf-footer-page"></span>
页 / 共
<span class="pdf-footer-page-count"></span>
页
</div>
</div>
import { outputPDF } from '@/utils/outputPDF'
let pdfContenter = ref(null) //获取dome
let pdfHeader = ref(null)
let pdfFooter = ref(null)
outputPDF({
element: pdfContenter.value,
footer: pdfFooter.value,
header: pdfHeader.value,
filename: '作业报告.pdf'
})