vue导出页面为pdf,完美解决分页截断、页眉页脚等各种问题需求

前言

本文介绍了一个基于html2canvas和jsPDF的PDF导出工具类实现方案,适用于Vue项目中DOM元素转PDF的需求。该工具类提供以下功能:

  • 支持添加页眉和自动计算分页位置
  • 提供分页功能,可根据指定类名自动分页
  • 可配置选项包括:缩放比例、DPI、页边距等
  • 自动处理跨域资源和画布污染问题

正文

第一步,在vue项目中安装好html2canvas和jspdf

npm i html2canvas jspdf

第二步,建一个js文件,代码如下:

ini 复制代码
import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

class PdfLoader {
    /**
     * PDF导出工具类
     * @param {HTMLElement} ele - 要导出为PDF的DOM元素
     * @param {string} pdfFileName - 导出的PDF文件名(不带扩展名)
     * @param {string} [splitClassName] - 用于分页判断的类名(可选)
     * @param {number} [scrollWidth] - 容器滚动宽度(可选)
     * @param {Object} [options] - 配置选项
     * @param {number} [options.scale=2] - 缩放比例
     * @param {number} [options.dpi=300] - DPI设置
     * @param {number} [options.margin=20] - PDF页边距
     * @param {boolean} [options.useCORS=true] - 是否使用CORS
     * @param {boolean} [options.allowTaint=true] - 是否允许污染画布
     */
    constructor(ele, pdfFileName, splitClassName, scrollWidth, options = {}) {
        if (!ele || !pdfFileName) {
            throw new Error("必须提供有效的DOM元素和PDF文件名");
        }

        this.ele = ele;
        this.pdfFileName = pdfFileName;
        this.splitClassName = splitClassName;
        this.scrollWidth = scrollWidth || ele.scrollWidth || ele.offsetWidth;

        this.A4_WIDTH = 595;
        this.A4_HEIGHT = 842;

        this.options = {
            scale: 2,
            dpi: 300,
            margin: 20,
            useCORS: true,
            allowTaint: true,
            ...options
        };
    }

    /**
     * 生成PDF并下载
     * @returns {Promise<void>}
     */
    async outPutPdfFn() {
        try {
            await this._preprocessForPagination();

            const canvas = await this._generateCanvas();

            this._cleanupPreprocessElements();

            const pdfData = await this._generatePdfFromCanvas(canvas);

            return pdfData;
        } catch (error) {
            console.error("生成PDF时出错:", error);
            throw error; // 重新抛出错误以便外部处理
        }
    }

    /**
     * 预处理:添加分页标记
     * @private
     */
    async _preprocessForPagination() {
        if (!this.splitClassName) return;
        const childList = this.ele.getElementsByClassName(this.splitClassName);
        const eleBounding = this.ele.getBoundingClientRect();
        const pageHeight = (this.scrollWidth / this.A4_WIDTH) * this.A4_HEIGHT;
        let pageNum = 1;

        for (const node of childList) {
            const bound = node.getBoundingClientRect();
            const diff2Ele = bound.top - eleBounding.top;// 当前元素距离容器顶部距离
            const shouldInPage = Math.ceil((bound.bottom - eleBounding.top) / pageHeight);

            if (pageNum < shouldInPage) {
                pageNum = shouldInPage;
                // console.log(pageNum, shouldInPage);
                // console.log(bound.bottom, eleBounding.top,pageHeight);

                const parentNode = node.parentNode;
                const emptyNode = this._createEmptyNode(pageHeight, pageNum, diff2Ele);
                const pageHead = this._createPageHeadNode();

                parentNode.insertBefore(emptyNode, node);
                parentNode.insertBefore(pageHead, node);
            }
        }
    }

    /**
     * 创建空节点用于分页
     * @private
     */
    _createEmptyNode(pageHeight, pageNum, diff2Ele) {
        const emptyNode = document.createElement("div");
        emptyNode.className = "emptyDiv";
        emptyNode.style.background = "transparent";
        emptyNode.style.width = "100%";
        emptyNode.style.height = `${pageHeight * (pageNum - 1) - diff2Ele + this.options.margin}px`;
        return emptyNode;
    }

    /**
     * 创建页眉节点
     * @private
     */
    _createPageHeadNode() {
        const pageHead = document.createElement("div");
        pageHead.className = "pageHead";
        pageHead.innerHTML = `<h3 style="margin: 0; padding: 0;">${this.pdfFileName}</h3>`;
        return pageHead;
    }

    /**
     * 生成canvas
     * @private
     */
    _generateCanvas() {
        return html2Canvas(this.ele, {
            width: this.scrollWidth,
            height: this.ele.offsetHeight,
            scale: this.options.scale,
            dpi: this.options.dpi,
            useCORS: this.options.useCORS,
            allowTaint: this.options.allowTaint,
            logging: false, // 关闭日志提高性能
        });
    }

    /**
     * 清理预处理添加的DOM元素
     * @private
     */
    _cleanupPreprocessElements() {
        if (!this.splitClassName) return;

        const emptyNodes = this.ele.querySelectorAll('.emptyDiv');
        const headNodes = this.ele.querySelectorAll('.pageHead');

        emptyNodes.forEach(item => item.parentNode?.removeChild(item));
        headNodes.forEach(item => item.parentNode?.removeChild(item));
    }

    /**
     * 从canvas生成PDF
     * @private
     */
    _generatePdfFromCanvas(canvas) {
        return new Promise((resolve) => {
            const contentWidth = canvas.width;
            const contentHeight = canvas.height;
            const pageHeight = (contentWidth / this.A4_WIDTH) * this.A4_HEIGHT;
            const imgWidth = this.A4_WIDTH - this.options.margin;
            const imgHeight = (this.A4_WIDTH / contentWidth) * contentHeight;
            const pageData = canvas.toDataURL('image/jpeg', 1.0);

            const pdf = new JsPDF('', 'pt', 'a4');
            let restHeight = contentHeight;
            let position = 0;

            if (restHeight < pageHeight) {
                // 单页
                // addImage(pageData, 'JPEG', 左,上,宽度,高度)
                pdf.addImage(pageData, 'JPEG', this.options.margin / 2, this.options.margin, imgWidth, imgHeight);
            } else {
                // 多页
                while (restHeight > 0) {
                    pdf.addImage(pageData, 'JPEG', this.options.margin / 2, position + this.options.margin, imgWidth, imgHeight);
                    restHeight -= pageHeight;
                    position -= this.A4_HEIGHT;

                    if (restHeight > 0) {
                        pdf.addPage();
                    }
                }
            }

            // 保存PDF
            pdf.save(`${this.pdfFileName}.pdf`);
            resolve();
        });
    }
}

export default PdfLoader;

最后在需要的页面导入该js,并引用

xml 复制代码
 <template>
  <div>
    <div>这一块是其他内容</div>
    <div class="v-pdfwrap">
      <div class="element">
        <p>报告日期:2199-13-14</p>
      </div>
      <div class="element">
        <A />
      </div>
      <div class="element">
        <B />
      </div>
      <div class="element">
        <C />
      </div>
      <div class="element">
        <D />
        <E />
      </div>
      <div class="element">
        <F />
      </div>
      <div class="element">
        <PartG />
      </div>
      <div class="element">
        <PartH />
      </div>
      <div class="element">
        <PartI />
      </div>
      <div class="element">
        <PartJ />
      </div>
      <div class="element">
        <PartK />
      </div>
    </div>
  </div>
</template>

<script>
import PdfLoader from '@/utils/pagetopdf.js';
import A from "./A.vue";
import B from "./B.vue";
import C from "./C.vue";
import D from "./D.vue";
import E from "./E.vue";
import F from "./F.vue";
import PartG from "./PartG.vue";
import PartH from "./PartH.vue";
import PartI from "./PartI.vue";
import PartJ from "./PartJ.vue";
import PartK from "./PartK.vue";
export default {
  data() {
    return {
    };
  },
  components: {
    A, B, C, D, E, F, PartG, PartH, PartI, PartJ, PartK
  },
  watch: {
  },
  methods: {
    exportPDF() {
      let pdf = new PdfLoader(
        document.querySelector(".v-pdfwrap"),
        "这是pdf名",
        "element",
        1920
      );
      pdf.outPutPdfFn().then(() => {
        console.log('PDF生成完毕');
      });
    },
  }
};
</script>

<style lang="scss"></style>

总结

实践出真知,要不断地尝试才知道代码如何修改才能匹配项目

相关推荐
Lupino7 分钟前
被 React “玩弄”的 24 小时:为了修一个不存在的 Bug,我给大模型送了顿火锅钱
前端·react.js
米丘14 分钟前
了解 Javascript 模块化,更好地掌握 Vite 、Webpack、Rollup 等打包工具
前端
Heo15 分钟前
深入 React19 Diff 算法
前端·javascript·面试
滕青山16 分钟前
个人所得税计算器 在线工具核心JS实现
前端·javascript·vue.js
小怪点点17 分钟前
手写promise
前端·promise
国思RDIF框架26 分钟前
RDIFramework.NET Web 敏捷开发框架 V6.3 发布 (.NET8+、Framework 双引擎)
前端
Mintopia27 分钟前
如何在有限的时间里,活出几倍的人生
前端
炫饭第一名28 分钟前
速通Canvas指北🦮——变形、渐变与阴影篇
前端·javascript·程序员
Neptune129 分钟前
让我带你迅速吃透React组件通信:从入门到精通(上篇)
前端·javascript
阿懂在掘金29 分钟前
Vue 表单避坑(一):为什么 v-model 绑定对象属性会偷偷修改父组件数据?
前端·vue.js