问题描述:

遇到一个需求,需要前端将页面上所有的图表导出,比如有echarts的图表(折线图、饼图、柱状图)、表格、文本、图片、标签(也是一段文字)等类型,保存成一个html格式的文档,在浏览器中直接打开这个html文档可以看到跟之前页面上一样的展示效果。

图一

解决思路:

在研究的过程中,像echarts图表和图片,都需要保存为base64图片格式来使用,图片转成base64是为了保证导出到html文档中之后,在不是内网的环境中打开html文档,图片可以正常显示,echarts是需要请求数据,导出的文档中再打开之后,肯定不可能再去请求数据了,所以也需要转成base64处理。

想要达到要求的效果,网上找了很久,最后记录下2种方案:
方案一、使用html2canvas组件,html2canvas的作用就是允许我们直接在用户浏览器上拍摄网页或某一部分的截图。它的屏幕截图是基于DOM元素的,实际上它不会生成实际的屏幕截图,而是基于页面上可用的信息构建屏幕截图。

exportReport (fileName) {//需要的dom元素,需要自己定位到拿得到let dom = this.$refs.exportTemplate.$parent.$parent.$parent.$refs.exportDiv.$refs.realDom// 给的dom元素必须是原生的dom元素,不能是elementUI在浏览器中生成的,不然html2canvas会报错: Element is not attached to a Documenthtml2canvas(dom, {backgroundColor: null,useCORS: true // 配置图片可跨域}).then(canvas => {// 转成图片,生成图片地址let imgUrl = canvas.toDataURL('image/png') // 可将 canvas 转为 base64 格式// 创建HTML内容const htmlContent = `导出的HTML文件

这是一个导出的文档

<img class="aligncenter" src="https://img.maxssl.com/uploads/?url=
${imgUrl}" alt="导出内容">`
// 创建Blob对象const blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' })// 创建下载链接const downloadLink = document.createElement('a')downloadLink.href = URL.createObjectURL(blob)downloadLink.download = `${fileName}.html`// 模拟点击下载链接downloadLink.click()// 释放URL对象URL.revokeObjectURL(downloadLink.href)})}

注意
1、使用html2Canvas只能截取当前页面中显示的内容,如果当前页面中存在滚动条,html2canvas方法第一个参数dom就要给整个包含所有的元素长度的最外层元素才能将滚动的内容都截取下来。
2、这种方法实现的效果,整个页面就像一个pdf,所有的交互都不存在了,如果页面上还存在操作(比如点击展示,点击收起)是无法实现的。

方案二、需要根据页面中已存在的内容先生成一个html模板,然后获取页面的数据,循环生成相应的dom结构,就是用原生的html元素再实现一遍图一;表格、echarts可以直接用base64图片。生成这些内容后,外面套个html模板,最后再导出。

 createHtml (fileName, templateName) {console.log(this.json.components) //所有的数据来源let str = ''this.json.components.forEach(data=> {if (data.componentType == 'Picture') {str += `<img class="aligncenter" src="https://img.maxssl.com/uploads/?url=${data.base64}" alt="图片">`} else if (data.componentType == 'Label') {str += `${data.attributes.fontSize};color:${data.attributes.fontColor}">${JSON.parse(data.translatedData)[data.bindData]}`} else if (['Histogram', 'Line-Chart', 'Pie-Chart'].includes(data.componentType)) {str += `${data.componentName}${data.attributes.showDesc " />.attributes.desc : ''} <img class="aligncenter" src="https://img.maxssl.com/uploads/?url=https://img.maxssl.com/uploads/?url=${data.base64}" alt="图表">`} else if (data.componentType == 'Rich-Text') {str += `${data.componentName}${data.attributes.showDesc " />.attributes.desc : ''}<textarea autocomplete="off" rows="${data.attributes.rowsNum}" class="el-textarea__inner">${JSON.parse(data.translatedData)[data.bindData]} `} else if (data.componentType == 'Form') {// 表单// 数据源:data.showListlet formHtml = ''data.showList.forEach(itemm => {formHtml += `${data.attributes.columns == '1' ? 'one-col' : 'two-col'}">${itemm.name}`if (itemm.style && itemm.style.location && itemm.style.location == 1) {formHtml += `<img class="aligncenter" src="https://img.maxssl.com/uploads/?url=https://img.maxssl.com/uploads/?url=${itemm.icon_base64}"token interpolation">${itemm.style.height}px;width:${itemm.style.width}px;">${itemm.advanced.fontColor}">${itemm.value}`} else if (itemm.style && itemm.style.location && itemm.style.location == 2) {formHtml += `${itemm.advanced.fontColor}">${itemm.value}<img class="aligncenter" src="https://img.maxssl.com/uploads/?url=${itemm.icon_base64}"token interpolation">${itemm.style.height}px;width:${itemm.style.width}px;">`} else {formHtml += `${itemm.advanced.fontColor}">${itemm.value}`}})str += `${data.componentName}${data.attributes.showDesc " />.attributes.desc : ''}${formHtml}`} else if (data.componentType == 'Standard-Form') {// mainTable 表头数据subTabledisplayName-表头数值field对应字段 tableData表格数值let table = '\n'table +=''if(data.attributes.tableNumber){table +=''}let tableHead = data.mainTabletableHead.forEach(obj=>{table +=`\n`})table +=''let tableData = data.tableDatalet subTable = data.subTable// row 表格一行数据tableData.forEach((row, index)=>{debugger// 创建主表内容table +=`\n`+(data.attributes.tableNumber ?``:'')tableHead.forEach(headRow=>{// table += `\n`table +=``}elseif(param && param.iconUrl && param.location ==2&&(originParam " />== param.condition :true)){table +=`${headRow.advanced.fontColor}">${row[headRow.field]}<img class="aligncenter" src="https://img.maxssl.com/uploads/?url=https://img.maxssl.com/uploads/?url=${row[headRow.field + '_style_base64']}"token interpolation">${param.height}px;width:${param.width}px;">`}else{table +=`${headRow.advanced.fontColor}">${row[headRow.field]}`}})table +=`\n`if(data.attributes.subTableIsShow){// 子表内容 colspan需要合并单元格才能一行空间都有table +=`<td colspan=${data.attributes.tableNumber " />.mainTable.length + 1 : data.mainTable.length}>\n`subTable.forEach(item=>{table +=`${item.displayName}`let param = row[item.field +'_style']let originParam = row[item.field +'1']if(param && param.iconUrl && param.location ==1&&(originParam ? originParam == param.condition :true)){table +=`<img class="aligncenter" src="https://img.maxssl.com/uploads/?url=https://img.maxssl.com/uploads/?url=${row[item.field + '_style_base64']}"token interpolation">${param.height}px;width:${param.width}px;">${item.advanced.fontColor}">${row[item.field]}`}elseif(param && param.iconUrl && param.location ==2&&(originParam " />== param.condition :true)){table +=`${item.advanced.fontColor}">${row[item.field]}<img class="aligncenter" src="https://img.maxssl.com/uploads/?url=https://img.maxssl.com/uploads/?url=${row[item.field + '_style_base64']}"token interpolation">${param.height}px;width:${param.width}px;">`}else{table +=`${item.advanced.fontColor}">${row[item.field]}`}})table +=`\n`}})table +='
序号 ${obj.displayName}
${index + 1} ${row[headRow.field]} `let param = row[headRow.field + '_style']let originParam = row[headRow.field + '1']if (param && param.iconUrl && param.location == 1 && (originParam ? originParam == param.condition : true)) {table += `<img class="aligncenter" src="https://img.maxssl.com/uploads/?url=https://img.maxssl.com/uploads/?url=${row[headRow.field + '_style_base64']}"token interpolation">${param.height}px;width:${param.width}px;">${headRow.advanced.fontColor}">${row[headRow.field]}
'
str += `${data.componentName}${data.attributes.showDesc " />.attributes.desc : ''}${table}`} else if (data.componentType == 'Dynamic-Table') {// 动态表格let translated = JSON.parse(data.translatedData)let table = '\n'table +=''let tableHead = translated[data.fieldNameList]tableHead.forEach(name=>{table +=`\n`})table +=''// 数据源 translatedData.dataListfieldWidthList 宽度fieldList字段列表let realData = translated[data.dataList]realData.forEach(dataItem=>{table +=`\n`translated[data.fieldList].forEach(columnsItem=>{table +=`\n`})table +=`\n`})table +='
${name}
${dataItem[columnsItem]}
'
str += `${data.componentName}${data.attributes.showDesc ? data.attributes.desc : ''}${table}`}})let html =`文件导出body {line-height: 1.5;font-size: 14px;}thead tr{background: #F5F7F9;}tr {height: 36px;page-break-inside: avoid !important;}th, td {text-align: center;border-top: none;border-left: none;border-right: none;}.image-class{width:100%;}.content{width: 80%;margin: 0 auto;}.report-div{margin-bottom: 15px;min-height: 350px;position: relative;border: 1px solid #cacaca;}.title {height: 50px;line-height: 50px;font-size: 16px;font-weight: bold;padding: 0 20px;background: #F7F7F7;}.desc {height: 20px;font-size: 14px;margin: 5px 20px;}.chart-height{height:calc(100% - 80px);text-align: center;}.chart-div{height:calc(100% - 90px);overflow-y:auto;margin: 5px 15px 10px;}.form-content{display: flex;flex-wrap: wrap;align-content:flex-start;}.el-textarea__inner {display: block;resize: vertical;padding: 5px 15px;line-height: 1.5;box-sizing: border-box;width: 100%;font-size: inherit;color: #606266;background-color: #FFF;border: 1px solid #DCDFE6;border-radius: 4px;transition: border-color .2s cubic-bezier(.645,.045,.355,1);}.title-div,.desc-div{line-height: 30px;padding: 0 10px;position: relative;}.title-div{border-right: 1px solid #dadada;}.one-col{width: 100%;display: flex;border-top: 1px solid #dadada;border-left: 1px solid #dadada;border-right: 1px solid #dadada;}.two-col{width: 49.8%;display: flex;border-bottom: 1px solid #dadada;border-left: 1px solid #dadada;border-right: 1px solid #dadada;height: 36px;line-height: 36px;}.one-col:last-child{border-bottom: 1px solid #dadada;}.one-col>div:first-child{width: 30%;background-color: #F7F7F7;}.one-col>div:last-child{width: 70%;}.two-col>div:nth-child(2n+1){width: 30%;background-color: #F7F7F7;}.two-col>div:nth-child(2n){width: 70%;}.two-col:first-child{border-top: 1px solid #dadada;}.two-col:nth-child(2){border-top: 1px solid #dadada;}.two-col:nth-child(2n){border-left: none;}.icon-img{height:16px;width:16px;vertical-align: middle}.sub-div-content{margin: 10px 20px;}.sub-div-content>:first-child {border: 1px solid #c9c9c9;}.sub-div-content>:not(:first-child){border-left: 1px solid #c9c9c9;border-right: 1px solid #c9c9c9;border-bottom: 1px solid #c9c9c9;}.sub-div{display: flex;}.title-div{width:20%;background-color: #ededed;text-align: right;border-right: 1px solid #c9c9c9;}.desc-div{width:80%;text-align:left}.sub{line-height: 30px;padding: 0 15px;position: relative;}

${templateName}

${str}`
console.log(html)// 创建Blob对象const blob = new Blob([html], { type: 'text/html;charset=utf-8' })// 创建下载链接const downloadLink = document.createElement('a')downloadLink.href = URL.createObjectURL(blob)downloadLink.download = `${fileName}.html`// 模拟点击下载链接downloadLink.click()// 释放URL对象URL.revokeObjectURL(downloadLink.href)}

导出效果:

注意:
这种自己写的html导出的方式,效果基本可以达到和页面展示的一样,如果还有事件交互,就需要在html中添加事件,本记录中就没有实现了,需要的可自行继续研究。