vue中使用html2canvas+jsPDF实现pdf的导出

导入依赖

html2canvas依赖

npm install html2canvas

jspdf依赖

npm install jspdf

pdf导出

以导出横向,A4大小的pdf为例
规律:1. html2canvas 中,在保持jsPDF中的宽高不变的情况下,设置html2canvas中的 width 和 height 值越小,导出的pdf越显示不全(会被放大,只能看到局部),反之值越大,导出的pdf越显示完整(值也不能过大,过大在pdf中就显示的越小)。
2. jsPDF 中,在保持html2canvas中的宽高不变的情况下,pdf.addImage(pageData, ‘JPEG’, 5, yPosition, width, height) width 和 height 值越小,导出的pdf越显示完整,反之导出的pdf越显示不全。
总结:html2canvas 与 jsPDF 设置刚好相反,合理设置大小,才能使数据撑满整个pdf。

index.vue执行导出pdf页面

<a-spin :spinning="pdfConfirmLoading">
  <div ref="pdfDiv">
     <pdf-template :ref="'pdfTemplate' + i" v-for="i in arrNum"></pdf-template>
   </div>
</a-spin>

<script>
  import {printPdf} from './utils/index'
  import pdfTemplate from './template/pdfTemplate'

  export default {
    name: 'courseTableList',
    components: {
      pdfTemplate
    },
    data() {
      return {
        pdfVisible: false,
        disableSubmit: false,
        arrNum: 1,
        pdfConfirmLoading: false,
      }
    },
    created() {

    },
    methods: {
      //导出pdf
      async handleExportPdf() {
        this.handleElement()
      },

      handleElement() {
        if(this.selectionRows.length > 0) {
          this.pdfConfirmLoading = true
          const groupedData = this.selectionRows.reduce((result, item) => {
            // 检查是否已存在以该teacher为键的分组
            if (!result[item.instructor]) {
              result[item.instructor] = [];
            }
            // 将当前项添加到对应的分组中
            result[item.instructor].push(item);
            return result;
          }, {});

          // 转换成数组形式
          const groupedArray = Object.values(groupedData);
          let len = groupedArray.length

          this.arrNum = len

          let that = this
          this.pdfVisible = true
          this.$nextTick(() => {
            for (let index = 0; index < groupedArray.length; index++) {
              const item = groupedArray[index];
              let num = index + 1
              let pageFooter = num + ' of ' + len;
              that.$refs['pdfTemplate' + num][0].setDataList(item, pageFooter);
            }

            this.pdfConfirmLoading = false
          })
        } else {
          this.$message.warning("请勾选数据")
        }
      },

      //确认导出pdf
      async confirmExportPdf() {
        this.pdfConfirmLoading = true
        await printPdf(this.$refs.pdfDiv, "courseSchedule")
        this.pdfConfirmLoading = false
        this.pdfVisible = false
      },
    }
  }
</script>

pdfTemplate.vue模板,根据需求自定义创建

<template>
  <div class="content" ref="pdfContent" :key="JSON.stringify(datasource)">
    <!-- 头部 -->
    <div class="header">
      <div class="header_row1">
        <div class="header_row1_v">
          <div class="header_row1_left">
            <span>Regd. User: YiZhong College</span>
          </div>
          <div class="header_row1_right">
            <span>Christine Xia</span>
          </div>
        </div>
        <div class="header_row1_middle">
          <span>Timetables</span>
        </div>
      </div>
      <div class="header_row2">
        <div class="header_row2_v">
          <div class="header_row2_left">
            <span>School: FD - YiZhong Cambridge International School</span>
          </div>
          <div class="header_row2_right">
            <span>FDCC1</span>
          </div>
        </div>
        <div class="header_row2_middle">
          <span>FD 2022-23 Teacher's Timetables</span>
        </div>
      </div>


      <div class="header_line"></div>
    </div>

    <!-- 表格 -->
    <div class="table_middle">
      <a-table :columns="columns" :data-source="datasource" bordered :pagination="false">
        <!-- 时间段 -->
        <tamplate slot="timePeriodSlot" slot-scope="text, record">
          <span>{{record.period}}<br>&nbsp;&nbsp;{{text}} ~ {{record.endTime}}</span>
        </tamplate>
      </a-table>
    </div>

    <!-- 页脚 -->
    <div class="footer">
      <div class="footer_line"></div>
      <div class="footer_end">
        <div class="footer_end_datetime">{{this.currentTime}}</div>
        <div class="footer_end_pages">{{this.pageNum}}</div>
      </div>
    </div>
  </div>
</template>

<script>

let columns = [
  {
    title: '教师',
    dataIndex: 'startTime',
    scopedSlots: { customRender: 'timePeriodSlot' },
    customCell: () => {
      return {
        style: {
          'min-width': '120px',
        },
      };
    },
  },
  {
    title: "Monday",
    children: [
      {
        title: '',
        dataIndex: 'monday',
        key: 'monday',
        customCell: () => {
          return {
            style: {
              'min-width': '180px',
            },
          };
        },
      },
    ],
  },
  {
    title: "Tues",
    children: [
      {
        title: '',
        dataIndex: 'tuesday',
        key: 'tuesday',
        scopedSlots: { customRender: 'childrenRender' },
        customCell: () => {
          return {
            style: {
              'min-width': '180px',
            },
          };
        },
      },
    ],
  },
  {
    title: "Wed",
    children: [
      {
        title: '',
        dataIndex: 'wednesday',
        key: 'wednesday',
        scopedSlots: { customRender: 'childrenRender' },
        customCell: () => {
          return {
            style: {
              'min-width': '180px',
            },
          };
        },
      },
    ],
  },
  {
    title: "Thurs",
    children: [
      {
        title: '',
        dataIndex: 'thursday',
        key: 'thursday',
        scopedSlots: { customRender: 'childrenRender' },
        customCell: () => {
          return {
            style: {
              'min-width': '180px',
            },
          };
        },
      },
    ],
  },
  {
    title: "Friday",
    children: [
      {
        title: '',
        dataIndex: 'friday',
        key: 'friday',
        scopedSlots: { customRender: 'childrenRender' },
        customCell: () => {
          return {
            style: {
              'min-width': '180px',
            },
          };
        },
      },
    ],
  },
];

export default {
  name: "pdfExport",
  data() {
    return {
      currentTime: '',
      pageNum: 'Page 1 of 1',
      datasource: [],
    }
  },
  computed: {
    columns() {
      return columns
    }
  },
  created() {
    this.getCurrentTime();
  },
  methods: {
    setDataList(list, pageFooter) {
      this.datasource = list
      if(this.datasource.length > 0) {
        this.columns[0].title = this.datasource[0].instructor
      }

      this.sortByStartTime()

      if(pageFooter) {
        this.pageNum = pageFooter
      }
    },
    sortByStartTime() {
      this.datasource.sort((a, b) => {
        const timeA = new Date(`2023/01/01 ${a.startTime}`);
        const timeB = new Date(`2023/01/01 ${b.startTime}`);
        return timeA - timeB;
      });
    },
    getCurrentTime() {
      const date = new Date();
      const month = date.getMonth() + 1;
      const day = date.getDate();
      const year = date.getFullYear();
      const hours = date.getHours();
      const minutes = date.getMinutes();
      const seconds = date.getSeconds();

      // 格式化为指定格式的字符串
      const dateString = `${this.formatNumber(month)}/${this.formatNumber(day)}/${year}`;
      const timeString = `${this.formatNumber(hours)}:${this.formatNumber(minutes)}:${this.formatNumber(seconds)}`;

      this.currentTime = `${dateString} ${timeString}`;
    },
    formatNumber(number) {
      return number < 10 ? `0${number}` : number;
    }
  }
}
</script>

<style scoped lang="less">
 .content {
   padding: 15px;
   color: black;
 }

 /**头部属性样式设置*/
 .header_row1 {
   display: flex;
   justify-content: center;
   font-size: 16px;
   margin-bottom: 3px;
   position: relative;
 }

 .header_row1 .header_row1_middle {
   font-weight: 700;
   position: absolute;
 }

 .header_row2 {
   display: flex;
   justify-content: center;
   font-size: 16px;
   margin-bottom: 15px;
   position: relative;
 }

 .header_row2 .header_row2_middle {
   font-size: 22px;
   font-weight: 700;
   position: absolute;
 }

 .header_row1_left,
 .header_row1_right,
 .header_row2_left,
 .header_row2_right {
   display: inline-block;
 }

 .header_row1_v,
 .header_row2_v {
   width: 100%;
 }

 .header_row1_left,
 .header_row2_left {
   float: left;
 }

 .header_row1_right,
 .header_row2_right {
   float: right;
 }

 .header_line {
   border: 1px solid black;
   margin-bottom: 5px;
 }

 /**中间属性样式设置*/
 /* 将表格的标题行背景设置为白色 */
 .ant-table-thead {
   ::v-deep & > tr > th {
     background: #fff;
   }
 }

 /* 将所有边框设置为黑色 */
 .table_middle {
   /deep/ .ant-table {
     color: black;
     font-size: 16px;
   }

   /deep/ .ant-table-bordered .ant-table-thead > tr:first-child > th:first-child {
     border-left: none;
     border-top: none;
     font-weight: 700;
     text-align: left;
   }

   /deep/ .ant-table-bordered .ant-table-thead > tr:not(:last-child) > th {
     border-bottom: 2px solid black;
     border-top: 2px solid black;
     font-weight: normal;
     text-align: center;
   }

   /deep/ .ant-table-bordered .ant-table-thead > tr > th{
     border-right: 2px solid black;
     border-bottom: 2px solid black;
   }

   /deep/.ant-table-bordered .ant-table-tbody > tr > td {
     border-right: 2px solid black;
     border-bottom: 2px solid black;
   }

   /deep/ .ant-table-bordered .ant-table-tbody > tr > td:first-child {
     border-left: 2px solid black;
   }

   /deep/ .ant-table-bordered.ant-table-empty .ant-table-placeholder {
     border: 2px solid black;
     border-top: 1px solid black;
   }
 }


 /**页脚属性样式设置*/
 .footer .footer_line {
   border: 1px solid black;
 }

 .footer {
   font-size: 16px;
 }

 .footer_line {
   margin-top: 75px;
   margin-bottom: 3px;
 }

 .footer_end {
   display: flex;
   justify-content: space-between;
 }
</style>

index.js pdf导出单页方法

export const printPdf = (dom, name = '文件') => {
  const printEle = dom

  let width = printEle.scrollWidth;
  let height = printEle.scrollHeight;

  html2canvas(printEle, {
    allowTaint: true, //允许跨域
    useCORS: true,
    width: width,
    height: height,
    background: '#FFFFFF', //如果指定的div没有设置背景色会默认成黑色
    scale: 2 // 按比例增加分辨率
  }).then(canvas => {
    let contentWidth = canvas.width
    let contentHeight = canvas.height

    //a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
    let pdf = new jsPDF('l', 'pt', 'a4')

    let imgWidth = pdf.internal.pageSize.getWidth()
    let imgHeight = (imgWidth / contentWidth) * contentHeight
    let pageData = canvas.toDataURL('image/jpeg', 1.0)
    //有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
    //当内容未超过pdf一页显示的范围,无需分页
    pdf.addImage(pageData, 'JPEG', 5, 5, imgWidth, imgHeight)

    pdf.save(`${name}.pdf`)
  })
}

index.js pdf导出分页方法

import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'

export const printPdf = (dom, name = '文件') => {
  let contents = dom.getElementsByClassName("content")
  return exportToPDF(contents, name)
}

//导出pdf
async function exportToPDF(contents, name = 'exportPdf') {
  // 创建一个空的横向A4大小的PDF文档对象
  const pdf = new jsPDF('l', 'pt', 'a4');

  let yPosition = 5; // 当前y坐标位置

  for (let i = 0; i < contents.length; i++) {
    const content = contents[i];

    let width = content.scrollWidth;
    let height = content.scrollHeight;

    // 使用html2canvas将content转换为canvas
    const canvas = await html2canvas(content, {
      allowTaint: true, //允许跨域
      useCORS: true,
      width: width * 1.02,
      height: height,
      background: '#FFFFFF', //如果指定的div没有设置背景色会默认成黑色
      scale: 2 // 按比例增加分辨率
    })

    // 如果是下一个content的内容,则创建新的页码
    if (i > 0) {
      pdf.addPage();
      yPosition = 5;
    }

    let pageData = canvas.toDataURL('image/jpeg', 1.0)
    //宽度使用pdf的宽度
    let imgWidth = pdf.internal.pageSize.getWidth()
    //高度根据宽度的比列计算
    let imgHeight = (imgWidth / canvas.width) * canvas.height

    // 将canvas添加到PDF中
    pdf.addImage(pageData, 'JPEG', 5, yPosition, imgWidth, imgHeight)

    yPosition += pdf.internal.pageSize.getHeight();
  }

  // 输出PDF文件
  pdf.save(`${name}.pdf`)

  return new Promise((resolve, reject) => {
    resolve()
  })
}

你可能感兴趣的:(vue,JavaScript,vue.js,pdf,前端,javascript)