Vue + ECharts 实现图表导出为图片功能详解

Vue + ECharts 实现图表导出为图片功能详解

前言

在数据可视化项目中,经常需要将图表导出为图片供用户下载或分享。本文将详细介绍如何在 Vue 项目中使用 ECharts 的 getDataURL() 方法实现图表导出功能。

一、实现原理

1.1 核心 API

ECharts 提供了 getDataURL() 方法,可以将图表转换为 Base64 编码的图片数据:

javascript 复制代码
const imageUrl = chart.getDataURL({
  type: 'png',           // 图片格式:'png' 或 'jpeg'
  pixelRatio: 2,         // 像素比,2 表示 2 倍分辨率(更清晰)
  backgroundColor: '#fff' // 背景色
})

1.2 下载机制

利用 HTML5 的 标签的 download 属性触发浏览器下载:

javascript 复制代码
const link = document.createElement('a')
link.href = imageUrl
link.download = '图表.png'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)

二、基础实现

2.1 保存单个图表

javascript 复制代码
<template>
  <div>
    <div id="myChart" style="width: 600px; height: 400px;"></div>
    <el-button @click="saveChartAsImage">保存图片</el-button>
  </div>
</template>

<script>
import * as echarts from 'echarts'

export default {
  data() {
    return {
      chart: null
    }
  },
  mounted() {
    this.initChart()
  },
  methods: {
    initChart() {
      this.chart = echarts.init(document.getElementById('myChart'))
      const option = {
        xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'] },
        yAxis: { type: 'value' },
        series: [{ data: [120, 200, 150, 80, 70], type: 'line' }]
      }
      this.chart.setOption(option)
    },
    
    saveChartAsImage() {
      if (!this.chart) {
        this.$message.warning('图表未初始化')
        return
      }
      
      try {
        // 将图表转换为图片 URL
        const imageUrl = this.chart.getDataURL({
          type: 'png',
          pixelRatio: 2,
          backgroundColor: '#fff'
        })
        
        // 创建下载链接
        const link = document.createElement('a')
        link.href = imageUrl
        link.download = `图表_${Date.now()}.png`
        
        // 触发下载
        document.body.appendChild(link)
        link.click()
        document.body.removeChild(link)
        
        this.$message.success('图片保存成功')
      } catch (error) {
        this.$message.error('保存失败: ' + error.message)
      }
    }
  },
  beforeDestroy() {
    if (this.chart) {
      this.chart.dispose()
    }
  }
}
</script>

2.2 保存多个图表

javascript 复制代码
<template>
  <div>
    <div id="chart1" style="width: 600px; height: 400px;"></div>
    <div id="chart2" style="width: 600px; height: 400px;"></div>
    <el-button @click="saveAllCharts" :loading="savingImages">保存所有图表</el-button>
  </div>
</template>

<script>
import * as echarts from 'echarts'

export default {
  data() {
    return {
      chart1: null,
      chart2: null,
      savingImages: false
    }
  },
  mounted() {
    this.initCharts()
  },
  methods: {
    initCharts() {
      // 初始化图表1
      this.chart1 = echarts.init(document.getElementById('chart1'))
      this.chart1.setOption({
        title: { text: '趋势图' },
        xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
        yAxis: { type: 'value' },
        series: [{ data: [120, 200, 150], type: 'line' }]
      })
      
      // 初始化图表2
      this.chart2 = echarts.init(document.getElementById('chart2'))
      this.chart2.setOption({
        title: { text: '柱状图' },
        xAxis: { type: 'category', data: ['A', 'B', 'C'] },
        yAxis: { type: 'value' },
        series: [{ data: [100, 150, 200], type: 'bar' }]
      })
    },
    
    async saveAllCharts() {
      this.savingImages = true
      try {
        const images = []
        
        // 收集所有图表
        if (this.chart1) {
          images.push({
            name: '趋势图',
            url: this.chart1.getDataURL({
              type: 'png',
              pixelRatio: 2,
              backgroundColor: '#fff'
            })
          })
        }
        
        if (this.chart2) {
          images.push({
            name: '柱状图',
            url: this.chart2.getDataURL({
              type: 'png',
              pixelRatio: 2,
              backgroundColor: '#fff'
            })
          })
        }
        
        if (images.length === 0) {
          this.$message.warning('没有可保存的图表')
          return
        }
        
        // 批量下载(添加延迟避免浏览器阻止)
        images.forEach((img, index) => {
          setTimeout(() => {
            const link = document.createElement('a')
            link.href = img.url
            link.download = `${img.name}_${Date.now()}.png`
            document.body.appendChild(link)
            link.click()
            document.body.removeChild(link)
          }, index * 100)
        })
        
        this.$message.success(`成功保存 ${images.length} 张图片`)
      } catch (error) {
        this.$message.error('保存失败: ' + error.message)
      } finally {
        this.savingImages = false
      }
    }
  },
  beforeDestroy() {
    if (this.chart1) this.chart1.dispose()
    if (this.chart2) this.chart2.dispose()
  }
}
</script>

三、进阶技巧

3.1 通过 ref 访问子组件图表

父组件:

javascript 复制代码
<template>
  <div>
    <ChartComponent ref="chartComponent" />
    <el-button @click="saveChart">保存图片</el-button>
  </div>
</template>

<script>
export default {
  methods: {
    saveChart() {
      // 通过 ref 访问子组件
      const chartComponent = this.$refs.chartComponent
      
      // 访问子组件的图表实例
      const chart = chartComponent.chart
      
      if (chart) {
        const imageUrl = chart.getDataURL({
          type: 'png',
          pixelRatio: 2,
          backgroundColor: '#fff'
        })
        
        const link = document.createElement('a')
        link.href = imageUrl
        link.download = `图表_${Date.now()}.png`
        document.body.appendChild(link)
        link.click()
        document.body.removeChild(link)
        
        this.$message.success('保存成功')
      }
    }
  }
}
</script>

子组件(ChartComponent.vue):

javascript 复制代码
<template>
  <div id="chart" style="width: 600px; height: 400px;"></div>
</template>

<script>
import * as echarts from 'echarts'

export default {
  data() {
    return {
      chart: null  // 暴露给父组件访问
    }
  },
  mounted() {
    this.chart = echarts.init(document.getElementById('chart'))
    this.chart.setOption({
      xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
      yAxis: { type: 'value' },
      series: [{ data: [120, 200, 150], type: 'line' }]
    })
  },
  beforeDestroy() {
    if (this.chart) {
      this.chart.dispose()
    }
  }
}
</script>

3.2 自定义文件名

javascript 复制代码
// 方式1:使用时间戳
link.download = `图表_${Date.now()}.png`
// 结果:图表_1703567890123.png

// 方式2:使用日期格式
const now = new Date()
const dateStr = `${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2,'0')}${now.getDate().toString().padStart(2,'0')}`
link.download = `图表_${dateStr}.png`
// 结果:图表_20231226.png

// 方式3:组合多个信息
const patientName = '张三'
const chartType = '趋势分析'
const timestamp = Date.now()
link.download = `${patientName}_${chartType}_${timestamp}.png`
// 结果:张三_趋势分析_1703567890123.png

3.3 调整图片质量

javascript 复制代码
// PNG 格式(无损压缩,文件较大,适合需要高质量的场景)
const imageUrl = chart.getDataURL({
  type: 'png',
  pixelRatio: 2,  // 1=标准, 2=高清, 3=超高清
  backgroundColor: '#fff'
})

// JPEG 格式(有损压缩,文件较小,适合对文件大小有要求的场景)
const imageUrl = chart.getDataURL({
  type: 'jpeg',
  pixelRatio: 2,
  backgroundColor: '#fff',
  excludeComponents: ['toolbox']  // 排除工具栏等组件
})

3.4 添加 Loading 状态

javascript 复制代码
<template>
  <div>
    <div id="chart" style="width: 600px; height: 400px;"></div>
    <el-button @click="saveChart" :loading="savingImage">
      {{ savingImage ? '保存中...' : '保存图片' }}
    </el-button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      chart: null,
      savingImage: false
    }
  },
  methods: {
    async saveChart() {
      this.savingImage = true
      try {
        const imageUrl = this.chart.getDataURL({
          type: 'png',
          pixelRatio: 2,
          backgroundColor: '#fff'
        })
        
        const link = document.createElement('a')
        link.href = imageUrl
        link.download = `图表_${Date.now()}.png`
        document.body.appendChild(link)
        link.click()
        document.body.removeChild(link)
        
        this.$message.success('保存成功')
      } catch (error) {
        this.$message.error('保存失败: ' + error.message)
      } finally {
        this.savingImage = false
      }
    }
  }
}
</script>

四、实战案例:综合报告多图表导出

在实际项目中,我们可能需要同时导出多个不同类型的图表。以下是一个完整的实战案例:

javascript 复制代码
<template>
  <div class="comprehensive-report">
    <!-- 选择要导出的图表 -->
    <div class="report-selection">
      <el-checkbox-group v-model="selectedTypes">
        <el-checkbox :label="1">趋势分析</el-checkbox>
        <el-checkbox :label="2">小时平均</el-checkbox>
        <el-checkbox :label="3">研究比较</el-checkbox>
        <el-checkbox :label="4">散点图</el-checkbox>
      </el-checkbox-group>
      
      <el-button 
        type="success" 
        @click="saveChartsAsImages" 
        :disabled="!reportData"
        :loading="savingImages">
        保存图片
      </el-button>
    </div>

    <!-- 报告内容 -->
    <div v-if="reportData" class="report-content">
      <!-- 趋势分析 -->
      <TrendChart 
        v-if="selectedTypes.includes(1)" 
        ref="trendChart" 
        :data="reportData.trendData" />
      
      <!-- 小时平均 -->
      <HourlyChart 
        v-if="selectedTypes.includes(2)" 
        ref="hourlyChart" 
        :data="reportData.hourlyData" />
      
      <!-- 研究比较(包含2个图表)-->
      <ResearchChart 
        v-if="selectedTypes.includes(3)" 
        ref="researchChart" 
        :data="reportData.researchData" />
      
      <!-- 散点图(包含4个图表)-->
      <ScatterChart 
        v-if="selectedTypes.includes(4)" 
        ref="scatterChart" 
        :data="reportData.scatterData" />
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selectedTypes: [1, 2, 3, 4],
      reportData: null,
      savingImages: false
    }
  },
  methods: {
    async saveChartsAsImages() {
      if (!this.reportData) {
        this.$message.warning('请先生成报告')
        return
      }

      this.savingImages = true
      try {
        const images = []
        
        // 1. 保存趋势分析图表
        if (this.selectedTypes.includes(1) && this.$refs.trendChart) {
          const chart = this.$refs.trendChart.chart
          if (chart) {
            images.push({
              name: '趋势分析',
              url: chart.getDataURL({
                type: 'png',
                pixelRatio: 2,
                backgroundColor: '#fff'
              })
            })
          }
        }

        // 2. 保存小时平均图表
        if (this.selectedTypes.includes(2) && this.$refs.hourlyChart) {
          const chart = this.$refs.hourlyChart.chart
          if (chart) {
            images.push({
              name: '小时平均',
              url: chart.getDataURL({
                type: 'png',
                pixelRatio: 2,
                backgroundColor: '#fff'
              })
            })
          }
        }

        // 3. 保存研究比较图表(2个图表)
        if (this.selectedTypes.includes(3) && this.$refs.researchChart) {
          const researchChart = this.$refs.researchChart
          
          if (researchChart.chart1) {
            images.push({
              name: '研究比较_趋势图',
              url: researchChart.chart1.getDataURL({
                type: 'png',
                pixelRatio: 2,
                backgroundColor: '#fff'
              })
            })
          }
          
          if (researchChart.chart2) {
            images.push({
              name: '研究比较_数据整理',
              url: researchChart.chart2.getDataURL({
                type: 'png',
                pixelRatio: 2,
                backgroundColor: '#fff'
              })
            })
          }
        }

        // 4. 保存散点图(最多4个图表)
        if (this.selectedTypes.includes(4) && this.$refs.scatterChart) {
          const scatterChart = this.$refs.scatterChart
          const charts = scatterChart.charts // 假设是一个对象 { chart0, chart1, chart2, chart3 }
          
          const scatterNames = [
            '散点图_舒张压-收缩压',
            '散点图_脉率-收缩压',
            '散点图_脉率-舒张压',
            '散点图_脉率-平均压'
          ]
          
          Object.keys(charts).forEach((key, index) => {
            const chart = charts[key]
            if (chart) {
              images.push({
                name: scatterNames[index] || `散点图_${index + 1}`,
                url: chart.getDataURL({
                  type: 'png',
                  pixelRatio: 2,
                  backgroundColor: '#fff'
                })
              })
            }
          })
        }

        if (images.length === 0) {
          this.$message.warning('没有可保存的图表')
          return
        }

        // 批量下载
        const patientName = this.reportData.patientName || '患者'
        const timestamp = Date.now()
        
        images.forEach((img, index) => {
          setTimeout(() => {
            const link = document.createElement('a')
            link.href = img.url
            link.download = `${patientName}_${img.name}_${timestamp}.png`
            document.body.appendChild(link)
            link.click()
            document.body.removeChild(link)
          }, index * 100) // 每个图表间隔100ms,避免浏览器阻止
        })

        this.$message.success(`成功保存 ${images.length} 张图片`)
      } catch (error) {
        this.$message.error('保存失败: ' + error.message)
      } finally {
        this.savingImages = false
      }
    }
  }
}
</script>

五、高级优化

5.1 Base64 转 Blob(减少内存占用)

javascript 复制代码
// 将 Base64 转换为 Blob 对象
function dataURLtoBlob(dataURL) {
  const arr = dataURL.split(',')
  const mime = arr[0].match(/:(.*?);/)[1]
  const bstr = atob(arr[1])
  let n = bstr.length
  const u8arr = new Uint8Array(n)
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n)
  }
  return new Blob([u8arr], { type: mime })
}

// 使用 Blob 下载
const imageUrl = chart.getDataURL({ type: 'png', pixelRatio: 2 })
const blob = dataURLtoBlob(imageUrl)
const url = URL.createObjectURL(blob)

const link = document.createElement('a')
link.href = url
link.download = '图表.png'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)

// 释放内存
URL.revokeObjectURL(url)

5.2 添加水印

javascript 复制代码
const imageUrl = chart.getDataURL({
  type: 'png',
  pixelRatio: 2,
  backgroundColor: '#fff'
})

// 创建 Canvas 添加水印
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const img = new Image()

img.onload = function() {
  canvas.width = img.width
  canvas.height = img.height
  
  // 绘制原图
  ctx.drawImage(img, 0, 0)
  
  // 添加水印
  ctx.font = '20px Arial'
  ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'
  ctx.fillText('© 2023 公司名称', 10, canvas.height - 10)
  
  // 导出带水印的图片
  const watermarkedUrl = canvas.toDataURL('image/png')
  
  const link = document.createElement('a')
  link.href = watermarkedUrl
  link.download = '图表_带水印.png'
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
}

img.src = imageUrl

六、常见问题

6.1 跨域问题

如果图表中使用了外部图片,可能会遇到跨域问题:

javascript 复制代码
// 解决方案:在图表配置中设置
const option = {
  graphic: [{
    type: 'image',
    style: {
      image: 'https://example.com/image.png',
      // 添加 crossOrigin 属性
      crossOrigin: 'anonymous'
    }
  }]
}

6.2 浏览器阻止多文件下载

某些浏览器会阻止同时下载多个文件,解决方案:

javascript 复制代码
// 添加延迟
images.forEach((img, index) => {
  setTimeout(() => {
    // 下载逻辑
  }, index * 100) // 每个文件间隔100ms
})

6.3 图片模糊问题

javascript 复制代码
// 提高 pixelRatio
const imageUrl = chart.getDataURL({
  type: 'png',
  pixelRatio: 3,  // 提高到3倍分辨率
  backgroundColor: '#fff'
})

七、总结

本文介绍了在 Vue + ECharts 项目中实现图表导出功能的完整方案,包括:

1、基础实现:单图表和多图表导出

2、进阶技巧:ref 访问、自定义文件名、质量调整

3、实战案例:综合报告多图表批量导出

4、高级优化:Blob 转换、添加水印

5、常见问题:跨域、浏览器限制、图片质量

核心要点:
使用 chart.getDataURL() 获取图片数据
使用 标签的 download 属性触发下载
通过 ref 访问子组件的图表实例
添加延迟避免浏览器阻止多文件下载
合理设置 pixelRatio 平衡质量和文件大小

希望本文对你有所帮助!如果有任何问题,欢迎在评论区讨论。

相关链接:

ECharts 官方文档
MDN - HTMLAnchorElement.download

相关推荐
用泥种荷花2 小时前
【LangChain学习笔记】输出解析器
前端
闲云一鹤2 小时前
Cesium 使用 Turf 实现坐标点移动(偏移)
前端·gis·cesium
Thomas游戏开发2 小时前
Unity3D IL2CPP如何调用Burst
前端·后端·架构
多仔ヾ2 小时前
Vue.js 前端开发实战之 01-Vue 基础入门
vue.js
想学后端的前端工程师2 小时前
【微前端架构实战指南:从原理到落地】
前端·架构·状态模式
Keya3 小时前
DevEco Studio 使用技巧全面解析
前端·前端框架·harmonyos
_Rookie._3 小时前
web请求 错误拦截
前端
青鸟北大也是北大3 小时前
CSS单位与字体样式全解析
前端·css·html