Skip to content

html2canvas、jspdf实现Vue2项目纯前端导出DOM为pdf

安装插件

sh
npm install --save html2canvas
npm install jspdf --save
npm install --save html2canvas
npm install jspdf --save

在src/untils目录下新建htmlToPdf.js文件,以下代码实现了多种功能,包括前端导出DOM为pdf和上传到服务器函数

js
// 页面导出为pdf格式
import html2Canvas from 'html2canvas'
import jsPDF from 'jspdf'

const htmlToPdf = {
  getPdf(title, url) {
    html2Canvas(document.querySelector('#pdfDom'), {
      allowTaint: false,
      taintTest: false,
      logging: false,
      useCORS: true,
      dpi: window.devicePixelRatio * 4, //将分辨率提高到特定的DPI 提高四倍
      scale: 4, //按比例增加分辨率
    }).then((canvas) => {
      var pdf = new jsPDF('p', 'mm', 'a4') //A4纸,纵向
      var ctx = canvas.getContext('2d'),
        // a4w = 190, a4h = 277,    //A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
        a4w = 210,
        a4h = 297, //A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
        imgHeight = Math.floor((a4h * canvas.width) / a4w), //按A4显示比例换算一页图像的像素高度
        renderedHeight = 0

      while (renderedHeight < canvas.height) {
        var page = document.createElement('canvas')
        page.width = canvas.width
        page.height = Math.min(imgHeight, canvas.height - renderedHeight) //可能内容不足一页

        //用getImageData剪裁指定区域,并画到前面创建的canvas对象中
        page
          .getContext('2d')
          .putImageData(
            ctx.getImageData(
              0,
              renderedHeight,
              canvas.width,
              Math.min(imgHeight, canvas.height - renderedHeight)
            ),
            0,
            0
          )
        pdf.addImage(
          page.toDataURL('image/jpeg', 1.0),
          'JPEG',
          0,
          0,
          a4w,
          Math.min(a4h, (a4w * page.height) / page.width)
        ) //添加图像到页面,保留10mm边距

        renderedHeight += imgHeight
        if (renderedHeight < canvas.height) {
          pdf.addPage() //如果后面还有内容,添加一个空页
        }
        // delete page;
      }
      //保存文件
      pdf.save(title + '.pdf')
    })
  },
  // 返回 PDF 文件对象,可用于上传
  async getPdfFile(title = '文件') {
    const canvas = await html2Canvas(document.querySelector('#pdfDom'), {
      allowTaint: false,
      taintTest: false,
      logging: false,
      useCORS: true,
      dpi: window.devicePixelRatio * 4,
      scale: 4,
    })

    const pdf = createPdfFromCanvas(canvas)
    const blob = pdf.output('blob') // 返回Blob对象
    const file = new File([blob], `${title}.pdf`, { type: 'application/pdf' })
    return file
  },
}

// 用来输出文件上传到服务器
function createPdfFromCanvas(canvas) {
  const pdf = new jsPDF('p', 'mm', 'a4')
  const ctx = canvas.getContext('2d')
  const a4w = 190,
    a4h = 277
  const imgHeight = Math.floor((a4h * canvas.width) / a4w)
  let renderedHeight = 0

  while (renderedHeight < canvas.height) {
    const page = document.createElement('canvas')
    page.width = canvas.width
    page.height = Math.min(imgHeight, canvas.height - renderedHeight)

    page
      .getContext('2d')
      .putImageData(
        ctx.getImageData(
          0,
          renderedHeight,
          canvas.width,
          Math.min(imgHeight, canvas.height - renderedHeight)
        ),
        0,
        0
      )

    pdf.addImage(
      page.toDataURL('image/jpeg', 1.0),
      'JPEG',
      10,
      10,
      a4w,
      Math.min(a4h, (a4w * page.height) / page.width)
    )

    renderedHeight += imgHeight
    if (renderedHeight < canvas.height) {
      pdf.addPage()
    }
  }

  return pdf
}

export default htmlToPdf
// 页面导出为pdf格式
import html2Canvas from 'html2canvas'
import jsPDF from 'jspdf'

const htmlToPdf = {
  getPdf(title, url) {
    html2Canvas(document.querySelector('#pdfDom'), {
      allowTaint: false,
      taintTest: false,
      logging: false,
      useCORS: true,
      dpi: window.devicePixelRatio * 4, //将分辨率提高到特定的DPI 提高四倍
      scale: 4, //按比例增加分辨率
    }).then((canvas) => {
      var pdf = new jsPDF('p', 'mm', 'a4') //A4纸,纵向
      var ctx = canvas.getContext('2d'),
        // a4w = 190, a4h = 277,    //A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
        a4w = 210,
        a4h = 297, //A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
        imgHeight = Math.floor((a4h * canvas.width) / a4w), //按A4显示比例换算一页图像的像素高度
        renderedHeight = 0

      while (renderedHeight < canvas.height) {
        var page = document.createElement('canvas')
        page.width = canvas.width
        page.height = Math.min(imgHeight, canvas.height - renderedHeight) //可能内容不足一页

        //用getImageData剪裁指定区域,并画到前面创建的canvas对象中
        page
          .getContext('2d')
          .putImageData(
            ctx.getImageData(
              0,
              renderedHeight,
              canvas.width,
              Math.min(imgHeight, canvas.height - renderedHeight)
            ),
            0,
            0
          )
        pdf.addImage(
          page.toDataURL('image/jpeg', 1.0),
          'JPEG',
          0,
          0,
          a4w,
          Math.min(a4h, (a4w * page.height) / page.width)
        ) //添加图像到页面,保留10mm边距

        renderedHeight += imgHeight
        if (renderedHeight < canvas.height) {
          pdf.addPage() //如果后面还有内容,添加一个空页
        }
        // delete page;
      }
      //保存文件
      pdf.save(title + '.pdf')
    })
  },
  // 返回 PDF 文件对象,可用于上传
  async getPdfFile(title = '文件') {
    const canvas = await html2Canvas(document.querySelector('#pdfDom'), {
      allowTaint: false,
      taintTest: false,
      logging: false,
      useCORS: true,
      dpi: window.devicePixelRatio * 4,
      scale: 4,
    })

    const pdf = createPdfFromCanvas(canvas)
    const blob = pdf.output('blob') // 返回Blob对象
    const file = new File([blob], `${title}.pdf`, { type: 'application/pdf' })
    return file
  },
}

// 用来输出文件上传到服务器
function createPdfFromCanvas(canvas) {
  const pdf = new jsPDF('p', 'mm', 'a4')
  const ctx = canvas.getContext('2d')
  const a4w = 190,
    a4h = 277
  const imgHeight = Math.floor((a4h * canvas.width) / a4w)
  let renderedHeight = 0

  while (renderedHeight < canvas.height) {
    const page = document.createElement('canvas')
    page.width = canvas.width
    page.height = Math.min(imgHeight, canvas.height - renderedHeight)

    page
      .getContext('2d')
      .putImageData(
        ctx.getImageData(
          0,
          renderedHeight,
          canvas.width,
          Math.min(imgHeight, canvas.height - renderedHeight)
        ),
        0,
        0
      )

    pdf.addImage(
      page.toDataURL('image/jpeg', 1.0),
      'JPEG',
      10,
      10,
      a4w,
      Math.min(a4h, (a4w * page.height) / page.width)
    )

    renderedHeight += imgHeight
    if (renderedHeight < canvas.height) {
      pdf.addPage()
    }
  }

  return pdf
}

export default htmlToPdf

页面代码

html
<template>
    <div style="width: 100%">
        <div class="preview-content-operateBtn" style="padding: 8px 16px">
            <!-- <el-button @click="uploadToService()" type="primary">上传至服务器</el-button>
            <el-input style="width: 200px; margin: 0 8px" v-model="sceneId"></el-input>
            <el-button @click="downLoad_api()" type="primary">下载</el-button> -->
            <!-- <el-button class="previewBtn" type="warning" @click="onClickDownLoad">DOM在前端直接转转换为PDF并下载</el-button> -->
        </div>
        <div id="pdfDom1">
            <pdf_page01 class="pdf-page"></pdf_page01>
            <pdf_page02 class="pdf-page"></pdf_page02>
            <pdf_page03 class="pdf-page"></pdf_page03>
            <pdf_page04 class="pdf-page"></pdf_page04>
            <pdf_page05 class="pdf-page"></pdf_page05>
            <pdf_page06 class="pdf-page"></pdf_page06>
            <pdf_page07 class="pdf-page"></pdf_page07>
        </div>
        <div id="pdfDom2">
            <pdf_page08 class="pdf-page"></pdf_page08>
            <pdf_page09 class="pdf-page"></pdf_page09>
            <pdf_page10 class="pdf-page"></pdf_page10>
            <pdf_page11 class="pdf-page"></pdf_page11>
            <pdf_page12 class="pdf-page"></pdf_page12>
            <pdf_page13 class="pdf-page"></pdf_page13>
            <pdf_page14 class="pdf-page"></pdf_page14>
        </div>

        <el-button class="back-to-top" type="primary" style="position: fixed; bottom: 60px; right: 10px; z-index: 9999"
            @click="onClickDownLoad()">导出PDF</el-button>

        <el-button class="back-to-top" style="position: fixed; bottom: 10px; right: 10px; z-index: 9999"
            @click="backToTop()">返回顶部</el-button>
    </div>
</template>

<script>
// import {
//   upload_file2, // 上传
// } from "@/api/system/SystemManagementApi";

// import {
//   download_file, // 下载
// } from "@/api/system/SystemManagementApi";

import pdf_page01 from '../components/pdf_page01.vue'
import pdf_page02 from '../components/pdf_page02.vue'
import pdf_page03 from '../components/pdf_page03.vue'
import pdf_page04 from '../components/pdf_page04.vue'
import pdf_page05 from '../components/pdf_page05.vue'
import pdf_page06 from '../components/pdf_page06.vue'
import pdf_page07 from '../components/pdf_page07.vue'
import pdf_page08 from '../components/pdf_page08.vue'
import pdf_page09 from '../components/pdf_page09.vue'
import pdf_page10 from '../components/pdf_page10.vue'
import pdf_page11 from '../components/pdf_page11.vue'
import pdf_page12 from '../components/pdf_page12.vue'
import pdf_page13 from '../components/pdf_page13.vue'
import pdf_page14 from '../components/pdf_page14.vue'
import htmlToPdf from "../untils/htmlToPdf.js"; 

export default {
    components: {
        pdf_page01,
        pdf_page02,
        pdf_page03,
        pdf_page04,
        pdf_page05,
        pdf_page06,
        pdf_page07,
        pdf_page08,
        pdf_page09,
        pdf_page10,
        pdf_page11,
        pdf_page12,
        pdf_page13,
        pdf_page14,
    },
    props: {},
    created () { },
    mounted () { },
    data () {
        return {
            sceneCode: "institutionLaw",
            sceneId: "",
            uploadFileUrl:
                process.env.VUE_APP_BASE_API +
                "/admin-api/infra/file/upload?sceneCode=", // 请求地址
        };
    },
    methods: {
        backToTop () { // 滚动回到页面顶部
            window.scrollTo(0, 0);
        },
        downLoad_api () {
            download_file({
                sceneId: this.sceneId,
                sceneCode: "initVisit",
            })
                .then((res) => {
                    this.$download.excel(res, "test.pdf");
                })
                .finally(() => { });
        },
        async uploadToService () {
            const file = await htmlToPdf.getPdfFile("测试文件名");
            const formData = new FormData();
            formData.append("file", file);
            const url = this.uploadFileUrl + this.sceneCode; // 拼接 sceneCode 参数
            upload_file2(formData, url).then((res) => {
                this.$message.success("上传成功");
                this.sceneId = res.data.fileId; // 上传成功后返回 sceneId
            });
        },
        onClickDownLoad () {
            htmlToPdf.getPdf("智家国内二季度税务风险报告");
        },
    },
};
</script>

<style scoped>
.pdf-page {
    width: 840px;
    height: 1188px;
    border: 1px solid #cccccc;
    box-sizing: border-box;
}
</style>
<template>
    <div style="width: 100%">
        <div class="preview-content-operateBtn" style="padding: 8px 16px">
            <!-- <el-button @click="uploadToService()" type="primary">上传至服务器</el-button>
            <el-input style="width: 200px; margin: 0 8px" v-model="sceneId"></el-input>
            <el-button @click="downLoad_api()" type="primary">下载</el-button> -->
            <!-- <el-button class="previewBtn" type="warning" @click="onClickDownLoad">DOM在前端直接转转换为PDF并下载</el-button> -->
        </div>
        <div id="pdfDom1">
            <pdf_page01 class="pdf-page"></pdf_page01>
            <pdf_page02 class="pdf-page"></pdf_page02>
            <pdf_page03 class="pdf-page"></pdf_page03>
            <pdf_page04 class="pdf-page"></pdf_page04>
            <pdf_page05 class="pdf-page"></pdf_page05>
            <pdf_page06 class="pdf-page"></pdf_page06>
            <pdf_page07 class="pdf-page"></pdf_page07>
        </div>
        <div id="pdfDom2">
            <pdf_page08 class="pdf-page"></pdf_page08>
            <pdf_page09 class="pdf-page"></pdf_page09>
            <pdf_page10 class="pdf-page"></pdf_page10>
            <pdf_page11 class="pdf-page"></pdf_page11>
            <pdf_page12 class="pdf-page"></pdf_page12>
            <pdf_page13 class="pdf-page"></pdf_page13>
            <pdf_page14 class="pdf-page"></pdf_page14>
        </div>

        <el-button class="back-to-top" type="primary" style="position: fixed; bottom: 60px; right: 10px; z-index: 9999"
            @click="onClickDownLoad()">导出PDF</el-button>

        <el-button class="back-to-top" style="position: fixed; bottom: 10px; right: 10px; z-index: 9999"
            @click="backToTop()">返回顶部</el-button>
    </div>
</template>

<script>
// import {
//   upload_file2, // 上传
// } from "@/api/system/SystemManagementApi";

// import {
//   download_file, // 下载
// } from "@/api/system/SystemManagementApi";

import pdf_page01 from '../components/pdf_page01.vue'
import pdf_page02 from '../components/pdf_page02.vue'
import pdf_page03 from '../components/pdf_page03.vue'
import pdf_page04 from '../components/pdf_page04.vue'
import pdf_page05 from '../components/pdf_page05.vue'
import pdf_page06 from '../components/pdf_page06.vue'
import pdf_page07 from '../components/pdf_page07.vue'
import pdf_page08 from '../components/pdf_page08.vue'
import pdf_page09 from '../components/pdf_page09.vue'
import pdf_page10 from '../components/pdf_page10.vue'
import pdf_page11 from '../components/pdf_page11.vue'
import pdf_page12 from '../components/pdf_page12.vue'
import pdf_page13 from '../components/pdf_page13.vue'
import pdf_page14 from '../components/pdf_page14.vue'
import htmlToPdf from "../untils/htmlToPdf.js"; 

export default {
    components: {
        pdf_page01,
        pdf_page02,
        pdf_page03,
        pdf_page04,
        pdf_page05,
        pdf_page06,
        pdf_page07,
        pdf_page08,
        pdf_page09,
        pdf_page10,
        pdf_page11,
        pdf_page12,
        pdf_page13,
        pdf_page14,
    },
    props: {},
    created () { },
    mounted () { },
    data () {
        return {
            sceneCode: "institutionLaw",
            sceneId: "",
            uploadFileUrl:
                process.env.VUE_APP_BASE_API +
                "/admin-api/infra/file/upload?sceneCode=", // 请求地址
        };
    },
    methods: {
        backToTop () { // 滚动回到页面顶部
            window.scrollTo(0, 0);
        },
        downLoad_api () {
            download_file({
                sceneId: this.sceneId,
                sceneCode: "initVisit",
            })
                .then((res) => {
                    this.$download.excel(res, "test.pdf");
                })
                .finally(() => { });
        },
        async uploadToService () {
            const file = await htmlToPdf.getPdfFile("测试文件名");
            const formData = new FormData();
            formData.append("file", file);
            const url = this.uploadFileUrl + this.sceneCode; // 拼接 sceneCode 参数
            upload_file2(formData, url).then((res) => {
                this.$message.success("上传成功");
                this.sceneId = res.data.fileId; // 上传成功后返回 sceneId
            });
        },
        onClickDownLoad () {
            htmlToPdf.getPdf("智家国内二季度税务风险报告");
        },
    },
};
</script>

<style scoped>
.pdf-page {
    width: 840px;
    height: 1188px;
    border: 1px solid #cccccc;
    box-sizing: border-box;
}
</style>

导出超出一定页面导致pdf黑屏,拆分为多个canvas再导出

调整htmlToPdf.js文件

js
import html2Canvas from 'html2canvas'
import jsPDF from 'jspdf'

const htmlToPdf = {
  // 下载 PDF 文件
  async getPdf(title = '文件') {
    const pdf = new jsPDF('p', 'mm', 'a4')

    const selectors = ['#pdfDom1', '#pdfDom2']
    for (let i = 0; i < selectors.length; i++) {
      const canvas = await html2Canvas(document.querySelector(selectors[i]), {
        allowTaint: false,
        taintTest: false,
        logging: false,
        useCORS: true,
        dpi: window.devicePixelRatio * 4,
        scale: 4,
      })

      await appendCanvasToPdf(canvas, pdf, i > 0) // 第一个不要 addPage,后面都要
    }

    pdf.save(`${title}.pdf`)
  },

  // 返回 PDF 文件对象
  async getPdfFile(title = '文件') {
    const pdf = new jsPDF('p', 'mm', 'a4')
    const selectors = ['#pdfDom1', '#pdfDom2']

    for (let i = 0; i < selectors.length; i++) {
      const canvas = await html2Canvas(document.querySelector(selectors[i]), {
        allowTaint: false,
        taintTest: false,
        logging: false,
        useCORS: true,
        dpi: window.devicePixelRatio * 4,
        scale: 4,
      })

      await appendCanvasToPdf(canvas, pdf, i > 0)
    }

    const blob = pdf.output('blob')
    const file = new File([blob], `${title}.pdf`, { type: 'application/pdf' })
    return file
  },
}

// 通用函数:将 canvas 分页加入 pdf
async function appendCanvasToPdf(canvas, pdf, addPageFirst = false) {
  const ctx = canvas.getContext('2d')
  const a4w = 210,
    a4h = 297
  const imgHeight = Math.floor((a4h * canvas.width) / a4w)
  let renderedHeight = 0

  while (renderedHeight < canvas.height) {
    const pageCanvas = document.createElement('canvas')
    pageCanvas.width = canvas.width
    pageCanvas.height = Math.min(imgHeight, canvas.height - renderedHeight)

    pageCanvas
      .getContext('2d')
      .putImageData(
        ctx.getImageData(
          0,
          renderedHeight,
          canvas.width,
          Math.min(imgHeight, canvas.height - renderedHeight)
        ),
        0,
        0
      )

    if (addPageFirst || renderedHeight > 0) {
      pdf.addPage()
    }

    pdf.addImage(
      pageCanvas.toDataURL('image/jpeg', 1.0),
      'JPEG',
      0,
      0,
      a4w,
      Math.min(a4h, (a4w * pageCanvas.height) / pageCanvas.width)
    )

    renderedHeight += imgHeight
  }
}

export default htmlToPdf
import html2Canvas from 'html2canvas'
import jsPDF from 'jspdf'

const htmlToPdf = {
  // 下载 PDF 文件
  async getPdf(title = '文件') {
    const pdf = new jsPDF('p', 'mm', 'a4')

    const selectors = ['#pdfDom1', '#pdfDom2']
    for (let i = 0; i < selectors.length; i++) {
      const canvas = await html2Canvas(document.querySelector(selectors[i]), {
        allowTaint: false,
        taintTest: false,
        logging: false,
        useCORS: true,
        dpi: window.devicePixelRatio * 4,
        scale: 4,
      })

      await appendCanvasToPdf(canvas, pdf, i > 0) // 第一个不要 addPage,后面都要
    }

    pdf.save(`${title}.pdf`)
  },

  // 返回 PDF 文件对象
  async getPdfFile(title = '文件') {
    const pdf = new jsPDF('p', 'mm', 'a4')
    const selectors = ['#pdfDom1', '#pdfDom2']

    for (let i = 0; i < selectors.length; i++) {
      const canvas = await html2Canvas(document.querySelector(selectors[i]), {
        allowTaint: false,
        taintTest: false,
        logging: false,
        useCORS: true,
        dpi: window.devicePixelRatio * 4,
        scale: 4,
      })

      await appendCanvasToPdf(canvas, pdf, i > 0)
    }

    const blob = pdf.output('blob')
    const file = new File([blob], `${title}.pdf`, { type: 'application/pdf' })
    return file
  },
}

// 通用函数:将 canvas 分页加入 pdf
async function appendCanvasToPdf(canvas, pdf, addPageFirst = false) {
  const ctx = canvas.getContext('2d')
  const a4w = 210,
    a4h = 297
  const imgHeight = Math.floor((a4h * canvas.width) / a4w)
  let renderedHeight = 0

  while (renderedHeight < canvas.height) {
    const pageCanvas = document.createElement('canvas')
    pageCanvas.width = canvas.width
    pageCanvas.height = Math.min(imgHeight, canvas.height - renderedHeight)

    pageCanvas
      .getContext('2d')
      .putImageData(
        ctx.getImageData(
          0,
          renderedHeight,
          canvas.width,
          Math.min(imgHeight, canvas.height - renderedHeight)
        ),
        0,
        0
      )

    if (addPageFirst || renderedHeight > 0) {
      pdf.addPage()
    }

    pdf.addImage(
      pageCanvas.toDataURL('image/jpeg', 1.0),
      'JPEG',
      0,
      0,
      a4w,
      Math.min(a4h, (a4w * pageCanvas.height) / pageCanvas.width)
    )

    renderedHeight += imgHeight
  }
}

export default htmlToPdf

解决pdf分页导致截断页面连续性问题

  1. 使用autoTable
  2. 不分页导出一整个长pdf
  3. 扫描空白自动算法
  4. 自动计算元素高度堆叠marginTop算法

开发过程中常见bug及其解决方案

  • 部分文字多次重叠问题
  • 浏览器缩放导致文字左右重叠问题
  • 导出文件体积过大,缩小ppi
  • 单元格合并导致内容丢失问题
  • 监听页面缩放百分比,解决导出pdf页面大小不一致问题
  • import更换为cdn引入方式获取依赖

解决方案及源代码

html
<template>
    <div>
        <!-- <el-button :loading="loading_index" type="primary" @click="test">测试</el-button> -->
        <div style="width: 100%; display: flex; justify-content: center">
            <div style="" class="pdf-card">
                当前缩放倍率:{{ Number(devicePixelRatio).toFixed(1) }}
            </div>
            <!-- <el-loading :fullscreen="true" lock text="加载中..." v-if="loading_index"></el-loading> -->
            <div ref="pdfPrintIndex" id="pdfPrintIndex">
                <pdf_page01 @getTitle="getTitle" id="pdfDom0" :props_obj="props_obj" class="pdf-print-pdf-page">
                </pdf_page01>
                <pdf_page02 id="pdfDom1" :props_obj="props_obj" class="pdf-print-pdf-page"></pdf_page02>
                <pdfPageCom01 class="pdf-print-pdf-page-com" id="pdfDom2" :props_obj="props_obj"
                    @childReady="onChildReady"></pdfPageCom01>
                <pdfPageCom02 class="pdf-print-pdf-page-com" id="pdfDom3" :props_obj="props_obj"
                    @childReady="onChildReady"></pdfPageCom02>
                <pdfPageCom03 class="pdf-print-pdf-page-com" style="margin-bottom: 20px;" id="pdfDom4"
                    :props_obj="props_obj" @childReady="onChildReady"></pdfPageCom03>
            </div>

            <el-button icon="el-icon-position" :loading="loading_btn" class="back-to-top" plain type="primary"
                style="position: fixed; bottom: 100px; right: 10px" @click="sendEmail()">发送邮件</el-button>
            <el-button icon="el-icon-upload2" :loading="loading_btn" class="back-to-top" plain type="primary"
                style="position: fixed; bottom: 60px; right: 10px" @click="uploadToService()">附件上传</el-button>
            <el-button icon="el-icon-document" :loading="loading_btn" class="back-to-top" type="primary"
                style="position: fixed; bottom: 20px; right: 10px" @click="onClickDownLoad()">导出PDF</el-button>
        </div>
    </div>
</template>

<script>
// import {
//   download_file, // 下载
// } from "@/api/system/SystemManagementApi";

import pdf_page01 from './componentsPdf/pdf_page01.vue'
import pdf_page02 from './componentsPdf/pdf_page02.vue'
import pdfPageCom01 from './componentsPdf/pdfPageCom01.vue'
import pdfPageCom02 from './componentsPdf/pdfPageCom02.vue'
import pdfPageCom03 from './componentsPdf/pdfPageCom03.vue'

// import { PdfLoader } from "../../utils/outputPDF";
import htmlToPdf2 from '../../utils/htmlToPdf2.js'

import {
    uploadFxbgToFtp, // 上传
    sendFxbgFromFtp
} from '@/api/fxgli/pdfPrint'
export default {
    components: {
        pdf_page01,
        pdf_page02,
        pdfPageCom01,
        pdfPageCom02,
        pdfPageCom03
    },
    props: {},
    created () {
        this.loading_index = this.$loading({
            lock: true,
            text: '数据加载中...',
            spinner: 'el-icon-loading',
            background: 'rgba(255, 255, 255, 0.9)'
        })
        this.props_obj = JSON.parse(this.$route.query.test || '{}')
        console.warn(this.props_obj)
    },
    mounted () {
        // 实时监听 devicePixelRatio
        this.updatePixelRatio = () => {
            this.devicePixelRatio = window.devicePixelRatio
        }
        window.addEventListener('resize', this.updatePixelRatio)
        window.addEventListener('zoom', this.updatePixelRatio)
        // 兼容部分浏览器缩放
        setInterval(this.updatePixelRatio, 500)
    },
    beforeDestroy () {
        window.removeEventListener('resize', this.updatePixelRatio)
        window.removeEventListener('zoom', this.updatePixelRatio)
    },
    data () {
        return {
            devicePixelRatio: window.devicePixelRatio,
            loading_index: true,
            loading_btn: false,
            props_obj: {},
            totalChildren: 3,
            finishedChildren: 0,
            pageTitle: ''
        }
    },
    methods: {
        getTitle (val) {
            this.pageTitle = val
            console.warn('标题:', this.pageTitle)
        },
        onChildReady () {
            this.finishedChildren++
            console.warn(
                '%cfinishedChildren➜:',
                'background:green;color:#fff;padding:4px;',
                this.finishedChildren
            )
            if (this.finishedChildren >= this.totalChildren) {
                this.loading_index.close()

                console.log('%c所有子组件加载完成,loading 关闭', 'color: green')
            }
        },
        async sendEmail () {
            this.loading_btn = true
            sendFxbgFromFtp({
                ssny: this.props_obj.ny,
                ssly: this.props_obj.lycy
            }).then((res) => {
                if (res.code == 1) {
                    this.$message.error(res.code.message)
                } else {
                    this.$message.success('邮件发送成功')
                }
            }).catch((error) => {
            }).finally(() => {
                this.loading_btn = false
            })
        },
        async uploadToService () {
            let loading = null
            loading = this.$loading({
                lock: true,
                text: '上传中...',
                spinner: 'el-icon-loading',
                background: 'rgba(255, 255, 255, 0.9)'
            })
            const file = await htmlToPdf2.getPdfFile(this.pageTitle)
            const formData = new FormData()
            formData.append('file', file)
            formData.append('ssny', this.props_obj.ny)
            formData.append('ssly', this.props_obj.lycy)
            uploadFxbgToFtp(formData).then((res) => {
                loading.close()
                this.$message.success(this.pageTitle, '上传至服务器成功')
            })
        },
        async onClickDownLoad () {
            console.log('缩放倍率1', window.devicePixelRatio)
            console.log('缩放倍率2', window.innerWidth)
            console.log('缩放倍率3', window.outerWidth)
            // 如果window.devicePixelRatio约等于1,继续,否则return
            function isApproximatelyEqual (a, b, tolerance = 0.1) {
                return Math.abs(a - b) > tolerance;
            }
            if (isApproximatelyEqual(window.devicePixelRatio, 1)) {
                this.$message.warning('请使用CTRL+滚轮缩放页面至100%后再导出PDF')
                return
            }
            let loading = null
            loading = this.$loading({
                lock: true,
                text: '导出中...',
                spinner: 'el-icon-loading',
                background: 'rgba(255, 255, 255, 0.9)'
            })
            await this.$nextTick()

            setTimeout(() => {
                htmlToPdf2
                    .getPdf('税务风险报告')
                    .then(async (res) => {
                        this.$message.success('导出成功!')
                        loading.close()
                    })
                    .catch((error) => {
                        this.$message.warning('导出失败,请重试!')
                    })
            }, 1000)
        }
        // test () {
        //     // 获得当前组件的DOM元素
        //     const element = document.getElementById('pdfPrintIndex');

        //     const pdfLoader = new PdfLoader(element, {
        //         contentWidth: 550,
        //         fileName: '技术报告.pdf',
        //         baseY: 15,
        //         isPageMessage: true,
        //         direction: 'p', // 竖向
        //         scale: window.devicePixelRatio * 2
        //     });
        //     // 生成PDF
        //     pdfLoader.getPdf().then(result => {
        //         console.log('PDF生成成功', result);
        //     }).catch(error => {
        //         console.error('PDF生成失败', error);
        //     });

        // },
    }
}
</script>

<style>
.pdf-print-pdf-page {
    width: 840px;
    height: 1188px;
    background-color: white;
}

.pdf-print-pdf-page-com {
    width: 840px;
    background-color: white;
    padding: 20px 0;
    /* margin-top: 20px; */
}

.pdf-print-title-text {
    color: white;
    font-size: 15px;
    font-weight: bold;
    position: absolute;
}

.pdf-print-gray-box {
    background-color: #eef0f7;
    margin: 5px 20px;
    display: flex;
}

.pdf-print-gray-box>div:first-child {
    border-left: 6px solid #e76869;
    padding: 8px 16px;
    color: black;
    font-weight: 600;
    width: 130px;
    height: 40px;
    line-height: 22px;
}

.pdf-print-gray-box>div:nth-child(2) {
    font-size: 13px;
    padding-top: 10px;
    margin-right: 25px;
    width: 650px;
    line-height: 22px;
}

.pdf-print-gray-box-2 {
    background-color: #eef0f7;
    margin: 5px 20px;
    display: flex;
}

.pdf-print-gray-box-2>div:first-child {
    border-left: 6px solid #66bd99;
    padding: 8px 16px;
    color: black;
    font-weight: 600;
    width: 130px;
    height: 40px;
    line-height: 22px;
}

.pdf-print-gray-box-2>div:nth-child(2) {
    font-size: 13px;
    padding-bottom: 10px;
    padding-top: 10px;
    margin-right: 25px;
    width: 650px;
    line-height: 22px;
}

.pdf-print-gradient-box {
    height: 35px;
    background: linear-gradient(to right, #3f8d69 0%, #84c9ac 50%, #3f8d69 100%);
    margin: 20px;
    margin-bottom: 5px;
    /* 居中对齐 */
    display: flex;
    align-items: center;
    justify-content: center;
    /* 文字样式 */
    color: black;
    font-weight: bold;
    font-size: 17px;
}

.pdf-print-gradient-box-origin {
    height: 35px;
    background: linear-gradient(to right, #f97319 0%, #ffba8b 50%, #f97319 100%);
    margin: 20px;

    margin-bottom: 5px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: black;
    font-weight: bold;
    font-size: 17px;
}

.pdf-print-custom-legend {
    display: flex;
    justify-content: center;
    margin-top: 10px;
    font-size: 12px;
}

.pdf-print-legend-item {
    display: flex;
    align-items: center;
    margin: 0 12px;
}

.pdf-print-legend-color {
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-right: 6px;
    border-radius: 2px;
}

.pdf-print-legend-green {
    background-color: #86caad;
    /* 已关差:绿色 */
}

.pdf-print-legend-orange {
    background-color: #feb77c;
    /* 未关差:橙色 */
}

.pdf-card {
    height: 100px;
    width: 200px;
    position: fixed;
    top: 120px;
    right: 15px;
    z-index: 50;
    background: #fff;
    border: 1px solid #cacaca;
    padding: 6px 12px;
    border-radius: 7px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 20px;
    font-weight: 600;
}
</style>
<template>
    <div>
        <!-- <el-button :loading="loading_index" type="primary" @click="test">测试</el-button> -->
        <div style="width: 100%; display: flex; justify-content: center">
            <div style="" class="pdf-card">
                当前缩放倍率:{{ Number(devicePixelRatio).toFixed(1) }}
            </div>
            <!-- <el-loading :fullscreen="true" lock text="加载中..." v-if="loading_index"></el-loading> -->
            <div ref="pdfPrintIndex" id="pdfPrintIndex">
                <pdf_page01 @getTitle="getTitle" id="pdfDom0" :props_obj="props_obj" class="pdf-print-pdf-page">
                </pdf_page01>
                <pdf_page02 id="pdfDom1" :props_obj="props_obj" class="pdf-print-pdf-page"></pdf_page02>
                <pdfPageCom01 class="pdf-print-pdf-page-com" id="pdfDom2" :props_obj="props_obj"
                    @childReady="onChildReady"></pdfPageCom01>
                <pdfPageCom02 class="pdf-print-pdf-page-com" id="pdfDom3" :props_obj="props_obj"
                    @childReady="onChildReady"></pdfPageCom02>
                <pdfPageCom03 class="pdf-print-pdf-page-com" style="margin-bottom: 20px;" id="pdfDom4"
                    :props_obj="props_obj" @childReady="onChildReady"></pdfPageCom03>
            </div>

            <el-button icon="el-icon-position" :loading="loading_btn" class="back-to-top" plain type="primary"
                style="position: fixed; bottom: 100px; right: 10px" @click="sendEmail()">发送邮件</el-button>
            <el-button icon="el-icon-upload2" :loading="loading_btn" class="back-to-top" plain type="primary"
                style="position: fixed; bottom: 60px; right: 10px" @click="uploadToService()">附件上传</el-button>
            <el-button icon="el-icon-document" :loading="loading_btn" class="back-to-top" type="primary"
                style="position: fixed; bottom: 20px; right: 10px" @click="onClickDownLoad()">导出PDF</el-button>
        </div>
    </div>
</template>

<script>
// import {
//   download_file, // 下载
// } from "@/api/system/SystemManagementApi";

import pdf_page01 from './componentsPdf/pdf_page01.vue'
import pdf_page02 from './componentsPdf/pdf_page02.vue'
import pdfPageCom01 from './componentsPdf/pdfPageCom01.vue'
import pdfPageCom02 from './componentsPdf/pdfPageCom02.vue'
import pdfPageCom03 from './componentsPdf/pdfPageCom03.vue'

// import { PdfLoader } from "../../utils/outputPDF";
import htmlToPdf2 from '../../utils/htmlToPdf2.js'

import {
    uploadFxbgToFtp, // 上传
    sendFxbgFromFtp
} from '@/api/fxgli/pdfPrint'
export default {
    components: {
        pdf_page01,
        pdf_page02,
        pdfPageCom01,
        pdfPageCom02,
        pdfPageCom03
    },
    props: {},
    created () {
        this.loading_index = this.$loading({
            lock: true,
            text: '数据加载中...',
            spinner: 'el-icon-loading',
            background: 'rgba(255, 255, 255, 0.9)'
        })
        this.props_obj = JSON.parse(this.$route.query.test || '{}')
        console.warn(this.props_obj)
    },
    mounted () {
        // 实时监听 devicePixelRatio
        this.updatePixelRatio = () => {
            this.devicePixelRatio = window.devicePixelRatio
        }
        window.addEventListener('resize', this.updatePixelRatio)
        window.addEventListener('zoom', this.updatePixelRatio)
        // 兼容部分浏览器缩放
        setInterval(this.updatePixelRatio, 500)
    },
    beforeDestroy () {
        window.removeEventListener('resize', this.updatePixelRatio)
        window.removeEventListener('zoom', this.updatePixelRatio)
    },
    data () {
        return {
            devicePixelRatio: window.devicePixelRatio,
            loading_index: true,
            loading_btn: false,
            props_obj: {},
            totalChildren: 3,
            finishedChildren: 0,
            pageTitle: ''
        }
    },
    methods: {
        getTitle (val) {
            this.pageTitle = val
            console.warn('标题:', this.pageTitle)
        },
        onChildReady () {
            this.finishedChildren++
            console.warn(
                '%cfinishedChildren➜:',
                'background:green;color:#fff;padding:4px;',
                this.finishedChildren
            )
            if (this.finishedChildren >= this.totalChildren) {
                this.loading_index.close()

                console.log('%c所有子组件加载完成,loading 关闭', 'color: green')
            }
        },
        async sendEmail () {
            this.loading_btn = true
            sendFxbgFromFtp({
                ssny: this.props_obj.ny,
                ssly: this.props_obj.lycy
            }).then((res) => {
                if (res.code == 1) {
                    this.$message.error(res.code.message)
                } else {
                    this.$message.success('邮件发送成功')
                }
            }).catch((error) => {
            }).finally(() => {
                this.loading_btn = false
            })
        },
        async uploadToService () {
            let loading = null
            loading = this.$loading({
                lock: true,
                text: '上传中...',
                spinner: 'el-icon-loading',
                background: 'rgba(255, 255, 255, 0.9)'
            })
            const file = await htmlToPdf2.getPdfFile(this.pageTitle)
            const formData = new FormData()
            formData.append('file', file)
            formData.append('ssny', this.props_obj.ny)
            formData.append('ssly', this.props_obj.lycy)
            uploadFxbgToFtp(formData).then((res) => {
                loading.close()
                this.$message.success(this.pageTitle, '上传至服务器成功')
            })
        },
        async onClickDownLoad () {
            console.log('缩放倍率1', window.devicePixelRatio)
            console.log('缩放倍率2', window.innerWidth)
            console.log('缩放倍率3', window.outerWidth)
            // 如果window.devicePixelRatio约等于1,继续,否则return
            function isApproximatelyEqual (a, b, tolerance = 0.1) {
                return Math.abs(a - b) > tolerance;
            }
            if (isApproximatelyEqual(window.devicePixelRatio, 1)) {
                this.$message.warning('请使用CTRL+滚轮缩放页面至100%后再导出PDF')
                return
            }
            let loading = null
            loading = this.$loading({
                lock: true,
                text: '导出中...',
                spinner: 'el-icon-loading',
                background: 'rgba(255, 255, 255, 0.9)'
            })
            await this.$nextTick()

            setTimeout(() => {
                htmlToPdf2
                    .getPdf('税务风险报告')
                    .then(async (res) => {
                        this.$message.success('导出成功!')
                        loading.close()
                    })
                    .catch((error) => {
                        this.$message.warning('导出失败,请重试!')
                    })
            }, 1000)
        }
        // test () {
        //     // 获得当前组件的DOM元素
        //     const element = document.getElementById('pdfPrintIndex');

        //     const pdfLoader = new PdfLoader(element, {
        //         contentWidth: 550,
        //         fileName: '技术报告.pdf',
        //         baseY: 15,
        //         isPageMessage: true,
        //         direction: 'p', // 竖向
        //         scale: window.devicePixelRatio * 2
        //     });
        //     // 生成PDF
        //     pdfLoader.getPdf().then(result => {
        //         console.log('PDF生成成功', result);
        //     }).catch(error => {
        //         console.error('PDF生成失败', error);
        //     });

        // },
    }
}
</script>

<style>
.pdf-print-pdf-page {
    width: 840px;
    height: 1188px;
    background-color: white;
}

.pdf-print-pdf-page-com {
    width: 840px;
    background-color: white;
    padding: 20px 0;
    /* margin-top: 20px; */
}

.pdf-print-title-text {
    color: white;
    font-size: 15px;
    font-weight: bold;
    position: absolute;
}

.pdf-print-gray-box {
    background-color: #eef0f7;
    margin: 5px 20px;
    display: flex;
}

.pdf-print-gray-box>div:first-child {
    border-left: 6px solid #e76869;
    padding: 8px 16px;
    color: black;
    font-weight: 600;
    width: 130px;
    height: 40px;
    line-height: 22px;
}

.pdf-print-gray-box>div:nth-child(2) {
    font-size: 13px;
    padding-top: 10px;
    margin-right: 25px;
    width: 650px;
    line-height: 22px;
}

.pdf-print-gray-box-2 {
    background-color: #eef0f7;
    margin: 5px 20px;
    display: flex;
}

.pdf-print-gray-box-2>div:first-child {
    border-left: 6px solid #66bd99;
    padding: 8px 16px;
    color: black;
    font-weight: 600;
    width: 130px;
    height: 40px;
    line-height: 22px;
}

.pdf-print-gray-box-2>div:nth-child(2) {
    font-size: 13px;
    padding-bottom: 10px;
    padding-top: 10px;
    margin-right: 25px;
    width: 650px;
    line-height: 22px;
}

.pdf-print-gradient-box {
    height: 35px;
    background: linear-gradient(to right, #3f8d69 0%, #84c9ac 50%, #3f8d69 100%);
    margin: 20px;
    margin-bottom: 5px;
    /* 居中对齐 */
    display: flex;
    align-items: center;
    justify-content: center;
    /* 文字样式 */
    color: black;
    font-weight: bold;
    font-size: 17px;
}

.pdf-print-gradient-box-origin {
    height: 35px;
    background: linear-gradient(to right, #f97319 0%, #ffba8b 50%, #f97319 100%);
    margin: 20px;

    margin-bottom: 5px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: black;
    font-weight: bold;
    font-size: 17px;
}

.pdf-print-custom-legend {
    display: flex;
    justify-content: center;
    margin-top: 10px;
    font-size: 12px;
}

.pdf-print-legend-item {
    display: flex;
    align-items: center;
    margin: 0 12px;
}

.pdf-print-legend-color {
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-right: 6px;
    border-radius: 2px;
}

.pdf-print-legend-green {
    background-color: #86caad;
    /* 已关差:绿色 */
}

.pdf-print-legend-orange {
    background-color: #feb77c;
    /* 未关差:橙色 */
}

.pdf-card {
    height: 100px;
    width: 200px;
    position: fixed;
    top: 120px;
    right: 15px;
    z-index: 50;
    background: #fff;
    border: 1px solid #cacaca;
    padding: 6px 12px;
    border-radius: 7px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 20px;
    font-weight: 600;
}
</style>
js
import html2Canvas from 'html2canvas'
// import jsPDF from 'jspdf'

var script0 = document.createElement('script')
script0.type = 'text/javascript'
script0.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.2/jspdf.umd.min.js'
document.head.appendChild(script0)

const htmlToPdf = {
  // 下载 PDF 文件
  async getPdf(title = '文件') {
    const pdf = new jsPDF('p', 'mm', 'a4') // 初始化,但后续每页尺寸自动调整

    const selectors = ['#pdfPrintIndex']
    let isFirst = true

    for (let i = 0; i < selectors.length; i++) {
      const el = document.querySelector(selectors[i])
      if (!el) continue

      const canvas = await html2Canvas(el, {
        allowTaint: false,
        taintTest: false,
        logging: false,
        useCORS: true,
        dpi: window.devicePixelRatio * 1,
        scale: 1,
        letterRendering: true
      })

      await appendFullCanvasToPdf(canvas, pdf, !isFirst)
      isFirst = false
    }

    pdf.save(`${title}.pdf`)
  },

  // 返回 PDF 文件对象
  async getPdfFile(title = '文件') {
    const pdf = new jsPDF('p', 'mm', 'a4')

    const selectors = ['#pdfPrintIndex']
    let isFirst = true

    for (let i = 0; i < selectors.length; i++) {
      const el = document.querySelector(selectors[i])
      if (!el) continue

      const canvas = await html2Canvas(el, {
        allowTaint: false,
        taintTest: false,
        logging: false,
        useCORS: true,
        dpi: window.devicePixelRatio * 1,
        scale: 1,
        letterRendering: true
      })

      await appendFullCanvasToPdf(canvas, pdf, !isFirst)
      isFirst = false
    }

    const blob = pdf.output('blob')
    const file = new File([blob], `${title}.pdf`, { type: 'application/pdf' })
    return file
  },
}

// ✅ 新的:不分页,整图插入
async function appendFullCanvasToPdf(canvas, pdf, addPage = false) {
  const imgData = canvas.toDataURL('image/jpeg', 1.0)

  // 像素转 mm:1 px ≈ 0.2646 mm
  const pxToMm = 0.2646
  const imgWidthMm = canvas.width * pxToMm
  const imgHeightMm = canvas.height * pxToMm

  // 如果不是第一页就添加新页面
  if (addPage) {
    pdf.addPage([imgWidthMm, imgHeightMm])
  } else {
    // 修改首页尺寸为当前图尺寸(首次 addPage 无法修改第一页尺寸)
    pdf.internal.pageSize.width = imgWidthMm
    pdf.internal.pageSize.height = imgHeightMm
  }

  // 插入图片
  pdf.addImage(
    imgData,
    'JPEG',
    0,
    0,
    imgWidthMm,
    imgHeightMm
  )
}

export default htmlToPdf
import html2Canvas from 'html2canvas'
// import jsPDF from 'jspdf'

var script0 = document.createElement('script')
script0.type = 'text/javascript'
script0.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.2/jspdf.umd.min.js'
document.head.appendChild(script0)

const htmlToPdf = {
  // 下载 PDF 文件
  async getPdf(title = '文件') {
    const pdf = new jsPDF('p', 'mm', 'a4') // 初始化,但后续每页尺寸自动调整

    const selectors = ['#pdfPrintIndex']
    let isFirst = true

    for (let i = 0; i < selectors.length; i++) {
      const el = document.querySelector(selectors[i])
      if (!el) continue

      const canvas = await html2Canvas(el, {
        allowTaint: false,
        taintTest: false,
        logging: false,
        useCORS: true,
        dpi: window.devicePixelRatio * 1,
        scale: 1,
        letterRendering: true
      })

      await appendFullCanvasToPdf(canvas, pdf, !isFirst)
      isFirst = false
    }

    pdf.save(`${title}.pdf`)
  },

  // 返回 PDF 文件对象
  async getPdfFile(title = '文件') {
    const pdf = new jsPDF('p', 'mm', 'a4')

    const selectors = ['#pdfPrintIndex']
    let isFirst = true

    for (let i = 0; i < selectors.length; i++) {
      const el = document.querySelector(selectors[i])
      if (!el) continue

      const canvas = await html2Canvas(el, {
        allowTaint: false,
        taintTest: false,
        logging: false,
        useCORS: true,
        dpi: window.devicePixelRatio * 1,
        scale: 1,
        letterRendering: true
      })

      await appendFullCanvasToPdf(canvas, pdf, !isFirst)
      isFirst = false
    }

    const blob = pdf.output('blob')
    const file = new File([blob], `${title}.pdf`, { type: 'application/pdf' })
    return file
  },
}

// ✅ 新的:不分页,整图插入
async function appendFullCanvasToPdf(canvas, pdf, addPage = false) {
  const imgData = canvas.toDataURL('image/jpeg', 1.0)

  // 像素转 mm:1 px ≈ 0.2646 mm
  const pxToMm = 0.2646
  const imgWidthMm = canvas.width * pxToMm
  const imgHeightMm = canvas.height * pxToMm

  // 如果不是第一页就添加新页面
  if (addPage) {
    pdf.addPage([imgWidthMm, imgHeightMm])
  } else {
    // 修改首页尺寸为当前图尺寸(首次 addPage 无法修改第一页尺寸)
    pdf.internal.pageSize.width = imgWidthMm
    pdf.internal.pageSize.height = imgHeightMm
  }

  // 插入图片
  pdf.addImage(
    imgData,
    'JPEG',
    0,
    0,
    imgWidthMm,
    imgHeightMm
  )
}

export default htmlToPdf