背景
多种出库单/入库单打印,之前是购买(按年)专业团队的成型程序去做这个打印,现在雇佣前公司(已跑路)去一次性做这个事情。
在项目初期,后端已经做了一版,后续已经准备上线,验收工作中,甲方发现有些字段信息展示不完整,不会换行,现在在要求前端去做这个事情。就到我手里了,这个文章就是想记录一下当时踩的坑,以及后续解决方案。共做两版。
实现方案
方法一:使用html2Dcanvas+jspdf
为啥用这个方案,不直接用window.print()呢?这里是开始的时候把表头表尾定义为了页眉页脚 window.print() 不太好定义,就选这个了,而且之前也使用过一次了,更熟悉一点,就直接用了。
遇到问题:
分页不完整,使用offsetHight获取高度有问题,
最后解决方案:
分一页,手动加1px
相关代码:
import jsPDF from 'jspdf'
import html2canvas from 'html2canvas'
const A4_WIDTH = (241 * 72) / 25.4
const A4_HEIGHT = (140 * 72) / 25.4
const newHeader = null
// const A4_WIDTH = 592.28
// const A4_HEIGHT = 841.89
jsPDF.API.output2 = function(outputType = 'save', filename = 'document.pdf') {
let result = null
switch (outputType) {
case 'file':
result = new File([this.output('blob')], filename, {
type: 'application/pdf',
lastModified: Date.now()
})
break
case 'save':
result = this.save(filename)
break
default:
result = this.output(outputType)
}
return result
}
jsPDF.API.addBlank = function(x, y, width, height) {
this.setFillColor(255, 255, 255)
this.rect(x, y, Math.ceil(width), Math.ceil(height), 'F')
}
jsPDF.API.toCanvas = async function(element, width) {
const canvas = await html2canvas(element, {
width: element.scrollWidth,
height: element.scrollHeight,
allowTaint: true,
useCORS: true,
scale: 5,
dpi: 3000
})
const canvasWidth = canvas.width
const canvasHeight = canvas.height
const height = (width / canvasWidth) * canvasHeight
const canvasData = canvas.toDataURL('image/jpeg', 1.0)
// // // 创建一个新的图像标签,并设置其源为data URL
// var img = new Image();
// img.src = canvasData;
// // 将图像添加到文档中
// document.body.appendChild(img);
return { width, height, data: canvasData }
}
jsPDF.API.addHeader = async function(x, width, header) {
if (!(header instanceof HTMLElement)) return
let __header
if (this.__header) {
__header = this.__header
} else {
__header = await this.toCanvas(header, width)
this.__header = __header
}
const { height, data } = __header
this.addImage(data, 'JPEG', x, 0, width, height)
}
jsPDF.API.addFooter = async function(x, width, footer, flag, contentHeight) {
if (!(footer instanceof HTMLElement)) return
let __footer
if (flag) {
// 是否需要每次都重新生成页脚插入
__footer = await this.toCanvas(footer, width)
} else {
if (this.__footer) {
__footer = this.__footer
} else {
__footer = await this.toCanvas(footer, width)
this.__footer = __footer
}
}
const { height, data } = __footer
this.addImage(data, 'JPEG', x, contentHeight, width, height)
}
/**
* 生成pdf(处理多页pdf截断问题)
* @param {Object} param
* @param {HTMLElement} param.element - 需要转换的dom根节点
* @param {number} [param.contentWidth=550] - 一页pdf的内容宽度,0-592.28
* @param {number} [param.contentHeight=800] - 一页pdf的内容高度,0-841.89
* @param {string} [param.outputType='save'] - 生成pdf的数据类型,添加了'file'类型,其他支持的类型见http://raw.githack.com/MrRio/jsPDF/master/docs/jsPDF.html#output
* @param {string} [param.filename='document.pdf'] - pdf文件名
* @param {number} param.x - pdf页内容距页面左边的高度,默认居中显示,为(A4宽度 - contentWidth) / 2)
* @param {number} param.y - pdf页内容距页面上边的高度,默认居中显示,为(A4高度 - contentHeight) / 2)
* @param {HTMLElement} param.header - 页眉dom元素
* @param {HTMLElement} param.footer - 页脚dom元素
* @param {boolean} [param.headerOnlyFirst=true] - 是否只在第一页添加页眉
* @param {boolean} [param.footerOnlyLast=true] - 是否只在最后一页添加页脚
* @param {string} [param.mode='adaptive'] - 生成pdf的模式,支持'adaptive'、'fixed','adaptive'需给dom添加标识,'fixed'需固定布局。
* @param {string} [param.itemName='item'] - 给dom添加元素标识的名字,'adaptive'模式需在dom中设置
* @param {string} [param.groupName='group'] - 给dom添加组标识的名字,'adaptive'模式需在dom中设置
* @returns {Promise} 根据outputType返回不同的数据类型
*/
async function outputPdf({
element,
contentWidth = 550,
contentHeight = 800,
outputType = 'save',
filename = 'document.pdf',
x,
y,
header,
footer,
headerOnlyFirst = true,
footerOnlyLast = true,
mode = 'adaptive',
itemName = 'item',
groupName = 'group'
}) {
if (!(element instanceof HTMLElement)) {
throw new Error('The root element must be HTMLElement.')
}
const pdf = new jsPDF({
unit: 'pt',
format: [(241 * 72) / 25.4, (140 * 72) / 25.4],
orientation: 'landscape' // 或者 'landscape'
})
// 获取页脚的高度
const { height: footerHeight } = await pdf.toCanvas(footer, contentWidth)
// 获取页头的高度
const { height: headerHeight } = await pdf.toCanvas(header, contentWidth)
// 出去页头页尾后每页的高度
const originalPageHeight = contentHeight - footerHeight - headerHeight
// 记录每一页的截取位置
const { width, height, data } = await pdf.toCanvas(element, contentWidth)
const rate = accDiv(contentWidth, element.offsetWidth)
const baseX = x == null ? accDiv(subtr(A4_WIDTH, contentWidth), 2) : accMul(x, rate)
const baseY = y == null ? accDiv(subtr(A4_HEIGHT, contentHeight), 2) : accMul(y, rate)
async function addHeader(isFirst) {
if (isFirst || !headerOnlyFirst) {
await pdf.addHeader(baseX, contentWidth, header)
}
}
async function addFooter(isLast, pageNum, now) {
if (isLast || !footerOnlyLast) {
if (footer.querySelector('.pageSize')) {
footer.querySelector('.pageSize').innerText = `第${now}/${pageNum}页`
}
await pdf.addFooter(
baseX,
contentWidth,
footer,
footer.querySelector('.pageSize') ? true : false,
contentHeight
)
//document.doc
}
}
function addImage(_x, _y) {
pdf.addImage(data, 'JPEG', _x, _y, width, height)
}
const params = {
element,
contentWidth,
contentHeight,
itemName,
groupName,
pdf,
baseX,
baseY,
width,
height,
addImage,
addHeader,
addFooter,
originalPageHeight
}
switch (mode) {
case 'adaptive':
await outputWithAdaptive(params)
break
case 'fixed':
default:
await outputWithFixedSize(params)
}
//
return pdf.output2(outputType, filename)
}
async function outputWithFixedSize({
pdf,
baseX,
baseY,
height,
addImage,
addHeader,
addFooter,
contentHeight
}) {
const pageNum = Math.ceil(height / contentHeight) // 总页数
const arr = Array.from({ length: pageNum }).map((_, i) => i)
for await (const i of arr) {
addImage(baseX, baseY - i * contentHeight)
const isFirst = i === 0
const isLast = i === arr.length - 1
if (!isFirst) {
// 用空白遮挡顶部需要隐藏的部分
pdf.addBlank(0, 0, A4_WIDTH, baseY)
}
if (!isLast) {
// 用空白遮挡底部需要隐藏的部分
pdf.addBlank(0, baseY + contentHeight, A4_WIDTH, A4_HEIGHT - (baseY + contentHeight))
}
await addHeader(isFirst)
await addFooter(isLast)
if (!isLast) {
pdf.addPage()
}
}
}
async function outputWithAdaptive({
element,
contentWidth,
itemName,
groupName,
pdf,
baseX,
baseY,
addImage,
addHeader,
addFooter,
contentHeight,
height,
originalPageHeight,
width
}) {
let canvasHight = height
let canvasWeight = width
// 从根节点遍历dom,计算出每页应放置的内容高度以保证不被截断
const splitElement = () => {
const res = []
let pos = 0 // 一页分割页内容占据高度
let totalHight = 0 // 当前已已放置的全部高度
const elementWidth = element.offsetWidth
const rate = accDiv(contentWidth, elementWidth)
function getRate1(sum) {
let data = 0
if (sum <= 15) {
data = 1
} else if (sum > 15 && sum <= 20) {
data = 2
} else if (sum > 20 && sum <= 40) {
data = Math.floor(res.length / 20) * 1
} else {
data = 2 + Math.floor(res.length / 50) * 1
}
return data
}
function updatePos(height, one) {
// 当前行 的offsetTop 的转为pdf对应大小后减去 之前以放置高度
// const top = Math.ceil((contentWidth / elementWidth) * one.offsetTop) - totalHight
const top = subtr(accMul(rate, one.offsetTop + getRate1(res.length)), totalHight)
const lastTop = Number(top)
// if(numAdd(lastTop, height)>originalPageHeight){
// // h 是o的几倍数
// const length = Math.floor(numAdd(lastTop, height)/originalPageHeight)
// for(let i=0;i<length;i++){
// res.push(originalPageHeight)
// totalHight+=originalPageHeight
// }
// pos = height-originalPageHeight*length
// totalHight+=pos
// return
// }
if (numAdd(pos, numAdd(lastTop, height)) < originalPageHeight) {
pos = numAdd(pos, numAdd(lastTop, height))
// pos += lastTop + height
// totalHight += lastTop + height
totalHight = numAdd(totalHight, numAdd(lastTop, height))
return
}
// console.log('pos',pos)
res.push(pos)
pos = numAdd(lastTop, height)
totalHight = numAdd(totalHight, numAdd(lastTop, height))
console.log('totalHight:', totalHight)
}
function traversingNodes(nodes) {
if (nodes.length === 0) return
nodes.forEach((one) => {
if (one.nodeType !== 1) return
const { [itemName]: item, [groupName]: group } = one.dataset
if (item != null) {
const { offsetHeight } = one
// dom高度转换成生成pdf的实际高度
// 代码不考虑dom定位、边距、边框等因素,需在dom里自行考虑,如将box-sizing设置为border-box
// console.log('traversingNodes-hight',offsetHeight)
updatePos(accMul(accDiv(contentWidth, elementWidth), offsetHeight), one)
} else if (group != null) {
traversingNodes(one.childNodes)
}
})
}
traversingNodes(element.childNodes)
res.push(pos)
return res
}
const elements = splitElement()
let accumulationHeight = 0
let currentPage = 0
for await (const elementHeight of elements) {
addImage(baseX, baseY - accumulationHeight)
// accumulationHeight += elementHeight
accumulationHeight = numAdd(accumulationHeight, elementHeight)
const isFirst = currentPage === 0
const isLast = currentPage === elements.length - 1
if (!isFirst) {
pdf.addBlank(0, 0, A4_WIDTH, baseY)
}
if (!isLast) {
pdf.addBlank(0, baseY + elementHeight, A4_WIDTH, A4_HEIGHT - (baseY + elementHeight))
}
await addHeader(isFirst)
await addFooter(isLast, elements.length, currentPage + 1)
if (!isLast) {
pdf.addPage()
}
currentPage++
}
}
/**
* 加法运算,避免数据相加小数点后产生多位数和计算精度损失
* @param num1加数1 | num2加数2
*/
function numAdd(num1, num2) {
let baseNum, baseNum1, baseNum2
try {
baseNum1 = num1.toString().split('.')[1].length
} catch (e) {
baseNum1 = 0
}
try {
baseNum2 = num2.toString().split('.')[1].length
} catch (e) {
baseNum2 = 0
}
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2))
return Number(((num1 * baseNum + num2 * baseNum) / baseNum).toFixed(2))
}
// 减
function subtr(arg1, arg2) {
let r1, r2, m, n
try {
r1 = arg1.toString().split('.')[1].length
} catch (e) {
r1 = 0
}
try {
r2 = arg2.toString().split('.')[1].length
} catch (e) {
r2 = 0
}
m = Math.pow(10, Math.max(r1, r2))
// last modify by deeka
// 动态控制精度长度
n = r1 >= r2 ? r1 : r2
return ((arg1 * m - arg2 * m) / m).toFixed(n)
}
// 除
function accDiv(arg1, arg2) {
let t1 = 0,
t2 = 0,
r1,
r2
try {
t1 = arg1.toString().split('.')[1].length
} catch (e) {}
try {
t2 = arg2.toString().split('.')[1].length
} catch (e) {}
r1 = Number(arg1.toString().replace('.', ''))
r2 = Number(arg2.toString().replace('.', ''))
return (r1 / r2) * Math.pow(10, t2 - t1)
}
// 乘
function accMul(arg1, arg2) {
let m = 0
const s1 = arg1.toString(),
s2 = arg2.toString()
try {
m += s1.split('.')[1].length
} catch (e) {}
try {
m += s2.split('.')[1].length
} catch (e) {}
return (Number(s1.replace('.', '')) * Number(s2.replace('.', ''))) / Math.pow(10, m)
}
export default outputPdf
放弃原因:
针式打印机,打印后出现点状文字,不完整,不符合用户要求。这里是因为开始的时候没有调研清楚,就直接开始开发了,针式打印机是一针一针的打,而该方法是将这个生成图片的pdf去打印,有漏针的情况。
方法二: window.print()
遇到问题:
-
自动分页,表头,表尾不能超过页面的25%,现在表头超过了25%,需要手动分页,
-
特殊纸张大小,打印分页不明确,给打印机设置自定义纸张
-
火狐兼容性问题
- 分页失效
- 打卡空白
printClick() {
this.loadingPrintBtn = true
const hasPageSize = [
'incomingWarehouseReceipt',
'interOfficeTransferOrders',
'officeDeliveryOrderPrintPdf241X140',
'officeDeliveryOrderPrintPdf241X140New',
'officeDeliveryOrderPrintPdfOne',
'officeDeliveryOrderPrintPdfOneNew',
'officeDeliveryOrderPrintPdfTwo'
] // 有页码组件
const noHasPageSize = [
'outboundWarehouseReceiptsOne',
'outboundWarehouseReceiptsTwo',
'simLanOutputRepositoryOrderOne'
] // 没有页码组件
// 针对于火狐浏览器单独区分是否需要手动分页
const noNeedPaging = [
'incomingWarehouseReceipt',
'officeDeliveryOrderPrintPdf241X140',
'officeDeliveryOrderPrintPdf241X140New',
'officeDeliveryOrderPrintPdfOne',
'officeDeliveryOrderPrintPdfOneNew',
'officeDeliveryOrderPrintPdfTwo'
] // 不需要手动分页
const needPaging = [
'interOfficeTransferOrders',
'outboundWarehouseReceiptsOne',
'outboundWarehouseReceiptsTwo',
'simLanOutputRepositoryOrderOne'
] // 需要手动分页
// 创建一个 iframe 元素
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
if (navigator.userAgent.match('Firefox')) {
iframe.src = 'javascript:'
}
document.body.appendChild(iframe)
// 获取 iframe 的 document 对象
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document
if (navigator.userAgent.match('Firefox')) {
if (needPaging.some((item) => item === this.printComponent))
iframeDoc.body.innerHTML = this.isFirefoxPaging()
if (noNeedPaging.some((item) => item === this.printComponent))
iframeDoc.body.innerHTML = this.isFirefox()
} else {
// 将需要打印的内容插入到 iframe 中
iframeDoc.body.innerHTML = this.selfPaging()
}
setTimeout(() => {
this.loadingPrintBtn = false
iframe.contentWindow.print()
// 移除 iframe 元素
document.body.removeChild(iframe)
}, 100)
},
selfPaging() {
let {
thead,
tbody,
tfoot,
theadHeight,
contentHeight,
currentPageHeight,
html,
hasTfoot
} = this.getElement()
function insertPageBreak() {
if (hasTfoot) {
html += `<tr><td style="height: 0; page-break-before: always;"></td></tr>`
}
html += `<thead style="width: 100%; border-top: none;">${thead.innerHTML}</thead>`
html += `<tbody style="width: 100%;">`
currentPageHeight = theadHeight
}
// 在第一页开始时插入 thead
insertPageBreak()
const rows = tbody.querySelectorAll('tr')
rows.forEach((row) => {
const isBreakCells = row.querySelectorAll('.is-break')
const maxCellHeight = Math.max(...Array.from(isBreakCells, (cell) => cell.offsetHeight))
const height = row.offsetHeight
if (currentPageHeight + maxCellHeight > contentHeight) {
// 如果当前行中包含 .is-break 的最高单元格高度超过一页的高度限制,插入分页符
if (!hasTfoot) {
html += `</tbody><tfoot style="width: 100%;">${tfoot.innerHTML}</tfoot>`
hasTfoot = true
}
insertPageBreak()
}
currentPageHeight += height
html += `<tr>${row.innerHTML}</tr>`
})
// 在最后一页插入 tfoot
if (!hasTfoot) {
html += `</tbody><tfoot style="width: 100%;">${tfoot.innerHTML}</tfoot>`
}
return `<div style="width: 241mm !important;">
<div
style="
margin: 0 auto;
max-width: 230mm !important;
color: #000;">
<table
border="0"
width="100%"
cellpadding="2"
cellspacing="0" style="table-layout: fixed; border: none !important; border-collapse: collapse;">
${html}
</table></div></div>`
},
getTotalPage() {
let { tbody, theadHeight, contentHeight, currentPageHeight } = this.getElement()
let pageTotal = 1 // 总页数
const rows = tbody.querySelectorAll('tr')
rows.forEach((row) => {
const isBreakCells = row.querySelectorAll('.is-break')
const maxCellHeight = Math.max(...Array.from(isBreakCells, (cell) => cell.offsetHeight))
const height = row.offsetHeight
if (currentPageHeight + maxCellHeight > contentHeight) {
currentPageHeight = theadHeight
pageTotal += 1
}
currentPageHeight += height
})
return pageTotal
},
hasPageSize() {
const pageSize = this.getTotalPage()
let currentPageSize = 0
let {
thead,
tbody,
tfoot,
theadHeight,
contentHeight,
currentPageHeight,
html,
hasTfoot
} = this.getElement()
function insertPageBreak() {
if (hasTfoot) {
html += `<tr><td style="height: 0; page-break-before: always;"></td></tr>`
}
html += `<thead style="width: 100%; border-top: none;">${thead.innerHTML}</thead>`
html += `<tbody style="width: 100%;">`
currentPageHeight = theadHeight
}
// 在第一页开始时插入 thead
if (!hasTfoot) {
insertPageBreak()
hasTfoot = true
}
const cloneTfoot = tfoot.cloneNode(true)
const pageLine = cloneTfoot.querySelector('.pageSize')
// originalElement.cloneNode(true)
const rows = tbody.querySelectorAll('tr')
rows.forEach((row) => {
const isBreakCells = row.querySelectorAll('.is-break')
const maxCellHeight = Math.max(...Array.from(isBreakCells, (cell) => cell.offsetHeight))
const height = row.offsetHeight
if (currentPageHeight + maxCellHeight > contentHeight) {
// 如果当前行中包含 .is-break 的最高单元格高度超过一页的高度限制,插入分页符
currentPageSize += 1
if (pageLine) {
pageLine.innerHTML = `第${currentPageSize}/${pageSize}页`
}
if (hasTfoot) {
html += `${cloneTfoot.innerHTML}</tbody>`
hasTfoot = true
insertPageBreak()
}
}
currentPageHeight += height
html += `<tr>${row.innerHTML}</tr>`
})
currentPageSize += 1
if (pageLine) {
pageLine.innerHTML = `第${currentPageSize}/${pageSize}页`
}
if (hasTfoot) {
html += `${cloneTfoot.innerHTML}</tbody>`
}
return `<div style="width: 241mm !important;">
<div
style="
margin: 0 auto;
max-width: 230mm !important;
color: #000;">
<table
border="0"
width="100%"
cellpadding="2"
cellspacing="0" style="table-layout: fixed; border: none !important; border-collapse: collapse;">
${html}
</table></div></div>`
},
getElement() {
const className = this.printComponent
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/([A-Z]+)([A-Z])([a-z])/g, '$1-$2-$3')
.toLowerCase()
const printBody = document.querySelector(`#${className}`)
let example = null
if (navigator.userAgent.match('Firefox')) {
example = document.querySelector('.exampleFirefox')
} else {
example = document.querySelector('.example')
}
const thead = printBody.querySelector('thead')
const tbody = printBody.querySelector('tbody')
const tfoot = printBody.querySelector('tfoot')
const exampleHeight = example.offsetHeight // 每一页的高度
const theadHeight = thead.offsetHeight
const tfootHeight = tfoot.offsetHeight
const contentHeight = exampleHeight - tfootHeight // 每一页去掉表头表尾的高度,即内容高度
let html = ''
let currentPageHeight = 0 // 当前页的高度
let hasTfoot = false // 是否已经插入过 tfoot
return {
className,
printBody,
example,
thead,
tbody,
tfoot,
exampleHeight,
theadHeight,
tfootHeight,
contentHeight,
currentPageHeight,
html,
hasTfoot
}
},
isFirefox() {
let { thead, tbody, tfoot, html } = this.getElement()
html += `<thead style="width: 100%; border-top: none;">${thead.innerHTML}</thead>`
html += `<tbody style="width: 100%;">${tbody.innerHTML}</tbody>`
html += `<tfoot style="width: 100%;">${tfoot.innerHTML}</tfoot>`
return `<div style="width: 241mm !important;">
<div
style="
margin: 0 auto;
max-width: 230mm !important;
color: #000;">
<table
border="0"
width="100%"
cellpadding="2"
cellspacing="0" style="table-layout: fixed; border: none !important;">
${html}
</table></div></div>`
},
/**
* 火狐
*/
isFirefoxPaging() {
const pageSize = this.getTotalPage()
let currentPageSize = 0
let {
thead,
tbody,
tfoot,
theadHeight,
contentHeight,
currentPageHeight,
html,
hasTfoot
} = this.getElement()
function insertPageBreak() {
html += `<div class="page-break" style="page-break-after: always;
page-break-inside: avoid;"><table
border="0"
width="100%"
cellpadding="2"
cellspacing="0" style="table-layout: fixed;">
<thead style="width: 100%; border-top: none;">${thead.innerHTML}</thead>`
html += `<tbody style="width: 100%;">`
currentPageHeight = theadHeight
}
// 在第一页开始时插入 thead
if (!hasTfoot) {
insertPageBreak()
hasTfoot = true
}
const cloneTfoot = tfoot.cloneNode(true)
const pageLine = cloneTfoot.querySelector('.pageSize')
const rows = tbody.querySelectorAll('tr')
rows.forEach((row) => {
const isBreakCells = row.querySelectorAll('.is-break')
const maxCellHeight = Math.max(...Array.from(isBreakCells, (cell) => cell.offsetHeight))
const height = row.offsetHeight
if (currentPageHeight + maxCellHeight > contentHeight) {
// 如果当前行中包含 .is-break 的最高单元格高度超过一页的高度限制,插入分页符
currentPageSize += 1
if (pageLine) {
pageLine.innerHTML = `第${currentPageSize}/${pageSize}页`
}
if (hasTfoot) {
html += `${cloneTfoot.innerHTML}</tbody></table></div>`
hasTfoot = true
insertPageBreak()
}
}
currentPageHeight += height
html += `<tr>${row.innerHTML}</tr>`
})
currentPageSize += 1
if (pageLine) {
pageLine.innerHTML = `第${currentPageSize}/${pageSize}页`
}
if (hasTfoot) {
html += `${cloneTfoot.innerHTML}</tbody></table></div>`
}
return `<div style="width: 241mm !important;">
<div
style="
margin: 0 auto;
max-width: 240mm !important;
color: #000;">
${html}
</div></div>`
}