vue + docxtemplater 导出 word 文档

一、痛点

word 导出 这种功能其实之前都是后端实现的,但最近有个项目没得后端。所以研究下前端导出。

ps: 前端还可以导出 pdf,但是其分页问题需要话精力去计算才可能实现,并且都不是很完善。可参考之前的文章:利用 html2canvas 和 jspdf 导出 echarts ( html页面 )为pdf

二、依赖安装

bash 复制代码
// 实现word下载的主要三方库
npm install docxtemplater pizzip  --save

// 文件操作;大佬们可以不需要,自己用fs、path等模块实现
npm install jszip jszip-utils --save 

// 文件存储
npm install file-saver --save

// 图片处理模块,没有图片需求可以不装
npm install docxtemplater-image-module-free  --save

三、创建导出word的公用方法 exportWord.js

ps:这个方法大同小异,网上很多

javascript 复制代码
import PizZip from 'pizzip'
import Docxtemplater from 'docxtemplater'
import JSZipUtils from 'jszip-utils'
import { saveAs } from 'file-saver'

// 将图片地址转为base64,导出word图片只能是base64
export function getBase64Sync(imgUrl) {
  return new Promise(function (resolve, reject) {
    // 一定要设置为let,不然图片不显示
    let image = new Image();
    // 解决跨域问题
    image.crossOrigin = 'anonymous';
    //图片地址
    image.src = imgUrl;
    // image.onload为异步加载
    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;
      //转成base64
      let dataurl = canvas.toDataURL('image/' + ext, quality);
      //返回
      resolve(dataurl);
    };
  });
}
/**
 * 将base64格式的数据转为ArrayBuffer
 * @param {Object} dataURL base64格式的数据
 */
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);
  } else {
    binaryString = new Buffer(stringBase64, 'base64').toString('binary');
  }
  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;
}

/**
 * 导出word,支持图片
 * @param {Object} tempDocxPath 模板文件路径
 * @param {Object} wordData 导出数据
 * @param {Object} fileName 导出文件名
 * @param {Object} imgSize 预留,自定义图片尺寸 => 暂没使用
 */
export const exportWord = (tempDocxPath, wordData, fileName, imgSize) => {
  // 这里要引入处理图片的插件
  var ImageModule = require('docxtemplater-image-module-free');


  JSZipUtils.getBinaryContent(tempDocxPath, function (error, content) {
    if (error) {
      throw error;
    }

    // 图片处理
    let opts = {};
    opts = {
      centered: true, //图像是否居中,true:在word中图片居中
      getImage: (chartId) => { 
        // 将base64转成ArrayBuffer
        return base64DataURLToArrayBuffer(chartId);
      },
      //自定义指定图像大小,此处可动态调试各别图片的大小
      getSize: (img, tagValue, tagName) => {
        // tagName 是指我们自己定义图片使用的字段名,如path、url等
        // if (tagName === 'imgurl') return [700, 350]; //设置图片宽高,tagName :传入的变量
        // return [200, 150]; 
        if (Object.prototype.hasOwnProperty.call(imgSize, tagName)) {
          return imgSize[tagName];
        } else {
          return [150, 150];
        }
      }
    };
    // 创建一个PizZip实例,内容为模板的内容
    let zip = new PizZip(content);
    // 创建并加载docxtemplater实例对象
    let doc = new Docxtemplater();
    doc.attachModule(new ImageModule(opts));
    doc.loadZip(zip);
    // 设置模板变量的值
    doc.setData(wordData);
    try {
      // 用模板变量的值替换所有模板变量
      doc.render();
    } catch (error) {
      // 抛出异常
      let e = {
        message: error.message,
        name: error.name,
        stack: error.stack,
        properties: error.properties
      };
      console.log(JSON.stringify({ error: e }));
      throw error;
    }
    // 生成一个代表docxtemplater对象的zip文件(不是一个真实的文件,而是在内存中的表示)
    let out = doc.getZip().generate({
      type: 'blob',
      mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
    });
    // 将目标文件对象保存为目标类型的文件,并命名
    saveAs(out, fileName);
  });
}

四、组件中调用

前几章都是基础,调用才是重点。

1. 创建模板

导出word其实就是解析我们提供的模板,然后将对应字段填入,最新进行导出即可。所以,模板 至关重要

  • 创建.docx文件
    该文件只能直接创建为docx 或者 另存为docx ;不能直接修改后缀名。
  • vue2 将模板放在static文件下;vue3 将模板放在public文件下
2. 模板语法
  • 语法用 { } 即可
  • 普通字段直接填入字段名即可
  • 如果字段是图片地址,需要加上 %,例如:
javascript 复制代码
{#imgList} 
    {%pathUrl}
{/imgList}

踩坑:图片这里我一直报错'%imgUrl',最后发现必须要换行写,而其他数组可以在一行写。

  • 遍历列表 以 {#list} 开头 ... 列表元素字段名 ... {/list} 结尾

    list: [
    {name:'张三', age:'18'},
    {name:'李四', age:'28'}
    ]

模板:

导出实际结果:

3. 组件调用
javascript 复制代码
<template>
  <div>
    <!-- 页面只有一个echarts 和 导出按钮 -->
    <div id="myChart6" :style="{ width: '800px', height: '800px' }"></div>
    <el-button type="primary" @click="exprodWord">导出word</el-button>
  </div>
</template>

<script>
  import * as echarts from 'echarts';
  import { onMounted } from 'vue'
  import { getBase64Sync, exportWord } from './exportFile'
  export default {
    name: 'WordTemplate',
    setup () {
      let myChartDom = null;

      const wordData = {
        title: '环境工业风险审核报告',
        des: '对于需要判断显示的要用{#isProblem}开始,{/isProblem}结束,isProblem的类型是Boolean,true的时候是显示。如下图,isFull==true的时候,才显示下面这句话',
        userList: [
          {
            indexNo: 1,
            name: '张三',
            age: '18',
            address: '上海',
            imgList: [
              {
                url: 'https://i.postimg.cc/qqcRNJ1y/b3c2e029c5deda297e29680e26a5c48c.jpg'
              },
              {
                url: 'https://i.postimg.cc/9Q5b3J7k/797c9c2bbf47b1ad4632670e508e0d5d.jpg'
              }
            ],
            status: 1,
          },
          {
            indexNo: 2,
            name: '李四',
            age: '28',
            address: '四川',
            imgList: [],
            status: 1,
          },
          {
            indexNo: 3,
            name: '王五',
            age: '38',
            address: '北京',
            imgList: [],
            status: 0,
          },
          {
            indexNo: 4,
            name: '张柳',
            age: '48',
            address: '成都',
            imgList: [],
            status: 0,
          }
        ]

      }
      const exprodWord = async () => { 
        const chartPath = getChartImg(); // 获取到echarts的图片地址
        const renderData = JSON.parse(JSON.stringify(wordData))
        renderData.chartPath = chartPath
        
        // 将图片转成base64是异步操作,需要等待图片base64返回,所以使用Promise.all
        renderData.userList = await Promise.all(
          renderData.userList.map(async item => {
            return {
              ...item,
              imgList: await Promise.all(
                item.imgList.map(async em => {
                  return {
                    ...em,
                    path: await getBase64Sync(em.url)
                  }
                })
              ) 
            };
          })
        )
        
        let imgSize = {
          imgurl: [200, 200], // 定义图片字段名为 'imgurl' 的尺寸, 该实例中没有图片字段名是imgurl,所以不生效
          chartPath: [1000, 800] // 定义图片字段名为 'chartPath' 的尺寸, 即该实例中的echarts图片
          // ... 更多
        }
        console.log('------------renderData', renderData)
        exportWord('template.docx', renderData, '环境工业风险审核报告.docx', imgSize)
      }
      
      // 基于准备好的dom,初始化echarts实例
      const initChart = () => {
        myChartDom = echarts.init(document.getElementById('myChart6'));
        // 绘制图表
        myChartDom.setOption({
          title: {
            text: 'ECharts 入门示例'
          },
          tooltip: {},
          xAxis: {
            data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
          },
          yAxis: {},
          series: [
            {
              name: '销量',
              type: 'bar',
              data: [5, 20, 36, 10, 10, 20]
            }
          ]
        });
      }
      // 获取图表base64图
      const getChartImg = () => {
        return myChartDom.getDataURL({
          pixelRatio: 2, // 解决模糊
          backgroundColor: '#fff'
        });
      }

      onMounted(() => {
        initChart()
      })
      return {
        exprodWord
      }
    }
  }
</script>

<style lang='scss' scoped>
</style>

注意:导出操作可能涉及异步操作,请多使用 Promise.all、nextTick等异步方法,尽量少使用setTimeout

五、导出word 结果

文章仅为本人学习过程的一个记录,仅供参考,如有问题,欢迎指出!

相关推荐
@解忧杂货铺5 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
苹果酱05677 小时前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
frankz618 小时前
word无法创建工作文件,检查临时环境变量。
word
web1309332039810 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
supermapsupport12 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
m0_7482548812 小时前
vue+elementui实现下拉表格多选+搜索+分页+回显+全选2.0
前端·vue.js·elementui
Hello_WOAIAI14 小时前
批量将 Word 文件转换为 HTML:Python 实现指南
python·html·word
苹果醋314 小时前
Golang的文件加密工具
运维·vue.js·spring boot·nginx·课程设计
关你西红柿子14 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
济南小草根14 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js