记录一下纯前端导出
前置
目前在开发垂类的ai应用,区别于普通的大模型提供的聊天功能,垂类即在某个领域内的继续深度挖掘,在此过程中就接到了很多特色的需求,如:
上面的这些应用都是训练大模型之后,由大模型返回字符串数据流,再由前端处理成各式各样的花里胡哨的功能。
例如 AI-数字活页教材
- 大模型返回的格式是这样的
(一开始只有正常的字符串,这是经过训练后的)
:
const str = `[0:《物流概论》] [1:项目一 物流基础知识] [2:任务一 物流的概念与发展] [3:任务背景] [6:物流作为现代经济的重要组成部分,对企业和社会的发展具有重要意义。] [3:任务要求] [6:要求学生了解物流的基本概念和发展历程。] [3:学习目标] [4:知识目标] [5:(1)掌握物流的定义和内涵。] [5:(2)熟悉物流的发展阶段和主要特点。] [4:技能目标] [5:(1)能够简要阐述物流的发展历程。] [5:(2)能够分析物流在现代经济中的作用。] [4:素质目标] [5:(1)培养学生对物流领域的兴趣和探索精神。] [5:(2)培养学生的宏观思维和综合分析能力。] [3:知识讲解] [4:一、物流的定义和范围] [5:(一)物流的概念] [6:明确物流的基本含义和涵盖的领域。] [5:(二)物流的范围] [6:包括采购物流、生产物流、销售物流等。] [4:二、物流的发展历程] [5:(一)传统物流阶段] [6:特点和主要形式。] [5:(二)现代物流阶段] [6:新技术和管理理念的应用。] [4:三、物流的分类] [5:(一)按照物流活动的主体分类] [6:1.企业自营物流] [6:2.第三方物流] [6:3.第四方物流] [5:(二)按照物流活动的地域范围分类]`
- 产品想要的是这样的:
- 所以只能先将字符串转成数组对象:
const parseContentToKeyValueArray = (scheduleString) => {
if (!scheduleString) return
if (scheduleString.match(/\[[^\]]*$/)) {
scheduleString += ']'
}
const pairs = scheduleString.match(/\[(.*?)]/gs) || []
return pairs.map(pair => {
const [key, value] = pair.slice(1, -1).split(':').map(str => str.trim())
return { key, value, id: uuId() }
}).filter(item => item)
};
再一通xjb编辑之后就成了一本活页教材,该导出
tip:
在导出pdf时,如果有多页,且页面的内容长度不固定时,那么恭喜你:终于掉坑了!
内容被拦腰截断这是很正常的,深呼吸就好!
用到的库:
npm install html2canvas --save
npm install jspdf --save
进入正题吧:
前端导出的时候只是根据你要导出的html结构帮你生成pdf,但是它并不管你其他,该断页的时候他就强硬断,不管你内容是否受到影响,所以我们只能是说在生成之前我们自己分好页,计算出哪里会被截断,然后添加新的空白元素将要截断的元素往下一页挤。
- 所以为了方便计算,我们应该尽可能的将每一个元素拆成最小的单位,也就是初始数据不要包含父子级关系,页面布局的时候除了最外层,里面也尽可能的不要有父子关系的嵌套,大家都是兄弟关系,从上往下排列
例如我上面的结构,都是同级关系,都是最小的计算单位,那么在计算的时候就更加方便。
当然,即使你有树形结构或者有父子级关系也不大,只是要多做相应的处理就可以了
页面布局
计算断页并添加空白
setTimeout(() => {
const fileList = document.getElementsByClassName('pdfRef')
if (fileList.length > 0) {
const pageHeight = 1122; //计算pdf高度
let currentHeight = 0; // 当前元素的高度
let yushu = 0
for (let i = 0; i < fileList.length; i++) {
// 当前页用了多少高度
yushu = currentHeight % pageHeight;
const domH = parseFloat(window.getComputedStyle(fileList[i]).height);
// 获取该div的父节点
const divParent = fileList[i].parentNode;
if (fileList[i].classList.contains('bookname')) {
currentHeight += domH;
} else if (fileList[i].classList.contains('project')) {
if (!yushu) {
divParent.insertBefore(getFooterElement(80), fileList[i + 1]);
currentHeight = currentHeight + domH + 80;
} else {
const newNode = getFooterElement(pageHeight - yushu);
divParent.insertBefore(newNode, fileList[i]);
divParent.insertBefore(getFooterElement(80), fileList[i + 1]);
currentHeight = currentHeight + domH + pageHeight - yushu + 80;
}
} else if (fileList[i].classList.contains('text')) {
if (yushu + domH > pageHeight - 30) {
const newNode = getFooterElement(pageHeight - yushu + 60);
divParent.insertBefore(newNode, fileList[i - 1]);
currentHeight = currentHeight + domH + pageHeight - yushu + 60;
} else {
currentHeight += domH;
}
} else if (yushu + domH > pageHeight - 30) {
const newNode = getFooterElement(pageHeight - yushu + 140);
divParent.insertBefore(newNode, fileList[i - 1]);
currentHeight = currentHeight + domH + pageHeight - yushu + 140;
} else {
currentHeight += domH;
}
}
yushu = currentHeight % pageHeight;
console.log('🚀 yushu 🚀======>', yushu)
if (yushu) {
const divParent = fileList[fileList.length -1].parentNode;
const newNode = getFooterElement(pageHeight - yushu);
divParent.insertBefore(newNode, null);
}
}
}, 1000)
// pdf截断需要一个空白位置来补充
const getFooterElement = (remainingHeight, fillingHeight = 0) => {
const newNode = document.createElement("div");
newNode.style.width = "calc(100% + 8px)";
newNode.classList.add("divRemove");
newNode.style.height = remainingHeight + fillingHeight + "px";
return newNode;
};
上面的代码是根据我的需求特殊改的,通过判断是否为某些特殊元素来决定是否添加空白,但原理都是一样的,计算该元素的高度,再对比当前页剩余的高度,如果不够摆,那就添加空白挤到下一页去。这里只是给个参考,需求不同不能直接复用!
下面给一个大概率能复用到的整个流程代码
import html2Canvas from "html2canvas";
import JsPDF from "jspdf";
export const htmlPdf = (title, html, fileList, type) => {
// type传有效值pdf则为横版
if (fileList.length > 0) {
const pageHeight = 1122; //手动测得的 pdf 高度
let currentHeight = 0;
for (let i = 0; i < fileList.length; i++) {
// 循环获取的元素高度
const domH = parseFloat(window.getComputedStyle(fileList[i]).height);
if (currentHeight != 0) {
// 取余得到 pdf 当前页渲染到了多少高度
const yushu = currentHeight % pageHeight;
// 如果继续渲染当前元素会超出截断 + 直接加空白
if (yushu + domH > pageHeight - 30) {
const divParent = fileList[i].parentNode; // 获取该div的父节点
// 因为我的PDF有封面要占满整个单页,做特殊处理
if (domH == 1122) {
const newNode = getFooterElement(pageHeight - yushu);
divParent.insertBefore(newNode, fileList[i]);
currentHeight = currentHeight + domH + pageHeight - yushu;
} else {
// 这里加上30是为了留出上下边距
const newNode = getFooterElement(pageHeight - yushu + 30);
divParent.insertBefore(newNode, fileList[i]);
currentHeight = currentHeight + domH + pageHeight - yushu + 30;
}
} else {
currentHeight += domH;
}
} else {
currentHeight += domH;
}
}
}
html2Canvas(html, {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 1,
scale: 1, // 按比例增加分辨率
}).then((canvas) => {
var pdf = new JsPDF("p", "mm", "a4"); // A4纸,纵向
var ctx = canvas.getContext("2d");
var a4w = type ? 297 : 210;
var a4h = type ? 210 : 297;
var imgHeight = Math.floor((a4h * canvas.width) / a4w); // 按A4显示比例换算一页图像的像素高度
var renderedHeight = 0;
while (renderedHeight < canvas.height) {
var page = document.createElement("canvas");
page.width = canvas.width;
page.height = Math.min(imgHeight, canvas.height - renderedHeight); // 可能内容不足一页
// 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
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",
0,
0,
a4w,
Math.min(a4h, (a4w * page.height) / page.width)
); // 添加图像到页面,保留10mm边距
renderedHeight += imgHeight;
if (renderedHeight < canvas.height) {
pdf.addPage(); // 如果后面还有内容,添加一个空页
}
}
// 保存文件
pdf.save(title + ".pdf");
});
};
// pdf截断需要一个空白位置来补充
const getFooterElement = (remainingHeight, fillingHeight = 0) => {
const newNode = document.createElement("div");
newNode.style.background = "#ffffff";
newNode.style.width = "100%";
newNode.classList.add("divRemove");
newNode.style.height = remainingHeight + fillingHeight + "px";
return newNode;
};
页面使用
import { htmlPdf } from "@/utils/htmlToPDF.js"
const handleExport = () => {
var fileName = '前端牛马PDF'
// 找出所有要计算的元素
const fileList = document.getElementsByClassName('pdfRef')
// #pdfRef 为要导出的整个结构
htmlPdf(fileName, document.querySelector('#pdfRef'), fileList)
}