vue3实现前端生成word并下载

1. 前情

在后台管理平台开发过程中,有这样一个需求:从后端获取到数据后,前端生成指定模板内容的word文件并下载到本地。当导出的文件过多时,为了减少响应时间,也加上了进行压缩下载部分。使用过程中可根据实际需求作出调整。

2. 准备

wqqqq

依赖

js 复制代码
npm install docxtemplater
 
npm install jszip
 
npm install pizzip
 
npm install file-saver
 
npm install jszip-utils
 
npm install angular-expressions

模板文件

需要按导出样式准备一个模板文件,放到 public 目录下。模板中占位符包含以下部分:

  • 单一变量:{变量名}。直接显示改变量的值。
  • 遍历:{#变量名}内容{/变量名}。会对该数据进行遍历展示
  • 显示/隐藏:{#变量名}内容{/变量名}。其中为true的时候显示,false则不显示。
  • 图片:{%变量名}。变量值为base64格式。
  • if-else:{#变量名}A{/变量名}{^变量名}B{/变量名}。其中值为true的时候显示"A",为false显示"B";
  • 复选框:{#变量名}选中{/变量名}{^变量名}非选中{/变量名} 以上部分是我在实际需求中使用到的,其他使用可参考:docxtemplater.com/docs/
js 复制代码
let obj={
    name:'张三',
    age:12,
    hobby:['basketball','swimming'],
    url:'xxxxxxxxxxxxxx.png',
    isPic:false
}

这里定一个名为 obj 的对象,接下来按照下面的模板生成:

3. 实现

docFiles.js

js 复制代码
import { imageHandle } from '../common/image'
import { saveAs } from 'file-saver';
import JsZip from 'jszip'
import PizZip from "pizzip";
import docxtemplater from "docxtemplater";
import JSZipUtils from "jszip-utils";
import expressions from "angular-expressions";
 
let promises: any[] = [];

下载单个文件

js 复制代码
 
/**导出单个word文件
 * @param {object} opts 配置项
 */
export const exportDocx = (opts) => {
  let {
    //模板文件路径(必填)
    tempDocxpath,
    //模板文件数据(必填)
    data,
    //导出文件名
    fileName,
    //是否包含图片
    imageable,
    //导出成功后的回调
    onSuccess,
    //导出失败后的回调
    onError,
  } = Object.assign({
    imageable: false,
    fileName:'新建文件1',
  }, opts)
 
  const promise = new Promise((resolver, reject) => {
    JSZipUtils.getBinaryContent(tempDocxpath, (error, content) => {
      if (error) {
        throw error;
      }
      expressions.filters.size = function (input, width, height) {
        return {
          data: input,
          size: [width, height],
        };
      }
 
      //创建一个PiZip示例,内容为模板的内容
      const zip = new PizZip(content);
 
      //创建并加载docxtemplater实例对象
      let doc = new docxtemplater();
 
      if (imageable) {
        let opts = imageHandle()
        doc.attachModule(new ImageModule(opts));
      }
 
      doc.loadZip(zip);
      doc.setData(data);
 
      try {
        //用模板变量的值替换所有模板变量
        doc.render();
      } catch (error) {
        const e = {
          "message": error.message,
          "name": error.name,
          "stack": error.stack,
          "properties": error.properties
        };
        console.log({ error: e });
        throw error;
      }
 
      //生成一个代表docxtemplater对象的zip文件(在内存中表示)
      const out = doc.getZip().generate({
        type: "blob",
        mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      });
      resolver(out);
    })
  })
 
  Promise.all([promise]).then((out) => {
    if(onSuccess) onSuccess()
    saveAs(out, fileName);
  }).catch(() => {
    if(onError) onError()
  })
}

压缩包形式批量下载

js 复制代码
/**多级目录的形式导出文件
 * @param {object} opts 配置项
 */
export const exportFile_MultiLevelDirectory = (opts) => {
  let {
    //模板文件路径
    tempDocxpath,
    //模板文件数据
    data,
    //一级目录名
    zipName,
    //下级目录对应数据路径
    path,
    //下级目录名称类型
    subFolderNameType,
    //文件名称类型
    fileNameType,
    //是否包含图片
    imageable,
    //导出成功后的回调
    onSuccess,
    //导出失败后的回调
    onError,
  } = Object.assign({
    imageable: false,
    zipName:'新建文件1'
  }, opts)
 
  const zips = new JsZip();
  //创建一级目录的压缩包
  const folder = zips.folder(zipName);
 
  let creatOpts = {
    tempDocxpath,
    data,
    path,
    fileNameType,
    subFolderNameType,
    imageable,
    folder,
    zips,
  }
  create_Directory(creatOpts)
  Promise.all(promises).then(() => {
    zips.generateAsync({ type: "blob" }).then(content => {
      //生成二进制流
      if(onSuccess) onSuccess()
      saveAs(content, zipName);
    }).catch(() => {
      if(onError) onError()
    })
  })
}
 
/**创建下级目录
 * @param {object} opts 配置项
 */
function create_Directory(opts) {
  let { 
     tempDocxpath,
     data,
     path,
     folder,
     subFolderNameType,
     fileNameType,
     imageable,
     zips
  } = opts
 
  //遍历数据,依次形成下级目录/文件
  data.forEach((item, index) => {
    if (item[path] || item[path]?.length) {
     item[path].forEach((item2, index2) => {
        //创建下级目录
        let subFolderName = item2.folderName
        let subFolder = folder.folder(subFolderName);
 
        let creatOpts = {
          tempDocxpath,
          data: item[path],
          fileNameType,
          subFolderNameType,
          imageable,
          zips,
          folder: subFolder
        }
        create_Directory(creatOpts)
      })
    }else {
      const fileName = item.fileName
      const promise = new Promise((resolver, reject) => {
        JSZipUtils.getBinaryContent(tempDocxpath, (error, content) => {
          if (error) {
            throw error;
          }
          expressions.filters.size = function (input, width, height) {
            return {
              data: input,
              size: [width, height],
            };
          };
          const zip = new PizZip(content);
          let doc = new docxtemplater();
          if (imageable) {
            let opts = imageHandle()
            doc.attachModule(new ImageModule(opts));
          }
          doc.loadZip(zip);
 
          doc.setData(item);
          try {
            doc.render();
          } catch (error) {
            const e = {
              "message": error.message,
              "name": error.name,
              "stack": error.stack,
              "properties": error.properties
            };
            console.log({ error: e });
            throw error;
          }
          const out = doc.getZip().generate({
            type: "blob",
            mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
          });
          folder.file(fileName, out, { binary: true });
          resolver('success');
        });
      })
      promises.push(promise);
    }
  }
}

注意:导出多个word文件时需注意文档命名问题,避免因名称重复产生的文件覆盖问题。

image.js

js 复制代码
/**
 * 图片处理选项配置生成函数。
 * @returns {Object} 返回一个包含图片处理选项的对象。
 */
export const imageHandle = () => {
  // 图片处理
  let opts = {}
 
  opts = {
    //图像是否居中
    centered: false
  };
  opts.getImage = (chartId) => {
    // 将base64的数据转为ArrayBuffer
    return base64DataURLToArrayBuffer(chartId);
  }
  opts.getSize = function (img, tagValue, tagName) {
    //自定义指定图像大小
    return [500, 400];
  }
  return opts
}
 
/**
 * 将base64格式的数据转为ArrayBuffer
 * @param {Object} dataURL base64格式的数据
 */
export 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;
}

应用页面文件

注释部分为批量导出

js 复制代码
<template>
    <div>
        <n-button type="primary" @click="downWord">导出word</el-button>
    </div>
</template>
<script setup>
    let obj={
        name:'张三',
        images:[]
    }
    
    /*let objList=[
        {
            folderName:'济南市',
            children:[
                {
                    fileName:'张村申请合同',
                    name:'张村',
                    count:72
                },
                {
                    fileName:'大王村申请合同',
                    name:'大王村',
                    count:56
                },
            ]
        },
        {
            folderName:'德州市',
            children:[
                {
                    fileName:'高家村申请合同',
                    name:'高家村',
                    count:10
                },
            ]
        },
        {
            fileName:'居户里村申请合同',
            name:'居户里村',
            count:74
        },
    ]*/
 
    function downWord(){
        let opts={
            tempDocxpath:'/moBan.docx',
            data:obj,
            fileName:'申请表'
        }
        exportDocx(opts)
        /*let opts={
            tempDocxpath:'/moBan.docx',
            data:objList,
            zipName:'合同申请'
        }*/
        //exportFile_MultiLevelDirectory(opts)
    }
</script>

4. 问题

(1)End of data reached (data length = 0, asked index = 4). Corrupted zip ?

原因:模板文件为空文件;

排查:检查模板文件引入是否正确

(2)导出的文件中图片显示undefined

原因:图片路径转换成base64后的格式不对

(3)文件数量不对

排查:是不是文件名重复覆盖导致数量不对

相关推荐
奶糖 肥晨25 分钟前
JS自动检测用户国家并显示电话前缀教程|vue uniapp react可用
javascript·vue.js·uni-app
Dr_哈哈36 分钟前
Node.js fs 与 path 完全指南
前端
啊花是条龙41 分钟前
《产品经理说“Tool 分组要一条会渐变的彩虹轴,还要能 zoom!”——我 3 步把它拆成 1024 个像素》
前端·javascript·echarts
C_心欲无痕43 分钟前
css - 使用@media print:打印完美网页
前端·css
青茶3601 小时前
【js教程】如何用jq的js方法获取url链接上的参数值?
开发语言·前端·javascript
脩衜者1 小时前
极其灵活且敏捷的WPF组态控件ConPipe 2026
前端·物联网·ui·wpf
Mike_jia1 小时前
Dockge:轻量开源的 Docker 编排革命,让容器管理回归优雅
前端
GISer_Jing1 小时前
前端GEO优化:AI时代的SEO新战场
前端·人工智能
没想好d1 小时前
通用管理后台组件库-4-消息组件开发
前端
文艺理科生1 小时前
Google A2UI 解读:当 AI 不再只是陪聊,而是开始画界面
前端·vue.js·人工智能