vue2 pdf word 下载 用Docxte pdf Word 下载 mplater和JSZip在Vue项目中生成Word文档

164 阅读6分钟

1. 安装必要的库

我们需要以下库来完成任务:

  • docxtemplater: 用于填充Word模板。
  • pizzip: 用于读取和解压缩docx文件。
  • jszip-utils: 用于加载二进制文件。
  • file-saver: 用于保存生成的文件。
  • angular-expressions: 用于在模板中进行数据运算。
  • docxtemplater-image-module-free: 用于在文档中插入图片

使用以下命令安装这些依赖:

npm install docxtemplater pizzip jszip-utils file-saver angular-expressions docxtemplater-image-module-free

使用Docxtemplater和JSZip在Vue项目中生成Word文档

在Vue.js项目中,有时我们需要根据模板生成带有动态数据和图片的Word文档。本文将介绍如何使用DocxtemplaterJSZip以及其他相关库来实现这一功能。

1. 安装必要的库

我们需要以下库来完成任务:

  • docxtemplater: 用于填充Word模板。
  • pizzip: 用于读取和解压缩docx文件。
  • jszip-utils: 用于加载二进制文件。
  • file-saver: 用于保存生成的文件。
  • angular-expressions: 用于在模板中进行数据运算。
  • docxtemplater-image-module-free: 用于在文档中插入图片。

使用以下命令安装这些依赖:

npm install docxtemplater pizzip jszip-utils file-saver angular-expressions docxtemplater-image-module-free

2. 设置环境

在项目中创建一个JavaScript文件,例如docx.js,导入所需的库:

import Docxtemplater from 'docxtemplater';
import PizZip from 'pizzip';
import JSZipUtils from 'jszip-utils';
import { saveAs } from 'file-saver';
import expressions from 'angular-expressions';
import ImageModule from 'docxtemplater-image-module-free';

3. 实现导出功能

3.1 基本文档生成函数

这个函数用于读取模板文件,填充数据,并生成Word文档。函数内部包含读取文件的异步操作,并且处理了可能的错误。

/**
 * 导出Word文档
 * @param {String} tempDocxPath 模板文件路径
 * @param {Object} data 需要填充的数据
 * @param {String} fileName 导出的文件名
 * @returns {Promise<Blob>} 返回包含生成文档的Blob对象
 */
export const exportDocx = (tempDocxPath, data, fileName) => {
  return new Promise((resolve, reject) => {
    // 模拟异步操作,如加载文件等
    setTimeout(() => {
      // 使用JSZipUtils读取模板文件的二进制内容
      JSZipUtils.getBinaryContent(tempDocxPath, (error, content) => {
        if (error) {
          reject(error); // 处理读取文件的错误
          return;
        }
        // 使用PizZip解压缩文件
        const zip = new PizZip(content);
        // 使用Docxtemplater加载解压缩的内容
        const doc = new Docxtemplater().loadZip(zip);
        doc.setData(data);
        try {
          // 填充数据
          doc.render();
        } catch (error) {
          // 处理渲染时的错误
          reject(error);
          return;
        }
        // 生成Blob对象
        const out = doc.getZip().generate({
          type: 'blob',
          mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
        });
        // 保存文件
        saveAs(out, fileName);
        resolve(out);
      });
    }, 1000); // 模拟延迟操作
  });
};
3.2 带图片的文档生成

如果文档中需要插入图片,可以使用docxtemplater-image-module-free插件。这一部分代码相对复杂,因为涉及到图像处理和数据转换。

/**
 * 获取包含图片的Word文档的Blob对象
 * @param {String} tempDocxPath 模板文件路径
 * @param {Object} data 需要填充的数据
 * @param {String} fileName 导出的文件名
 * @returns {Promise<Blob>} 返回包含生成文档的Blob对象
 */
export const getDocxToBlob = (tempDocxPath, data, fileName) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const ImageModule = require('docxtemplater-image-module-free');
      const expressions = require('angular-expressions');
      const assign = require('lodash/assign');
      const last = require('lodash/last');

      // 定义自定义过滤器和解析器
      expressions.filters.lower = function(input) {
        return input ? input.toLowerCase() : input;
      };

      function angularParser(tag) {
        tag = tag.replace(/^\.$/, 'this').replace(/(’|‘)/g, "'").replace(/(“|”)/g, '"');
        const expr = expressions.compile(tag);
        return {
          get: function(scope, context) {
            let obj = {};
            const index = last(context.scopePathItem);
            const scopeList = context.scopeList;
            const num = context.num;
            for (let i = 0, len = num + 1; i < len; i++) {
              obj = assign(obj, scopeList[i]);
            }
            obj = assign(obj, { $index: index });
            return expr(scope, obj);
          }
        };
      }

      // 读取模板文件
      JSZipUtils.getBinaryContent(tempDocxPath, (error, content) => {
        if (error) {
          reject(error);
          return;
        }

        // 定义图像处理选项
        let opts = {
          centered: true,
          getImage: (chartId) => {
            return base64DataURLToArrayBuffer(chartId);
          },
          getSize: function(img, tagValue, tagName) {
            return [70, 100];
          }
        };

        const zip = new PizZip(content);
        const doc = new Docxtemplater().loadZip(zip);
        doc.attachModule(new ImageModule(opts));
        doc.setOptions({ parser: angularParser });
        doc.setData(data);

        try {
          doc.render();
        } catch (error) {
          reject(error);
          return;
        }

        const out = doc.getZip().generate({
          type: 'blob',
          mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
        });
        resolve(out);
      });
    }, 1000);
  });
};

4. 辅助函数

这些辅助函数用于处理图像数据,将图片转换为Base64格式,并进一步转换为ArrayBuffer格式,以便在文档中使用。

/**
 * 将图片URL转换为Base64格式
 * @param {String} imgUrl 图片的URL
 * @returns {Promise<String>} 返回Base64格式的图片数据
 */
export function getBase64Sync(imgUrl) {
  return new Promise((resolve, reject) => {
    let image = new Image();
    image.src = imgUrl;
    image.setAttribute('crossOrigin', '*');
    image.onload = function() {
      let canvas = document.createElement('canvas');
      canvas.width = image.width;
      canvas.height = image.height;
      let context = canvas.getContext('2d');
      context.drawImage(image, 0, 0, image.width, image.height);
      let ext = image.src.substring(image.src.lastIndexOf('.') + 1).toLowerCase();
      let quality = 0.8;
      let dataurl = canvas.toDataURL('image/' + ext, quality);
      resolve(dataurl);
    };
  });
}
4.2 将Base64转换为ArrayBuffer
/**
 * 将Base64格式的数据转换为ArrayBuffer
 * @param {String} dataURL Base64格式的数据
 * @returns {ArrayBuffer} 转换后的ArrayBuffer
 */
function base64DataURLToArrayBuffer(dataURL) {
  const base64Regex = /^data:image\/(png|jpg|jpeg|svg|svg\+xml);base64,/;
  if (!base64Regex.test(dataURL)) {
    return false;
  }
  const stringBase64 = dataURL.replace(base64Regex, '');
  let binaryString = window.atob(stringBase64);
  const len = binaryString.length;
  const bytes = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    const ascii = binaryString.charCodeAt(i);
    bytes[i] = ascii;
  }
  return bytes.buffer;
}

5. 使用示例

在Vue组件中,可以调用这些函数来生成和下载Word文档。

import { exportDocx, getDocxToBlob } from '@/utils/docx';

export default {
  methods: {
    generateDocument() {
      const data = {
        firstName: 'John',
        lastName: 'Doe'
      };
      const templatePath = 'path/to/template.docx';
      const fileName = 'generated-document.docx';

      exportDocx(templatePath, data, fileName).then((blob) => {
        console.log('文档生成成功');
      }).catch((error) => {
        console.error('文档生成失败', error);
      });
    }
  }
}

6. 完整代码

6.1 使用页面
<template>
  <div class="examinee" v-loading="loading">
    <!-- 整个组件的容器 -->
    <div class="container" id="admissionTicket">
      <!-- 遍历 examineeList,每个考场生成一个页面 -->
      <div
        class="page-a4"
        :id="'pdf' + index"
        v-for="(item, index) in examineeList"
        :key="index"
      >
        <!-- 显示考场编号的标题 -->
        <div class="title">
          <span>第-{{ index }}-考场</span>
        </div>
        <!-- 考生信息的包装器 -->
        <div class="a4-wrap">
          <!-- 遍历 item 中的每个考生,展示其信息 -->
          <div class="person-item" v-for="(per, pIndex) in item" :key="pIndex">
            <img class="icon" :src="per.oneInchPhoto" alt="" />
            <div class="text">
              姓名:{{ per.talentName }}
              {{ per.talentGender == 0 ? "男" : "女" }}
            </div>
            <div class="text">座位号:{{ per.seatNum }}</div>
            <div class="text">准考证:{{ per.ticketNum }}</div>
            <div class="text">身份证号:</div>
            <div class="text">{{ per.talentIdCard }}</div>
            <div class="text">本人签字:</div>
          </div>
        </div>
      </div>
    </div>
    <!-- 导出为 PDF 的按钮 -->
    <div class="export-btn">
      <el-button
        type="success"
        icon="el-icon-download"
        size="mini"
        v-loading.fullscreen.lock="pageLoading"
        element-loading-text="正在生成PDF请稍后..."
        element-loading-spinner="el-icon-loading"
        element-loading-background="rgba(255, 255, 255, 0.8)"
        @click="previewImage"
      >导 出</el-button>
    </div>
  </div>
</template>

<script>
// 导入生成PDF和处理图像所需的库
import html2Canvas from "html2canvas";
import JsPDF from "jspdf";
import { getBase64Sync } from "@/utils/docx";
import { getTalentInfo } from "@/api/exam/examInfo";

export default {
  name: "examinee",
  data() {
    return {
      pageLoading: false, // 指示页面是否正在加载
      loading: false, // 指示数据是否正在获取
      examineeList: {}, // 存储考生列表
    };
  },
  created() {
    this.getList(); // 组件创建时获取考生列表
  },
  methods: {
    // 获取考生信息
    getList() {
      const id = this.$route.query.id;
      this.loading = true; // 设置 loading 状态为 true
      getTalentInfo({ examId: id })
        .then((res) => {
          // 将图像 URL 转换为 base64 格式(如果需要)
          // for (const key in res.data) {
          //   res.data[key].forEach(async (ele) => {
          //     ele.oneInchPhoto = await getBase64Sync(ele.oneInchPhoto);
          //   });
          // }
          this.examineeList = res.data; // 更新考生列表数据
          this.loading = false; // 设置 loading 状态为 false
        })
        .finally(() => {
          this.loading = false; // 确保请求完成后将 loading 状态设置为 false
        });
    },
    // 生成考生信息的 PDF 预览
    async previewImage() {
      this.pageLoading = true; // 设置页面加载状态为 true
      try {
        let itemDomList = document.getElementsByClassName("page-a4");
        let imageDataList = []; // 存储每个页面的图像数据
        for (let i = 0; i < itemDomList.length; i++) {
          let pdfDom = document.getElementById("pdf" + (i + 1));
          await html2Canvas(pdfDom, {
            useCORS: true, // 允许跨域
            allowTaint: false,
            backgroundColor: "#fff", // 设置背景颜色为白色
            scale: 2, // 缩放因子,提升图像质量
            async: false,
          }).then(function (canvas) {
            let imageData = canvas.toDataURL("image/jpeg", 0.5);
            imageDataList.push(imageData); // 将图像数据添加到列表中
          });
        }
        let PDF = new JsPDF("p", "mm", "a4"); // 创建新的 PDF 实例
        let width = PDF.internal.pageSize.getWidth();
        let height = PDF.internal.pageSize.getHeight();
        for (let i = 0; i < imageDataList.length; i++) {
          PDF.addImage(imageDataList[i], "JPEG", 10, 10, 190, 277); // 添加图像到 PDF
          if (i !== imageDataList.length - 1) {
            PDF.addPage(); // 如果不是最后一页,添加新的页面
          }
        }
        let pdfData = PDF.output("datauristring");
        var a = document.createElement("a");
        var event = new MouseEvent("click");
        a.download = "考场信息"; // 自定义下载的文件名
        a.href = pdfData;
        a.dispatchEvent(event); // 触发下载
        this.pageLoading = false; // 设置页面加载状态为 false
      } catch (error) {
        console.log(error); // 打印错误信息
        this.pageLoading = false; // 确保出错时页面加载状态也设置为 false
      }
    },
  },
};
</script>

<style lang="scss" scoped>
.examinee {
  width: 100%;
  height: 100%;
  position: relative;
  background: #fff;
  overflow: hidden;
  .container {
    .page-a4 {
      width: 840px;
      height: 1188px;
      margin: 10px auto;
      .title {
        text-align: center;
        font-size: 16px;
        margin-bottom: 5px;
      }
      .a4-wrap {
        height: calc(100% - 27px);
        border: 1px solid #7b7b7b;
        padding: 4px;
        display: flex;
        justify-content: space-between;
        flex-wrap: wrap;
        .person-item {
          width: 16.3%;
          border: 1px solid #333;
          margin-bottom: 3px;
          padding: 5px;
          .icon {
            display: block;
            margin: 0 auto 5px;
            height: 80px;
          }
          .text {
            font-size: 12px;
            // line-height: 12px;
          }
        }
      }
    }
  }
  .export-btn {
    position: fixed;
    top: 110px;
    right: 20px;
    z-index: 111;
  }
}
</style>
6.1.1主要点
  1. 数据获取getList 方法从服务器获取数据,并将其存储在 examineeList 中。
  2. PDF 生成previewImage 方法使用 html2canvas 截取每个页面的内容,并用 JsPDF 将这些内容合成 PDF。
  3. 样式:使用了 Scoped SCSS 样式,使样式仅应用于此组件。
  4. 导出按钮:提供了一个导出为 PDF 的按钮。
6.2 docx 文件:
import Docxtemplater from 'docxtemplater'
import PizZip from 'pizzip'
import JSZipUtils from 'jszip-utils'
import { saveAs } from 'file-saver'
import expressions from 'angular-expressions'
import ImageModule from 'docxtemplater-image-module-free'

/**
 * 导出DOCX文件
 * 
 * 这个函数用于从给定的模板文件生成一个DOCX文件,并将其保存为指定名称的文件。
 * 
 * @param {String} tempDocxPath - 模板文件的URL路径。
 * @param {Object} data - 用于填充模板的动态数据对象。
 * @param {String} fileName - 导出文件的名称,包括扩展名。
 * @returns {Promise} - 返回一个Promise对象,成功时解析为生成的Blob对象。
 */
export const exportDocx = (tempDocxPath, data, fileName) => {
  return new Promise((resolve, reject) => {
    // 使用setTimeout模拟异步操作,实际上可以用实际的异步操作代替
    setTimeout(() => {
      // 通过JSZipUtils获取模板文件的二进制内容
      JSZipUtils.getBinaryContent(tempDocxPath, (error, content) => {
        if (error) {
          reject(error)
          return
        }
        // 将二进制内容加载到PizZip实例中
        const zip = new PizZip(content)
        // 创建Docxtemplater实例并加载zip文件
        const doc = new Docxtemplater().loadZip(zip)
        // 设置模板数据
        doc.setData(data)
        try {
          // 渲染文档,根据数据替换模板中的占位符
          doc.render()
        } catch (error) {
          console.log({ error })
          reject(error)
          return
        }
        // 生成最终的Blob对象
        const out = doc.getZip().generate({
          type: 'blob',
          mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
        })
        // 使用file-saver库保存文件
        saveAs(out, fileName)
        resolve(out)
      }, 1000) // 模拟1秒延迟
    })
  })
}

/**
 * 获取DOCX文件的Blob对象(支持图片处理)
 * 
 * 这个函数与exportDocx类似,但它允许处理DOCX模板中的图片,并返回生成的Blob对象。
 * 
 * @param {String} tempDocxPath - 模板文件的URL路径。
 * @param {Object} data - 用于填充模板的动态数据对象。
 * @param {String} fileName - 导出文件的名称,包括扩展名。
 * @returns {Promise} - 返回一个Promise对象,成功时解析为生成的Blob对象。
 */
export const getDocxToBlob = (tempDocxPath, data, fileName) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 引入处理图片的模块和自定义表达式解析器
      var ImageModule = require('docxtemplater-image-module-free')
      var expressions = require('angular-expressions')
      var assign = require('lodash/assign')
      var last = require('lodash/last')

      // 自定义过滤器,用于将字符串转换为小写
      expressions.filters.lower = function (input) {
        if (!input) return input
        return input.toLowerCase()
      }

      // 自定义Angular表达式解析器
      function angularParser(tag) {
        tag = tag
          .replace(/^\.$/, 'this')
          .replace(/(’|‘)/g, "'")
          .replace(/(“|”)/g, '"')
        const expr = expressions.compile(tag)
        return {
          get: function (scope, context) {
            let obj = {}
            const index = last(context.scopePathItem)
            const scopeList = context.scopeList
            const num = context.num
            for (let i = 0, len = num + 1; i < len; i++) {
              obj = assign(obj, scopeList[i])
            }
            // 将索引值添加到数据对象中
            obj = assign(obj, { $index: index })
            return expr(scope, obj)
          }
        }
      }

      // 读取模板文件的二进制内容
      JSZipUtils.getBinaryContent(tempDocxPath, (error, content) => {
        if (error) {
          reject(error)
          return
        }

        // 自定义过滤器,用于调整图像大小
        expressions.filters.size = function (input, width, height) {
          return {
            data: input,
            size: [width, height],
          }
        }

        let opts = {
          centered: true // 图像是否居中
        }

        opts.getImage = (chartId) => {
          // 将base64格式的图像数据转换为ArrayBuffer
          return base64DataURLToArrayBuffer(chartId)
        }

        opts.getSize = function (img, tagValue, tagName) {
          // 自定义图像大小
          return [70, 100]
        }

        // 加载模板文件并创建Docxtemplater实例
        const zip = new PizZip(content)
        const doc = new Docxtemplater().loadZip(zip)
        // 附加图像模块
        doc.attachModule(new ImageModule(opts))
        // 设置自定义的Angular表达式解析器
        doc.setOptions({ parser: angularParser })
        // 设置模板数据
        doc.setData(data)
        try {
          // 渲染文档
          doc.render()
        } catch (error) {
          console.log({ error })
          reject(error)
          return
        }

        // 生成Blob对象
        const out = doc.getZip().generate({
          type: 'blob',
          mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
        })
        resolve(out)
      }, 1000) // 模拟1秒延迟
    })
  })
}

/**
 * 获取DOCX文件的Blob对象并自动保存(支持图片处理)
 * 
 * 这个函数与getDocxToBlob类似,但在生成Blob对象后会直接保存文件。
 * 
 * @param {String} tempDocxPath - 模板文件的URL路径。
 * @param {Object} data - 用于填充模板的动态数据对象。
 * @param {String} fileName - 导出文件的名称,包括扩展名。
 * @returns {Promise} - 返回一个Promise对象,成功时解析为生成的Blob对象。
 */
export const getDocxToBlobTwo = (tempDocxPath, data, fileName) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      var ImageModule = require('docxtemplater-image-module-free')
      var expressions = require('angular-expressions')
      var assign = require('lodash/assign')
      var last = require('lodash/last')

      expressions.filters.lower = function (input) {
        if (!input) return input
        return input.toLowerCase()
      }

      function angularParser(tag) {
        tag = tag
          .replace(/^\.$/, 'this')
          .replace(/(’|‘)/g, "'")
          .replace(/(“|”)/g, '"')
        const expr = expressions.compile(tag)
        return {
          get: function (scope, context) {
            let obj = {}
            const index = last(context.scopePathItem)
            const scopeList = context.scopeList
            const num = context.num
            for (let i = 0, len = num + 1; i < len; i++) {
              obj = assign(obj, scopeList[i])
            }
            obj = assign(obj, { $index: index })
            return expr(scope, obj)
          }
        }
      }

      JSZipUtils.getBinaryContent(tempDocxPath, (error, content) => {
        if (error) {
          reject(error)
          return
        }

        expressions.filters.size = function (input, width, height) {
          return {
            data: input,
            size: [width, height],
          }
        }

        let opts = {
          centered: true
        }

        opts.getImage = (chartId) => {
          return base64DataURLToArrayBuffer(chartId)
        }

        opts.getSize = function (img, tagValue, tagName) {
          return [70, 100]
        }

        const zip = new PizZip(content)
        const doc = new Docxtemplater().loadZip(zip)
        doc.attachModule(new ImageModule(opts))
        doc.setOptions({ parser: angularParser })
        doc.setData(data)
        try {
          doc.render()
        } catch (error) {
          console.log({ error })
          reject(error)
          return
        }

        const out = doc.getZip().generate({
          type: 'blob',
          mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
        })
        saveAs(out, fileName) // 保存文件
        resolve(out)
      }, 1000) // 模拟1秒延迟
    })
  })
}

/**
 * 将图片的URL路径转换为base64格式的数据
 * 
 * 该函数创建一个图像对象,将其绘制到画布上,然后将图像转换为base64格式的数据URL。
 * 
 * @param {String} imgUrl - 图片的URL路径。
 * @returns {Promise} - 返回一个Promise对象,成功时解析为base64格式的图片数据。
 */
export function getBase64Sync(imgUrl) {
  return new Promise((resolve, reject) => {
    let image = new Image()
    image.src = imgUrl
    image.setAttribute("crossOrigin", '*') // 处理跨域图片
    image.onload = function () {
      let canvas = document.createElement("canvas")
      canvas.width = image.width
      canvas.height = image.height
      let context = canvas.getContext("2d")
      context.drawImage(image, 0, 0, image.width, image.height)
      let ext = image.src.substring(image.src.lastIndexOf(".") + 1).toLowerCase()
      let quality = 0.8
      let dataurl = canvas.toDataURL("image/" + ext, quality)
      resolve(dataurl)
    }
  })
}

/**
 * 将base64格式的数据转换为ArrayBuffer
 * 
 * 该函数用于将base64编码的图像数据转换为ArrayBuffer,以便在DOCX文件中插入图像。
 * 
 * @param {String} dataURL - base64格式的图像数据。
 * @returns {ArrayBuffer} - 返回转换后的ArrayBuffer对象。
 */
function base64DataURLToArrayBuffer(dataURL) {
  const base64Regex = /^data:image\/(png|jpg|jpeg|svg|svg\+xml);base64,/
  if (!base64Regex.test(dataURL)) {
    return false
  }
  const stringBase64 = dataURL.replace(base64Regex, "")
  let binaryString
  if (typeof window !== "undefined") {
    binaryString = window.atob(stringBase64) // 在浏览器环境中解码base64
  } else {
    binaryString = Buffer.from(stringBase64, "base64").toString("binary") // 在Node.js环境中解码base64
  }
  const len = binaryString.length
  const bytes = new Uint8Array(len)
  for (let i = 0; i < len; i++) {
    bytes[i] = binaryString.charCodeAt(i)
  }
  return bytes.buffer
}