react结合Docxtemplater纯前端方式实现word文档导出

442 阅读2分钟

最近写了几个导出word的需求,于是总结一波。

'use strict';

import React, { useState } from 'react';
import Docxtemplater from 'docxtemplater';
import PizZip from 'pizzip';
import expressionParser from 'docxtemplater/expressions';
var ImageModule = require('docxtemplater-image-module-free');
import FileSaver from 'file-saver';
import { Button, message } from 'antd';
import * as echarts from 'echarts';

 function Export2Word  (props)  {
  const {
    btnDescribe = '生成报告',
    templateWordUrl,
    ratio = 1,
    OutPutName = '报告',
    obtainData,
  } = props;

  const [loading, setLoading] = useState(false);

  const generateReport = async () => {
    try {
      setLoading(true);
      const generateData=await obtainData();
      //处理数据
      let newGenerateData = await handleData(generateData);
      let tempContent = await loadTemplate(templateWordUrl);
      const zip = new PizZip(tempContent);
      let opts = {
        centered: false,
        fileType: 'docx',
        getImage(id) {
          return base64DataURLToArrayBuffer(id);
        },
        getSize(img, url, tagName) {
          return new Promise(function (resolve, reject) {
            const image = new Image();
            image.src = url;
            image.onload = function () {
              //图片缩放比例
              resolve([image['width'] * ratio, image['height'] * ratio]);
            };
            image.onerror = function (e) {
              alert('An error occured while loading ' + url);
              reject(e);
            };
          });
        },
      };
      let out = null;
      let ImageM = new ImageModule(opts);
      const doc = new Docxtemplater(zip, {
        modules: [ImageM],
        paragraphLoop: true,
        linebreaks: true,
        parser: expressionParser,
      });
      const result = await doc.renderAsync(newGenerateData);
      if (result?.errors?.length === 0) {
        out = doc.getZip().generate({
          type: 'blob',
          mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        });
        FileSaver.saveAs(out, `${OutPutName}.docx`);
        setLoading(false);
        message.success('生成报表成功!');
      } else {
        message.error('生成报表失败!');
        setLoading(false);
        throw new Error('生成报表失败!!!');
      }
      setLoading(false);
    } catch (error) {
      message.error('生成报表失败!');
      console.log('生成报表失败原因', error);
      setLoading(false);
    }
  };
  //处理数据,将图片数据处理成base64位字符串
  const handleData = async (generateData) => {
    let newGenerateData = {};
    for (let key in generateData) {
      if (Object.prototype.toString.call(generateData?.[key]) === '[object Object]') {
        if (generateData[key].type === 'img') {
          //值可以是数组或者字符串
          if (Object.prototype.toString.call(generateData?.[key]?.value) === '[object Array]') {
            if (generateData?.[key]?.value?.length) {
              let handledImgUrl = await Promise.all(generateData?.[key]?.value?.map(getBase64Sync));
              //需要将数组图片处理成对象,以便于word中遍历
              newGenerateData[key] = handledImgUrl?.map((item) => ({
                [generateData?.[key]?.['url']]: item,
              }));
              continue;
            } else {
              newGenerateData[key] = '';
              continue;
            }
          }
          newGenerateData[key] = await getBase64Sync(generateData[key]?.value);
          continue;
        }
        if (generateData[key].type === 'echarts') {
          const dom = document.createElement('div');
          let parent=document.getElementById('chart');
          parent.appendChild(dom);
          dom.style.width = (generateData[key].width ? `${generateData[key].width}px` : '300px');
          dom.style.height = (generateData[key].height ? `${generateData[key].height}px` : '300px');
          dom.style.visibility = 'hidden';
          // dom.style.visibility = 'visible';
          const chart = echarts.init(dom);
          chart.setOption({ ...generateData[key].option, animation: false });
          chart.resize();
          let chartBase64 = chart.getDataURL({
            type: generateData[key]['type']&&generateData[key]['type'] || 'png',
            pixelRatio: generateData[key]['ratio']&&generateData[key]['ratio'] || 1,
            backgroundColor: generateData[key]['color']&&generateData[key]['color'] || '#fff',
          });
          newGenerateData[key] = chartBase64
          document.getElementById('chart').removeChild(dom);
          continue;
        }
        newGenerateData[key] = generateData[key].value;
        continue;
      }
      newGenerateData[key] = generateData[key];
    }
    return newGenerateData;
  };
  return (
    <div id='chart'>
      <Button loading={loading} onClick={generateReport}>
        {btnDescribe}
      </Button>
    </div>
  );
};
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 = Buffer.from(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;
}
const getBase64Sync = (imgUrl) => {
  return new Promise(function (resolve, reject) {
    if (imgUrl) {
      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);
      };
    } else {
      resolve('');
    }
  });
};
async function loadTemplate(url) {
  const response = await fetch(url);
  const blob = await response.blob();
  const arrayBuffer = await blob.arrayBuffer();
  return arrayBuffer;
}
//generateData格式example
/**
 let generateData = {
    name: '张三',
    //图片需要写成对象形式,其type必须为img
    photo: { type: 'img', 
      value: 'https://www.baidu.com/img/bd_logo1.png' 
  },
    photo: {
      type: 'img',
      value: ['https://www.baidu.com/img/bd_logo1.png', 'https://www.baidu.com/img/bd_logo1.png'],
      url: 'phurl',//模板中遍历图片的key
    },
    echartsPhoto: {
      type: 'echarts',//必填
      option: {},//必填,就是echarts的option
      color: '',//导出echarts颜色,不必填,默认值#fff
      width: 300,//导出echarts宽度,不必填,默认值300
      height: 300,//导出echarts高度,不必填,默认值300
      type: 'png',//导出echarts图片格式,不必填,默认值png
      ratio: 1,//导出echarts图片缩放比例,不必填,默认值1
    }, 
  };
 */

  export default Export2Word

上面是导出组件的代码 下面代码是使用示例

   <Export2Word
                btnDescribe='确认'
                templateWordUrl='/assets/template/return_repair_report_example.docx'
                obtainData={obtainData}
              />

obtainData需要写成异步的形式,适用于获取word模板中的变量的方法。简单说就是说,如果是普通字段就直接整型或者字符串,如果是图片需要传递对象并标记其type为img,如果是需要生成echarts的图片,需要传递对象,并且标记其type为echarts。

word模板中编写说明: 1.对于简单类型 企业微信截图_1731305741144.png 2.单图 企业微信截图_1731305829481.png

3.多图,相当于你需要将这个组装成数组,每一项的url放在,你遍历的变量上。数据遍历也是如此。 企业微信截图_173130590473.png