问题描述:
遇到一个需求,需要前端将页面上所有的图表导出,比如有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 Document html2canvas(dom, { backgroundColor: null, useCORS: true // 配置图片可跨域 }).then(canvas => { // 转成图片,生成图片地址 let imgUrl = canvas.toDataURL('image/png') // 可将 canvas 转为 base64 格式 // 创建HTML内容 const htmlContent = ` <!DOCTYPE html> <html> <head> <title>导出的HTML文件</title> </head> <body> <h2 style="text-align:center"> 这是一个导出的文档 </h2> <div style="width:100%;text-align:center"> <img src="${imgUrl}" alt="导出内容"> </div> </body> </html> ` // 创建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 += `<div> <img class="image-class" src="${data.base64}" alt="图片"> </div>` } else if (data.componentType == 'Label') { str += `<div style="font-size:${data.attributes.fontSize};color:${data.attributes.fontColor}"> <span>${JSON.parse(data.translatedData)[data.bindData]}</span> </div>` } else if (['Histogram', 'Line-Chart', 'Pie-Chart'].includes(data.componentType)) { str += `<div class="report-div"> <div class="title"><span>${data.componentName}</span></div> <div class="desc"><span>${data.attributes.showDesc ? data.attributes.desc : ''}</span></div> <div class="chart-height"> <img src="${data.base64}" alt="图表"></div> </div>` } else if (data.componentType == 'Rich-Text') { str += `<div class="report-div"> <div class="title"><span>${data.componentName}</span></div> <div class="desc"><span>${data.attributes.showDesc ? data.attributes.desc : ''}</span></div> <div class="chart-div"> <div> <textarea autocomplete="off" rows="${data.attributes.rowsNum}" class="el-textarea__inner" style="resize: none; min-height: 30px;">${JSON.parse(data.translatedData)[data.bindData]}</textarea> </div> </div> </div>` } else if (data.componentType == 'Form') { // 表单 // 数据源:data.showList let formHtml = '' data.showList.forEach(itemm => { formHtml += ` <div class="${data.attributes.columns == '1' ? 'one-col' : 'two-col'}"> <div class="title-div"> <span class="con-span">${itemm.name}</span> </div> <div class="desc-div">` if (itemm.style && itemm.style.location && itemm.style.location == 1) { formHtml += ` <img class="icon-img" src="${itemm.icon_base64}" style="height:${itemm.style.height}px;width:${itemm.style.width}px;"> <span class="con-span" style="color:${itemm.advanced.fontColor}" > ${itemm.value} </span> </div> </div>` } else if (itemm.style && itemm.style.location && itemm.style.location == 2) { formHtml += ` <span class="con-span" style="color:${itemm.advanced.fontColor}" > ${itemm.value} </span> <img class="icon-img" src="${itemm.icon_base64}" style="height:${itemm.style.height}px;width:${itemm.style.width}px;"> </div> </div>` } else { formHtml += ` <span class="con-span" style="color:${itemm.advanced.fontColor}" > ${itemm.value} </span> </div> </div>` } }) str += `<div class="report-div"> <div class="title"><span>${data.componentName}</span></div> <div class="desc"><span>${data.attributes.showDesc ? data.attributes.desc : ''}</span></div> <div class="chart-div form-content"> ${formHtml} </div> </div>` } else if (data.componentType == 'Standard-Form') { // mainTable 表头数据 subTable displayName-表头数值 field对应字段 tableData表格数值 let table = '<table border="1" style="border-collapse: collapse;width:100%;">\n' table += '<thead>' if (data.attributes.tableNumber) { table += '<th>序号</th>' } let tableHead = data.mainTable tableHead.forEach(obj => { table += `<th>${obj.displayName}</th>\n` }) table += '</thead>' let tableData = data.tableData let subTable = data.subTable // row 表格一行数据 tableData.forEach((row, index) => { debugger // 创建主表内容 table += `<tr>\n` + (data.attributes.tableNumber ? `<td>${index + 1}</td>` : '') tableHead.forEach(headRow => { // table += `<td>${row[headRow.field]}</td>\n` table += `<td>` 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="icon-img" src="${row[headRow.field + '_style_base64']}" style="height:${param.height}px;width:${param.width}px;"> <span class="con-span" style="color:${headRow.advanced.fontColor}" > ${row[headRow.field]} </span> </td>` } else if (param && param.iconUrl && param.location == 2 && (originParam ? originParam == param.condition : true)) { table += ` <span class="con-span" style="color:${headRow.advanced.fontColor}" > ${row[headRow.field]} </span> <img class="icon-img" src="${row[headRow.field + '_style_base64']}" style="height:${param.height}px;width:${param.width}px;"> </td>` } else { table += ` <span class="con-span" style="color:${headRow.advanced.fontColor}" > ${row[headRow.field]} </span> </td>` } }) table += `</tr>\n` if (data.attributes.subTableIsShow) { // 子表内容 colspan需要合并单元格才能一行空间都有 table += `<tr><td colspan=${data.attributes.tableNumber ? data.mainTable.length + 1 : data.mainTable.length}><div class="sub-div-content">\n` subTable.forEach(item => { table += `<div class="sub-div"> <div class="title-div sub"> <span class="con-span">${item.displayName}</span> </div> <div class="desc-div sub">` 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="icon-img" src="${row[item.field + '_style_base64']}" style="height:${param.height}px;width:${param.width}px;"> <span class="con-span" style="color:${item.advanced.fontColor}" > ${row[item.field]} </span> </div> </div>` } else if (param && param.iconUrl && param.location == 2 && (originParam ? originParam == param.condition : true)) { table += ` <span class="con-span" style="color:${item.advanced.fontColor}" > ${row[item.field]} </span> <img class="icon-img" src="${row[item.field + '_style_base64']}" style="height:${param.height}px;width:${param.width}px;"> </div> </div>` } else { table += ` <span class="con-span" style="color:${item.advanced.fontColor}" > ${row[item.field]} </span> </div> </div>` } }) table += `</div></td></tr>\n` } }) table += '</table>' str += `<div class="report-div"> <div class="title"><span>${data.componentName}</span></div> <div class="desc"><span>${data.attributes.showDesc ? data.attributes.desc : ''}</span></div> <div class="chart-div"> <div> ${table} </div> </div> </div>` } else if (data.componentType == 'Dynamic-Table') { // 动态表格 let translated = JSON.parse(data.translatedData) let table = '<table border="1" style="border-collapse: collapse;width:100%;">\n' table += '<thead>' let tableHead = translated[data.fieldNameList] tableHead.forEach(name => { table += `<th>${name}</th>\n` }) table += '</thead>' // 数据源 translatedData.dataList fieldWidthList 宽度 fieldList字段列表 let realData = translated[data.dataList] realData.forEach(dataItem => { table += `<tr>\n` translated[data.fieldList].forEach(columnsItem => { table += `<td>${dataItem[columnsItem]}</td>\n` }) table += `</tr>\n` }) table += '</table>' str += `<div class="report-div"> <div class="title"><span>${data.componentName}</span></div> <div class="desc"><span>${data.attributes.showDesc ? data.attributes.desc : ''}</span></div> <div class="chart-div"> ${table} </div> </div>` } }) let html = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>文件导出</title> <style type="text/css"> 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; } </style> </head> <body> <div style=""> <h1 style="text-align:center"> ${templateName} </h1> <div class="content"> ${str} </div> </div> </body> </html>` 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中添加事件,本记录中就没有实现了,需要的可自行继续研究。