需求
项目前端使用vue3.0 + Element Plus搭建,要求纯前端实现word文档的预览和下载功能。废话不多说,开干。
效果展示
先给大家录个屏,方便看效果:
把下载下来的word文档打开后就是这样:
用到的插件:
- docxtemplater
- pizzip
- file-saver
- docxtemplater-image-module-free
- angular-expressions:
- lodash
- docx-preview
实现思路:
创建模板文件,必须是docx的格式;
在vue3.0项目中把准备好的模板文件,放到项目的public文件夹下,等待调用;
开始构建环境,上代码;
基础模块
# @/utils/exportFile
// 引入基本模块
import Docxtemplater from "docxtemplater";
import PizZip from "pizzip";
import PizZipUtils from "pizzip/utils/index.js";
import { saveAs } from "file-saver";
// 图片模块
import ImageModule from "docxtemplater-image-module-free";
// 解析语法模块
import expressions from "angular-expressions";
import assign from "lodash/assign";
// 文档预览模块
import { renderAsync } from "docx-preview";
expressions.filters.lower = function (input) {
if (!input) return input;
return input.toLowerCase();
};
function angularParser(tag) {
tag = tag
.replace(/^\.$/, "this")
.replace(/('|')/g, "'")
.replace(/("|")/g, '"');
const expr = expressions.compile(tag);
return {
get: function (scope, context) {
let obj = {};
const scopeList = context.scopeList;
const num = context.num;
for (let i = 0, len = num + 1; i < len; i++) {
obj = assign(obj, scopeList[i]);
}
return expr(scope, obj);
},
};
}
// 加载文件
function loadFile(url, callback) {
PizZipUtils.getBinaryContent(url, callback);
}
// 配置空值替换函数 作为配置参数可配置在setOptions中
function nullGetter(part, scopeManager) {
if (!part.module) {
return "-null-";
}
if (part.module === "rawxml") {
return "";
}
return "--";
}
预览的实现思路
# @/utils/exportFile
/**
* 预览word,支持图片
* @param {Object} tempDocxPath 模板文件路径
* @param {Object} wordData 导出数据
* @param {Object} fileName 导出文件名
* @param {Arrsy} imgSize 自定义图片尺寸
*/
export const getWordImage = (tempDocxPath, wordData, imgSize, file) => {
// 本地word.docx文件需要放在public目录下
loadFile(tempDocxPath, (error, content) => {
if (error) {
throw error;
}
// 图片配置
const imageOpts = {
getImage: function (tagValue, tagName) {
return new Promise(function (resolve, reject) {
PizZipUtils.getBinaryContent(tagValue, function (error, content) {
if (error) {
return reject(error);
}
return resolve(content);
});
});
},
getSize: function (img, tagValue, tagName) {
const size = imgSize[tagName] ? imgSize[tagName] : [150, 150]
return size;
},
};
let imageModule = new ImageModule(imageOpts);
const zip = new PizZip(content);
// 实例化有两种方式 这里是链式
const doc = new Docxtemplater()
.loadZip(zip)
.setOptions({
// delimiters: { start: "[[", end: "]]" },
paragraphLoop: true,
linebreaks: true,
nullGetter: nullGetter,
parser: angularParser,
})
.attachModule(imageModule)
.compile();
doc.renderAsync(wordData).then(() => {
const out = doc.getZip().generate({
type: "blob",
mimeType:
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
});
renderAsync(out, file);
});
});
}
下载的实现思路
# @/utils/exportFile
/**
* 导出word,不支持图片
* @param {Object} tempDocxPath 模板文件路径
* @param {Object} wordData 导出数据
* @param {Object} fileName 导出文件名
*/
export const exportWord = (tempDocxPath, wordData, fileName) => {
// 本地word.docx文件需要放在public目录下
loadFile(tempDocxPath, (error, content) => {
if (error) {
throw error;
}
const zip = new PizZip(content);
// 没有配置解析语法,深层次对象语法(obj.xx.xx)不可识别
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
});
doc.render(wordData);
const out = doc.getZip().generate({
type: "blob",
mimeType:
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
});
// Output the document using Data-URI
saveAs(out, `${fileName}.docx`);
});
}
/**
* 导出word,支持图片
* @param {Object} tempDocxPath 模板文件路径
* @param {Object} wordData 导出数据
* @param {Object} fileName 导出文件名
* @param {Arrsy} imgSize 自定义图片尺寸
*/
export const exportWordImage = (tempDocxPath, wordData, fileName, imgSize) => {
// 本地word.docx文件需要放在public目录下
loadFile(tempDocxPath, (error, content) => {
if (error) {
throw error;
}
// 图片配置
const imageOpts = {
getImage: function (tagValue, tagName) {
return new Promise(function (resolve, reject) {
PizZipUtils.getBinaryContent(tagValue, function (error, content) {
if (error) {
return reject(error);
}
return resolve(content);
});
});
},
getSize: function (img, tagValue, tagName) {
const size = imgSize[tagName] ? imgSize[tagName] : [150, 150]
return size;
},
};
let imageModule = new ImageModule(imageOpts);
const zip = new PizZip(content);
// 实例化有两种方式 这里是链式
const doc = new Docxtemplater()
.loadZip(zip)
.setOptions({
// delimiters: { start: "[[", end: "]]" },
paragraphLoop: true,
linebreaks: true,
nullGetter: nullGetter,
parser: angularParser,
})
.attachModule(imageModule)
.compile();
doc.renderAsync(wordData).then(function () {
const out = doc.getZip().generate({
type: "blob",
mimeType:
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
});
saveAs(out, `${fileName}.docx`);
});
});
}
开始使用
# CreateWordDocx.vue
<template>
<div style="height: 90%; background: #fff; padding: 24px">
<div style="margin-bottom: 17px; text-align: left">
<el-button type="primary" @click="downLoad"> 下载启动方案 </el-button>
<el-button type="primary" @click="goPreview"> 预览启动方案 </el-button>
</div>
<el-divider />
<div style="margin-top: 24px">
<!--搜索区域-->
<el-form :model="startSchemeTemplate" label-width="110px">
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="启动方案名称:">
<el-input
v-model="startSchemeTemplate.name"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预定启动时间:">
<el-date-picker
v-model="startSchemeTemplate.time"
type="date"
placeholder="请选择"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24" style="height: 280px">
<el-col :span="12">
<el-form-item label="启动范围:">
<el-input
v-model="startSchemeTemplate.scope"
placeholder="请输入"
type="textarea"
:autosize="{ minRows: 13.5, maxRows: 14 }"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="调试项目:">
<el-input
v-model="startSchemeTemplate.projectAdjuster"
placeholder="请输入"
type="textarea"
:autosize="{ minRows: 13.5, maxRows: 14 }"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24" style="height: 280px">
<el-col :span="12">
<el-form-item label="启动条件:">
<el-input
v-model="startSchemeTemplate.condition"
placeholder="请输入"
type="textarea"
:autosize="{ minRows: 13.5, maxRows: 14 }"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="调试步骤:">
<el-input
v-model="startSchemeTemplate.stepAdjuster"
placeholder="请输入"
type="textarea"
:autosize="{ minRows: 13.5, maxRows: 14 }"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</div>
<el-dialog
v-model="dialogVisible"
@opened="handleOpened"
title="流程图"
width="1200px"
top="5vh"
>
<div class="docWrap">
<div ref="file"></div>
</div>
</el-dialog>
</template>
<script>
import { exportWordImage, getWordImage } from "@/utils/exportFile";
export default {
name: "CreateWordDocx",
data() {
return {
dialogVisible: false,
htmlTitle: "启动方案",
imgSize: {
imgPath: [150, 150],
imgPath1: [550, 250],
},
startSchemeTemplate: {
name: "XXXXXXX启动调度实施方案",
time: "2023.4.20",
scope: `1.XXXX所有一、二次设备
2.XXXX主变、XXXX主变(XX管辖)`,
projectAdjuster: `1.XXXX,XXXX主变冲击五次、核相。
2.XXXXX设备冲击一次,XXXXXXX二次定相。
3.XXXXXX,XXXX差动保护带负荷试验。(XX管辖)
4.XXXXXX备自投实跳试验。`,
condition: `1.XXX启动范围内的所有一、二次设备施工结束,验收合格,监控信息与相应调控人员核对完备,设备可以带电,站内一次设备相位正确。
2.XXX待用XXXXXX、待用XXXXXX、待用XXXXXX、待用XXXXXX、待用XXXXXX、待用XXXXXX、待用XXXXXX、银标XXXXXX、银阳XXXXXX、银区XXXXXX、待用XXXXXX、待用XXXXXX、待用XXXXXX、XXXXXX开关保护按定值单整定并投入。
3.启动范围内所有设备均为冷备用状态。`,
stepAdjuster: `1.XXXXXX冲击一次、定相。
2.XXXXXX一次设备冲击(见附图2)`,
imgPath: "https://docxtemplater.com/puffin.png",
},
};
},
methods: {
downLoad() {
exportWordImage(
"../template.docx",
this.startSchemeTemplate,
this.htmlTitle,
this.imgSize
);
},
goPreview() {
this.dialogVisible = true;
},
handleOpened() {
getWordImage(
"../template.docx",
this.startSchemeTemplate,
this.imgSize,
this.$refs.file
);
},
},
};
</script>
<style scoped>
.btn {
float: left;
margin: 0 0 24px;
}
.docWrap {
height: 700px;
overflow: auto;
clear: both;
}
</style>
参考文章
* [wordDown](https://github.com/H-newborn/wordDown)
* [vue中使用docx-preview插件预览word文档](https://zhuanlan.zhihu.com/p/437059185)
备注
源码已上传gitee,功能已集成至项目:VUE-ADMIN-MS,大家自行取用,记得给 Star 哦 !!!