效果图:
写之前参考了网上的一些文章思路传送门,以为可以找到可以直接使用的轮子,可惜或多或少都有问题或者不能满足需求,所以只能自己写。
思路:
1.在页面建一个渲染数据的div.pdf-box
,可以设置这个div不可见(注意:这个div只是负责渲染数据,获取节点生成图片的不是这个节点)
2.建一个负责渲图的div#pdfDom
,把div.pdf-box
的子元素渲染进来(注意:子元素要有统一个标识,比如给一个同名的class
)
- 2.1 创建一个以A4纸尺寸为比例的
div.newNode1
,循环div.pdf-box
获取里面的子元素添加到div.newNode1
中,同时计算添加到里面的子元素的高(offsetHeight,有边距的还要加上边距
)是否大于页面的高,如果大于则停止添加,将div.newNode1
添加到div#pdfDom
中,然后再生成一个新的div.newNode1
重复这个操作,一直到把所有元素添加完 - 2.2 上一步中有一个要注意的点,当子元素撑满一页后,新建了一个
div.newNode1
,记得将循环的下标回退到当前对象,否则会导致当前子元素丢失 - 2.3 最后一页有三种情况:
a.只有一个模块:从上一页移除最后一个元素放到当前模块前面,使最后一页有两个元素(不要问为什么要这样,产品给的需求);
b.有两个模块:不用管,正常渲染;
c.大于两个模块:判断除了最后两个元素外,其余元素的高相加是否大于页面高度的60%(这个比例自己定,主要看剩下的高度能不能放得下剩余的两个元素),大于则说明不够放剩余两个模块,把最后的两个放到下一页。 - 2.4 在最后一个模块的前面插入
div
将该模块撑到底部
3.通过html2canvas
将div#pdfDom
转成图片进行预览
4.通过jspdf
将图片转成pdf
格式并下载
html代码:
<template>
// 负责渲染节点内容
<div ref="_pdfHtml" id="pdfHtml" class="pdf-box">
// 里面是页面结构
<div class="js_p"></div>
<div class="js_p"></div>
<div class="js_p"></div>
</div>
// 负责渲图的节点
<div ref="pdfDom" id="pdfDom"></div>
</template>
js代码:
<script>
import html2canvas from "html2canvas"
import JsPDF from 'jspdf'
import dayjs from 'dayjs'
methods:{
async applyDom1() {
// console.log('this.pdfData---', this.pdfData);
document.documentElement.scrollTop = 0;
document.documentElement.scrollLeft = 0;
document.body.scrollTop = 0;
document.body.scrollLeft = 0;
// pdf下载思路:先将数据渲染在id为pdfHtml的dom上,然后遍历里面的子div放到class为pdfDiv的div中,通过子元素相加的ofsetHeight得到saveH,通过saveH判断内容高度是否已超过A4纸高度,如果超过就将已生成的pdfDiv放入id为pdfDom的元素中(这个元素是渲图用的),然后新生成一个div循环存放下一个子元素继续这个操作,否则继续相加offsetHieght,直到最后一个元素。
try{
let imgUrl = '', canvas = '';
// A4纸尺寸
const A4_WIDTH = 592.28, A4_HEIGHT = 841.89;
// 获取html节点及要插入的子元素
let imageWrapper = document.querySelector("#pdfHtml")
let lableListID = imageWrapper.querySelectorAll(".js_div");
// 要渲染成图片的dom对象
let pdfDiv = document.querySelector("#pdfDom")
// 按照A4的宽高比
let pageH = pdfDiv.offsetWidth / A4_WIDTH * A4_HEIGHT
// 记录的一页中子元素的高和保存子元素的数组
let saveH = 0, divSaveArr = []
// 循环子元素
for (let i = 0, len = lableListID.length; i < len; i++) {
// 累加子元素的高,30是模块间的margin-top值
saveH += lableListID[i].offsetHeight + 30
// console.log('当前i---', i, lableListID[i]);
if(saveH >= pageH) {
// 高度超过一页,把之前生成的一页数据插入到渲染的div中,重新生成新的一页数据并回退i到当前子元素,清空数组
// console.log('divSaveArr111---', i, divSaveArr);
if(divSaveArr.length >= 1) {
console.log('i000---', i);
let newNode1 = this.addDiv(divSaveArr, pageH, saveH)
$(pdfDom).append(newNode1)
saveH = 0
divSaveArr = []
i = i - 1 // 回退到当前的i
} else if(divSaveArr.length < 1) {
let _h = 0,
obj = $(lableListID[i]).find('.js_line'),
arr = []
for(let j = 0, len = obj.length; j < len; j++) {
_h += obj[j].offsetHeight + 30
if(_h >= pageH) {
let divDom = document.createElement('div');
$(divDom).addClass($(lableListID[i]).attr('class'))
$(divDom).trigger("create");
let table = document.createElement('table');
$(table).css({
width: '100%',
background: '#FFFFFF',
border: '2px solid #CD3C3A',
borderRadius: '10px',
color: '#333333',
marginTop: '30px',
})
arr.forEach(item => { $(table).append(item) })
$(divDom).append(table)
let newNode1 = this.createDiv(pageH)
$(newNode1).append(divDom)
$(pdfDom).append(newNode1)
_h = 0
arr = []
j = j - 1
} else {
arr.push(obj[j])
if(j === obj.length - 1) {
let divDom = document.createElement('div');
$(divDom).addClass($(lableListID[i]).attr('class'))
let newTabel = document.createElement('table');
$(newTabel).css({
width: '100%',
background: '#FFFFFF',
border: '2px solid #CD3C3A',
borderRadius: '10px',
color: '#333333',
marginTop: '30px',
})
let newH = 0
arr.forEach(item => {
$(newTabel).append(item)
setTimeout(() => {
newH += item.offsetHeight + 30
console.log('item forEach--', item, item.offsetHeight);
saveH = newH
}, 250);
})
$(divDom).append(newTabel)
// saveH = newH
divSaveArr.push(divDom)
_h = 0
newH = 0
arr = []
}
}
}
}
} else {
// console.log('saveH < pageH-- ',saveH, pageH);
// 未超过一页,将子元素添加到保存的数组中
divSaveArr.push(lableListID[i])
// console.log('i 2222--', i, divSaveArr, lableListID[i]);
// 循环到最后一个元素时
if(i == lableListID.length - 1) {
// console.log('循环到最后一个元素时--');
// 最后一页只有一个div,移除上一页最后一个元素加入最后一页
if(divSaveArr.length === 1) {
// console.log('===1');
$(pdfDom).children("div:last").children("div:last").remove()
divSaveArr.splice(divSaveArr.length - 1, 0, lableListID[i - 1])
let newNode1 = this.addDiv(divSaveArr, pageH, saveH)
$(pdfDom).append(newNode1)
saveH = 0
divSaveArr = []
} else if(divSaveArr.length > 2) {
// console.log('>>>> 2', divSaveArr);
// 最后一页的元素大于2,分两种情况,页面高度足够页面签名模块,放一起,不够,则把倒数第二块内容连同签名模块一起放到最后一页,其余内容放到倒数第二页
let _h = 0, arr2 = [lableListID[i-1], lableListID[i]]
for(let j = 0, len = divSaveArr.length - 2; j < len; j ++) {
_h += divSaveArr[j].offsetHeight
}
// 最后一页除了最后两个内容,其余内容>页面高度的70%,分两页渲染
if(_h >= pageH * 0.7) {
// console.log('_h >= pageH * 0.7---', _h);
// 渲染倒数第二页
let newNode1 = this.createDiv(pageH)
for(let k = 0, len = divSaveArr.length - 2; k < len; k ++) {
$(newNode1).append(divSaveArr[k])
}
$(pdfDom).append(newNode1)
// 渲染最后一页
let newNode2 = this.addDiv(arr2, pageH, lableListID[i-1].offsetHeight, true)
$(pdfDom).append(newNode2)
saveH = 0
divSaveArr = []
} else {
// console.log('在一页---');
// 多个模块都在最后一页
let newNode1 = this.addDiv(divSaveArr, pageH, saveH, true)
$(pdfDom).append(newNode1)
saveH = 0
divSaveArr = []
}
} else {
// console.log('===2');
// 剩余两个子元素
let newNode1 = this.addDiv(divSaveArr, pageH, saveH, true)
$(pdfDom).append(newNode1)
saveH = 0
divSaveArr = []
}
}
}
}
await html2canvas(pdfDom, {
allowTaint: true,
x: pdfDom.getBoundingClientRect().left, // 绘制的dom元素相对于视口的位置
y: pdfDom.getBoundingClientRect().top,
width: pdfDom.offsetWidth, // 因为多出的需要剪裁掉,
height: pdfDom.offsetHeight,
backgroundColor: "#F9F0E9", //一定要设置背景颜色,否则有的浏览器就会变花~,比如Edge
useCORS: true,
// scale: 2, // 图片模糊
dpi: 350, //
}).then((res) => {
// console.log(res);
let dataURL = res.toDataURL("image/png");
imgUrl = dataURL;
canvas = res
})
this.tableLoading = false
this.$refs._download.open(imgUrl, canvas)
} catch(err) {
console.log(err);
this.tableLoading = false
this.$message.warning('导出出错,请联系系统维护人员')
}
},
// 创建新的容器div
createDiv(pageH) {
let newNode1 = document.createElement('div');
$(newNode1).addClass(' pdfDiv').css({
width: '100%',
height: pageH + 'px',
backgroundImage: `url(${bgImg})`,
backgroundSize: '100% 100%',
padding: '46px',
fontSize: '18px',
})
return newNode1
},
// 设置模块位置在页面底部
addDiv(divSaveArr, pageH, saveH, type) {
let newNode1 = this.createDiv(pageH)
// 占位div,把签名模块往下顶
if(type) {
let emptyDiv = document.createElement('div');
emptyDiv.style.height = pageH - saveH - 100 + 'px'
// emptyDiv.style.background = '#fff'
divSaveArr.splice(divSaveArr.length - 1, 0, emptyDiv)
}
// 循环保存子元素的数组,将节点添加在一页中
divSaveArr.forEach(item => {
$(newNode1).append(item)
})
return newNode1
},
// 确定导出PDF图片
downloadPDF() {
// console.log('确定导出---', this.canvasData);
if(this.downloading) return;
this.downloading = true
let canvas = this.canvasData
//内容的宽度
let contentWidth = canvas.width;
//内容高度
let contentHeight = canvas.height;
//一页pdf显示html页面生成的canvas高度,a4纸的尺寸[595.28,841.89];
let pageHeight = contentWidth / 592.28 * 841.89;
//未生成pdf的html页面高度
let leftHeight = contentHeight;
//页面偏移
let position = 0;
//a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
let imgWidth = 595.28;
// let imgHeight = 841.89 / contentWidth * contentHeight;
let imgHeight = 592.28 / contentWidth * contentHeight;
//canvas转图片数据
let pageData = canvas.toDataURL('image/jpeg', 1.0);
//新建JsPDF对象
let PDF = new JsPDF('', 'pt', 'a4');
//判断是否分页
if (leftHeight < pageHeight) {
PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
} else {
while (leftHeight > 0) {
PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight);
leftHeight -= pageHeight;
position -= 841.89;
// 还有多余的另起一页
if (leftHeight > 2) PDF.addPage()
}
}
this.downloading = false
//保存文件
PDF.save(`xxx-${dayjs().valueOf()}` + '.pdf')
},
}
</script>
代码可以直接复用。
新增
因为以上方法不满足需求,会有过多空白处,所以只能继续细化,将每个div里的内容都单独抽出来弄成一个子元素,即原先的方式是:
<div class="js_div">
<p>内容</p>
<p>内容</p>
<p>内容</p>
<p>内容</p>
</div>
<div class="js_div">
<p>内容</p>
<p>内容</p>
<p>内容</p>
<p>内容</p>
</div>
改成
<div class="js_div">
<p>内容</p>
</div>
<div class="js_div">
<p>内容</p>
</div>
<div class="js_div">
<p>内容</p>
</div>
然后通过拿到所有的js_div
按照上面的方式去写就可以了,方法也是差不多的,就是要注意下样式丢失的问题。贴一下渲染的方法:
// 导出的html渲染方法
async applyDom() {
let ST = document.documentElement.scrollTop || document.body.scrollTop;
let SL = document.documentElement.scrollLeft || document.body.scrollLeft;
document.documentElement.scrollTop = 0;
document.documentElement.scrollLeft = 0;
document.body.scrollTop = 0;
document.body.scrollLeft = 0;
try{
let imgUrl = '', canvas = '';
// A4纸尺寸
const A4_WIDTH = 592.28, A4_HEIGHT = 841.89;
// 获取html节点及要插入的子元素
let imageWrapper = document.querySelector("#pdfHtmlNew")
let lableListID = imageWrapper.querySelectorAll(".js_div");
// 要渲染成图片的dom对象
let pdfDiv = document.querySelector("#pdfDom")
// 按照A4的宽高比
let pageH = pdfDiv.offsetWidth / A4_WIDTH * A4_HEIGHT
// 记录的一页中子元素的高和保存子元素的数组
let saveH = 0, divSaveArr = [], mtH = 0
// 循环子元素
for (let i = 0, len = lableListID.length; i < len; i++) {
// 累加子元素的高,30是模块间的margin-top值
mtH = $(lableListID[i]).hasClass('mt30') ? 30 : 0
saveH += lableListID[i].offsetHeight + mtH
// 46 * 2 是页面设置的padding值
if(saveH >= pageH - 46 * 2) {
// 高度超过一页,把之前生成的一页数据插入到渲染的div中,重新生成新的一页数据并回退i到当前子元素,清空数组
// 最后一个元素如果没有底部边框的加上
if($(divSaveArr[divSaveArr.length-1]).hasClass('none-bdb')) {
$(divSaveArr[divSaveArr.length-1]).removeClass('none-bdb')
}
let newNode1 = this.addDiv(divSaveArr, pageH, saveH)
$(pdfDom).append(newNode1)
saveH = 0
mtH = 0
divSaveArr = []
i = i - 1 // 回退到当前的i
} else {
// 未超过一页,将子元素添加到保存的数组中
divSaveArr.push(lableListID[i])
// 循环到最后一个元素时
if(i == lableListID.length - 1) {
// console.log('循环到最后一个元素时--');
// 最后一页只有一个div,移除上一页最后一个元素加入最后一页
if(divSaveArr.length === 1) {
// console.log('===1');
$(pdfDom).children("div:last").remove()
divSaveArr.splice(divSaveArr.length - 1, 0, lableListID[i - 1])
let newNode1 = this.addDiv(divSaveArr, pageH, saveH)
$(pdfDom).append(newNode1)
saveH = 0
mtH = 0
divSaveArr = []
} else if(divSaveArr.length > 2) {
// 最后一页的元素大于2,分两种情况,页面高度足够页面签名模块,放一起,不够,则把倒数第二块内容连同签名模块一起放到最后一页,其余内容放到倒数第二页
let _h = 0, arr2 = [lableListID[i-1], lableListID[i]]
for(let j = 0, len = divSaveArr.length - 2; j < len; j ++) {
_h += divSaveArr[j].offsetHeight
}
// 最后一页除了最后两个内容,其余内容>页面高度的70%,分两页渲染
if(_h >= pageH * 0.8) {
// 渲染倒数第二页
let newNode1 = this.createDiv(pageH)
for(let k = 0, len = divSaveArr.length - 2; k < len; k ++) {
$(newNode1).append(divSaveArr[k])
}
$(pdfDom).append(newNode1)
// 渲染最后一页
let newNode2 = this.addDiv(arr2, pageH, lableListID[i-1].offsetHeight, true)
$(pdfDom).append(newNode2)
saveH = 0
mtH = 0
divSaveArr = []
} else {
// console.log('在一页---');
// 多个模块都在最后一页
let newNode1 = this.addDiv(divSaveArr, pageH, saveH, true)
$(pdfDom).append(newNode1)
saveH = 0
mtH = 0
divSaveArr = []
}
} else {
// console.log('===2');
// 剩余两个子元素
let newNode1 = this.addDiv(divSaveArr, pageH, saveH, true)
$(pdfDom).append(newNode1)
saveH = 0
mtH = 0
divSaveArr = []
}
}
}
}
document.body.scrollTop = document.documentElement.scrollTop = 0;
// 防止html没有组成完就开始生成图片
setTimeout(async () => {
await html2canvas(pdfDom, {
allowTaint: true,
width: pdfDom.offsetWidth, // 因为多出的需要剪裁掉,
height: pdfDom.offsetHeight,
backgroundColor: "#F9F0E9", //一定要设置背景颜色,否则有的浏览器就会变花~,比如Edge
useCORS: true,
scale: 2, // 图片模糊
dpi: window.devicePixelRatio * 2, //
}).then((res) => {
let dataURL = res.toDataURL("image/png");
imgUrl = dataURL;
canvas = res
})
this.tableLoading = false
document.querySelector("#pdfDom").innerHTML = "";
this.$refs._download.open(imgUrl, canvas)
}, 1000)
} catch(err) {
console.log(err);
this.tableLoading = false
this.$message.warning('导出出错')
}
},
另外记录一个遇到的问题:
1.html2canvas偏移
项目的版本是1.0.0-rc.7
,通过网上查到设置scrollX: 0,scrollY: 0
无效,设置setTimeout
无效,然后死命想怎么设置scrollX,scrollY,x,y
都没用,直到找到这篇,福音啊,想来想去就是没想到版本的问题,这篇里有两种方式,第一种没验证,直接更新版本和设置对应的配置解决了。
处理方法:
下载新版本(目前是1.4.1版本), 配置参数dpi: window.devicePixelRatio, scale:2
html2canvas(pdfDom, {
allowTaint: true,
width: pdfDom.offsetWidth, // 因为多出的需要剪裁掉,
height: pdfDom.offsetHeight,
backgroundColor: "#F9F0E9", //一定要设置背景颜色,否则有的浏览器就会变花~,比如Edge
useCORS: true,
scale: 1, // 图片模糊
dpi: window.devicePixelRatio * 2, //
}).then((res) => {
let dataURL = res.toDataURL("image/png");
imgUrl = dataURL;
})
2.html2canvas截图不完整,底部有空白
这个问题是随机的,有些浏览器可以有些不行,查到有说法因为内容长度超出了html2canvas的有效区域,所以只能通过缩小放大比例来解决。方法是有用的,但是原因感觉应该不是这个,先记着吧。
处理方法:
scale
设置为1(scale: 1
)。