uniapp-vue2 页面生成PDF

1. 场景

unapp-vue2 app上预览pdf,并且需要可以下载pdf文件。

2.问题

uniappapp上并不能直接操作dom,导致不能直接使用 html2canvas 或者 jsPDF 这类库来生成pdf文件。

3. 解决方案

找了很久,在uniapp的插件商店上找到了可以使用的插件,sp-html2pdf-render 文档地址

本来以为问题可以解决了,预览PDF发现,分页没有做适配,当项数据在当前页放不下时,直接另一半在下一页出来了,正常来说,当前项数据在这一页放不下时,应改放到下一页渲染的,而不是上一页渲染一半,下一页又渲染一办,这样及其不美观

后来打算在组件源码上解决

4.修改代码

找到组件 sp-html2pdf-render.vue 的代码 ,找到 <script module="h2pRender" lang="renderjs"> 里面的renderDom()方法

1
2
3
4
if(!el) {
console.error('dom盒子未加载成功,请先确保dom渲染完成,再检查你的domId是否有误');
return
}

后面添加一下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
 // 设置A4 宽高
const A4Width = this.A4Width
const A4Height = this.A4Height

// 获取全部子组件的dom节点
const mainElement = document.getElementById('pdf-detail-main');
const dom2 = Array.from(mainElement.querySelector('.pdf-container').children);
const msgListElement = mainElement.querySelector('.pdf-container > #msg-list');
if (msgListElement) {
const msgListChildren = Array.from(msgListElement.children);
dom2.push(...msgListChildren);
}

// 一页的对应px
const realHeight = el.offsetWidth / A4Width * A4Height
// 所有子组件的offsetHeight高度数组
let childrenList = []
Array.from(dom2).forEach(item => {
childrenList.push(item.offsetHeight)
});

// result 每一页打印内容数据统计
let result = []
// temp 每一页的临时数据统计
let temp = []
// 打印头尾padding值
const paddingTB = 40
// 每一页上下padding40,所以初始化80,不需要padding就设置0
let sum = paddingTB * 2

// 遍历子组件的offsetHeight高度数组
for(let i = 0; i < childrenList.length; i++){
sum += childrenList[i]
temp.push(childrenList[i])
// 如果dom的classname包含header关键字,则另起一页
// 或者累计元素的高度之和大于一页的高度,则另起一页
if((dom2[i + 1] && dom2[i + 1].id.indexOf('header') > -1) || sum + childrenList[i+1] >= realHeight){
const size = result.length >= 10 ? 0.1 : 0.2 // 只是个大概优化,还是会出现误差的
result.push({
index: i, // 当前页结束的dom索引值
height: sum, // 当前页高度
list: temp, // 当前页dom累计临时数据
id: dom2[i].id, // 当前页结束dom的classname
idFirst: dom2[i - temp.length + 1].id, // 当前页第一个dom的classname
whiteDivHeight: realHeight - sum + (temp.length * size), // 另起一页前,空白区域填充的高度(因为计算存在误差,所以需要调整加一下高度)
})
sum = paddingTB * 2
temp = []
}
}

// 如果还有内容未满足header关键字或者累计元素的高度之和小于一页的高度,则另起一页
if(result.length === 0 || dom2.length - 1 > result[result.length - 1].index){
const size = result.length >= 10 ? 0.1 : 0.2 // 只是个大概优化,还是会出现误差的
result.push({
index: childrenList.length - 1,
height: sum,
list: temp,
id: dom2[childrenList.length - 1].id,
idFirst: dom2[childrenList.length - temp.length].id,
whiteDivHeight: realHeight - sum - (dom2.length * size) // 最后一页可以减小小
})
}

// 遍历每一页打印内容数据统计
await result.forEach((item, index) => {
const firsttarget = document.querySelector(`#${item.idFirst}`)
const target = document.querySelector(`#${item.id}`)
// 上方:padding区域
const divElement = document.createElement("div")
divElement.style.height = `${paddingTB}px`
firsttarget.parentNode.insertBefore(divElement, firsttarget);
// 下方:空白区域 + padding区域
const divElement2 = document.createElement("div")
divElement2.style.height = `calc(${item.whiteDivHeight + paddingTB}px)`
target.parentNode.insertBefore(divElement2, target.nextSibling);
})

思路其实就是获取每一页的高度,然后获取每一个元素的高,计算当前元素插入当前页后是否超出页面的高,如果超出了就另一起一页,为了美观,在每一页的页头页尾都添加了padding区域
改变页面的布局后,再渲染成 html2canvas 后转成base64,具体获取每个节点高度得根据具体项目来修改

5.遗留未解决的问题

  1. 生成的pdf文件有点过大,目前还没解决方案
  2. 因为业务需求,需要将生成的PDF传给后端,后端再返回给前端,但是后端返回的是application/pdf格式的文件流,app并不支撑使用 new Blob,所以目前没办法把后端返回的文件流保存下来,后续再做优化