web端生成pdf,前端生成pdf导出并自定义页眉页脚

2024-01-03:修复清晰度问题

在配置里添加了 quality 参数,默认是 st 标清,https://gitee.com/zgp-qz/generate-pdf-web-vue-js
在这里插入图片描述
使用的时候可以这样:
在这里插入图片描述

2023-06-15:根据码友 @咔布哩 需求补充完善一丢丢功能

评论区码友@咔布哩使用遇到困难,在力所能及的情况下完善一下代码[嘘~]🤫🤫,千万不能让领导看见我墨鱼🦑🦑~

修改过后的代码放这里啦https://gitee.com/zgp-qz/generate-pdf-web-vue-js,自己下载吧,generatePdfFile/Methods.js 需要更新一下
在这里插入图片描述

2023-05-08:重新整理

看评论里有几位用的有问题,因为已经用好久了,项目也交给别人负责了,就不调试以前代码了,所以我把我们现在用的直接弄上来吧老代码暂时先不删除了,我们已经在用的代码,再有问题的话…需要远程协助一下吗?提示:技术栈还是老的,逻辑思路也是老的,就是封装了一下:

封装目录结构图

在这里插入图片描述

generatePdfFile/config.js
const defaultOptions = {
    pdf: null, // jsPDF 对象
    a4: {
      // 准备好不同英寸点数对应的不同尺寸规格,一般这里只用 xDpi 就足够了,特殊的机子暂时没碰到,碰到了也不管,爱谁谁
      72: [595, 842],
      96: [794, 1123],
      120: [1487, 2105],
      150: [1240, 1754],
      300: [2480, 3508],
    },
    
    ratio: 1, // 物理像素和设备像素之间的比率
    xDpi: 0, // 每英寸x方向点数
    yDpi: 0, // 每英寸y方向点数
    a4W: 0, // a4纸宽转px之后
    a4H: 0, // a4纸高转px之后
  
    headerHeight: 50, // 页眉宽
    footerHeight: 50, // 页脚高
    headerFlag: false, // 是否展示页眉
    footerFlag: false, // 是否展示页脚

    headEl: null, // 页眉元素对象
    footEl: null, // 页脚元素对象
    homeEl: null, // 首页元素对象
    pagesEl: null, // 内容页元素对象

    headCanvas: null, // 页眉内容canvas对象
    footCanvas: null, // 页脚内容canvas对象
    homeCanvas: null, // 首页内容canvas对象
    pagesCanvas: [], // 合同内容canvas对象

    previewBox: null, // 要预览的盒子元素对象
  
    copyCanvas: null, // 临时的 canvas
    copyContext: null, // 临时的 context
  
    totalPage: 1, // pdf总页数,默认就是1页
    currentPage: 1, // pdf当前页码 默认第一页
    pageClass: '',
  
    copyCanvasRemainingH: 0, // 临时的 canvas 剩余高度
    currentCanvasRemainingH: 0, // 当前绘制的 canvas 对象剩余高度

    showLoading: true,
    loading: null,
  }

export default defaultOptions
generatePdfFile/index.js
import Methods from './Methods'
const generatePdfFiles = {
  install: function (Vim) {
    Vim.prototype.$generatePdfFiles = function (options = {}) {
      return new Methods(this, options)
    }
  }
}


export default generatePdfFiles
generatePdfFile/Methods.js
import defaultOptions from "./config";
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
class Methods {
    constructor(Vim, options = {}) {
        this.$Vim = Vim
        this.inOptions = Object.assign({}, options)
        this.options = Object.assign({}, defaultOptions, options)
        this.count = 0
        this.init()
    }
    init() {
        this.initPdf()
        this.getA4WH()
        this.createTempCanvas()
    }
    // 初始化 jspdf
    initPdf() {
        let pdfOption = {
            orientation: "p", // Orientation of the first page. Possible values are "portrait" or "landscape" (or shortcuts "p" or "l")
            unit: "pt", // Measurement unit (base unit) to be used when coordinates are specified. Possible values are "pt" (points), "mm", "cm", "in", "px", "pc", "em" or "ex". Note that in order to get the correct scaling for "px" units, you need to enable the hotfix "px_scaling" by setting options.hotfixes = ["px_scaling"]
            format: "a4", // The format of the first page
            floatPrecision: 16, // or "smart", default is 16
        };
        this.options.pdf = new jsPDF(pdfOption);
    }
    // 获取a4纸张大小转换的px
    getA4WH() {
        // 获取设备像素比率
        this.options.ratio = window.devicePixelRatio || 1;

        let arrDPI = this.js_getDPI()
        this.options.xDpi = arrDPI[0];
        this.options.yDpi = arrDPI[1];
        this.options.a4W = this.options.a4[this.options.xDpi][0];
        this.options.a4H = this.options.a4[this.options.yDpi][1];
    }
    // 获取 deviceXYDPI(每英寸水平的点数)
    js_getDPI() {
        let arrDPI = [];
        if (window.screen.deviceXDPI != undefined) {
            arrDPI[0] = window.screen.deviceXDPI;
            arrDPI[1] = window.screen.deviceYDPI;
        } else {
            let tmpNode = document.createElement("div");
            tmpNode.style.cssText =
                "width:1in;height:1in;position:absolute;left:0px;top:0px;z-index:99;visibility:hidden";
            document.body.appendChild(tmpNode);
            arrDPI[0] = parseInt(tmpNode.offsetWidth);
            arrDPI[1] = parseInt(tmpNode.offsetHeight);
            tmpNode.parentNode.removeChild(tmpNode);
        }
        return arrDPI;
    }
    // 创建一个临时的canvas
    createTempCanvas() {
        this.options.copyCanvas = document.createElement("canvas");
        this.options.copyCanvas.width = this.options.a4W * this.options.ratio; // 同步元素对象生成的 canvas对象宽度和临时的canvas宽度一致,防止绘制的图像模糊和拉伸
        this.options.copyCanvas.height = this.options.a4H * this.options.ratio; // 同步元素对象生成的 canvas对象高度和临时的canvas高度一致,防止绘制的图像模糊和拉伸
        this.options.copyCanvas.style.width = this.options.a4W + 'px'; // 使用 css 设置临时的 canvas宽度和元素对象生成的 canvas对象css宽度一致,防止绘制的图像模糊和拉伸
        this.options.copyCanvas.style.height = this.options.a4H + 'px'; // 使用 css 设置临时的 canvas高度和元素对象生成的 canvas对象css高度一致,防止绘制的图像模糊和拉伸
        this.options.copyContext = this.options.copyCanvas.getContext("2d", { willReadFrequently: true });
        this.options.copyContext.fillStyle = "#ffffff"; // 填充背景默认为白色
        this.options.copyContext.fillRect(0, 0, this.options.copyCanvas.width, this.options.copyCanvas.height); // 填充至整个页面
    }
    // 生成文件
    async generateFile({ homeEl = null, headEl = null, footEl = null, pagesEl = [[]], previewBox = null }) {
        return new Promise(async (resolve, reject) => {
            try {
                if (this.options.showLoading) {
                    this.showLoading('请勿关闭浏览器,文件生成中...')
                }
                // 存储html元素
                this.options.homeEl = homeEl
                this.options.headEl = headEl
                this.options.footEl = footEl
                this.options.pagesEl = pagesEl
                // 预览盒子元素对象
                this.options.previewBox = previewBox
                // 存储转换的 canvas
                this.options.headCanvas = await html2canvas(headEl);
                this.options.footCanvas = await html2canvas(footEl);
                this.options.homeCanvas = await html2canvas(homeEl);

                let pagesCanvas = [];
                for (let i = 0; i < pagesEl.length; i++) {
                    let part = []
                    let pageEl = pagesEl[i];
                    for (let j = 0; j < pageEl.length; j++) {
                        let elCvs = await html2canvas(pageEl[j]);
                        part.push(elCvs)
                    }
                    pagesCanvas.push(part)
                }
                this.options.pagesCanvas = pagesCanvas;
                this.dealPages(resolve)
            } catch (e) {
                reject(e)
            }
        })
    }
    // 导出pdf
    async dealPages(resolve) {
        // 绘制首页
        this.drawCopy({ canvas: this.options.homeCanvas }); // 绘制副本
        this.turnCanvasToPdf(); // 塞入pdf文件里边
        for (let i = 0; i < this.options.pagesCanvas.length; i++) { // 遍历获取每一部分
            this.options.copyCanvasRemainingH = 0
            this.options.currentCanvasRemainingH = 0
            this.options.standardCutHeight = 0 // 标准允许截断的高度,剩余高度如果在允许截断的高度之内,则去检测截断位置,否则直接从剩余高度底部位置截断
            await this.addPage(); // 先添加一页
            let els = this.options.pagesCanvas[i]
            for (let j = 0; j <= els.length; j++) { // 遍历每一个对象
                let canvas = null;
                if (this.options.currentCanvasRemainingH > 0) { // 如果上一页没画完,接着画上一页剩余的东西,其实这里可以用 currentCanvasRemainingH 来计算的,但是刚开始没想到,于是就这么扔这儿了,想优化的可以把这里修改一下
                    await this.addPage();
                    j -= 1; // pages 对应的还是上一页
                }
                canvas = els[j]; // 还有这里,可以直接用 pages[i] 来获取,因为在上一行做了 i -= 1 的操作,这里你们看着来就行,我就不做优化了,等封装的时候再做优化
                if (canvas) { // 这里是因为 上边的循环 用了 <= length ,所以最后一次会是 undefined,这里做个中断的操作
                    this.drawCopy({ canvas: canvas, headerFlag: true, footerFlag: true }); // 绘制副本
                    this.turnCanvasToPdf(); // 塞入pdf文件里边
                } else {
                    break;
                }
            }
        }
        if (this.options.showLoading) {
            this.hideLoading()
        }
        resolve(this)
    }
    // 上传
    uploadFile({ url, filename = '生成的pdf文件.pdf', params = {} }) {
        return new Promise((resolve, reject) => {
            if (this.options.showLoading) {
                this.showLoading('请勿关闭浏览器,文件上传中...')
            }
            let namesArr = filename.split('.')
            let ext = namesArr[namesArr.length - 1]
            if (ext !== 'pdf') {
                filename += '.pdf'
            }
            let blob = this.options.pdf.output('blob', { filename })
            let tmpFile = new File([blob], filename)
            this.upload(tmpFile, url, params, resolve, reject)
        })
    }
    // 导出
    exportFile(filename = '导出的pdf文件') {
        this.options.pdf.save(filename)
    }
    resetPreviewBox() {
        this.options.previewBox.innerHTML = ''
        this.resetFile()
    }
    resetFile() {
        let totalPage = this.options.totalPage
        while (totalPage > 0) {
            this.options.pdf.deletePage(totalPage)
            totalPage--
        }
        this.options.pdf.addPage()
        this.options.totalPage = 1;
        this.options.currentPage = 1;
        this.options.copyCanvasRemainingH = 0
        this.options.currentCanvasRemainingH = 0
    }
    // 绘制副本 
    drawCopy({ canvas, headerFlag = false, footerFlag = false }) {
        let diff = 0, // 计算差值
            calcHeight = 0, // 参与计算的高度
            useLessHeight = 98, // 剩余高度小于无用高度,直接舍弃 归 0
            // 准备 canvas 的切片需要的数据,具体参数详见:https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/drawImage
            dw = {
                sx: 0, // 源x
                sy: 0, // 源y
                sw: 0, // 源w
                sh: 0, // 源h
                dx: 0, // 目标x
                dy: 0, // 目标y
                dw: 0, // 目标w
                dh: 0, // 目标h
            };
        if (this.options.currentCanvasRemainingH > 0) {
            // 如果有剩余 page 说明上一个 canvas 画了一部分,还剩余一部分没画上去,canvas还是之前的那个 canvas 对象
            this.createTempCanvas(); // 创建一个新的临时画布
            calcHeight = this.options.copyCanvas.height;
            if (headerFlag) {
                // 有页眉
                calcHeight -= this.options.headCanvas.height;
            }
            if (footerFlag) {
                // 有页脚
                calcHeight -= this.options.footCanvas.height;
            }
            diff = this.options.currentCanvasRemainingH - calcHeight;
            dw.sx = 0;
            dw.sy = canvas.height - this.options.currentCanvasRemainingH;
            dw.sw = canvas.width;
            dw.dx = 0;
            dw.dy = headerFlag ? this.options.headCanvas.height : 0;
            dw.dw = this.options.copyCanvas.width;
            if (diff > 0) {
                // 当前绘制对象剩余高度大于临时 canvas 对象高度
                dw.sh = calcHeight;
                dw.dh = calcHeight;
                // 因为需要截断当前页面,所以需要计算截断的点位
                let position = this.checkCutPosition(canvas, dw.sy + dw.sh, dw.sy + dw.sh - this.options.standardCutHeight);
                let positionDiff = dw.sy + dw.sh - position
                dw.sh = position - dw.sy
                dw.dh = position - dw.sy
                this.options.currentCanvasRemainingH = diff + positionDiff;
                this.options.copyCanvasRemainingH = 0;
            } else {
                // 当前绘制对象剩余高度小于或者等于临时 canvas 对象高度
                dw.sh = this.options.currentCanvasRemainingH;
                dw.dh = this.options.currentCanvasRemainingH;

                this.options.currentCanvasRemainingH = 0;
                let tmpCopyCanvasRemainingH = Math.abs(diff)
                this.options.copyCanvasRemainingH = tmpCopyCanvasRemainingH < useLessHeight ? 0 : tmpCopyCanvasRemainingH;
            }
        } else {
            // 没有剩余 page 说明这里是一个新的 canvas
            if (this.options.copyCanvasRemainingH > 0) {
                // 如果临时 canvas 对象剩余高度 > 0,说明临时画布没画满,继续在当前画布上画一个新的 canvas 对象
                diff = canvas.height - this.options.copyCanvasRemainingH;
                dw.sx = 0;
                dw.sy = 0;
                dw.sw = canvas.width;
                dw.dx = 0;
                dw.dy =
                    this.options.copyCanvas.height -
                    this.options.copyCanvasRemainingH -
                    (headerFlag ? this.options.headCanvas.height : 0);
                dw.dw = this.options.copyCanvas.width;
                if (diff > 0) {
                    // 当前绘制对象高度比临时 canvas 剩余的高度大
                    dw.sh = this.options.copyCanvasRemainingH;
                    dw.dh = this.options.copyCanvasRemainingH;

                    // 因为需要截断当前页面,所以需要计算截断的点
                    let position = this.checkCutPosition(canvas, dw.sh, dw.sh > this.options.standardCutHeight ? (dw.sh - this.options.standardCutHeight) : 0);
                    let positionDiff = dw.sh - position
                    dw.sh = position
                    dw.dh = position

                    this.options.currentCanvasRemainingH = diff + positionDiff;
                    this.options.copyCanvasRemainingH = 0;
                } else {
                    // 当前绘制对象高度比临时 canvas 剩余的高度小或相等
                    dw.sh = canvas.height;
                    dw.dh = canvas.height;

                    this.options.currentCanvasRemainingH = 0;
                    let tmpCopyCanvasRemainingH = Math.abs(diff)
                    this.options.copyCanvasRemainingH = tmpCopyCanvasRemainingH < useLessHeight ? 0 : tmpCopyCanvasRemainingH;
                }
            } else {
                // 临时 canvas 没有剩余高度,用一个新的 临时 canvas 对象画
                this.createTempCanvas(); // 创建临时画布
                calcHeight = this.options.copyCanvas.height;
                if (headerFlag) {
                    // 有页眉
                    calcHeight -= this.options.headCanvas.height;
                }
                if (footerFlag) {
                    // 有页脚
                    calcHeight -= this.options.footCanvas.height;
                }
                this.options.standardCutHeight = Math.floor(calcHeight * 0.3)
                diff = canvas.height - calcHeight;
                dw.sx = 0;
                dw.sy = 0;
                dw.sw = canvas.width;
                dw.dx = 0;
                dw.dy = headerFlag ? this.options.headCanvas.height : 0;
                dw.dw = this.options.copyCanvas.width;
                if (diff > 0) {
                    dw.sh = calcHeight;
                    dw.dh = calcHeight;
                    // 因为需要截断当前页面,所以需要计算截断的点位
                    let position = this.checkCutPosition(canvas, dw.sh, dw.sh - this.options.standardCutHeight);
                    let positionDiff = dw.sh - position
                    dw.sh = position
                    dw.dh = position

                    this.options.currentCanvasRemainingH = diff + positionDiff;
                    this.options.copyCanvasRemainingH = 0;
                } else {
                    // 当前绘制对象高度比临时 canvas高度小或者相同
                    dw.sh = canvas.height;
                    dw.dh = canvas.height;

                    this.options.currentCanvasRemainingH = 0;
                    let tmpCopyCanvasRemainingH = Math.abs(diff)
                    this.options.copyCanvasRemainingH = tmpCopyCanvasRemainingH < useLessHeight ? 0 : tmpCopyCanvasRemainingH;
                }
            }
        }
        this.options.copyContext.drawImage(
            canvas,
            dw.sx,
            dw.sy,
            dw.sw,
            dw.sh,
            dw.dx,
            dw.dy,
            dw.dw,
            dw.dh
        );
        if (headerFlag) {
            // 有页眉
            this.options.copyContext.drawImage(
                this.options.headCanvas,
                0,
                0,
                this.options.headCanvas.width,
                this.options.headCanvas.height,
                0,
                0,
                this.options.copyCanvas.width,
                this.options.headCanvas.height
            );
        }
        if (footerFlag) {
            // 有页脚
            this.options.copyContext.drawImage(
                this.options.footCanvas,
                0,
                0,
                this.options.footCanvas.width,
                this.options.footCanvas.height,
                0,
                this.options.copyCanvas.height - this.options.footCanvas.height,
                this.options.copyCanvas.width,
                this.options.footCanvas.height
            );
        }
    }
    // 找到当前点位是否可以截断,防止分页的时候文字分成了一半
    checkCutPosition(canvas, start, end) {
        let ctx = canvas.getContext("2d", { willReadFrequently: true })
        let checkRows = 0; // 记录白色行数
        let position = start // 这里需要除以像素比率,否则下方获取不到截断位置的颜色,将全部是白色,不知道什么情况,反正后边除以这个就正常了
        let endPosition = end; // 这里需要除以像素比率,否则下方获取不到截断位置的颜色,将全部是白色,不知道什么情况,反正后边除以这个就正常了
        // if(this.options.ratio != 1){
        //     position = start / this.options.ratio // 这里需要除以像素比率,否则下方获取不到截断位置的颜色,将全部是白色,不知道什么情况,反正后边除以这个就正常了
        //     endPosition = end / this.options.ratio; // 这里需要除以像素比率,否则下方获取不到截断位置的颜色,将全部是白色,不知道什么情况,反正后边除以这个就正常了
        // }
        // 从定好的高度页面底部开始循环遍历canvas的每个点,找到可以截断的地方
        for (let i = position; i >= endPosition; i--) {
            
            let checkCols = 0; // 记录非白色点数
            let isWhite = true; // 是否是白色
            for (let j = 0; j < canvas.width; j++) {
                let canvasData = ctx.getImageData(j, i, 1, 1).data;
                // 如果该单位的颜色不是白色c[0]  c[1]  c[2] 分别代表r g b 255
                if (
                    canvasData[0] != 0xff ||
                    canvasData[1] != 0xff ||
                    canvasData[2] != 0xff
                ) {
                    checkCols++; // 非白色 + 1
                }
                // 如果这行有超过max_unit个单位都不是白色,退出当前循环
                let max_unit = 10
                if (checkCols > max_unit) {
                    isWhite = false;
                    checkCols = 0
                    break;
                }
            }
            if (isWhite) {
                checkRows++;
                // 如果有超过white_lines行都是白色的,canvas在这里可以截断了
                let white_lines = 8
                if (checkRows >= white_lines) {
                    position = i + white_lines / 2;
                    break;
                }
            } else {
                checkRows = 0;
            }
        }
        // if(this.options.ratio != 1){
        //     return position * this.options.ratio // 这里需要再乘以像素比率
        // } else {
            return position // 这里需要再乘以像素比率
        // }
    }
    // 转换pdf
    turnCanvasToPdf() {
        // 转换 pdf 一只往里边塞入创建的临时 canvas 就行,所有的处理都在临时 canvas 上处理过了
        let canvas = this.options.copyCanvas;
        let cvsWidth = canvas.width;
        let cvsHeight = canvas.height;

        this.options.pdf.addImage(
            canvas.toDataURL("image/jpeg", 1.0),
            "jpeg",
            0,
            0,
            this.options.a4[72][0],
            (this.options.a4[72][0] / cvsWidth) * cvsHeight
        );
        if (this.options.previewBox) {
            this.options.previewBox.appendChild(canvas)
        }
    }
    // 添加一页PDF
    async addPage() {
        this.options.pdf.addPage();
        this.options.totalPage += 1
        this.options.currentPage += 1; // 当前页 + 1
        await this.generateFooterCanvas()
    }
    // 生成页脚页码
    async generateFooterCanvas() {
        // 由于要绘制页码,这里需要重新获取一下页脚的 element 并修改页码,再次绘制出来页脚的 canvas
        let pagerEl = this.options.footEl.querySelector("#pageNumber");
        pagerEl.innerHTML = `${this.options.currentPage - 1}`; // 由于首页不需要首页页码,所以这里减去首页的页码 1
        this.options.footCanvas = await html2canvas(this.options.footEl);
    }
    showLoading(title = 'loading...') {
        this.options.loading = this.$Vim.$loading({
            lock: true,
            text: title,
            spinner: 'el-icon-loading',
            background: 'rgba(0, 0, 0, 0.7)'
        })
    }
    hideLoading() {
        this.options.loading.close()
        this.options.loading = null
    }
    // 上传方法
    upload(file, url, others = {}, resolve = () => { }, reject = () => { }) {
        let self = this
        let data = Object.assign({ type: 'file' }, others)
        let formData = new FormData();
        for (let k in data) {
            formData.append(k, data[k]);
        }
        formData.append('file', file);
        let xhr = this.getXhr();
        xhr.open('POST', url, true);
        xhr.withCredentials = true;
        xhr.send(formData);
        // 处理返回数据
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4) {
                if (self.options.showLoading) {
                    self.hideLoading()
                }
                if (xhr.status == 200) {
                    resolve(JSON.parse(xhr.response))
                } else {
                    reject(JSON.parse(xhr.response))
                }
            }
        }
    }
    getXhr() {
        let xhr = null;
        if (window.XMLHttpRequest) {
            xhr = new XMLHttpRequest();
        } else {
            //为了兼容IE6
            xhr = new ActiveXObject('Microsoft.XMLHTTP');
        }
        return xhr;
    }
}

export default Methods

使用

引入
import generatePdfFile from '@/components/generatePdfFile'
Vue.use(generatePdfFile)
页面调用 - 页面结构

在这里插入图片描述

假数据 - pdfData.js

const pdfData = {
    baseInfo: {
        code: 'BH-878257485732', // 合同编码
        title: '服务合作协议', // 合同名称
        partA: '如常集团科技有限公司', // 甲方
        partB: '憧橙集团北京科技有限公司', // 乙方
        date: '2022-10-21', // 合同时间
    },
    content: {
        main: `《中华人民共和国残疾人保障法》规定“国家实行按比例安排残疾人就业制度”。根据《中国残联办公厅关于明确按比例就业联网认证“跨省通办”工作有关事项的通知》(残联厅函[2022]63号))文件的相关要求,经双方友好协商,本着平等、互利、自愿的原则,达成本合作协议:`,
        clause: [
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
        ]
    },
    supplement: {
        main: `《中华人民共和国残疾人保障法》规定“国家实行按比例安排残疾人就业制度”。根据《中国残联办公厅关于明确按比例就业联网认证“跨省通办”工作有关事项的通知》(残联厅函[2022]63号))文件的相关要求,经双方友好协商,本着平等、互利、自愿的原则,达成本合作协议:`,
        clause: [
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
        ]
    },
    enclosure: {
        main: `《中华人民共和国残疾人保障法》规定“国家实行按比例安排残疾人就业制度”。根据《中国残联办公厅关于明确按比例就业联网认证“跨省通办”工作有关事项的通知》(残联厅函[2022]63号))文件的相关要求,经双方友好协商,本着平等、互利、自愿的原则,达成本合作协议:`,
        clause: [
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
        ]
    },
}
module.exports = pdfData
单页面 - turnPdf.vue
<template>
  <div>
    <el-button @click="exportPdf">导出pdf</el-button>
    <div ref="pdf" :style="'background: #efefef;width: ' + pageWidth + 'px;'">
      <div class="page-head" :style="'height:' + headerHeight + 'px'">
        <div class="page-head-left"></div>
        <div class="page-head-center">
          <img class="page-head-center-img" src="@/assets/avatar.png" alt="" />
        </div>
        <div class="page-head-right"></div>
      </div>
      <div class="page-foot" :style="'height:' + footerHeight + 'px'">
        <div class="page-foot-left"></div>
        <div class="page-foot-center">
          <span></span><span id="pageNumber">{{ currentPage }}</span
          ><span></span>
        </div>
        <div class="page-foot-right"></div>
      </div>
      <div
        class="page-home"
        :style="'width: ' + pageWidth + 'px;height:' + pageHeight + 'px'"
      >
        <div class="page-home-top">合同编号:{{ pdfData.baseInfo.code }}</div>
        <div class="page-home-title">{{ pdfData.baseInfo.title }}</div>
        <div class="page-home-info">
          <div class="page-home-info-line">
            <div class="page-home-info-line-tt">甲方:</div>
            <div class="page-home-info-line-txt">
              {{ pdfData.baseInfo.partA }}
            </div>
          </div>
          <div class="page-home-info-line">
            <div class="page-home-info-line-tt">乙方:</div>
            <div class="page-home-info-line-txt">
              {{ pdfData.baseInfo.partB }}
            </div>
          </div>
          <div class="page-home-info-line">
            <div class="page-home-info-line-tt date">签约时间:</div>
            <div class="page-home-info-line-txt date">
              {{ pdfData.baseInfo.date }}
            </div>
          </div>
        </div>
      </div>
      <div class="page-cont">
        <div class="page-cont-title">服务内容</div>
        <div class="page-cont-txt">{{ pdfData.content.main }}</div>
        <div
          class="page-cont-part"
          v-for="(cont, contIndex) of pdfData.content.clause"
          :key="'cont_content_' + contIndex"
        >
          <div class="page-cont-part-tt">
            {{ (contIndex + 1) | turnSort }}、{{ cont.title }}
          </div>
          <div class="page-cont-part-list">
            <div
              class="page-cont-part-list-item"
              v-for="(minor, minorIndex) of cont.minorItems"
              :key="'cont_minor_' + minorIndex"
            >
              <div class="page-cont-part-list-item-txt">{{ minor.origin }}</div>
            </div>
          </div>
        </div>
      </div>
      <!-- <div class="page-cont">
        <div class="page-cont-title">补充协议</div>
        <div class="page-cont-txt">{{ pdfData.supplement.main }}</div>
        <div
          class="page-cont-part"
          v-for="(cont, contIndex) of pdfData.supplement.clause"
          :key="'supple_cont_' + contIndex"
        >
          <div class="page-cont-part-tt">
            {{ (contIndex + 1) | turnSort }}、{{ cont.title }}
          </div>
          <div class="page-cont-part-list">
            <div
              class="page-cont-part-list-item"
              v-for="(minor, minorIndex) of cont.minorItems"
              :key="'supple_minor_' + minorIndex"
            >
              <div class="page-cont-part-list-item-txt">{{ minor.origin }}</div>
            </div>
          </div>
        </div>
      </div> -->
      <!-- <div class="page-cont">
        <div class="page-cont-title">附件</div>
        <div class="page-cont-txt">{{ pdfData.enclosure.main }}</div>
        <div
          class="page-cont-part"
          v-for="(cont, contIndex) of pdfData.enclosure.clause"
          :key="'enclosure_cont_' + contIndex"
        >
          <div class="page-cont-part-tt">
            {{ (contIndex + 1) | turnSort }}、{{ cont.title }}
          </div>
          <div class="page-cont-part-list">
            <div
              class="page-cont-part-list-item"
              v-for="(minor, minorIndex) of cont.minorItems"
              :key="'enclosure_minor_' + minorIndex"
            >
              <div class="page-cont-part-list-item-txt">{{ minor.origin }}</div>
            </div>
          </div>
        </div>
      </div> -->
      <div class="page-part">
        <div class="page-cont-title">部分1</div>
        <div class="page-cont-txt">{{ pdfData.enclosure.main }}</div>
        <div
          class="page-cont-part"
          v-for="(cont, contIndex) of pdfData.enclosure.clause"
          :key="'enclosure_cont_' + contIndex"
        >
          <div class="page-cont-part-tt">
            {{ (contIndex + 1) | turnSort }}、{{ cont.title }}
          </div>
          <div class="page-cont-part-list">
            <div
              class="page-cont-part-list-item"
              v-for="(minor, minorIndex) of cont.minorItems"
              :key="'enclosure_minor_' + minorIndex"
            >
              <div class="page-cont-part-list-item-txt">{{ minor.origin }}</div>
            </div>
          </div>
        </div>
      </div>
      <!-- <div class="page-part">
        <div class="page-cont-title">部分2</div>
        <div class="page-cont-txt">{{ pdfData.enclosure.main }}</div>
        <div
          class="page-cont-part"
          v-for="(cont, contIndex) of pdfData.enclosure.clause"
          :key="'enclosure_cont_' + contIndex"
        >
          <div class="page-cont-part-tt">
            {{ (contIndex + 1) | turnSort }}、{{ cont.title }}
          </div>
          <div class="page-cont-part-list">
            <div
              class="page-cont-part-list-item"
              v-for="(minor, minorIndex) of cont.minorItems"
              :key="'enclosure_minor_' + minorIndex"
            >
              <div class="page-cont-part-list-item-txt">{{ minor.origin }}</div>
            </div>
          </div>
        </div>
      </div> -->
      <!-- <div class="page-part">
        <div class="page-cont-title">部分3</div>
        <div class="page-cont-txt">{{ pdfData.enclosure.main }}</div>
        <div
          class="page-cont-part"
          v-for="(cont, contIndex) of pdfData.enclosure.clause"
          :key="'enclosure_cont_' + contIndex"
        >
          <div class="page-cont-part-tt">
            {{ (contIndex + 1) | turnSort }}、{{ cont.title }}
          </div>
          <div class="page-cont-part-list">
            <div
              class="page-cont-part-list-item"
              v-for="(minor, minorIndex) of cont.minorItems"
              :key="'enclosure_minor_' + minorIndex"
            >
              <div class="page-cont-part-list-item-txt">{{ minor.origin }}</div>
            </div>
          </div>
        </div>
      </div> -->
    </div>
    <br />
    <br />
    <br />
    <div>展示转换之后的的canvas <el-button @click="resetPreview">重置预览</el-button></div>
    <div ref="showpdf"></div>
  </div>
</template>

<script>
import { turnNumToText } from "@/utils";
import pdfData from "./pdfData";
export default {
  data() {
    return {
      pdfData: pdfData, // 准备的模拟数据

      headerHeight: 80,
      footerHeight: 80,

      pageHeight: 0,
      pageWidth: 0,

      headEl: null, // 页眉
      footEl: null, // 页脚
      homeEl: null, // 封面
      els: [[]], // 合同内容

      totalPage: 0, // pdf总页码
      currentPage: 1,
    };
  },
  filters: {
    // 注册一个自定义过滤器,返回数字序号转换的中文序号
    turnSort(num) {
      return turnNumToText(num);
    },
  },
  mounted() {
    // 初始化
    this.init();
  },
  methods: {
    init() {
      this.generatePdfInstance = this.$generatePdfFile({
        headerHeight: this.headerHeight,
        footerHeight: this.footerHeight,
      });
      this.pageHeight = this.generatePdfInstance.options.a4H;
      this.pageWidth = this.generatePdfInstance.options.a4W;
    },
    getEls() {
      this.headEl = this.$refs.pdf.querySelector(".page-head");
      this.footEl = this.$refs.pdf.querySelector(".page-foot");
      this.homeEl = this.$refs.pdf.querySelector(".page-home");
      let part1El = this.$refs.pdf.querySelectorAll(".page-cont");
      let part2El = this.$refs.pdf.querySelectorAll(".page-part");
      let previewBox = this.$refs.showpdf
      this.els = [part1El, part2El];
      return {
        headEl: this.headEl,
        footEl: this.footEl,
        homeEl: this.homeEl,
        pagesEl: this.els,
        previewBox
      };
    },
    async exportPdf() {
      let elsOptions = this.getEls();
      let info = await this.generatePdfInstance.generateFile(elsOptions);
      console.log(info)
      // let uploadInfo = await this.generatePdfInstance.uploadFile({
      //   url: this.$api.upload.upload_file,
      //   filename: "我晕",
      // });
      // console.log('uploadInfo',uploadInfo)
      this.generatePdfInstance.exportFile('大橙子')
    },
    resetPreview(){
      this.generatePdfInstance.resetPreviewBox()
    }
  },
};
</script>

<style lang="scss" scoped>
.page-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  color: #cccccc;
  font-size: 14px;
  .page-head-left {
  }
  .page-head-center {
    box-sizing: border-box;
    padding-bottom: 6px;
    .page-head-center-img {
      width: 24px;
      height: 24px;
    }
  }
  .page-head-right {
  }
}
.page-foot {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  color: #cccccc;
  font-size: 14px;
  .page-foot-left {
  }
  .page-foot-center {
  }
  .page-foot-right {
  }
}
.page-home {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  color: #333333;
  font-size: 16px;
  .page-home-top {
    width: 100%;
    box-sizing: border-box;
    text-align: right;
    padding-top: 24px;
    padding-right: 24px;
  }
  .page-home-title {
    width: 100%;
    height: 50%;
    box-sizing: border-box;
    font-size: 32px;
    font-weight: bold;
    text-align: center;
    letter-spacing: 6px;
  }
  .page-home-info {
    width: 40%;
    box-sizing: border-box;
    padding-bottom: 24px;
    font-size: 18px;
    .page-home-info-line {
      display: flex;
      justify-content: flex-start;
      align-items: flex-start;
      padding-top: 12px;
      .page-home-info-line-tt {
        width: 60px;
        &.date {
          width: 100px;
        }
      }
      .page-home-info-line-txt {
        width: calc(100% - 60px);
        &.date {
          width: calc(100% - 100px);
        }
      }
    }
  }
}
.page-cont,
.page-part {
  font-size: 14px;
  color: rgb(51, 51, 51);
  box-sizing: border-box;
  padding: 0 24px;
  .page-cont-title {
    font-size: 18px;
    text-align: center;
    font-weight: bold;
    padding: 24px 0px;
  }
  .page-cont-txt {
    text-indent: 2em;
    line-height: 2;
  }
  .page-cont-part {
    .page-cont-part-tt {
      font-weight: bold;
      line-height: 2;
    }
    .page-cont-part-list {
      .page-cont-part-list-item {
        .page-cont-part-list-item-txt {
          text-indent: 2em;
          line-height: 2;
        }
      }
    }
  }
}
</style>
导出结果展示

在这里插入图片描述
在这里插入图片描述

老版本

描述

前端导出pdf文件,并在页眉页脚加上企业logo或者是企业名称

解决办法

前端导出pdf

技术栈

这里的项目用的 vue-element-admin 的模板,vue 版本是 2.X
1、前端基础 html + css + js
2、vue
3、html2canvas
4、jspdf

在这里插入图片描述

安装依赖:

npm install html2canvas --save
npm install jspdf --save 
逻辑

1、初始化数据,html页面渲染
2、将html渲染出来的合同页面全部使用 html2canvas 转换为 canvas
3、准备随时生成临时的 对应 a4 纸页面大小的 canvas 的方法
4、将合同页面生成的 canvas 分成一页一页的绘制到即时生成临时的 canvas 上,并一页一页的塞给 jspdf
5、导出

直接上代码,后边再唠叨,注释写的还算清晰吧
<template>
  <div>
    <el-button @click="exportPdf">导出pdf</el-button>
    <div ref="pdf" :style="'width: ' + a4W + 'px;'">
      <div class="page-head" :style="'height:' + headH + 'px'">
        <div class="page-head-left"></div>
        <div class="page-head-center">
          <img class="page-head-center-img" src="@/assets/avatar.png" alt="" />
        </div>
        <div class="page-head-right"></div>
      </div>
      <div class="page-foot" :style="'height:' + footH + 'px'">
        <div class="page-foot-left"></div>
        <div class="page-foot-center">第{{ currentPage - 1 }}页</div>
        <div class="page-foot-right"></div>
      </div>
      <div
        class="page-home"
        :style="'width: ' + a4W + 'px;height:' + a4H + 'px'"
      >
        <div class="page-home-top">合同编号:{{ pdfData.baseInfo.code }}</div>
        <div class="page-home-title">{{ pdfData.baseInfo.title }}</div>
        <div class="page-home-info">
          <div class="page-home-info-line">
            <div class="page-home-info-line-tt">甲方:</div>
            <div class="page-home-info-line-txt">
              {{ pdfData.baseInfo.partA }}
            </div>
          </div>
          <div class="page-home-info-line">
            <div class="page-home-info-line-tt">乙方:</div>
            <div class="page-home-info-line-txt">
              {{ pdfData.baseInfo.partB }}
            </div>
          </div>
          <div class="page-home-info-line">
            <div class="page-home-info-line-tt date">签约时间:</div>
            <div class="page-home-info-line-txt date">
              {{ pdfData.baseInfo.date }}
            </div>
          </div>
        </div>
      </div>
      <div class="page-cont">
        <div class="page-cont-title">服务内容</div>
        <div class="page-cont-txt">{{ pdfData.content.main }}</div>
        <div
          class="page-cont-part"
          v-for="(cont, contIndex) of pdfData.content.clause"
          :key="'cont_content_' + contIndex"
        >
          <div class="page-cont-part-tt">
            {{ (contIndex + 1) | turnSort }}、{{ cont.title }}
          </div>
          <div class="page-cont-part-list">
            <div
              class="page-cont-part-list-item"
              v-for="(minor, minorIndex) of cont.minorItems"
              :key="'cont_minor_' + minorIndex"
            >
              <div class="page-cont-part-list-item-txt">{{ minor.origin }}</div>
            </div>
          </div>
        </div>
      </div>
      <div class="page-cont">
        <div class="page-cont-title">补充协议</div>
        <div class="page-cont-txt">{{ pdfData.supplement.main }}</div>
        <div
          class="page-cont-part"
          v-for="(cont, contIndex) of pdfData.supplement.clause"
          :key="'supple_cont_' + contIndex"
        >
          <div class="page-cont-part-tt">
            {{ (contIndex + 1) | turnSort }}、{{ cont.title }}
          </div>
          <div class="page-cont-part-list">
            <div
              class="page-cont-part-list-item"
              v-for="(minor, minorIndex) of cont.minorItems"
              :key="'supple_minor_' + minorIndex"
            >
              <div class="page-cont-part-list-item-txt">{{ minor.origin }}</div>
            </div>
          </div>
        </div>
      </div>
      <div class="page-cont">
        <div class="page-cont-title">附件</div>
        <div class="page-cont-txt">{{ pdfData.enclosure.main }}</div>
        <div
          class="page-cont-part"
          v-for="(cont, contIndex) of pdfData.enclosure.clause"
          :key="'enclosure_cont_' + contIndex"
        >
          <div class="page-cont-part-tt">
            {{ (contIndex + 1) | turnSort }}、{{ cont.title }}
          </div>
          <div class="page-cont-part-list">
            <div
              class="page-cont-part-list-item"
              v-for="(minor, minorIndex) of cont.minorItems"
              :key="'enclosure_minor_' + minorIndex"
            >
              <div class="page-cont-part-list-item-txt">{{ minor.origin }}</div>
            </div>
          </div>
        </div>
      </div>
    </div>
    <br />
    <br />
    <br />
    <div>展示转换之后的的canvas</div>
    <div ref="showpdf"></div>
  </div>
</template>

<script>
import { turnNumToText } from "@/utils";
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
import pdfData from "./pdfData";
export default {
  data() {
    return {
      pdfData: pdfData, // 准备的模拟数据
      pdf: null, // jsPDF 对象
      a4: { // 准备好不同英寸点数对应的不同尺寸规格,一般这里只用 xDpi 就足够了,特殊的机子暂时没碰到,碰到了也不管,爱谁谁
        72: [595, 842],
        96: [794, 1123],
        120: [1487, 2105],
        150: [1240, 1754],
        300: [2480, 3508],
      },
      ratio: 1, // 物理像素和设备像素之间的比率
      xDpi: 0, // 每英寸x方向点数
      yDpi: 0, // 每英寸y方向点数
      a4W: 0, // a4纸宽转px之后
      a4H: 0, // a4纸高转px之后
      headH: 50, // 页眉宽
      footH: 50, // 页脚高

      header: null, // 页眉内容canvas对象
      footer: null, // 页脚内容canvas对象
      home: null, // 页脚内容canvas对象
      pages: [], // 合同内容canvas对象

      copyCvs: null, // 临时的 canvas
      copyCtx: null, // 临时的 context

      totalPage: 0, // pdf总页码
      currentPage: 1, // pdf当前页码 默认第一页

      currentCvs: null, // 当前剩余的 page
      copyCvsRemainingH: 0, // 临时的 canvas 剩余高度
      currentCvsRemainingH: 0, // 当前绘制的 canvas 对象剩余高度
    };
  },
  filters: { 
    // 注册一个自定义过滤器,返回数字序号转换的中文序号
    turnSort(num) {
      return turnNumToText(num);
    },
  },
  mounted() {
    // 初始化
    this.init();
  },
  methods: {
    init() {
      this.initPdf();
      this.getData();
      this.getA4WH();
    },
    // 初始化 jspdf
    initPdf() {
      let pdfOption = {
        orientation: "p", // Orientation of the first page. Possible values are "portrait" or "landscape" (or shortcuts "p" or "l")
        unit: "pt", // Measurement unit (base unit) to be used when coordinates are specified. Possible values are "pt" (points), "mm", "cm", "in", "px", "pc", "em" or "ex". Note that in order to get the correct scaling for "px" units, you need to enable the hotfix "px_scaling" by setting options.hotfixes = ["px_scaling"]
        format: "a4", // The format of the first page
        floatPrecision: 16, // or "smart", default is 16
      };
      this.pdf = new jsPDF(pdfOption);
    },
    // 获取a4纸张大小转换的px
    getA4WH() {
      // 获取设备像素比率
      this.ratio = window.devicePixelRatio || 1;

      let arrDPI = this.js_getDPI()
      this.xDpi = arrDPI[0];
      this.yDpi = arrDPI[1];
      this.a4W = this.a4[this.xDpi][0];
      this.a4H = this.a4[this.yDpi][1];
    },
    // 获取 deviceXYDPI(每英寸水平的点数)
    js_getDPI() {
      let arrDPI = [];
      if (window.screen.deviceXDPI != undefined) {
        arrDPI[0] = window.screen.deviceXDPI;
        arrDPI[1] = window.screen.deviceYDPI;
      } else {
        let tmpNode = document.createElement("div");
        tmpNode.style.cssText =
          "width:1in;height:1in;position:absolute;left:0px;top:0px;z-index:99;visibility:hidden";
        document.body.appendChild(tmpNode);
        arrDPI[0] = parseInt(tmpNode.offsetWidth);
        arrDPI[1] = parseInt(tmpNode.offsetHeight);
        tmpNode.parentNode.removeChild(tmpNode);
      }
      return arrDPI;
    },
    // 导出pdf
    async exportPdf() {
      // 先获取所有要导出的元素转换成canvas的对象
      await this.getCvsEls();
      // 绘制首页
      this.drawCopy({ canvas: this.home }); // 绘制副本
      this.turnCanvasToPdf(); // 塞入pdf文件里边
      await this.addPage(); // 先添加一页
      for (let i = 0; i <= this.pages.length; i++) { // 遍历合同主文档页
        let canvas = null;
        if (this.currentCvs) { // 如果上一页没画完,接着画上一页剩余的东西,其实这里可以用 currentCvsRemainingH 来计算的,但是刚开始没想到,于是就这么扔这儿了,想优化的可以把这里修改一下
          await this.addPage();
          i -= 1; // pages 对应的还是上一页
          canvas = this.currentCvs; // 还有这里,可以直接用 pages[i] 来获取,因为在上一行做了 i -= 1 的操作,这里你们看着来就行,我就不做优化了,等封装的时候再做优化
        } else {
          canvas = this.pages[i];
        }
        if (canvas) { // 这里是因为 上边的循环 用了 <= length ,所以最后一次会是 undefined,这里做个中断的操作
          this.drawCopy({ canvas: canvas, headerFlag: true, footerFlag: true }); // 绘制副本
          this.turnCanvasToPdf(); // 塞入pdf文件里边
        } else {
          break;
        }
      }
      // 导出
      this.pdf.save("导出的PDF");
    },
    // 创建一个临时的canvas
    getTempCtx() {
      this.copyCvs = document.createElement("canvas");
      this.copyCvs.width = this.a4W * this.ratio; // 同步元素对象生成的 canvas对象宽度和临时的canvas宽度一致,防止绘制的图像模糊和拉伸
      this.copyCvs.height = this.a4H * this.ratio; // 同步元素对象生成的 canvas对象高度和临时的canvas高度一致,防止绘制的图像模糊和拉伸
      this.copyCvs.style.width = this.a4W + 'px'; // 使用 css 设置临时的 canvas宽度和元素对象生成的 canvas对象css宽度一致,防止绘制的图像模糊和拉伸
      this.copyCvs.style.height = this.a4H + 'px'; // 使用 css 设置临时的 canvas高度和元素对象生成的 canvas对象css高度一致,防止绘制的图像模糊和拉伸
      this.copyCtx = this.copyCvs.getContext("2d");
      this.copyCtx.fillStyle = "#ffffff"; // 填充背景默认为白色
      this.copyCtx.fillRect(0, 0, this.copyCvs.width, this.copyCvs.height); // 填充至整个页面
    },
    // 绘制副本
    drawCopy({ canvas, headerFlag = false, footerFlag = false }) {
      let diff = 0, // 计算差值
        calcHeight = 0, // 参与计算的高度
        // 准备 canvas 的切片需要的数据,具体参数详见:https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/drawImage
        dw = {
          sx: 0, // 源x
          sy: 0, // 源y
          sw: 0, // 源w
          sh: 0, // 源h
          dx: 0, // 目标x
          dy: 0, // 目标y
          dw: 0, // 目标w
          dh: 0, // 目标h
        };
      if (this.currentCvs) {
        // 如果有剩余 page 说明上一个 canvas 画了一部分,还剩余一部分没画上去,canvas还是之前的那个 canvas 对象
        this.getTempCtx(); // 创建临时画布
        calcHeight = this.copyCvs.height;
        if (headerFlag) {
          // 有页眉
          calcHeight -= this.header.height;
        }
        if (footerFlag) {
          // 有页脚
          calcHeight -= this.footer.height;
        }
        diff = this.currentCvsRemainingH - calcHeight;
        dw.sx = 0;
        dw.sy = canvas.height - this.currentCvsRemainingH;
        dw.sw = canvas.width;
        dw.dx = 0;
        dw.dy = headerFlag ? this.header.height : 0;
        dw.dw = this.copyCvs.width;
        if (diff > 0) {
          // 当前绘制对象剩余高度大于临时 canvas 对象高度
          dw.sh = calcHeight;
          dw.dh = calcHeight;
          // 因为需要截断当前页面,所以需要计算截断的点位
          let position = this.checkCutPosition(canvas, dw.sh, dw.dy);
          let positionDiff = dw.sh - position
          dw.sh = position
          dw.dh = position
          this.currentCvs = canvas;
          this.currentCvsRemainingH = diff + positionDiff;
          this.copyCvsRemainingH = 0;
        } else {
          // 当前绘制对象剩余高度小于或者等于临时 canvas 对象高度
          dw.sh = this.currentCvsRemainingH;
          dw.dh = this.currentCvsRemainingH;

          this.currentCvs = null;
          this.currentCvsRemainingH = 0;
          this.copyCvsRemainingH = Math.abs(diff);
          let tmpCopyCanvasRemainingH = Math.abs(diff) 
          this.options.copyCanvasRemainingH = tmpCopyCanvasRemainingH < 48 ? 0 : tmpCopyCanvasRemainingH;
        }
      } else {
        // 没有剩余 page 说明这里是一个新的 canvas
        if (this.copyCvsRemainingH > 0) {
          // 如果临时 canvas 对象剩余高度 > 0,说明临时画布没画满,继续在当前画布上画一个新的 canvas 对象
          diff = canvas.height - this.copyCvsRemainingH;
          dw.sx = 0;
          dw.sy = 0;
          dw.sw = canvas.width;
          dw.dx = 0;
          dw.dy =
            this.copyCvs.height -
            this.copyCvsRemainingH -
            (headerFlag ? this.header.height : 0);
          dw.dw = this.copyCvs.width;
          if (diff > 0) {
            // 当前绘制对象高度比临时 canvas 剩余的高度大
            dw.sh = this.copyCvsRemainingH;
            dw.dh = this.copyCvsRemainingH;

            // 因为需要截断当前页面,所以需要计算截断的点位
            let position = this.checkCutPosition(canvas, dw.sh, dw.sy);
            let positionDiff = dw.sh - position
            dw.sh = position
            dw.dh = position

            this.currentCvs = canvas;
            this.currentCvsRemainingH = diff + positionDiff;
            this.copyCvsRemainingH = 0;
          } else {
            // 当前绘制对象高度比临时 canvas 剩余的高度小或相等
            dw.sh = canvas.height;
            dw.dh = canvas.height;

            this.currentCvs = null;
            this.currentCvsRemainingH = 0;
            let tmpCopyCanvasRemainingH = Math.abs(diff) 
            this.options.copyCanvasRemainingH = tmpCopyCanvasRemainingH < 48 ? 0 : tmpCopyCanvasRemainingH;
          }
        } else {
          // 临时 canvas 没有剩余高度,用一个新的 临时 canvas 对象画
          this.getTempCtx(); // 创建临时画布
          calcHeight = this.copyCvs.height;
          if (headerFlag) {
            // 有页眉
            calcHeight -= this.header.height;
          }
          if (footerFlag) {
            // 有页脚
            calcHeight -= this.footer.height;
          }
          diff = canvas.height - calcHeight;
          dw.sx = 0;
          dw.sy = 0;
          dw.sw = canvas.width;
          dw.dx = 0;
          dw.dy = headerFlag ? this.header.height : 0;
          dw.dw = this.copyCvs.width;
          if (diff > 0) {
            dw.sh = calcHeight;
            dw.dh = calcHeight;
            // 因为需要截断当前页面,所以需要计算截断的点位
            let position = this.checkCutPosition(canvas, dw.sh, dw.sy);
            let positionDiff = dw.sh - position
            dw.sh = position
            dw.dh = position

            this.currentCvs = canvas;
            this.currentCvsRemainingH = diff + positionDiff;
            this.copyCvsRemainingH = 0;
          } else {
            // 当前绘制对象高度比临时 canvas高度小或者相同
            dw.sh = canvas.height;
            dw.dh = canvas.height;

            this.currentCvs = null;
            this.currentCvsRemainingH = 0;
            let tmpCopyCanvasRemainingH = Math.abs(diff) 
            this.options.copyCanvasRemainingH = tmpCopyCanvasRemainingH < 48 ? 0 : tmpCopyCanvasRemainingH;
          }
        }
      }
      this.copyCtx.drawImage(
        canvas,
        dw.sx,
        dw.sy,
        dw.sw,
        dw.sh,
        dw.dx,
        dw.dy,
        dw.dw,
        dw.dh
      );
      if (headerFlag) {
        // 有页眉
        this.copyCtx.drawImage(
          this.header,
          0,
          0,
          this.header.width,
          this.header.height,
          0,
          0,
          this.copyCvs.width,
          this.header.height
        );
      }
      if (footerFlag) {
        // 有页脚
        this.copyCtx.drawImage(
          this.footer,
          0,
          0,
          this.footer.width,
          this.footer.height,
          0,
          this.copyCvs.height - this.footer.height,
          this.copyCvs.width,
          this.footer.height
        );
      }
    },
    // 找到当前点位是否可以截断,防止分页的时候文字分成了一半
    checkCutPosition(canvas, start, end) {
      let ctx = canvas.getContext("2d")
      let checkRows = 0, // 记录白色行数
        position = start / this.ratio, // 这里需要除以像素比率,否则下方获取不到截断位置的颜色,将全部是白色,不知道什么情况,反正后边除以这个就正常了
        endPosition = end / this.ratio; // 这里需要除以像素比率,否则下方获取不到截断位置的颜色,将全部是白色,不知道什么情况,反正后边除以这个就正常了
      // 从定好的高度页面底部开始循环遍历canvas的每个点,找到可以截断的地方
      for (let i = position; i >= endPosition; i--) {
        let checkCols = 0; // 记录非白色点数
        let isWhite = true; // 是否是白色
        for (let j = 0; j < canvas.width; j++) {
          let canvasData = ctx.getImageData(j, i, 1, 1).data;
          // 如果该单位的颜色不是白色c[0]  c[1]  c[2] 分别代表r g b 255
          if (
            canvasData[0] != 0xff ||
            canvasData[1] != 0xff ||
            canvasData[2] != 0xff
          ) {
            checkCols++; // 非白色 + 1
          }
          // 如果这行有超过10个单位都不是白色,退出当前循环
          if (checkCols > 10) {
            isWhite = false;
            checkCols = 0
            break;
          }
        }
        if (isWhite) {
          checkRows++;
          // 如果有超过20行都是白色的,canvas在这里可以截断了,这里是根据自己页面的样式调整,可能不够20行的话需要设置更小的值,比如设置 14行
          if (checkRows >= 20) {
            position = i;
            break;
          }
        } else {
          checkRows = 0;
        }
      }
      return position * this.ratio // 这里需要再乘以像素比率
    },
    // 获取 canvas
    async getCvsEls() {
      let headEl = this.$refs.pdf.querySelector(".page-head");
      let footEl = this.$refs.pdf.querySelector(".page-foot");
      let homeEl = this.$refs.pdf.querySelector(".page-home");
      let pagesEl = this.$refs.pdf.querySelectorAll(".page-cont");
      // 1、转换 canvas
      this.header = await html2canvas(headEl);
      this.footer = await html2canvas(footEl);
      this.home = await html2canvas(homeEl);
      let pages = [];
      for (let i = 0; i < pagesEl.length; i++) {
        let page = pagesEl[i];
        let tmpCvs = await html2canvas(page);
        pages.push(tmpCvs);
      }
      this.pages = pages;
    },
    // 转换pdf
    turnCanvasToPdf() {
      // 转换 pdf 一只往里边塞入创建的临时 canvas 就行,所有的处理都在临时 canvas 上处理过了
      let canvas = this.copyCvs;
      let cvsWidth = canvas.width;
      let cvsHeight = canvas.height;

      this.pdf.addImage(
        canvas.toDataURL("image/jpeg", 1.0),
        "jpeg",
        0,
        0,
        this.a4[72][0],
        (this.a4[72][0] / cvsWidth) * cvsHeight
      );
    },
    // 添加一页PDF
    async addPage() {
      this.pdf.addPage();
      this.currentPage += 1; // 当前页 + 1
      // 由于要绘制页码,这里需要重新获取一下页脚的 element 并修改页码,再次绘制出来页脚的 canvas
      let footEl = this.$refs.pdf.querySelector(".page-foot");
      let pagerEl = footEl.querySelector(".page-foot-center");
      pagerEl.innerHTML = `${this.currentPage - 1}`; // 由于首页不需要页码,所以这里减去首页的页码 1
      this.footer = await html2canvas(footEl);
    },
    // 获取数据
    getData() {
      console.log(this.pdfData);
    },
  },
};
</script>

<style lang="scss" scoped>
.page-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  color: #cccccc;
  font-size: 14px;
  .page-head-left {
  }
  .page-head-center {
    box-sizing: border-box;
    padding-bottom: 6px;
    .page-head-center-img {
      width: 24px;
      height: 24px;
    }
  }
  .page-head-right {
  }
}
.page-foot {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  color: #cccccc;
  font-size: 14px;
  .page-foot-left {
  }
  .page-foot-center {
  }
  .page-foot-right {
  }
}
.page-home {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  color: #333333;
  font-size: 16px;
  .page-home-top {
    width: 100%;
    box-sizing: border-box;
    text-align: right;
    padding-top: 24px;
    padding-right: 24px;
  }
  .page-home-title {
    width: 100%;
    height: 50%;
    box-sizing: border-box;
    font-size: 32px;
    font-weight: bold;
    text-align: center;
    letter-spacing: 6px;
  }
  .page-home-info {
    width: 40%;
    box-sizing: border-box;
    padding-bottom: 24px;
    font-size: 18px;
    .page-home-info-line {
      display: flex;
      justify-content: flex-start;
      align-items: flex-start;
      padding-top: 12px;
      .page-home-info-line-tt {
        width: 60px;
        &.date {
          width: 100px;
        }
      }
      .page-home-info-line-txt {
        width: calc(100% - 60px);
        &.date {
          width: calc(100% - 100px);
        }
      }
    }
  }
}
.page-cont {
  font-size: 14px;
  color: rgb(51,51,51);
  box-sizing: border-box;
  padding: 0 24px;
  .page-cont-title {
    font-size: 18px;
    text-align: center;
    font-weight: bold;
    padding: 24px 0px;
  }
  .page-cont-txt {
    text-indent: 2em;
    line-height: 2;
  }
  .page-cont-part {
    .page-cont-part-tt {
      font-weight: bold;
      line-height: 2;
    }
    .page-cont-part-list {
      .page-cont-part-list-item {
        .page-cont-part-list-item-txt {
          text-indent: 2em;
          line-height: 2;
        }
      }
    }
  }
}
</style>

用到的方法

自定义过滤器用的方法:turnNumToText 原生JS数字序号转文字序号,数字序号转中文序号

模拟数据

const pdfData = {
    baseInfo: {
        code: 'BH-878257485732', // 合同编码
        title: '服务合作协议', // 合同名称
        partA: '如常集团科技有限公司', // 甲方
        partB: '憧橙集团北京科技有限公司', // 乙方
        date: '2022-10-21', // 合同时间
    },
    content: {
        main: `《中华人民共和国残疾人保障法》规定“国家实行按比例安排残疾人就业制度”。根据《中国残联办公厅关于明确按比例就业联网认证“跨省通办”工作有关事项的通知》(残联厅函[2022]63号))文件的相关要求,经双方友好协商,本着平等、互利、自愿的原则,达成本合作协议:`,
        clause: [
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
        ]
    },
    supplement: {
        main: `《中华人民共和国残疾人保障法》规定“国家实行按比例安排残疾人就业制度”。根据《中国残联办公厅关于明确按比例就业联网认证“跨省通办”工作有关事项的通知》(残联厅函[2022]63号))文件的相关要求,经双方友好协商,本着平等、互利、自愿的原则,达成本合作协议:`,
        clause: [
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
        ]
    },
    enclosure: {
        main: `《中华人民共和国残疾人保障法》规定“国家实行按比例安排残疾人就业制度”。根据《中国残联办公厅关于明确按比例就业联网认证“跨省通办”工作有关事项的通知》(残联厅函[2022]63号))文件的相关要求,经双方友好协商,本着平等、互利、自愿的原则,达成本合作协议:`,
        clause: [
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
            {
                title: '合作内容',
                minorItems: [
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                    { 
                        origin: `4、甲方有义务每月按固定日期为残疾人员工足额发放工资(工资采用本企业银行户代发,不得以现金形式发放,工资卡实际到账金额不得低于同期本市在职职工最低工资标准)、缴纳社会保险(包括养老保险、医疗保险、失业保险、生育保险以及工伤保险,社会保险个人缴纳部分与企业缴纳部分全部由企业承担。)和申报个税。`, 
                        after: '', 
                    },
                ]
            },
        ]
    },
}
module.exports = pdfData
最终版截图扔这儿一个

在这里插入图片描述
在这里插入图片描述

开始唠叨

需求

项目需求是这样的:
合同内容都是动态生成的,需要在OA端可以自行更改合同内容,更改的内容需要在前端体现出来更改之前是什么,更改之后又是什么,并且可以相互对比,可以清晰明了的看出来改的都是哪一条记录,方便法务进行查看并审核,最后将改好的内容导出生成合同pdf文档

梳理

合同内容都存入数据库里边,前端提供几个合同模板的样式,将对应内容查出来并体现在页面里边,如果有更改过的,将更改过的以其他样式体现到指定位置就好,这个没啥难的,就是在导出 pdf 这里出了点小问题

刚开始需求评审的时候,负责 oa 项目的前端小伙伴说这个各种不好搞,尤其是导出自定义 pdf 文档,我当时在忙其他项目,也没空管,于是决定,由接口php)自动生成 pdf 文档进行存储并供前端进行下载,可是后端小伙伴说研究了好些天~,变了好几种~技术,最后使用 python生成的pdf,但是自定义页眉页脚这里总是有问题,还有就是合同首页和主内容页和附件页之间不知道该怎么分页,于是反馈给了领导那里,领导决定只做前端的合同内容对比,暂时不做 pdf 导出,于是就搁置了,但是领导说这个功能是董事长点名必须要的东西,暂时是只做前端合同内容对比那块的东西,至于导出自定义的合同…那就不知道何年何月了

决定

这不,刚好到了周六日了嘛,我就寻思着,董事长点名要的东西,到技术这里就给砍半?不合适吧,也显得技术人都是干sha吃的啊(吃干饭的啊~),于是就…就各种查资料,找文档,做demo,因为我以前也没弄过这个啊,最后的最后,算是搞出来了这么个东西,用了用,还算不错,因为是从头到尾自己个儿写的逻辑,中间涉及到 canvas 的技术也复习了复习,算是也涨姿势了吧

嘘~偷偷的说一句:领导后边儿知道了,奖励了包华子,还不是平常抽的那种什么软硬华子,是另外的那个~,嘿嘿嘿~

缺点

说归说,做归做,缺点还是有的,就是比较费脑子~~~
错了,是比较依赖性能,因为是前端直接导出,导出之前的准备全部都是写在缓存里的,合同页数比较多且电脑性能不足的话容易~崩(蹦沙卡拉卡~),所以,如果可以的话,最好还是放在服务端去导出~,希望我们后端的小伙伴加油啦~(哇,搞php的,研究了好几种技术,最后用的python,流弊,yyds~,V587~)

  • 8
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 42
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 42
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值