Vue3.0 实现 word 文档的预览和下载

4,111 阅读1分钟

需求

项目前端使用vue3.0 + Element Plus搭建,要求纯前端实现word文档的预览和下载功能。废话不多说,开干。

效果展示

先给大家录个屏,方便看效果:

word20230421_063000-min.gif

把下载下来的word文档打开后就是这样:

image.png

用到的插件:

  • docxtemplater
  • pizzip
  • file-saver
  • docxtemplater-image-module-free
  • angular-expressions:
  • lodash
  • docx-preview

实现思路:

创建模板文件,必须是docx的格式;

image.png

在vue3.0项目中把准备好的模板文件,放到项目的public文件夹下,等待调用;

image.png

开始构建环境,上代码;

基础模块

# @/utils/exportFile

// 引入基本模块
import Docxtemplater from "docxtemplater";
import PizZip from "pizzip";
import PizZipUtils from "pizzip/utils/index.js";
import { saveAs } from "file-saver";
// 图片模块
import ImageModule from "docxtemplater-image-module-free";
// 解析语法模块
import expressions from "angular-expressions";
import assign from "lodash/assign";
// 文档预览模块
import { renderAsync } from "docx-preview";

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 scopeList = context.scopeList;
      const num = context.num;
      for (let i = 0, len = num + 1; i < len; i++) {
        obj = assign(obj, scopeList[i]);
      }
      return expr(scope, obj);
    },
  };
}

// 加载文件
function loadFile(url, callback) {
  PizZipUtils.getBinaryContent(url, callback);
}

// 配置空值替换函数 作为配置参数可配置在setOptions中
function nullGetter(part, scopeManager) {
  if (!part.module) {
    return "-null-";
  }
  if (part.module === "rawxml") {
    return "";
  }
  return "--";
}

预览的实现思路

# @/utils/exportFile

/**
 * 预览word,支持图片
 * @param {Object} tempDocxPath 模板文件路径
 * @param {Object} wordData 导出数据
 * @param {Object} fileName 导出文件名
 * @param {Arrsy} imgSize 自定义图片尺寸
 */
export const getWordImage = (tempDocxPath, wordData, imgSize, file) => {
  // 本地word.docx文件需要放在public目录下
  loadFile(tempDocxPath, (error, content) => {
    if (error) {
      throw error;
    }

    // 图片配置
    const imageOpts = {
      getImage: function (tagValue, tagName) {
        return new Promise(function (resolve, reject) {
          PizZipUtils.getBinaryContent(tagValue, function (error, content) {
            if (error) {
              return reject(error);
            }
            return resolve(content);
          });
        });
      },
      getSize: function (img, tagValue, tagName) {
        const size = imgSize[tagName] ? imgSize[tagName] : [150, 150]
        return size;
      },
    };

    let imageModule = new ImageModule(imageOpts);

    const zip = new PizZip(content);

    // 实例化有两种方式 这里是链式
    const doc = new Docxtemplater()
      .loadZip(zip)
      .setOptions({
        // delimiters: { start: "[[", end: "]]" },
        paragraphLoop: true,
        linebreaks: true,
        nullGetter: nullGetter,
        parser: angularParser,
      })
      .attachModule(imageModule)
      .compile();

    doc.renderAsync(wordData).then(() => {
      const out = doc.getZip().generate({
        type: "blob",
        mimeType:
          "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      });
      renderAsync(out, file);
    });
  });
}

下载的实现思路

# @/utils/exportFile

/**
 * 导出word,不支持图片
 * @param {Object} tempDocxPath 模板文件路径
 * @param {Object} wordData 导出数据
 * @param {Object} fileName 导出文件名
 */
export const exportWord = (tempDocxPath, wordData, fileName) => {
  // 本地word.docx文件需要放在public目录下
  loadFile(tempDocxPath, (error, content) => {
    if (error) {
      throw error;
    }
    const zip = new PizZip(content);
    // 没有配置解析语法,深层次对象语法(obj.xx.xx)不可识别
    const doc = new Docxtemplater(zip, {
      paragraphLoop: true,
      linebreaks: true,
    });

    doc.render(wordData);

    const out = doc.getZip().generate({
      type: "blob",
      mimeType:
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    });
    // Output the document using Data-URI
    saveAs(out, `${fileName}.docx`);
  });
}

/**
 * 导出word,支持图片
 * @param {Object} tempDocxPath 模板文件路径
 * @param {Object} wordData 导出数据
 * @param {Object} fileName 导出文件名
 * @param {Arrsy} imgSize 自定义图片尺寸
 */
export const exportWordImage = (tempDocxPath, wordData, fileName, imgSize) => {
  // 本地word.docx文件需要放在public目录下
  loadFile(tempDocxPath, (error, content) => {
    if (error) {
      throw error;
    }

    // 图片配置
    const imageOpts = {
      getImage: function (tagValue, tagName) {
        return new Promise(function (resolve, reject) {
          PizZipUtils.getBinaryContent(tagValue, function (error, content) {
            if (error) {
              return reject(error);
            }
            return resolve(content);
          });
        });
      },
      getSize: function (img, tagValue, tagName) {
        const size = imgSize[tagName] ? imgSize[tagName] : [150, 150]
        return size;
      },
    };

    let imageModule = new ImageModule(imageOpts);

    const zip = new PizZip(content);

    // 实例化有两种方式 这里是链式
    const doc = new Docxtemplater()
      .loadZip(zip)
      .setOptions({
        // delimiters: { start: "[[", end: "]]" },
        paragraphLoop: true,
        linebreaks: true,
        nullGetter: nullGetter,
        parser: angularParser,
      })
      .attachModule(imageModule)
      .compile();

    doc.renderAsync(wordData).then(function () {
      const out = doc.getZip().generate({
        type: "blob",
        mimeType:
          "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      });
      saveAs(out, `${fileName}.docx`);
    });
  });
}

开始使用

# CreateWordDocx.vue

<template>
  <div style="height: 90%; background: #fff; padding: 24px">
    <div style="margin-bottom: 17px; text-align: left">
      <el-button type="primary" @click="downLoad"> 下载启动方案 </el-button>
      <el-button type="primary" @click="goPreview"> 预览启动方案 </el-button>
    </div>
    <el-divider />
    <div style="margin-top: 24px">
      <!--搜索区域-->
      <el-form :model="startSchemeTemplate" label-width="110px">
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="启动方案名称:">
              <el-input
                v-model="startSchemeTemplate.name"
                placeholder="请输入"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="预定启动时间:">
              <el-date-picker
                v-model="startSchemeTemplate.time"
                type="date"
                placeholder="请选择"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24" style="height: 280px">
          <el-col :span="12">
            <el-form-item label="启动范围:">
              <el-input
                v-model="startSchemeTemplate.scope"
                placeholder="请输入"
                type="textarea"
                :autosize="{ minRows: 13.5, maxRows: 14 }"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="调试项目:">
              <el-input
                v-model="startSchemeTemplate.projectAdjuster"
                placeholder="请输入"
                type="textarea"
                :autosize="{ minRows: 13.5, maxRows: 14 }"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24" style="height: 280px">
          <el-col :span="12">
            <el-form-item label="启动条件:">
              <el-input
                v-model="startSchemeTemplate.condition"
                placeholder="请输入"
                type="textarea"
                :autosize="{ minRows: 13.5, maxRows: 14 }"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="调试步骤:">
              <el-input
                v-model="startSchemeTemplate.stepAdjuster"
                placeholder="请输入"
                type="textarea"
                :autosize="{ minRows: 13.5, maxRows: 14 }"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </div>
  </div>
  <el-dialog
    v-model="dialogVisible"
    @opened="handleOpened"
    title="流程图"
    width="1200px"
    top="5vh"
  >
    <div class="docWrap">
      <div ref="file"></div>
    </div>
  </el-dialog>
</template>

<script>
import { exportWordImage, getWordImage } from "@/utils/exportFile";

export default {
  name: "CreateWordDocx",
  data() {
    return {
      dialogVisible: false,
      htmlTitle: "启动方案",
      imgSize: {
        imgPath: [150, 150],
        imgPath1: [550, 250],
      },
      startSchemeTemplate: {
        name: "XXXXXXX启动调度实施方案",
        time: "2023.4.20",
        scope: `1.XXXX所有一、二次设备 
2.XXXX主变、XXXX主变(XX管辖)`,
        projectAdjuster: `1.XXXX,XXXX主变冲击五次、核相。
2.XXXXX设备冲击一次,XXXXXXX二次定相。
3.XXXXXX,XXXX差动保护带负荷试验。(XX管辖)
4.XXXXXX备自投实跳试验。`,
        condition: `1.XXX启动范围内的所有一、二次设备施工结束,验收合格,监控信息与相应调控人员核对完备,设备可以带电,站内一次设备相位正确。
2.XXX待用XXXXXX、待用XXXXXX、待用XXXXXX、待用XXXXXX、待用XXXXXX、待用XXXXXX、待用XXXXXX、银标XXXXXX、银阳XXXXXX、银区XXXXXX、待用XXXXXX、待用XXXXXX、待用XXXXXX、XXXXXX开关保护按定值单整定并投入。
3.启动范围内所有设备均为冷备用状态。`,
        stepAdjuster: `1.XXXXXX冲击一次、定相。
2.XXXXXX一次设备冲击(见附图2)`,
        imgPath: "https://docxtemplater.com/puffin.png",
      },
    };
  },
  methods: {
    downLoad() {
      exportWordImage(
        "../template.docx",
        this.startSchemeTemplate,
        this.htmlTitle,
        this.imgSize
      );
    },
    goPreview() {
      this.dialogVisible = true;
    },
    handleOpened() {
      getWordImage(
        "../template.docx",
        this.startSchemeTemplate,
        this.imgSize,
        this.$refs.file
      );
    },
  },
};
</script>

<style scoped>
.btn {
  float: left;
  margin: 0 0 24px;
}
.docWrap {
  height: 700px;
  overflow: auto;
  clear: both;
}
</style>

参考文章

*   [wordDown](https://github.com/H-newborn/wordDown)
*   [vue中使用docx-preview插件预览word文档](https://zhuanlan.zhihu.com/p/437059185)

备注

源码已上传gitee,功能已集成至项目:VUE-ADMIN-MS,大家自行取用,记得给 Star 哦 !!!