PDF导出

91 阅读1分钟

基于Vue框架开发,实现PDF文件导出功能,使用html2canvas + jspdf来实现。大致原理是使用html2canvas将dom内容截图生成图片格式,然后使用jspdf插件将图片转换成pdf格式。

要求:根据所提供的页面在前端实现PDF的导出,导出的pdf内容一页展示不下,第二页及后面的都要拼接上表头

效果:

image.png

image.png

实现:

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>