vue3實(shí)現(xiàn)html轉(zhuǎn)成pdf并導(dǎo)出的示例代碼
方案一:html2canvas + jspdf
代碼
import html2Canvas from "html2canvas";
import JsPDF from "jspdf";
import moment from "moment";
import axios from "axios";
var noTableHeight = 0; //table外的元素高度
/**
* @param title pdf名稱(chēng)
* @param dom 想要打印的元素
* @param fileList 想要打印的元素?cái)?shù)組(同類(lèi)名元素)
* @param type 縱向還是橫向
* @param isDownLoad 轉(zhuǎn)換完成是否下載
*/
export const htmlPdf = async (title, dom, fileList, type, isDownLoad) => {
return new Promise((resolve) => {
const html = document.querySelector(dom);
let htmlHeight = 0;
for (let i = 0; i < fileList.length; i++) {
htmlHeight = htmlHeight + fileList[i].offsetHeight + 30;
}
html.style.height = htmlHeight + "px";
const divParentHeight = JSON.parse(
JSON.stringify(document.querySelector(dom).offsetHeight)
);
let uploadUrl = "";
if (fileList) {
const pageHeight = Math.floor((277 * html.scrollWidth) / 190) + 20; //計(jì)算pdf高度
for (let i = 0; i < fileList.length; i++) {
//循環(huán)獲取的元素
const multiple = Math.ceil(
(fileList[i].offsetTop + fileList[i].offsetHeight) / pageHeight
); //元素的高度
if (isSplit(fileList, i, multiple * pageHeight)) {
//計(jì)算是否超出一頁(yè)
var _H = ""; //向pdf插入空白塊的內(nèi)容高度
if (fileList[i].localName !== "tr") {
//判斷是不是表格里的內(nèi)容
_H =
multiple * pageHeight -
(fileList[i].offsetTop + fileList[i].offsetHeight);
} else {
_H =
multiple * pageHeight -
(fileList[i].offsetTop +
fileList[i].offsetHeight +
noTableHeight) +
20;
}
var newNode = getFooterElement(_H); //向pdf插入空白塊的內(nèi)容
const divParent = fileList[i].parentNode; // 獲取該div的父節(jié)點(diǎn)
const next = fileList[i].nextSibling; // 獲取div的下一個(gè)兄弟節(jié)點(diǎn)
const isTruncate = true;
// 判斷兄弟節(jié)點(diǎn)是否存在
if (next && !isTruncate) {
// 存在則將新節(jié)點(diǎn)插入到div的下一個(gè)兄弟節(jié)點(diǎn)之前,即div之后
divParent.insertBefore(newNode, next);
document.querySelector(dom).style.height =
newNode.offsetHeight + divParentHeight + "px";
} else {
// 否則向節(jié)點(diǎn)添加最后一個(gè)子節(jié)點(diǎn)
divParent.appendChild(newNode);
}
}
}
}
html2Canvas(html, {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 1,
scale: 1, // 按比例增加分辨率
}).then(async (canvas) => {
//type==true橫向
var pdfType = type ? "l" : "p";
var pdf = new JsPDF(pdfType, "mm", "a4"); // A4紙,縱向
var ctx = canvas.getContext("2d");
var a4w = type ? 277 : 190;
var a4h = type ? 190 : 277; // A4大小,210mm x 297mm,四邊各保留10mm的邊距,顯示區(qū)域190x277
var imgHeight = Math.floor((a4h * canvas.width) / a4w); // 按A4顯示比例換算一頁(yè)圖像的像素高度
var renderedHeight = 0;
while (renderedHeight < canvas.height) {
var page = document.createElement("canvas");
page.width = canvas.width;
page.height = Math.min(imgHeight, canvas.height - renderedHeight); // 可能內(nèi)容不足一頁(yè)
// 用getImageData剪裁指定區(qū)域,并畫(huà)到前面創(chuàng)建的canvas對(duì)象中
page
.getContext("2d")
.putImageData(
ctx.getImageData(
0,
renderedHeight,
canvas.width,
Math.min(imgHeight, canvas.height - renderedHeight)
),
0,
0
);
pdf.addImage(
page.toDataURL("image/jpeg", 1.0),
"JPEG",
10,
10,
a4w,
Math.min(a4h, (a4w * page.height) / page.width)
); // 添加圖像到頁(yè)面,保留10mm邊距
renderedHeight += imgHeight;
if (renderedHeight < canvas.height) {
pdf.addPage(); // 如果后面還有內(nèi)容,添加一個(gè)空頁(yè)
}
// delete page;
}
// 保存文件
if (isDownLoad) {
pdf.save(title + ".pdf");
resolve();
} else {
// var pdfData = pdf.output('datauristring'); //獲取base64Pdf
const pdfBlob = pdf.output("blob"); //獲取blob
console.log(pdfBlob)
//const pdfBlob = base64ToBlob(pdfData.split(',')[1], 'application/pdf');
const pdfFile = blobToFile(pdfBlob, title + ".pdf");
const FD = new FormData();
const nowDate = moment(new Date()).format("YYYYMMDDHHmmss");
FD.append("file", pdfFile, nowDate + title + ".pdf");
await uploadFile(null, FD).then((res) => {
const result = res.result;
uploadUrl = result.url;
resolve(uploadUrl);
});
}
document.getElementsByClassName("divRemove")[0]?.remove();
document.querySelector(dom).style.height = divParentHeight + "px";
//利用promise 回調(diào)到頁(yè)面中去
});
});
// type傳有效值pdf則為橫版
};
// pdf截?cái)嘈枰粋€(gè)空白位置來(lái)補(bǔ)充
const getFooterElement = (remainingHeight, fillingHeight = 0) => {
const newNode = document.createElement("div");
newNode.style.background = "#ffffff";
newNode.style.width = "calc(100% + 8px)";
newNode.style.marginLeft = "-4px";
newNode.style.marginBottom = "0px";
newNode.classList.add("divRemove");
newNode.style.height = remainingHeight + fillingHeight + "px";
return newNode;
};
const isSplit = (nodes, index, pageHeight) => {
// 判斷是不是tr 如果不是高度存起來(lái)
// 表格里的內(nèi)容要特殊處理
// tr.offsetTop 是tr到table表格的高度
// 所以計(jì)算高速時(shí)候要把表格外的高度加起來(lái)
// 生成的pdf沒(méi)有表格了這里可以不做處理 直接計(jì)算就行
if (nodes[index].localName !== "tr") {
//判斷元素是不是tr
noTableHeight += nodes[index].clientHeight;
}
if (nodes[index].localName !== "tr") {
return (
nodes[index].offsetTop + nodes[index].offsetHeight < pageHeight &&
nodes[index + 1] &&
nodes[index + 1].offsetTop + nodes[index + 1].offsetHeight > pageHeight
);
} else {
return (
nodes[index].offsetTop + nodes[index].offsetHeight + noTableHeight <
pageHeight &&
nodes[index + 1] &&
nodes[index + 1].offsetTop +
nodes[index + 1].offsetHeight +
noTableHeight >
pageHeight
);
}
};
export function base64ToBlob(base64, type) {
let binary = atob(base64);
let array = [];
for (let i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i));
}
return new Blob([new Uint8Array(array)], { type });
}
export function blobToFile(theBlob, fileName) {
theBlob.lastModifiedDate = new Date();
theBlob.name = fileName;
return theBlob;
}
export const uploadFile = async (
uploadUrl,
params,
module = "CA",
fileSymbol = "",
customFileName = "",
renameFlag = true
) => {
return await new Promise((resolve) => {
const api = uploadUrl || window?.uploadOptions?.upload?.action
axios
.post(
`${api}?module=${module}&fileSymbol=${fileSymbol}&customFileName=${customFileName}&renameFlag=${renameFlag}`,
params,
{
headers: {
"Content-Type": "multipart/form-data",
},
}
)
.then((res) => {
resolve(res.data);
});
});
};
/* */
原理: 先用 html2canvas 將 HTML 內(nèi)容轉(zhuǎn)換為圖片,然后將圖片放入 jsPDF 中生成 PDF。
優(yōu)點(diǎn)
- 純前端完成,不依賴(lài)服務(wù)器。
- 可以實(shí)現(xiàn)一鍵下載。
缺點(diǎn)
- 質(zhì)量堪憂:生成的是圖片型 PDF,文字無(wú)法選中、搜索,清晰度不高(尤其對(duì)高分辨率大頁(yè)面)。
- 性能差,轉(zhuǎn)換大頁(yè)面容易卡頓甚至崩潰。
- 分頁(yè)控制非常麻煩。
適用場(chǎng)景: 對(duì)PDF質(zhì)量要求不高、內(nèi)容量很小的場(chǎng)景,或者需要生成“快照”式PDF。
方案二:html2canvas + pdf-lib
代碼
import html2Canvas from "html2canvas";
import { PDFDocument, rgb } from 'pdf-lib';
import moment from "moment";
import axios from "axios";
var noTableHeight = 0; //table外的元素高度
export const htmlPdf = async (title, dom, fileList, type, isDownLoad) => {
return new Promise(async (resolve) => {
const html = document.querySelector(dom);
let htmlHeight = 0;
const divParentHeight = JSON.parse(
JSON.stringify(document.querySelector(dom).offsetHeight)
);
let uploadUrl = "";
if (fileList) {
for (let i = 0; i < fileList.length; i++) {
htmlHeight = htmlHeight + fileList[i].offsetHeight + 30;
}
html.style.height = htmlHeight + "px";
const pageHeight = Math.floor((277 * html.scrollWidth) / 190) + 20; //計(jì)算pdf高度
for (let i = 0; i < fileList.length; i++) {
//循環(huán)獲取的元素
const multiple = Math.ceil(
(fileList[i].offsetTop + fileList[i].offsetHeight) / pageHeight
); //元素的高度
if (isSplit(fileList, i, multiple * pageHeight)) {
//計(jì)算是否超出一頁(yè)
var _H = ""; //向pdf插入空白塊的內(nèi)容高度
if (fileList[i].localName !== "tr") {
//判斷是不是表格里的內(nèi)容
_H =
multiple * pageHeight -
(fileList[i].offsetTop + fileList[i].offsetHeight);
} else {
_H =
multiple * pageHeight -
(fileList[i].offsetTop +
fileList[i].offsetHeight +
noTableHeight) +
20;
}
var newNode = getFooterElement(_H); //向pdf插入空白塊的內(nèi)容
const divParent = fileList[i].parentNode; // 獲取該div的父節(jié)點(diǎn)
const next = fileList[i].nextSibling; // 獲取div的下一個(gè)兄弟節(jié)點(diǎn)
const isTruncate = true;
// 判斷兄弟節(jié)點(diǎn)是否存在
if (next && !isTruncate) {
// 存在則將新節(jié)點(diǎn)插入到div的下一個(gè)兄弟節(jié)點(diǎn)之前,即div之后
divParent.insertBefore(newNode, next);
document.querySelector(dom).style.height =
newNode.offsetHeight + divParentHeight + "px";
} else {
// 否則向節(jié)點(diǎn)添加最后一個(gè)子節(jié)點(diǎn)
divParent.appendChild(newNode);
}
}
}
}
if (!html) {
throw new Error('DOM元素未找到');
}
// 1. 分塊處理大文檔
const chunks = await splitContentIntoChunks(html);
// 2. 創(chuàng)建PDF文檔
const pdfDoc = await PDFDocument.create();
// 3. 處理每個(gè)分塊
const reusableCanvas = document.createElement('canvas');
const ctx = reusableCanvas.getContext('2d');
// for (const chunk of chunks) {
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
console.log(chunk)
reusableCanvas.width = html.offsetWidth * 1 // type ? 277 : 190//window.innerWidth * 0.5;
reusableCanvas.height = chunk.height;
// 使用 html2Canvas 渲染到現(xiàn)有 Canvas
await html2Canvas(chunk.html, {
allowTaint: false,
logging: false,
useCORS: true,
scale: 1,
y: chunk.offset,
windowHeight: chunk.offset,
canvas: reusableCanvas, // 指定使用現(xiàn)有 Canvas
});
// 4. 將canvas添加到PDF
await addCanvasToPdf(pdfDoc, reusableCanvas);
// 不需要手動(dòng)釋放內(nèi)存,因?yàn)槲覀儠?huì)復(fù)用同一個(gè) Canvas
// 只需清除上一塊的內(nèi)容
ctx.clearRect(0, 0, reusableCanvas.width, reusableCanvas.height);
// 釋放內(nèi)存
reusableCanvas.width = 1;
reusableCanvas.height = 1;
// }
}
// 5. 保存或上傳PDF
const pdfBytes = await pdfDoc.save();
if (isDownLoad) {
downloadPDF(pdfBytes, title);
return;
} else {
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const pdfFile = blobToFile(blob, title + ".pdf");
const FD = new FormData();
const nowDate = moment(new Date()).format("YYYYMMDDHHmmss");
FD.append("file", pdfFile, nowDate + title + ".pdf");
await uploadFile(null, FD).then((res) => {
const result = res.result;
uploadUrl = result.url;
resolve(uploadUrl);
});
}
})
}
// pdf截?cái)嘈枰粋€(gè)空白位置來(lái)補(bǔ)充
const getFooterElement = (remainingHeight, fillingHeight = 0) => {
const newNode = document.createElement("div");
newNode.style.background = "#ffffff";
newNode.style.width = "calc(100% + 8px)";
newNode.style.marginLeft = "-4px";
newNode.style.marginBottom = "0px";
newNode.classList.add("divRemove");
newNode.style.height = remainingHeight + fillingHeight + "px";
return newNode;
};
const isSplit = (nodes, index, pageHeight) => {
// 判斷是不是tr 如果不是高度存起來(lái)
// 表格里的內(nèi)容要特殊處理
// tr.offsetTop 是tr到table表格的高度
// 所以計(jì)算高速時(shí)候要把表格外的高度加起來(lái)
// 生成的pdf沒(méi)有表格了這里可以不做處理 直接計(jì)算就行
if (nodes[index].localName !== "tr") {
//判斷元素是不是tr
noTableHeight += nodes[index].clientHeight;
}
if (nodes[index].localName !== "tr") {
return (
nodes[index].offsetTop + nodes[index].offsetHeight < pageHeight &&
nodes[index + 1] &&
nodes[index + 1].offsetTop + nodes[index + 1].offsetHeight > pageHeight
);
} else {
return (
nodes[index].offsetTop + nodes[index].offsetHeight + noTableHeight <
pageHeight &&
nodes[index + 1] &&
nodes[index + 1].offsetTop +
nodes[index + 1].offsetHeight +
noTableHeight >
pageHeight
);
}
};
export function base64ToBlob(base64, type) {
let binary = atob(base64);
let array = [];
for (let i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i));
}
return new Blob([new Uint8Array(array)], { type });
}
export function blobToFile(theBlob, fileName) {
theBlob.lastModifiedDate = new Date();
theBlob.name = fileName;
return theBlob;
}
/**
* 將大內(nèi)容分割成多個(gè)可處理的部分
*/
async function splitContentIntoChunks(html) {
const chunks = [];
const totalHeight = html.scrollHeight;
const viewportHeight = window.innerHeight;
const chunkHeight = Math.min(viewportHeight, 960) * 1.5; // 每塊最多5000px
for (let offset = 0; offset < totalHeight; offset += chunkHeight) {
const height = Math.min(chunkHeight, totalHeight - offset);
chunks.push({
html,
height,
offset
});
// 避免瀏覽器卡頓
await new Promise(resolve => setTimeout(resolve, 10));
}
return chunks;
}
/**
* 將canvas添加到PDF文檔
*/
async function addCanvasToPdf(pdfDoc, canvas) {
// 轉(zhuǎn)換為PNG并嵌入
const pngImage = await pdfDoc.embedPng(canvas.toDataURL('image/png'));
//
const page = pdfDoc.addPage([canvas.width, canvas.height]); // A4尺寸
console.log(pngImage)
// 計(jì)算縮放比例以適應(yīng)頁(yè)面
let scale = Math.min(
(page.getWidth() - 40) / pngImage.width, // 左右各20pt邊距
(page.getHeight() - 40) / pngImage.height // 上下各20pt邊距
);
page.drawImage(pngImage, {
x: 20,
y: 20,
width: pngImage.width * scale,
height: pngImage.height * scale,
});
}
/**
* 下載PDF文件
*/
function downloadPDF(pdfBytes, fileName) {
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `${fileName}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
}
export const uploadFile = async (
uploadUrl,
params,
module = "CA",
fileSymbol = "",
customFileName = "",
renameFlag = true
) => {
return await new Promise((resolve) => {
const api = uploadUrl || window?.uploadOptions?.upload?.action
axios
.post(
`${api}?module=${module}&fileSymbol=${fileSymbol}&customFileName=${customFileName}&renameFlag=${renameFlag}`,
params,
{
headers: {
"Content-Type": "multipart/form-data",
},
}
)
.then((res) => {
resolve(res.data);
});
});
};
原理: 用方案一,一旦遇到超長(zhǎng)內(nèi)容打印的時(shí)候就會(huì)出現(xiàn)文檔內(nèi)容全部是黑屏現(xiàn)象,原因是因?yàn)閏anvas的畫(huà)布高度是有限的,方案二使用分塊的形式,對(duì)內(nèi)容進(jìn)行分割得到多個(gè)canvas,然后逐一添加到pdf,可以避免黑屏問(wèn)題,
優(yōu)點(diǎn)
- 純前端完成,不依賴(lài)服務(wù)器。
- 可以打印復(fù)雜量多內(nèi)容。
缺點(diǎn)
- 質(zhì)量堪憂:生成的是圖片型 PDF,文字無(wú)法選中、搜索,清晰度不高(尤其對(duì)高分辨率大頁(yè)面)。
- 性能差,轉(zhuǎn)換超長(zhǎng)內(nèi)容時(shí)用時(shí)很久。
使用時(shí)注意事項(xiàng)
- 注意如果是轉(zhuǎn)換彈框中modal中的內(nèi)容,一定要另外起一個(gè)新頁(yè)面,避免內(nèi)容中太多靜態(tài)資源,因?yàn)閏anvas轉(zhuǎn)換成base64圖片的時(shí)候會(huì)一直加載容器底下的所有靜態(tài)資源。
- 想要控制一頁(yè)展示內(nèi)容多少,用方法
splitContentIntoChunks中的chunkHeight就行,參數(shù)值越大,一頁(yè)展示的內(nèi)容越多,生成的canvas越少,會(huì)節(jié)省時(shí)間,但是打印出來(lái)的內(nèi)容會(huì)很小,所以要調(diào)整好自己合適的尺寸。 - 將canvas添加到PDF文檔的方法addCanvasToPdf使用時(shí)
const page = pdfDoc.addPage([canvas.width, canvas.height]); // A4尺寸
這行代碼里面的image的寬高就直接用canvas的,不要在自定義,不然容易出現(xiàn)變形
適用場(chǎng)景: 對(duì)PDF質(zhì)量要求不高、內(nèi)容量很小的場(chǎng)景,或者需要生成“快照”式PDF。
方案三:window.print()
如果只是想單純的得到一份pdf直接右擊另存為pdf格式的就能實(shí)現(xiàn)
方案四:提供靜態(tài)模版給服務(wù)端生成
這個(gè)方案是最好的,就是費(fèi)服務(wù)端,需要前后端配合,不論是性能還是得到的pdf清晰度都是比較好的。
到此這篇關(guān)于vue3實(shí)現(xiàn)html轉(zhuǎn)成pdf并導(dǎo)出的示例代碼的文章就介紹到這了,更多相關(guān)vue3實(shí)現(xiàn)html轉(zhuǎn)pdf內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 在Vue項(xiàng)目中實(shí)現(xiàn)HTML轉(zhuǎn)PDF功能的詳細(xì)步驟
- Vue使用html2canvas和jspdf實(shí)現(xiàn)PDF文件導(dǎo)出
- vue實(shí)現(xiàn)html轉(zhuǎn)化pdf并復(fù)制文字
- vue2利用html2canvas+jspdf動(dòng)態(tài)生成多頁(yè)P(yáng)DF方式
- vue2中Print.js的使用超詳細(xì)講解(pdf、html、json、image)
- vue使用html2PDF實(shí)現(xiàn)將內(nèi)容導(dǎo)出為PDF
- 基于Vue實(shí)現(xiàn)HTML轉(zhuǎn)PDF并導(dǎo)出
相關(guān)文章
Element ui table表格內(nèi)容超出隱藏顯示省略號(hào)問(wèn)題
這篇文章主要介紹了Element ui table表格內(nèi)容超出隱藏顯示省略號(hào)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,2023-11-11
vue.js集成codeMirror代碼編輯器的詳細(xì)代碼
這篇文章主要介紹了vue.js集成codeMirror代碼編輯器的相關(guān)資料,代碼分為前端代碼和后端代碼,感興趣的朋友一起看看吧2025-05-05
element ui 對(duì)話框el-dialog關(guān)閉事件詳解
下面小編就為大家分享一篇element ui 對(duì)話框el-dialog關(guān)閉事件詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-02-02
vue編寫(xiě)的功能強(qiáng)大的swagger-ui頁(yè)面及使用方式
swagger是一種標(biāo)準(zhǔn)的數(shù)據(jù)格式的定義,對(duì)于不同語(yǔ)言進(jìn)行實(shí)現(xiàn)一些注解API式的東西,能快速生成這種描述restful格式的api信息的json串,本文給大家詳細(xì)介紹vue編寫(xiě)的功能強(qiáng)大的swagger-ui頁(yè)面,感興趣的朋友跟隨小編一起看看吧2022-02-02
Vue導(dǎo)入Echarts實(shí)現(xiàn)折線散點(diǎn)圖
這篇文章主要為大家詳細(xì)介紹了Vue導(dǎo)入Echarts實(shí)現(xiàn)折線散點(diǎn)圖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10
Vue數(shù)據(jù)監(jiān)聽(tīng)方法watch的使用
這篇文章主要介紹了Vue數(shù)據(jù)監(jiān)聽(tīng)方法watch的使用,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03
關(guān)于Vue.js一些問(wèn)題和思考學(xué)習(xí)筆記(2)
這篇文章主要為大家分享了關(guān)于Vue.js一些問(wèn)題和思考的學(xué)習(xí)筆記,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12
vue實(shí)現(xiàn)移動(dòng)端拖拽懸浮按鈕
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)移動(dòng)端拖拽懸浮按鈕,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07

