以下是导出的方法:
// 通过截图、分页、处理文字截断后从dom生成pdf并导出
import { nextTick } from 'vue'
import { BxmMessage } from 'bxm-ui3'
import html2Canvas from 'html2canvas'
import JsPDF from 'jspdf'
/**
*
* @param {*} dom 导出的模块
* @param {*} fileName 导出文件名称
* @param {*} splitClassName 需要处理分页的类名(请将可能需要处理分页(即可能出现文字被分割)的元素都加上统一的类名)这里要保证整个dom中没有上下的margin,且每一行文字、图片所在的小容器都要设置统一的类名,把这个类名传入函数进行处理
*/
export async function exportFileByPDF(dom, fileName, splitClassName) {
// 获取正确的a4纸转换成像素值宽高
let { a4Width, a4Height, currentDPT, dptValue } = getA4Size()
a4Width = a4Width / dptValue
a4Height = a4Height / dptValue
// dom所在为起点
let startHeight = dom.getBoundingClientRect().top
// 处理分页元素
let questionTitleList = dom.querySelectorAll('.' + splitClassName)
await checkBandary(questionTitleList, a4Width, a4Height, startHeight, fileName)
// 开始截图
nextTick(() => {
html2Canvas(dom, {
scale: 2, // 设置缩放(设置成1导出文件文本框右边有一块灰色部分)
allowTaint: true, // 允许跨域渲染图片
useCORS: true, // 使用CORS从服务器加载图像
logging: false, // 是否打印日志
bgcolor: '#ffffff', // 背景色
}).then((canvas) => {
// 比例采用计算的比例,不固定
let scale = canvas.width / a4Width
// 用px单位生成pdf
let pdf = new JsPDF('p', 'px', 'a4') //A4纸,纵向
// 处理数据
let ctx = canvas.getContext('2d')
// 按A4显示比例换算一页图像的像素高度,截图为了清晰度设置了scale:2,所以这里是乘,在最后加到pdf中时在缩小
// let height = Math.floor(((a4Height / scale) * canvas.width / (a4Width / scale)))
let imgHeight = a4Height * scale
let renderedHeight = 0
// 分页
while (renderedHeight < canvas.height) {
let page = document.createElement('canvas')
page.width = canvas.width
let pageHeight = Math.min(imgHeight, canvas.height - renderedHeight)
// 可能内容不足一页
page.height = pageHeight
// 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
// 这时等于是宽高放大两倍的
page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, pageHeight), 0, 0)
// 添加图像到页面
// 加到pdf页面中的时候需要缩小,让a4纸放得下
// 0.2质量因子, jpeg格式压缩图片大小,质量因子控制图片质量,在0-1之间,值越低压缩得越小,质量越差
// 两边各留25宽,让内容居中显示
pdf.addImage(page.toDataURL('image/jpeg', 1), 'jpeg', 25, 25, a4Width / scale, Math.min(a4Height / scale, (a4Width / scale) * pageHeight / canvas.width))
// 这里不要用png,png下载文件太大了,页面会卡,下载出文件太大
// pdf.addImage(page.toDataURL('image/png', 0.2), 'png', 0, 0, a4Width / scale, Math.min(a4Height / scale, (a4Width / scale) * pageHeight / canvas.width))
renderedHeight += imgHeight
if (renderedHeight < canvas.height) {
pdf.addPage() // 如果后面还有内容,添加一个空页
}
}
pdf.save(fileName + '.pdf')
// 下载完毕后隐藏分页,避免影响到预览效果
let emptyDomList = document.getElementsByClassName('emptyDiv')
Promise.all([...Array.from(emptyDomList).map(el =>
new Promise((resolve, reject) => {
try {
el.remove()
resolve()
} catch {
reject()
}
})
)])
BxmMessage({
type: 'success',
message: '试卷下载完成!'
})
})
})
}
// 处理边界元素
export async function checkBandary(domList, a4Width, a4Height, startHeight, fileName) {
return new Promise((resolve, reject) => {
if (!domList.length) resolve(false)
try {
// 进行分割操作,当dom内容已超出a4的高度,则将该dom前插入一个空盒子,把他挤下去,分割
for (let i = 0; i < domList.length; i++) {
// 获取实时高度
let topHeight = domList[i].getBoundingClientRect().top
let clientHeight = domList[i].getBoundingClientRect().height
// let clientHeight = domList[i].clientHeight
// 计算页数
let multiple = Math.ceil((topHeight + clientHeight - startHeight) / a4Height)
let height = startHeight + (multiple - 1) * a4Height
// 判断元素是否跨页
if (isSplit(domList, i, a4Height, height)) {
// 获取该div的父节点
let divParent = domList[i].parentNode
// 创建页脚
let newNode = document.createElement('div')
newNode.className = 'emptyDiv'
// newNode.innerHTML = `第${multiple}页`
newNode.style.cssText = `display: flex; align-items: center; justify-content: center; width: calc(${a4Width}px - 70px); background: transparent; font-size: 10px;`
// 高度为当前元素距离当前页底部的距离
let _H = a4Height - (topHeight + clientHeight - height)
// newNode.style.height = _H < 120 ? 120 + 'px' : _H + 'px'
// newNode.style.height = 120 + 'px
newNode.style.height = _H + 'px'
// 获取兄弟节点
let next = getNextNode(domList[i])
// 判断兄弟节点是否存在
if (next) {
// 存在则将新节点插入到div的下一个兄弟节点之前,即div之后
divParent.insertBefore(newNode, next)
} else {
// 不存在则直接添加到最后,appendChild默认添加到divParent的最后
divParent.append(newNode)
}
}
}
resolve()
} catch {
reject()
}
})
}
// 判断当前元素是否跨页
/**
*
* @param {*} nodes
* @param {*} index
* @param {*} a4Height
* @param {*} startHeight
* @param {*} preFooterH 前一页的底部空白高度
* @returns
*/
export function isSplit(nodes, index, a4Height, startHeight) {
let topHeight = nodes[index].getBoundingClientRect().top
let clientHeight = nodes[index].getBoundingClientRect().height
if (index < nodes.length - 1) {
let topHeightNext = nodes[index + 1].getBoundingClientRect().top
let clientHeightNext = nodes[index + 1].getBoundingClientRect().height
// 当前元素不跨页,下一个元素要跨页,说明在当前元素之后要分页
if (nodes[index + 1]
&& topHeight + clientHeight - startHeight + 60 <= a4Height
&& (topHeightNext + clientHeightNext - startHeight + 60 > a4Height
|| (a4Height - (topHeight + clientHeight - startHeight) < clientHeightNext))) {
return true
}
return false
}
return false
}
// 根据不同分辨率获取到a4纸不同的宽高
export function getA4Size() {
// A4纸的标准尺寸为210 mm x 297 mm,换算成英寸大约是8.27" x 11.69"。
// 使用公式 像素 = 实际尺寸(英寸)× DPI 可以得到不同DPI下的像素值
// 大多浏览器
let defaultDPI = 96; // 默认DPI值
let currentDPT = 0
let dptValue = 1
// IE浏览器
if (window.screen.deviceXDPI !== undefined) {
currentDPT = window.screen.deviceXDPI
} else if (window.devicePixelRatio) { // 一般浏览器
currentDPT = defaultDPI * window.devicePixelRatio
dptValue = window.devicePixelRatio
} else {
currentDPT = defaultDPI
}
return {
a4Width: 8.27 * currentDPT,
a4Height: 11.69 * currentDPT,
currentDPT,
dptValue
}
}
// 递归查找节点,因为有的节点v-if为false被视为注释节点
export function getNextNode(node, nextNode = null) {
if (!node.nextSibling) return null
let next = node.nextSibling
if (next.nodeType !== Node.COMMENT_NODE) {
nextNode = next
return nextNode
} else {
getNextNode(next)
}
}
在导出之前需要保证图片被完全加载,否则会影响到高度计算
// 保证图片加载完毕
const loadImage = (imgElement) => {
return new Promise((resolve, reject) => {
if (imgElement.complete) {
resolve(imgElement)
} else {
imgElement.onload = () => resolve(imgElement)
imgElement.onerror = () => reject(`图片加载失败: ${imgElement.src}`)
}
})
}
最后的使用方法:
nextTick(async () => {
let dom = document.getElementById('pdfDom')
let imgs = dom.getElementsByTagName('img')
// 有图片时,要先将图片加载完整,否则导出时图片高度计算错误导致页面文字分割
if (imgs.length) {
exportLoading.value = true
BxmMessage({
type: 'warning',
message: '文件数据加载中,该试卷==文件中图片较多,加载缓慢,为避免影响导出文件排版,请不要滚动页面和关闭弹窗,耐心等待!',
showClose: true,
duration: imgs.length * 1500
})
const promises = Array.from(imgs).map(async img => { await loadImage(img) })
Promise.all(promises).then(() => {
// 等待浏览器重绘
let timer = setTimeout(() => {
clearTimeout(timer)
BxmMessage({
type: 'warning',
message: '文件加载完毕,正在导出文件,文件中图片较多,下载缓慢,请耐心等待,不要关闭弹窗!',
showClose: true
})
try {
exportFileByPDF(dom, '文件名', 'question-content-text')
} catch {
BxmMessage({
type: 'warning',
message: '导出失败!',
showClose: true
})
}
}, 1500)
}).catch(() => {
BxmMessageBox.confirm('部分图片加载失败,导出文件展示可能不全,是否继续导出?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
exportLoading.value = false
try {
exportFileByPDF(dom, '文件名', 'question-content-text')
} catch {
BxmMessage({
type: 'warning',
message: '导出失败!',
showClose: true
})
}
}).catch(() => {
BxmMessage({
type: 'info',
message: '已取消导出!',
showClose: true
})
exportLoading.value = false
})
})
} else {
BxmMessage({
message: '文件加载完毕,正在导出文件,请不要关闭弹窗!',
type: 'warning',
showClose: true
})
try {
exportFileByPDF(dom, '文件名', 'question-content-text')
} catch {
BxmMessage({
type: 'warning',
message: '导出失败!',
showClose: true
})
}
}
})
在图片加载完毕之后确实能够导出完整的文件,其中文字、图片不会被分割,但这个方法有一个缺点是图片太多了,页面等待时间太长,效率不高的问题,还没找到如何解决。
导出的文件示例: