一、效果

在这里插入图片描述

二、 需求

根据表格中的开票名称的金额,生成对应的单位对账函,然后发给各单位的负责人进行审核。由于word模板样式一致,故可用模板填充数据,进行企业对账函的一次性生成

三、前端vue技术

前端采用vue、avue、elementUI 技术

1、前端主要代码

<template>
  <div style="padding: 20px">
    <div style="display: flex">
      <div style="margin-right: 20px">
        <el-upload
          :show-file-list="false"
          action="action"
          :on-change="handleChange"
        >
          <el-button type="primary">上传销售金额Excel</el-button>
        </el-upload>
      </div>
      <div style="margin-right: 20px">
        <el-date-picker
          v-model="daterange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          format="yyyy年MM月dd日"
          value-format="yyyy年MM月dd日"
          end-placeholder="结束日期"
        >
        </el-date-picker>
      </div>

      <el-button type="primary" @click="createWordFiles">生成对账函</el-button>
    </div>

    <avue-crud :data="data" :option="option" ref="crud"></avue-crud>
  </div>
</template>
<script>
import { createWordFilesApi } from "../api/api";
export default {
  data() {
    return {
      data: [],
      daterange: [],
      title: "",
      option: {
        index: true,
        menu: false,
        addBtn: false,
        page: false,
        column: [
          {
            label: "单位",
            prop: "company",
          },
          {
            label: "金额",
            prop: "totalPrice",
          },
          {
            label: "开票名称",
            prop: "ticketName",
          },
        ],
      },
    };
  },
  methods: {
    handleChange(file, fileLis) {
      this.$export.xlsx(file.raw).then((data) => {
        console.log("data", data);
        this.title = data.header[0];
        let re = data.results;
        let result = [];
        for (let i = 1; i < re.length; i++) {
          let item = re[i];
          result.push({
            company: item[this.title],
            totalPrice: item.__EMPTY,
            ticketName: item.__EMPTY_1 ? item.__EMPTY_1 : "",
          });
        }
        this.data = result;
      });
    },
    createWordFiles() {
      let params = {
        title: this.title,
        data: JSON.stringify(this.data),
        startDate: this.daterange[0],
        endDate: this.daterange[1],
      };
      createWordFilesApi(params).then((res) => {
          console.log('res: ', res);
          let  data =res.data.data;
          let a = document.createElement('a');
          a.style.display='none';
          a.href="http://localhost/upload"+data.fileUrl;
          a.download = data.fileName;
          document.body.append(a);
          a.click();
          console.log("我单击了a标签")
          document.body.removeChild(a)


        // this.$message({
        //   type: "success",
        //   message: "生成成功!",
        // });
      }).catch((res)=>{
          console.log('fail-res: ', res);

      });
    },
  },
};
</script>
<style>
</style>

四、nodejs 技术

express +nodejs

1、生成文件并压缩文件夹

var fs = require("fs");
var util = require("../public/util");

const PizZip = require("pizzip");
const Docxtemplater = require("docxtemplater");
const path = require("path");

const archiver = require("archiver");

const compressing = require("compressing");

var fstream = require("fstream");
var zlib = require("zlib");
var tar = require("tar");

let info = {
  createWordFilesApi: async function (req, res) {
    let result = req.body;
    let floderName = result.title + "-" + util.getNowFormatDate();
    let floder = "./企业对账函/" + floderName;
    let path = floder + "/";
    //生成文件夹目录
    fs.mkdirSync(path);

    // 加载docx模板 注意:只能用docx后缀名,如果为doc后缀名,需手动另存为docx
    const content = fs.readFileSync("./word.docx", "binary");
    //生成docx文档
    let fileData = JSON.parse(result.data);
    for (let i = 0; i < fileData.length; i++) {
      let item = fileData[i];
      //注意:let zip = new PizZip(content); 需每次实例化,不然渲染出来所有的文件都为第一条数据
      let zip = new PizZip(content);
      let doc = new Docxtemplater(zip);
      //设置参数
      doc.setData({
        ticketName: item.ticketName ? item.ticketName : item.company,
        totalPrice: item.totalPrice,
        uppercaseTotalPrice: util.transCnMoney(item.totalPrice),
        startDate: result.startDate,
        endDate: result.endDate,
      });
      // 渲染
      doc.render();
      // 导出数据
      const buf = doc.getZip().generate({ type: "nodebuffer" });
      fs.writeFileSync(path + item.company + ".docx", buf);
    }
    let downLoadFile = floder + "-对账函.tgz";
    //压缩文件 因为需下载生成的压缩文件,故采用gzip压缩,试了其他两种压缩方式,均未能正常下载
    tar
      .c(
        {
          gzip: true,
          file: downLoadFile,
        },
        [floder]
      )
      .then(() => {
        let data = {};
        data.code = "200";
        data.data = {
          fileName: floderName + "-对账函.zip",
          fileUrl: "/" + downLoadFile.split("./")[1],
        };
        data.message = "生成文件夹成功";
        res.send(data);
      });

    // const output = fs.createWriteStream(downLoadFile);

    // // const archive = archiver("zip", {
    // //   zlib: { level: 9 }, // Sets the compression level.
    // // });

    // const archive = archiver("tar", {
    //     gzip:true,
    //     gzipOptions:{
    //         level: 9
    //     }
    // });

    // archive.pipe(output);

    // archive.directory(path, false);
    // archive.finalize();

    //await compressing.tgz.compressDir(floder, downLoadFile)

    //  let data = {};
    //     data.code = "200";
    //     data.data = {
    //         fileName: floderName + "-对账函.zip",
    //         fileUrl: "/" + downLoadFile.split("./")[1],
    //     };
    //     data.message = "生成文件夹成功";
    //     res.send(data);
  },
};

module.exports = info;

2、下载文件

const path = require('path')
let express = require('express')
let fs = require('fs')
var mime = require('mime');
let router = express.Router()

router.get('/*', (req,res) => {
	var filePath = "./"+req.params["0"];
  //  var filePath = "./企业对账函/2022年1月份销售-20220211083510-对账函.zip";
//     console.log('filePath: ', filePath);
// 	console.log('download');
//  var f=filePath;
//   //var f = req.params[0];
//   f = path.resolve(f);
//   console.log('Download file: %s', f);
//   res.download(f);

var file = fs.readFileSync(filePath, 'binary');
console.log('file: ', file.length);

    var file=filePath;
   var f = fs.createReadStream(file);

    var filename = path.basename(file);
    console.log('filename: ', filename);
    var mimetype = mime.getType(file);
    console.log('mimetype: ', mimetype);
  
    // res.setHeader('Content-Disposition', 'attachment; filename=' + filename);
    // res.setHeader('Content-Type', mimetype); //application/zip
   
    // var filestream = fs.createReadStream(file);
    // filestream.pipe(res)
    
    // res.writeHead(200, {
    //   'Content-Type': 'application/force-download',
    //   'Content-Disposition': 'attachment; filename=price_letter.zip'
    // });
      res.writeHead(200, {
      'Content-Type': "application/octet-stream",
      'Content-Disposition': 'attachment; filename=price_letter.gzip',
      //'Content-Length':352*1024,
      'Content-Encoding':'gzip ',
     //'Content-Encoding':'deflate',//gzip
      'Transfer-Encoding':'chunked'
    });
    f.pipe(res);
})

module.exports = router 


五、所遇问题

1、在这里插入图片描述
所遇问题的node版本为14.X.X,升级为16.X.X 解决了此问题

2、下载文件损坏
在这里插入图片描述
需了解:1、Content-Encoding值

gzip  表明实体采用GNU zip编码

compress 表明实体采用Unix的文件压缩程序

deflate  表明实体是用zlib的格式压缩的

identity  表明没有对实体进行编码。当没有Content-Encoding header时, 就默认为这种情况

gzip, compress, 以及deflate编码都是无损压缩算法,用于减少传输报文的大小,不会导致信息损失。 其中gzip通常效率最高, 使用最为广泛。
2、Content-Length

Content-Length用于描述HTTP消息实体的传输长度the transfer-length of the message-body。在HTTP协议中,消息实体长度和消息实体的传输长度是有区别,比如说gzip压缩下,消息实体长度是压缩前的长度,消息实体的传输长度是gzip压缩后的长度

3、Transfer-Encoding: chunked

注意:压缩时需要采用gzip压缩,才不会出现问题

Transfer-Encoding: chunked 表示输出的内容长度不能确定,普通的静态页面、图片之类的基本上都用不到这个。

但动态页面就有可能会用到,但我也注意到大部分asp,php,asp.net动态页面输出的时候大部分还是使用Content-Length,没有使用Transfer-Encoding: chunked。

不过如果结合:Content-Encoding: gzip 使用的时候,Transfer-Encoding: chunked还是比较有用的。

记得以前实现:Content-Encoding: gzip 输出时,先把整个压缩后的数据写到一个很大的字节数组里(如 ByteArrayOutputStream),然后得到数组大小 -> Content-Length。

如果结合Transfer-Encoding: chunked使用,就不必申请一个很大的字节数组了,可以一块一块的输出,更科学,占用资源更少。

这在http协议中也是个常见的字段,用于http传送过程的分块技术,原因是http服务器响应的报文长度经常是不可预测的,使用Content-length的实体搜捕并不是总是管用。

参考链接:nodejs提取excel中的信息填充到word文件,批量生成合同
node docx模板引擎

Nodejs做后端,实现文件压缩下载的几种方案(archiver、compressing、linux的zip命令)

NodeJS下载文件实例

transfer-encoding:chunked的含义

HTTP协议中Content-Length的详细解读

Logo

前往低代码交流专区

更多推荐