介绍
需求:在开发过程中,遇到了产品要求将展示的可视化报告直接导出到word格式的需求,导出的html包括Echarts图、文字、表格。经过一番查找,找到一个插件html-docx-js。
功能实现
exportWord函数:主函数接受一个dom元素,通过a标签导出文件
ini
import htmlDocx from 'html-docx-js/dist/html-docx'
export async function exportWord(ele) {
let htmlContent = await wordStyleProcess(ele)
const docxBlob = htmlDocx.asBlob(htmlContent)
const URL = window.URL || window.webkitURL
let a = document.createElement('a')
a.download = 'name'
a.rel = 'noopener'
a.target = '_blank'
a.href = URL.createObjectURL(docxBlob)
a.click()
URL.revokeObjectURL(a.href) // 20
}
wordStyleProcess函数:对元素进行样式处理。首先,复制元素,防止修改样式影响页面样式。使用shortId标记元素,通过id找到页面渲染的原始元素,对样式进行调整。
javascript
export async function wordStyleProcess(ele) {
if (!ele) return console.warn('未传入元素')
// 设置标记, 方便复制样式,通过data-export-file-id标记,用于后续找到页面的真实渲染样式的元素
Array.from(ele.querySelectorAll('*')).forEach((item) => {
item.setAttribute('data-export-file-id', 'str' + stortId.generate())
})
//不影响页面样式,复制dom
let exportFileEle = ele.cloneNode(true)
// 遍历iframe中的元素,将类样式设置行内样式
Array.from(exportFileEle.querySelectorAll('*')).forEach((item) => {
let dataExportFileId = item.getAttribute('data-export-file-id')
let originDom = ele.querySelector('[data-export-file-id="' + dataExportFileId + '"]') //原始dom才能获取真实样式
if (originDom) {
let originSty = getComputedStyle(originDom) //利用getComputedStyle获取真实页面dom的样式
if (originSty.display === 'none' || originSty.opacity === '0') return item.remove()
setStyle(item, originSty)
}
//处理table类型的元素
if (item.getAttribute('data-transform-table')) {
const wordTable = convertElTableToHtmlTableWord(item, ele)
const table = document.createElement('div')
const parent = item.parentNode
parent.style.height = 'auto'
table.innerHTML = wordTable
if (item.nextSibling) {
parent.insertBefore(table, item.nextSibling)
} else {
parent.appendChild(table)
}
item.remove()
}
})
// 处理echarts类型元素:遍历真实页面中的元素,带有自定义属性data-transform-image的元素为需要转换的元素,真实页面的canvas转换为图片,替换到iframe中对应位置
await convertHTMLToImage(Array.from(ele.querySelectorAll('[data-transform-image="true"]')), exportFileEle)
//
const htmlContent = exportFileEle.innerHTML
if (exportFileEle) {
exportFileEle.remove()
}
return htmlContent
}
setStyle函数:获取到的元素没有类样式,通过getComputedStyle获取的页面真实样式赋值到导出的元素中。
css
/**
* 把元素类样式设置行内样式
*
* @param ele DOM元素
* @param sty 包含样式属性的对象
*/
export function setStyle(ele, sty) {
if (ele.nodeName.toLowerCase() !== 'img') {
ele.setAttribute(
'style',
(ele.getAttribute('style') || '') +
`;font-size: ${sty.fontSize};color: ${sty.color};font-style: ${sty.fontStyle};line-height: ${sty.lineHeight};font-weight: ${sty.fontWeight};
font-family: ${sty.fontFamily};text-align: ${sty.textAlign};text-indent: ${sty.textIndent}; margin: ${sty.margin}; padding: ${sty.padding};width: ${sty.width}; height: ${sty.height};
white-space:${sty.whiteSpace};word-break:${sty.wordBreak};display:${sty.display}; flex-direction:${sty.flexDirection};align-items: ${sty.alignItems}; `
)
}
}
convertHTMLToImage:对echarts图表元素导出的问题,一开始我使用的html2canvas,网上的资料的很多也是用的这个插件,但是这个插件实在的太太太太慢了!!!!,几个echarts图,居然十几秒。 后面我翻了一下资料,echarts实例是支持导出base64的,然后尝试了一下,直接秒导出,快太多了。 html2canvas版本:
javascript
export async function convertHTMLToImage(elements, exportFileEle, canvasWidth) {
// 使用html2canvas捕捉DOM元素
for (let element of elements) {
const option = {
// 使用html2Canvas进行截图
logging: false,
allowTaint: true, // 允许跨域图片渲染
useCORS: true, // 使用跨域资源
imageTimeout: 0, // 图片加载延迟,默认延迟为0,单位毫秒
scale: 1.2, // 设置缩放比例
willReadFrequently: true,
}
const canvas = await html2canvas(element, option)
canvas.setAttribute('data-export-file-id', element.getAttribute('data-export-file-id'))
const url = canvas.toDataURL('image/jpg', 1.0)
let img = new Image()
img.src = url
canvasWidth ? (img.width = canvasWidth) : (img.style.width = '100%')
//移除原来的图表元素,替换为图片
let canvasEle = exportFileEle.querySelector(`[data-export-file-id=${element.getAttribute('data-export-file-id')}]`)
if (canvasEle) {
const parent = canvasEle.parentNode
if (canvasEle.nextSibling) {
parent.insertBefore(img, canvasEle.nextSibling)
} else {
parent.appendChild(img)
}
canvasEle.remove()
}
}
}
使用echart的版本:从这里看好像也差不多,但是element有个data-url的自定义属性,这个需要在外部配合,将获取到的base64赋值给元素的自定义属性。
javascript
export async function convertHTMLToImage(elements, exportFileEle, canvasWidth) {
// 使用html2canvas捕捉DOM元素
for (let element of elements) {
const option = {
// 使用html2Canvas进行截图
logging: false,
allowTaint: true, // 允许跨域图片渲染
useCORS: true, // 使用跨域资源
imageTimeout: 0, // 图片加载延迟,默认延迟为0,单位毫秒
scale: 1.2, // 设置缩放比例
willReadFrequently: true,
}
const url = element.getAttribute('data-url')
let img = new Image()
img.src = url
canvasWidth ? (img.width = canvasWidth) : (img.style.width = '100%')
//移除原来的图表元素,替换为图片
let canvasEle = exportFileEle.querySelector(`[data-export-file-id=${element.getAttribute('data-export-file-id')}]`)
if (canvasEle) {
const parent = canvasEle.parentNode
if (canvasEle.nextSibling) {
parent.insertBefore(img, canvasEle.nextSibling)
} else {
parent.appendChild(img)
}
canvasEle.remove()
}
}
}
echart组件核心代码:这里只简单写了一下,主要核心意思就是,在echarts渲染完成之后,生成base64,然后赋值到元素的自定义属性上。
kotlin
<div ref="refEchartsContainer" class="echarts-container" :style="cardStyle" data-transform-image="true" :data-url="url"/>
this.chartInstance.on('finished', () => {
this.url = this.chartInstance.getDataURL({ type: 'png' })
})
convertElTableToHtmlTableWord:处理el-table表格
javascript
export function convertElTableToHtmlTableWord(elTable, renderEle) {
if (!elTable) return ''
const tableEmptyBlockStyHeight = '100%'
// 获取 el-table 的表头数据,包括多级表头
const tableOptions = {
tableStyle: 'border-collapse: collapse; width:100%',
headerStyle: 'border: 1px solid black; width: 60px;height: 50px;',
rowStyle: 'mso-yfti-irow:0; mso-yfti-firstrow:yes; mso-yfti-lastrow:yes; page-break-inside:avoid;height: 20px;',
cellStyle: 'border: 1px solid black; min-width: 50px;max-width: 100px;height: 20px;',
}
const theadRows = elTable.querySelectorAll('thead tr') || []
// 获取 el-table 的数据行
const tbodyRows = elTable.querySelectorAll('tbody tr') || []
// 开始构建 HTML 表格的字符串,设置表格整体样式和边框样式
let htmlTable = `<table style="${tableOptions.tableStyle}"><thead>`
let columnsNum = 0
// 处理表头
theadRows.forEach((row) => {
htmlTable += `<tr style="${tableOptions.rowStyle}">`
const columns = row.querySelectorAll('th') || []
// 获取表头列数,用于后面暂无数据的colspan属性
columnsNum = columns.length || 0
columns.forEach((column) => {
if (column.style.display !== 'none') {
const colspan = column.getAttribute('colspan') || '1'
const rowspan = column.getAttribute('rowspan') || '1'
htmlTable += `<th colspan="${colspan}" rowspan="${rowspan}" style="${tableOptions.headerStyle}">${column.innerText}</th>`
}
})
htmlTable += '</tr>'
})
htmlTable += '</thead><tbody>'
// 构建数据
if (tbodyRows && tbodyRows.length > 0) {
tbodyRows.forEach((row) => {
htmlTable += `<tr style="${tableOptions.rowStyle}">`
const cells = row.querySelectorAll('td') || []
cells.forEach((cell) => {
if (cell.querySelector('div')) {
htmlTable += `<td style="${tableOptions.cellStyle}">${cell.querySelector('div').innerHTML}</td>`
} else {
htmlTable += `<td style="${tableOptions.cellStyle}">${cell.innerText}</td>`
}
})
htmlTable += '</tr>'
})
} else {
htmlTable += `<tr style="${tableOptions.rowStyle}"><td colspan="${columnsNum}" style="width: 100%; border: 1px solid black; height: ${tableEmptyBlockStyHeight};"><div style="display: inline-block;width: 100%; text-align: center;">暂无数据</div></td></tr>`
}
htmlTable += '</tbody></table>'
return htmlTable
}