前端页面导出pdf功能,完美解决分页截断问题,添加页眉页脚

前言

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

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

正文

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

npm i html2canvas jspdf -S

第二步,建一个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>

结语

实践出真知,遇到难点不要慌

相关推荐
console.log('npc')42 分钟前
[特殊字符] AI Agent 工作台前端实战教程 · 新手入门指南
前端·人工智能
gCode Teacher 格码致知1 小时前
前端教学2:ul标签的应用场合与经典案例
前端
const_qiu1 小时前
第 2.5 章 2.5.6:前端登录、Token 无感续签与路由守卫
java·前端·人工智能·状态模式
用户83134859306981 小时前
Vue3 事件:常用用法与开发高频坑点
前端·javascript·vue.js
weixin_446260851 小时前
ReAct‑Evolve:基于轨迹反馈的ReAct智能体自主提示演化系统
前端·react.js·前端框架
其实防守也摸鱼1 小时前
存储管理:页面置换算法学习任务(测验题要求给出置换页面顺序,缺页次数,缺页率)
android·前端·数据库·学习·oracle·自动化
怒放的生命19911 小时前
前端项目 Docker 打包实操指南
前端·docker·容器
tedcloud1232 小时前
Docker Compose 部署 Immich:手机照片自动备份与常见故障排查
运维·前端·docker·容器·智能手机·开源·流程图
时间的情敌2 小时前
《WebGIS 前端的成长路线,三维可视化工程师需要掌握什么》
前端·状态模式
漫步人生走在路上2 小时前
用小语种SEO突破外贸B2B天花板:法语、德语市场的实测数据
开发语言·前端·javascript·人工智能·搜索引擎·ecmascript