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>

总结

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

相关推荐
Amodoro22 分钟前
nuxt更改页面渲染的html,去除自定义属性、
前端·html·nuxt3·nuxt2·nuxtjs
Wcowin30 分钟前
Mkdocs相关插件推荐(原创+合作)
前端·mkdocs
伍哥的传说1 小时前
CSS+JavaScript 禁用浏览器复制功能的几种方法
前端·javascript·css·vue.js·vue·css3·禁用浏览器复制
lichenyang4531 小时前
Axios封装以及添加拦截器
前端·javascript·react.js·typescript
Trust yourself2431 小时前
想把一个easyui的表格<th>改成下拉怎么做
前端·深度学习·easyui
苹果醋31 小时前
iview中实现点击表格单元格完成编辑和查看(span和input切换)
运维·vue.js·spring boot·nginx·课程设计
武昌库里写JAVA2 小时前
iView Table组件二次封装
vue.js·spring boot·毕业设计·layui·课程设计
三口吃掉你2 小时前
Web服务器(Tomcat、项目部署)
服务器·前端·tomcat
Trust yourself2432 小时前
在easyui中如何设置自带的弹窗,有输入框
前端·javascript·easyui
烛阴2 小时前
Tile Pattern
前端·webgl