vue3中html导出word文档实践

其实在调研之初是知道几个插件的,但大部分都不在维护了且导出效果不理想。
本文主要使用docxtemplater导出word文档并记录以下详细步骤。
实现效果:
原始页面是有一个列表和两个echarts图表


企业微信截图_17262755197124.png

导出的word效果:


企业微信截图_17262756411764.png

其中列表是可修改的,echarts是两张图片不可修改。
详细实现步骤
1.安装依赖

pnpm install  docxtemplater pizzip --save  
pnpm install  jszip-utils --save
pnpm install  jszip --save   
pnpm install  file-saver --save  
pnpm install  docxtemplater-image-module-free

docxtemplater:这个插件可以通过预先写好的word,excel等文件模板生成对应带数据的文件
pizzip:这个插件用来创建,读取或编辑.zip的文件(同步的,还有一个插件是jszip,异步的)
jszip-utils:与jszip/pizzip一起使用,jszip-utils 提供一个getBinaryContent(path, data)接口,path即是文件的 路径,支持AJAX get请求,data为读取的文件内容。
file-saver:适合在客户端生成文件的工具,它提供的接口saveAs(blob, "1.docx")将会使用到,方便我们保存 文件。
docxtemplater-image-module-free:导出图片的话需要这个插件

2.创建word模板文件
创建名为test的word模板,放至项目的public文件夹内:public/test.docx


企业微信截图_17262770268607.png
  1. docxtemplater 语法

{%img} 图片
{#list}{/list} 循环、if判断
{#list}{/list}{^list}{/list} if else
{str} 文字

这一步将说明如何在test.docx内编写模板


企业微信截图_1726277127573.png

列表部分需要自己先在word内插入一张表格,然后表头自己定义好。如果有什么特殊样式自己都可以在word内提前编辑好。
列表数据对应字段是logList,列表循环以{#logList}开始,又以{/logList}结尾。注意这里的所有字段都要与代码传入的一致。{%leftImage}和{%rightImage}就是那两张echarts图所在的位置。

4.代码实现部分

import JSZipUtils from "jszip-utils";
import docxtemplater from "docxtemplater";
import PizZip from "pizzip";
import saveAs from "file-saver"
import ImageModule from "docxtemplater-image-module-free"

由于需要拿到echarts相关dom所以需要定义ref并绑定

  <div class="h-400px flex">
        <LineChart
          id="item0"
          :options="chatOptions"
          height="400px"
          width="100%"
          ref="leftChartRef"
          class="bg-[var(--el-bg-color-overlay)]"/>
        <LineChart
          id="item1"
          :options="chatOptions2"
          height="400px"
          width="100%"
          ref="rightChartRef"
          class="bg-[var(--el-bg-color-overlay)]"/>
      </div>
const leftChartRef = ref(HTMLDivElement)
const leftImage = ref(null) //存储图片资源

const rightChartRef = ref(HTMLDivElement)
const rightImage = ref(null)

const logList = ref<LogPageVO[]>(); //接口获取的表格数据

导出按钮点击和调用的相关方法

//导出点击
const exportWordClick = ()=> {
  leftImage.value = leftChartRef.value.getChartsImage();
  rightImage.value = rightChartRef.value.getChartsImage();
  let docxData= {
    logList: logList.value //列表数据,类似于[{name:'系统管理员',ip:'114.0.57....'}]
  }
  downLoadDoc('public/test.docx',docxData,"下载测试模板")
}


const downLoadDoc = (demoUrl, docxData, fileName)=>{
  // 读取并获得模板文件的二进制内容
  JSZipUtils.getBinaryContent(
    demoUrl,
    function (error, content) {
      // 抛出异常
      if (error) {
        throw error;
      }

      // 图片处理
      let opts = { centered: false }
      opts.getImage = chartId => {
        return base64DataURLToArrayBuffer(chartId)
      }
      opts.getSize =  (img, tagValue, tagName)=> {
        //自定义指定图像大小,此处可动态调试各别图片的大小
        return [500, 300]  //例子:宽500px  高度300px
      }

      // 创建一个PizZip实例,内容为模板的内容
      let zip = new PizZip(content);
      // 创建并加载docxtemplater实例对象
      let doc = new docxtemplater().loadZip(zip).attachModule(new ImageModule(opts));
      // 去除未定义值所显示的undefined
      doc.setOptions({
        nullGetter: function () {
          return "";
        }
      });
      // 设置模板变量的值,对象的键需要和模板上的变量名一致,值就是你要放在模板上的值
      doc.setData({
        ...docxData,
        leftImage: leftImage.value,
        rightImage: rightImage.value
      });

      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);
    }
  );
}

const base64DataURLToArrayBuffer = (dataURL)=> {
  const base64Regex = /^data:image\/(png|jpg|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
}

这里额外需要说的一点是导出点击方法内

  leftImage.value = leftChartRef.value.getChartsImage();
  rightImage.value = rightChartRef.value.getChartsImage();

这也是我们为什么定义ref的原因,因为需要将echarts图片转为base64格式。
echarts子组件内

//将echarts图片转为base64格式
const getChartsImage = ()=>{
  return chart.value.getDataURL({
    pixelRatio: 2, // 导出的图片分辨率比例,默认为 1。
    backgroundColor: 'transparent' // 导出的图片背景色,默认使用 option 里的 backgroundColor
  })
}

/**
 * 当使用 <script setup> 写法会导致父组件无法访问到子组件中的属性和方法。
 * 使用 <script setup> 的组件,想要让父组件访问到它的属性和方法需要借助与defineExpose来指定需要暴露给父组件的属性。
 * */
defineExpose({
  getChartsImage
})

5.结语
至此,我们在vue3中使用docxtemplater导出word文档的实践就告一段落了。文章从介绍docxtemplater基本使用语法,再到完整的代码示例实现表格和echarts图表的导出功能。如果有什么想法也可以继续留言交流,感谢!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容