vue 前端导出页面图表保存为Html格式文档

问题描述:

	遇到一个需求,需要前端将页面上所有的图表导出,比如有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中添加事件,本记录中就没有实现了,需要的可自行继续研究。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于SSM框架的网上系统调查的开发结合Vue.js技术,旨在为用户提供一个便捷、高效的在线调查平台,具有以下主要特点: 问卷设计与管理功能:系统提供了问卷设计与管理功能,管理员可以创建问卷、编辑问题、添加选项,并设置问卷的截止日期和可见性等。管理员还可以对已发布的问卷进行统计分析和结果导出。 问卷填写和提交功能:用户可以浏览并填写可见的问卷,根据问题类型选择合适的答案或填写文本内容。用户可以随时保存并继续填写问卷,最后提交问卷结果。 数据统计与分析功能:系统支持对问卷结果进行数据统计和分析,管理员可以查看问卷的回答情况、问题的答案分布和统计图表等,为决策提供数据支持。 问卷分享和传播功能:系统实现了问卷的分享和传播功能,用户可以通过生成问卷链接或分享到社交媒体等方式邀请他人参与调查,扩大问卷的调查范围和样本量。 响应式界面和良好的用户体验:系统利用Vue.js技术实现了响应式的用户界面,用户可以在不同的设备上进行问卷填写和查看,保证了良好的用户体验和使用便捷性。 通过整合Vue.js技术,系统实现了前端页面的动态渲染和交互,提供了用户友好的界面和流畅的操作体验。同时,系统基于SSM框架构建,具有良好的扩展性和稳定性,适用于各类组织和机构的在线调查需求。
基于Springboot+Vue的房价可视化监测系统源码+项目说明.zip 【资源说明】 1、该资源内项目代码都是经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载使用,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能。 一个基于Springboot+Vue的房价可视化监测系统,数据抓取自链家,通过Echarts进行可视化展现。 ### 1.1 项目介绍 [1] 爬取链家的新楼盘、二手房、小区、租房信息作为分析数据,支持北上广深等国内21个主要城市。 [2] 通过 IP 代理池、伪装 User-Agent 、随机延时、多线程等手段快速稳定抓取数据。 [3] 通过 xxl-job 定时任务实现爬虫的自动抓取及数据的自动汇总处理。 [4] 有完善的用户、角色、权限配置及管理机制,支持页面级、按钮级的权限管理。 [5] 通过 AOP 切面及自定义注解实现系统日志记录,可在系统管理页面查看操作日志。 [6] 集成 Druid 数据连接池、Screw 导出数据库结构、Swagger 生成接口文档。 [7] 通过 axios 请求后端接口,使用 VueX 缓存用户登录状态及系统字典。 [8] 使用 Element-UI 组件库开发页面,Echarts 开发可视化图表,二者均简单易用。 ### 1.2 项目技术栈 后端部分:[Springboot](https://github.com/spring-projects/spring-boot)(后端核心框架)、[Mybatis](https://github.com/mybatis/mybatis-3)(持久化框架)、[Shiro](https://github.com/apache/shiro)(权限框架)、[Druid](https://github.com/alibaba/druid)(数据库连接池)、[Screw](https://github.com/pingfangushi/screw)(数据库表结构生成器)、[Swagger](https://github.com/swagger-api/swagger-ui)(接口文档)、[Hutool](https://github.com/looly/hutool)(Java工具类库)、[Lombok](https://www.projectlombok.org/)(简化代码)、[MySQL](https://www.mysql.com/cn/)(数据库)、[Redis](https://redis.io/)(高速缓存) 前端部分:[Vue](https://cn.vuejs.org/v2/guide/index.html)(前端核心框架)、[VueX](https://vuex.vuejs.org/zh/)(状态管理)、[axios](https://github.com/axios/axios)(HTTP请求库)、[Element-UI](https://element.eleme.cn/#/zh-CN/component/installation)(基础组件库)、[Echarts](https://echarts.baidu.com/theme-builder/)(可视化图表) 爬虫部分:[Xpath](https://www.w3.org/TR/xpath/)(Python爬虫核心模块)、[ProxyPool](https://github.com/jhao104/proxy_pool)(Python爬虫IP代理池) 定时任务:[xxl-job](https://github.com/xuxueli/xxl-job/)(分布式任务调度平台,分为调度中心 xxl-job-admin 和执行器 xxl-job-executor 两部分) 开发工具:IDEA、PyCharm、VScode、Git、Maven、Navicat ### 1.3 项目结构 项目主要结构如下: ``` house-price-monitor ├── admin │ ├── pom.xml │ ├── src/main/java/com/house │ ├── ├── AdminApplication.java │ ├── ├── aop │ ├── ├── config │ ├── ├── constant │ ├── ├── controller

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值