一、效果
二、 需求
根据表格中的开票名称的金额,生成对应的单位对账函,然后发给各单位的负责人进行审核。由于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模板引擎