vue 使用 docxtemplater 导出 word 文件
docxtemplater 导出 word
docxtemplater 是一种邮件合并工具,以编程方式使用并处理条件、循环,并且可以扩展以插入任何内容(表格、html、图像)
一、安装 docxtemplater
// 安装 docxtemplater
npm install docxtemplater pizzip --save
// 安装 jszip-utils
npm install jszip-utils --save
// 安装 FileSaver
npm install file-saver --save
// 引入处理图片的插件
npm install docxtemplater-image-module-free --save
//docx-preview预览插件
npm i docx-preview --save
二、docxtemplater 语法
1. 单一变量使用 {} 包含
{
name: 'lisi',
age: 18
}
在 word 模板文件中表示为:
{name}、{age}
2. json数组格式,则包裹一个循环对象:
list: [
{name:'lisi', class: '4-1', age:'18'},
{name:'wangwu', class: '4-2', age:'16'}
]
在 word 模板文件中表示为:
姓名 | 班级 | 年龄 |
---|---|---|
内容默认居左 | 内容居中 | 内容居右 |
{#list}{name} | {class} | {age}{/list} |
3. 使用图片地址:
{%imgUrl}
4. 如果多张图片地址时,与循环列表一样, 但必须换行:
{#imglist}
{%imgUrl}
{/imglist}
三、封装JS
新建 outputWord.js
文件
/**
* 导出word文档
*
// 安装 docxtemplater
npm install docxtemplater pizzip --save
// 安装 jszip-utils
npm install jszip-utils --save
// 安装 FileSaver
npm install file-saver --save
// 引入处理图片的插件
npm install docxtemplater-image-module-free --save
//docx-preview预览插件
npm i docx-preview --save
// 安装全部
npm install docxtemplater pizzip jszip-utils file-saver docxtemplater-image-module-free docx-preview --save
*/
import Docxtemplater from 'docxtemplater'
import ImageModule from 'docxtemplater-image-module-free'
import PizZip from 'pizzip'
import JSZipUtils from 'jszip-utils'
import { renderAsync } from 'docx-preview'
import { saveAs } from 'file-saver'
import * as echarts from 'echarts'
let imageMaxWidth = 640
let createdTime = ''
let imageSizeObjs = {}
/**
* 将base64格式的数据转为ArrayBuffer
* @param {Object} dataURL base64格式的数据
*/
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
}
/**
* 将图片的url路径转为base64路径
* 可以用await等待Promise的异步返回
* @param {Object} imgUrl 图片路径
*/
export const getBase64Sync = (imgUrl) => {
return new Promise((resolve, reject) => {
// 一定要设置为let,不然图片不显示
let image = new Image()
//图片地址
image.src = imgUrl
// 解决跨域问题
image.setAttribute("crossOrigin", '*') // 支持跨域图片
// image.onload为异步加载
image.onload = () => {
let canvas = document.createElement("canvas")
const {width, height} = image
canvas.width = width
canvas.height = height
let context = canvas.getContext("2d")
context.drawImage(image, 0, 0, width, height)
//图片后缀名
let ext = image.src.substring(image.src.lastIndexOf(".") + 1).toLowerCase()
//图片质量
let quality = 0.8
//转成base64
let dataurl = canvas.toDataURL("image/" + ext, quality)
// 将缩放后的尺寸信息保存
let key = fomratKey(dataurl)
imageSizeObjs[key] = imageScaleSize(width, height)
// 解除引用
image = null
//返回
resolve(dataurl)
};
})
}
// 截取base64后25位,加key,作为imageSizeObjs的key值
const fomratKey = (base64Data) => {
const last25 = base64Data.substring(base64Data.length - 25)
return `${last25}`
}
// 缩放图片尺寸
const imageScaleSize = (w, h) => {
if (w > imageMaxWidth) {
const scale = imageMaxWidth / w
w *= scale
h *= scale
return([Math.ceil(w), Math.ceil(h)])
} else {
return([w, h])
}
}
/**
* 将echart转为base64
* 可以用await等待Promise的异步返回
* @param {Object} imgUrl 图片路径
*/
export const chartToBase64Pic = (opt) => {
const {dom, bgColor = '#fff'} = opt || {}
return new Promise((resolve, reject) => {
if (!dom) {
console.warn('未传入echarts的dom')
resolve(null)
}
const echartDom = echarts.getInstanceByDom(dom)
const base64Str = echartDom.getDataURL({
type: 'png',
//导出的图片分辨率比例,默认为 1
pixelRatio: 1,
// 导出的图片背景色,默认使用 option 里的 backgroundColor
backgroundColor: bgColor
})
// 将缩放后的尺寸信息保存
let key = fomratKey(base64Str)
imageSizeObjs[key] = imageScaleSize(dom.offsetWidth, dom.offsetHeight)
resolve(base64Str)
})
}
// 获取年月日时分秒
const getDate = () => {
const now = new Date()
const year = now.getFullYear()
const month = (now.getMonth() + 1).toString().padStart(2, '0')
const day = now.getDate().toString().padStart(2, '0')
const hour = now.getHours().toString().padStart(2, '0')
const minute = now.getMinutes().toString().padStart(2, '0')
const second = now.getSeconds().toString().padStart(2, '0')
return [
`${year}-${month}-${day} ${hour}:${minute}:${second}`,
`${year}年${month}月${day}日${hour}时${minute}分${second}秒`,
`${year}-${month}-${day}`,
`${year}年${month}月${day}日`
]
}
/**
* 递归所有数据,并对图片处理,返回新的对象
* 避免污染原数据
*/
const deepCopyAndHandleBase64 = async (obj) => {
if (obj === null || typeof obj !== 'object') {
return obj
}
let copy = Array.isArray(obj)? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
const val = obj[key]
if (val instanceof Element) {
// 将 echart dom 的对象转成base64
copy[key] = await chartToBase64Pic({dom: val})
} else if (typeof(val) === 'string' && val.indexOf('http') !== -1) {
// 将http的图片转成base64
copy[key] = await getBase64Sync(val)
} else {
copy[key] = await deepCopyAndHandleBase64(obj[key])
}
}
}
return copy
}
/**
* 渲染文档
* @param {Object} outputWordData 必填,要导出的数据列表
* @param {Object} fileConfigInfo 选填,配置文件,内容包括
* @param {String} templateDocxPath 必填,模板路径
* @param {String} imgMaxWidth 选填,图片的最大宽度,超过按比例缩放
* @param {String} placeInfo 选填,页面的信息,会混入 outputWordData
*/
export const ExportBriefDataDocx = async (outputWordData, fileConfigInfo) => {
imageSizeObjs = {}
const {imgMaxWidth, placeInfo = {}, templateDocxPath} = fileConfigInfo || {}
// 如果有传入 imgMaxWidth
if (imgMaxWidth) imageMaxWidth = imgMaxWidth
let outData = await deepCopyAndHandleBase64(outputWordData)
outData = Object.assign(outData, placeInfo)
// 如果没有createdTime的对象,就创建当前时间
const [fullDate, fullDateCn, date, dateCn] = getDate()
if (!outData.createdTime) {
outData.createdTime = fullDate
createdTime = fullDate
} else {
createdTime = outData.createdTime
}
outData.fullDateCn = fullDateCn
outData.dateCn = dateCn
return new Promise((resolve) => {
//这里要引入处理图片的插件
JSZipUtils.getBinaryContent(templateDocxPath, (error, content) => {
if (error) {
console.log(error)
}
let imageOpts = {
//图像是否居中
centered: true,
//将base64的数据转为ArrayBuffer
getImage: (chartId) => {
return base64DataURLToArrayBuffer(chartId)
},
getSize: (img, tagValue, tagName) => {
const key = fomratKey(tagValue)
return imageSizeObjs[key] || []
}
}
// 创建一个JSZip实例,内容为模板的内容
const zip = new PizZip(content)
// 创建并加载 Docxtemplater 实例对象
// 设置模板变量的值
let doc = new Docxtemplater()
doc.attachModule(new ImageModule(imageOpts))
doc.loadZip(zip)
doc.setData(outData)
try {
// 呈现文档,会将内部所有变量替换成值,
doc.render()
} catch (error) {
const e = {
message: error.message,
name: error.name,
stack: error.stack,
properties: error.properties
}
console.log('err',{ error: e })
// 当使用json记录时,此处抛出错误信息
throw error
}
// 生成一个代表Docxtemplater对象的zip文件(不是一个真实的文件,而是在内存中的表示)
const out = doc.getZip().generate({
type: 'blob',
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
})
resolve(out)
})
})
}
export const ViewWordFile = (out, dom) => {
dom
? renderAsync(out, dom)
: console.warn('dom 未定义')
}
export const DownWordFile = (out, outFileName) => {
outFileName
? saveAs(out, `${outFileName}(${createdTime})`)
: console.warn('outFileName 未定义')
}
四、封装vue组件
新建 outputWord.vue
文件
- 例子使用 elementUI,可以删除相关代码
- 例子是支持 echart 图形导出,
需要引入 echart
/*
Component: outputWord
*/
<template>
<div class="outputWord" style="display:inline-block;">
<el-button :size="size" type="success" icon="el-icon-download" @click="exportWordFile" >
{{title}}
</el-button>
<el-dialog
fullscreen
append-to-body
title="生成智能报告"
:visible.sync="dialogVisible">
<div ref="dailyContent" style="height:calc(100vh - 155px); overflow: auto;"></div>
<span slot="footer" class="dialog-footer">
<el-button type="success" icon="el-icon-download" @click="download">导出</el-button>
<el-button @click="dialogVisible = false">关闭</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import {
ExportBriefDataDocx,
ViewWordFile,
DownWordFile
} from '@/utils/methods/outputWord/outputWord.js'
export default {
props: {
title: {
default: '导出预览'
},
templateName: {
default: ''
},
outputWordData: {
type: Object,
default: () => null
},
outFileName: {
type: String,
default: '未定义'
},
size: {
type: String,
default: ''
}
},
data () {
return {
docx: null,
dialogVisible: false,
fileConfig: {
agjlxtj: {
outFileName: "测试word文档",
templateDocxPath: "/测试word文档.docx",
imgMaxWidth: 750,
placeInfo: {
headerTitle: "信息科技有限公司",
headerSubTitle: "信息科技有限公司"
}
}
}
}
},
mounted () {
},
methods:{
async exportWordFile (){
const fileConfigInfo = this.fileConfig[this.templateName]
this.docx = null
this.dialogVisible = true
this.docx = await ExportBriefDataDocx(this.outputWordData, fileConfigInfo)
ViewWordFile(this.docx, this.$refs.dailyContent)
},
download () {
DownWordFile(this.docx, this.outFileName)
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
>>> .el-dialog .el-dialog__body
padding 0
</style>
五、调用方法
新建 index.vue
文件
/*
Component: index
*/
<template>
<div class="index">
<outputWord
title="生成报表"
outFileName="测试统计报表"
templateName="agjlxtj"
:outputWordData="outputWordData"
/>
<el-button style="margin-left: 10px;" @click="getDataList">生成数据</el-button>
<el-table :data="outputWordData.list" border style="width: 100%">
<el-table-column prop="name" label="名称" />
<el-table-column prop="value" label="数量" />
</el-table>
<div ref="pieChart" style="width: 500px; height: 500px;"></div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import outputWord from "@/utils/methods/outputWord/outputWord.vue"
export default {
name: "index",
components: {
outputWord
},
props: {},
data () {
return {
outputWordData: {
title: '测试word文档',
subtitle1: '统计列表',
subtitle2: '占比统计',
list: [],
imglist:[
{
imgUrl: "https://t7.baidu.com/it/u=4198287529,2774471735&fm=193&f=GIF"
},
{
imgUrl: "https://t7.baidu.com/it/u=2621658848,3952322712&fm=193&f=GIF"
}
],
echartBase64: ''
}
}
},
mounted() {
this.pieChartRef = echarts.init(this.$refs.pieChart)
// 将要导出图形的dom赋值
this.outputWordData.echartBase64 = this.$refs.pieChart
this.getDataList()
},
methods: {
random (limit = 1001) {
return Math.floor(Math.random() * limit)
},
getDataList () {
const length = this.random(10) + 5
const list = []
for(let i = 0; i < length; i++) {
const index = i + 1
list.push({
index, name: `person-${index}`, value: this.random()
})
}
this.outputWordData.list = list
this.drawPicChart(list)
},
drawPicChart (data) {
const option = {
tooltip: {
trigger: 'item'
},
legend: {
top: '5%',
left: 'center'
},
series: [
{
name: 'Access From',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
labelLine: {
show: false
},
data
}
]
}
this.pieChartRef.setOption(option)
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.index
padding 10px
</style>