基于Vue框架开发,实现PDF文件导出功能,使用html2canvas + jspdf来实现。大致原理是使用html2canvas将dom内容截图生成图片格式,然后使用jspdf插件将图片转换成pdf格式。
要求:根据所提供的页面在前端实现PDF的导出,导出的pdf内容一页展示不下,第二页及后面的都要拼接上表头
效果:
实现:
1.定义需要生成PDF文件的模板内容
ps: pdfTemplate.vue
<template>
<div ref="pdfTemple" class="pdfTemple" style="position:absolute;bottom:-999px;width:1920px;">
<div v-if="data.type == 'COMMON'">
<div class="title">PURCHASE QUOTATAION</div>
<div class="flex-align flex-spacebetween" style="border-bottom: 2px solid #ccc;margin-bottom:10px;">
<div>
<div class="form-item flex-align">
<div class="form-label">Document Date:</div>
<div class="form-value">{{ data.auditTime }}</div>
</div>
<div class="form-item flex-align">
<div class="form-label">Purchase order Number:</div>
<div class="form-value">{{ data.code }}</div>
</div>
<div class="form-item flex-align">
<div class="form-label">Supllier ID:</div>
<div class="form-value">{{ data.supplierCode }}</div>
</div>
<div class="form-item flex-align">
<div class="form-label">Purchase Amt:</div>
<div class="form-value">{{ purchaseAmt }}</div>
</div>
</div>
<img :src="$getPublicImage('sello.jpg')" style="width:160px;height:80px;" />
<div>
<div class="form-item flex-align">
<div class="form-label">Sello Products Pty Ltd</div>
</div>
<div class="form-item flex-align">
<div class="form-label">7 Duigan Drive</div>
</div>
<div class="form-item flex-align">
<div class="form-label">MOORABBIN AIRPORT VIC</div>
</div>
<div class="form-item flex-align">
<div class="form-label">3194</div>
</div>
<div class="form-item flex-align">
<div class="form-label">AUSTRALIA</div>
</div>
</div>
</div>
<div class="flex-align flex-spacebetween" style="margin:20px 0">
<div class="flex">
<div class="form-item">
<div class="form-label">Suplier Information:</div>
<div class="form-value">{{ data.supplierName }}</div>
</div>
<div class="form-item">
<div class="form-label">Ship To:</div>
<div class="form-value">Sello Products Pty Ltd</div>
<div class="form-value">7 Duigan Drive</div>
<div class="form-value">MOORABBIN AIRPORT VIC</div>
<div class="form-value">3194</div>
<div class="form-value">AUSTRALIA</div>
</div>
</div>
<div>
<div class="form-item flex-align">
<div class="form-label" style="width: 100px;">Order Date:</div>
<div class="form-value" style="width: 160px;">{{ data.creationDate }}</div>
</div>
<div class="form-item flex-align">
<div class="form-label" style="width: 100px;">Delivery By:</div>
<div class="form-value" style="width: 160px;">{{ data.finishTime }}</div>
</div>
<div class="form-item flex-align">
<div class="form-label" style="width: 100px;">Cancel Date:</div>
<div class="form-value" style="width: 160px;">{{ data.expectedDate }}</div>
</div>
</div>
</div>
<el-table class="emptyItem" :data="data.goodsList">
<el-table-column prop="spuPic" width="100" align="center">
<template slot-scope="{ row }">
<BjViewUrl v-if="row.spuPic && row.spuPic!== 'null'" :url="row.spuPic" height="60px" preview />
</template>
</el-table-column>
<el-table-column label="Item" prop="rpcGoodsResVO.skuCode" align="center" />
<el-table-column label="Description" prop="rpcGoodsResVO.skuName" align="center" />
<el-table-column label="Brand" prop="rpcGoodsResVO.brandName" align="center" />
<el-table-column label="Inner Barcode" prop="rpcGoodsResVO.barCode" align="center" />
<el-table-column label="Carton Barcode" prop="rpcGoodsResVO.cartonBoxBarcode" align="center" />
<el-table-column label="Ctn Qty" prop="rpcGoodsResVO.qty" align="center" />
<el-table-column label="RMB" prop="rmb" align="center" />
<el-table-column label="Unit price" prop="taxUnitPrice" align="center" />
<el-table-column label="Quantity" prop="planQty" align="center" />
<el-table-column label="Amount(AUD)" prop="totalPrice" align="center" />
<el-table-column label="Buyer" prop="data.buyerName" align="center">
<template slot-scope>{{ data.buyerName }}</template>
</el-table-column>
<el-table-column label="Category" prop="rpcGoodsResVO.categoryStr" align="center" />
</el-table>
<div class="flex-align flex-spacebetween" style="margin-top:20px">
<div></div>
<div>
<div class="form-item flex-align">
<div class="form-label">Subtotal</div>
<div class="form-value">AUD {{ purchaseAmt }}</div>
</div>
<div class="form-item flex-align">
<div class="form-label">Total Tax</div>
<div class="form-value">AUD 0</div>
</div>
<div class="form-item flex-align">
<div class="form-label">Total</div>
<div class="form-value">AUD {{ purchaseAmt }}</div>
</div>
</div>
</div>
<div class="flex-align">
<b>Signature______________________________</b>
<b style="margin-left:5px;">Date______________________________</b>
</div>
</div>
<div v-else>
<div class="title">PURCHASE QUOTATAION</div>
<div class="flex-align flex-spacebetween" style="border-bottom: 2px solid #ccc;margin-bottom:10px;">
<div>
<div class="form-item flex-align">
<div class="form-label">Document Date:</div>
<div class="form-value">{{ data.auditTime }}</div>
</div>
<div class="form-item flex-align">
<div class="form-label">Purchase order Number:</div>
<div class="form-value">{{ data.code }}</div>
</div>
<div class="form-item flex-align">
<div class="form-label">Supllier ID:</div>
<div class="form-value">{{ data.supplierCode }}</div>
</div>
<div class="form-item flex-align">
<div class="form-label">Purchase Amt:</div>
<div class="form-value">{{ purchaseAmt }}</div>
</div>
</div>
<img :src="$getPublicImage('sello.jpg')" style="width:160px;height:80px;" />
<div>
<div class="form-item flex-align">
<div class="form-label">Sello Products Pty Ltd</div>
</div>
<div class="form-item flex-align">
<div class="form-label">7 Duigan Drive</div>
</div>
<div class="form-item flex-align">
<div class="form-label">MOORABBIN AIRPORT VIC</div>
</div>
<div class="form-item flex-align">
<div class="form-label">3194</div>
</div>
<div class="form-item flex-align">
<div class="form-label">AUSTRALIA</div>
</div>
</div>
</div>
<div class="flex-align flex-spacebetween" style="margin:20px 0">
<div class="flex">
<div class="form-item">
<div class="form-label">Suplier Information:</div>
<div class="form-value">{{ data.supplierName }}</div>
</div>
<div class="form-item">
<div class="form-label">Ship To:</div>
<div class="form-value">Sello Products Pty Ltd</div>
<div class="form-value">7 Duigan Drive</div>
<div class="form-value">MOORABBIN AIRPORT VIC</div>
<div class="form-value">3194</div>
<div class="form-value">AUSTRALIA</div>
</div>
</div>
<div>
<div class="form-item flex-align">
<div class="form-label" style="width: 100px;">Order Date:</div>
<div class="form-value" style="width: 160px;">{{ data.creationDate }}</div>
</div>
<div class="form-item flex-align">
<div class="form-label" style="width: 100px;">Delivery By:</div>
<div class="form-value" style="width: 160px;">{{ data.finishTime }}</div>
</div>
<div class="form-item flex-align">
<div class="form-label" style="width: 100px;">Cancel Date:</div>
<div class="form-value" style="width: 160px;">{{ data.expectedDate }}</div>
</div>
</div>
</div>
<el-table class="emptyItem" :data="data.goodsList">
<el-table-column prop="spuPic" width="100" align="center">
<template slot-scope="{ row }">
<BjViewUrl v-if="row.spuPic && row.spuPic!== 'null'" :url="row.spuPic" height="60px" preview />
</template>
</el-table-column>
<el-table-column label="Item" prop="rpcGoodsResVO.skuCode" align="center" />
<el-table-column label="Description" prop="rpcGoodsResVO.skuName" align="center" />
<el-table-column label="Brand" prop="rpcGoodsResVO.brandName" align="center" />
<el-table-column label="Unit price" prop="taxUnitPrice" align="center" />
<el-table-column label="Quantity" prop="planQty" align="center" />
<el-table-column label="Amount(AUD)" prop="totalPrice" align="center" />
</el-table>
<div class="flex-align flex-spacebetween" style="margin-top:20px">
<div></div>
<div>
<div class="form-item flex-align">
<div class="form-label">Subtotal</div>
<div class="form-value">AUD {{ purchaseAmt }}</div>
</div>
<div class="form-item flex-align">
<div class="form-label">Total Tax</div>
<div class="form-value">AUD {{ taxTotal || 0 }}</div>
</div>
<div class="form-item flex-align">
<div class="form-label">Total</div>
<div class="form-value">AUD {{ Number(purchaseAmt)+Number(taxTotal ||0) }}</div>
</div>
</div>
</div>
<div class="flex-align">
<b>Signature______________________________</b>
<b style="margin-left:5px;">Date______________________________</b>
</div>
</div>
</div>
</template>
<script>
import html2canvas from 'html2canvas';
import JsPDF from 'jspdf';
export default {
props: {
data: {
type: Object,
default: null,
}
},
computed: {
purchaseAmt() {
let total = 0;
this.data.goodsList && this.data.goodsList.forEach(item => {
total += item.totalPrice;
});
return total;
},
taxTotal() {
return Number(this.purchaseAmt) * 0.1 + Number(this.data.totalFreight || 0);
}
},
methods: {
async exportPage() {
const PDFView = this.$refs.pdfTemple;
html2canvas(PDFView, {
allowTaint: true,
useCORS: true,
dpi: 120, // 图片清晰度问题
background: '#FFFFFF', // 如果指定的div没有设置背景色会默认成黑色,这里是个坑
}).then(canvas => {
// 未生成pdf的html页面高度
var leftHeight = canvas.height;
var a4Width = 841.89;
var a4Height = 595.28; // A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
// 一页pdf显示html页面生成的canvas高度;
var a4HeightRef = Math.floor((canvas.width / a4Width) * a4Height);
// pdf页面偏移
var position = 0;
var pageData = canvas.toDataURL('image/jpeg', 1.0);
var pdf = new JsPDF('l', 'pt', 'a4'); // A4纸,纵向
var index = 1; var canvas1 = document.createElement('canvas'); var height;
pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen');
function createImpl(canvas) {
console.log(leftHeight, a4HeightRef);
if (leftHeight > 0) {
index++;
var checkCount = 0;
if (leftHeight > a4HeightRef) {
var i = position + a4HeightRef;
for (i = position + a4HeightRef; i >= position; i--) {
var isWrite = true;
for (var j = 0; j < canvas.width; j++) {
var c = canvas.getContext('2d').getImageData(j, i, 1, 1).data;
if (c[0] != 0xff || c[1] != 0xff || c[2] != 0xff) {
isWrite = false;
break;
}
}
if (isWrite) {
checkCount++;
if (checkCount >= 10) {
break;
}
} else {
checkCount = 0;
}
}
height = Math.round(i - position) || Math.min(leftHeight, a4HeightRef);
if (height <= 0) {
height = a4HeightRef;
}
} else {
height = leftHeight;
}
canvas1.width = canvas.width;
canvas1.height = height;
console.log(index, 'height:', height, 'pos', position);
var ctx = canvas1.getContext('2d');
ctx.drawImage(canvas, 0, position, canvas.width, height, 0, 0, canvas.width, height);
// var pageHeight = Math.round((a4Width / canvas.width) * height);
// pdf.setPageSize(null, pageHeight)
if (position != 0) {
pdf.addPage();
}
pdf.addImage(
canvas1.toDataURL('image/jpeg', 1.0), 'JPEG',
10, 10, a4Width, (a4Width / canvas1.width) * height
);
leftHeight -= height;
position += height;
if (leftHeight > 0) {
setTimeout(createImpl, 500, canvas);
} else {
pdf.save('采购单.pdf');
}
}
}
// 当内容未超过pdf一页显示的范围,无需分页
if (leftHeight < a4HeightRef) {
pdf.addImage(pageData, 'JPEG', 0, 0, a4Width, (a4Width / canvas.width) * leftHeight);
pdf.save('采购单.pdf');
} else {
try {
pdf.deletePage(0);
setTimeout(createImpl, 500, canvas);
} catch (err) {
console.log(err);
}
}
});
},
isSplit (nodes, index, pageHeight) {
// 计算当前这块dom是否跨越了a4大小,以此分割
return !!(nodes[index].offsetTop + nodes[index].offsetHeight < pageHeight && nodes[index + 1] && nodes[index + 1].offsetTop + nodes[index + 1].offsetHeight > pageHeight);
},
};
</script>
<style lang="scss" scoped>
.pdfTemple {
font-size: 12px;
padding: 20px;
.title {
font-size: 18px;
color: #8f8f8f;
font-weight: bold;
}
.form-item {
margin-bottom: 5px;
}
.form-label {
font-weight: bold;
width: 200px;
}
}
::v-deep .emptyItem th.el-table__cell > .cell {
padding: 0;
}
2.在需要导出的页面引入
ps : detail.vue
<template>
<el-button type="primary" @click="outPutPdfFn">导出PDF</el-button>
<PdfTemplate id="pdfRef" ref="pdfRef" :data="data"></PdfTemplate>
</template>
<script>
import PdfTemplate from './pdfTemplate';
export default {
components: {
PdfTemplate,
},
methods: {
outPutPdfFn () {
this.$refs.pdfRef.exportPage();
},
}
}
</script>