前端使用html2canvas和jspdf实现分页导出pdf

318 阅读2分钟

主要步骤

  1. 使用html2canvas/dom-to-image将页面元素转成图片(dom-to-image会比html2canvas快一些) blog.csdn.net/HYeeee/arti…
  2. 使用jspdf生成pdf页面
  3. 把图片加到pdf页面上

主要代码

  • js文件,处理上述步骤逻辑

import i18n from '@/Common/i18n/index';
import * as JsPDF from 'jspdf';
import html2Canvas from 'html2canvas';
import domtoimage from 'dom-to-image';
const pagePaddingLeft = 40;
const pagePaddingTop = 40;
const A4Height = 841.89;
const A4Width = 592.28;
const A4PageWidth = A4Width - 2 * pagePaddingLeft;
const A4PageHeight = A4Height - pagePaddingTop;
// 指定PDF大小,将导出内容放在一页上
const addOneImg = (
  { src, marginTop = 0, imgWidth, imgHeight },
  { usedHeight = 0 },
  PDF) => {
  return new Promise((resolve) => {
    if (!src || src === '') {
      resolve();
      return;
    }
    let pageData = src;
    let position = usedHeight + marginTop;
    PDF.addImage(
      pageData,
      'JPEG',
      pagePaddingLeft,
      position,
      imgWidth,
      imgHeight
    );
    resolve();
  });
};
const getImgCanvas = (el, params = {}) => {
  // return html2Canvas(el, {
   //  allowTaint: true,
  //  ...params
  //});
  return domtoimage.toPng(el, { cacheBust: true }).then((dataUrl)=>{
    return dataUrl;
  });
  
};
const getImgByCanvas = (canvas) => {
  return new Promise((resolve) => {
    const img = new Image();
    img.src = canvas;
    if (!canvas || canvas === '') {
      resolve();
      return;
    }
    img.onload = () => {
      resolve(img);
    };
    img.onerror = () => {
      resolve(img);
    };
  });
};
const getHtmlImgList = async (elConfigs, isFitA4 = false) => {
  let imgList = [];
  let usedHeight = 0;
  for (let i = 0; i < elConfigs.length; i++) {
    const config = elConfigs[i];
   const canvas = await getImgCanvas(config.el, { scale: 1 });
    //const img = await getImgByCanvas(canvas.toDataURL('image/jpeg', 1.0));
     const img = await getImgByCanvas(canvas);
    let imgWidth = img.width;
    let imgHeight = img.height;
    // note 是否启用A4大小
    if (isFitA4) {
      let contentWidth = img.width;
      let contentHeight = img.height;
      imgWidth = A4PageWidth;
      imgHeight = (imgWidth / contentWidth) * contentHeight;
    }
    // 分页需要判断当前元素是否超过一页
    let tempUsedHeight = usedHeight + config.marginTop + imgHeight;
    if (tempUsedHeight > A4Height) {
      usedHeight = 0;
    }
    imgList.push({
      canvas,
      src: img,
      imgWidth,
      imgHeight,
      marginTop: usedHeight === 0 ? 40 : config.marginTop,
      usedHeight: usedHeight
    });
    usedHeight = usedHeight + config.marginTop + imgHeight;
  }
  return imgList;
};
const html2Pdf = async (elConfigs = [], fileName = '', isFitA4 = true) => {
  if (elConfigs.length === 0) {
    return;
  }
  const imgList = await getHtmlImgList(elConfigs, isFitA4);
  const pdfWidth = isFitA4 ? A4Width : imgList[0].imgWidth + 2 * pagePaddingLeft;
  const { imgHeight, marginTop, usedHeight: allHeight } = imgList[imgList.length - 1];
  const pdfHeight = isFitA4 ? A4Height : allHeight + imgHeight + marginTop + pagePaddingTop * 2;
  // let PDF = new JsPDF('', 'pt', 'a4');
  let PDF = new JsPDF('', 'pt', [pdfWidth, pdfHeight]);
  for (let i = 0; i < imgList.length; i++) {
    const { canvas, imgWidth, imgHeight, marginTop, usedHeight } = imgList[i];
    if (i > 0 && usedHeight === 0) {
      PDF.addPage();
    }
    //await addOneImg({ src: canvas.toDataURL('image/jpeg', 1.0), imgWidth, imgHeight, marginTop }, { usedHeight }, PDF);
    await addOneImg({ src: canvas, imgWidth, imgHeight, marginTop }, { usedHeight }, PDF);
  }
  const exportFileName = fileName ? `${fileName}.pdf` : `${i18n.t('hcp_eduInspect_record_name')}_${new Date().format('yyyyMMddHHssmm')}.pdf`;
  PDF.save(exportFileName);
};
export {
  addOneImg,
  html2Pdf
};
  • vue文件,对以上js的使用
// 引入
import { html2Pdf } from '@/EduInspect/view/Components/html2Pdf.js';
// 使用
exportPdf() {
      this.showExport = false;
      this.$nextTick(()=>{
        const exportDate = new Date();
        this.exportTime = exportDate.format('yyyy/MM/dd HH:mm:ss');
        const exportFileName = this.$t('hcp_eduInspect_record_name') + exportDate.format('yyyyMMddHHssmm');
        this.$nextTick(async () => {
          let exportData = [
            { el: this.$refs['top-course-title'], marginTop: 40 },
            { el: this.$refs['course-info-detail'], marginTop: 0},
            { el: this.$refs['comment-title'], marginTop: 0}
          ];
          for (let i = 0; i <= this.EvaluateRecordList.length - 1; i++) {
            exportData.push(
              {el: this.$refs['comment-record-item' + i][0], marginTop: i === 0 ? 20 : 0}
            );
          }
          await html2Pdf(exportData, exportFileName);
          this.showExport = true;
        });
      });
    },
//页面元素
 <div class="detail-content" id="inspect_course_record" ref="inspect_course_record">
      <div class="top-course-title" ref="top-course-title">
        <div class="left-title">{{ $t('hcp_eduInspect_courseTitle_name') }}</div>
        <div class="right-export">
          <el-button icon="h-icon-export" v-if="showExport" @click="exportPdf">{{ $t('hcp_eduInspect_export_button') }}</el-button>
        </div>
      </div>
      <div class="course-info-detail" ref="course-info-detail">
        <div class="detail-item" v-for="(item,index) in courseInfo" v-show="item.value" :key="item.label+index">
          <div class="label-name record-overflow-line" :title="item.label">{{ item.label }} : </div>
          <div class="record-value record-overflow-line" :title="item.value">{{ item.value }}</div>
        </div>
      </div>
      <div class="comment-title" ref="comment-title">{{ $t('hcp_eduInspect_inspect_course_name') }}</div>
      <div class="comment-record">
        <div class="comment-record-item" :ref="`comment-record-item`+index" v-for="(item,index) in EvaluateRecordList" :key="item.time+index">
          <div class="top-line">
            <div class="cirle"></div>
            <div class="time">{{ item.time }}</div>
            <div class="inspector record-overflow-line" :title="InspectRecord.InspectUserName">{{ $t('hcp_eduInspect_inspector_name').replace('{0}',InspectRecord.InspectUserName) }}</div>
          </div>
          <div class="comment-content" :class="index===EvaluateRecordList.length-1?'no-border':''">
            <div class="text-line">{{ item.EvaluateRecord }}</div>
            <div class="picture-line">
              <div class="pic-box" v-for="(ele,index1) in item.PictureInfoList" :key="index+''+index1">
                <img :src="ele.PictureURL" alt="">
                <div class="picture-mask">
                  <div class="btn-box">
                    <el-button icon="h-icon-zoom_in" size="mini" @click="previewPic(index,index1,true)" />
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

demo导出结果展示

image.png

总结

  • 因为需要考虑pdf分页时,页面元素是否会被截断,所以需要提前考虑好页面元素的布局,如果元素高度固定的话,可以提前计算好一页高度能包含多少元素,然后放到一个div盒子里面,这样的话,可以少截几次图片,适当提高效率;如果确定元素高度,可以像我demo那样,多截几次图,然后再把多张图片组合成一页pdf。
  • 因为截图时对图片的处理花费时间较长,所以图片数量较多时,整个过程会有点慢。