【web应用】若依框架前端报表制作与导出全攻略(ECharts + html2canvas + jsPDF)

发布于:2025-07-12 ⋅ 阅读:(14) ⋅ 点赞:(0)


前言

在若依Vue3框架中,实现报表功能是常见的业务需求。报表通常需要包含表格数据和可视化图表,并支持导出为PDF格式。本文将详细介绍如何使用ECharts实现数据可视化,结合html2canvas和jsPDF实现报表导出功能,并提供完整的代码实现和优化建议。

一、ECharts准备工作

1. 检查ECharts安装

首先需要确认项目中是否已安装ECharts。在项目根目录下执行以下命令:

npm list echarts

如果看到类似以下输出,则表示已安装:

└── echarts@5.4.3

如果没有安装,可以通过以下命令安装:

npm install echarts --save

2. 导入ECharts

在Vue组件中导入ECharts:

import * as echarts from 'echarts'
import { ref, onMounted, onBeforeUnmount } from 'vue'

3. 创建饼图组件

下面是一个完整的饼图实现示例,包含数据加载、图表渲染和销毁逻辑:

export default {
  setup() {
    const numbers = ref(['加载中', '加载中'])
    
    onMounted(() => {
      // 模拟数据加载
      setTimeout(() => {
        numbers.value = ['10', '30']
        
        const present = parseInt(numbers.value[0])
        const total = parseInt(numbers.value[1])
        const absent = total - present

        // 获取DOM元素
        const chartDom = document.getElementById('attendanceChart')
        if (!chartDom) return
        
        // 初始化图表
        const myChart = echarts.init(chartDom)

        // 图表配置
        const option = {
          tooltip: {
            trigger: 'item',
            formatter: '{a} <br/>{b}: {c} ({d}%)'
          },
          legend: {
            top: '0%',
            left: 'center',
            textStyle: {
              color: '#A6CAF4',
              fontSize: 14
            }
          },
          series: [
            {
              name: '出勤统计',
              type: 'pie',
              radius: '100%',
              top: '20%',
              data: [
                {
                  value: present,
                  name: '出勤',
                  itemStyle: {
                    color: '#91CC75'
                  }
                },
                {
                  value: absent,
                  name: '缺勤',
                  itemStyle: {
                    color: '#409EF0'
                  }
                }
              ],
              label: {
                show: false
              },
              labelLine: {
                show: false
              },
              emphasis: {
                label: {
                  show: true,
                  position: 'inside',
                  formatter: '{b}: {d}%',
                  color: '#fff',
                  fontSize: 14
                },
                itemStyle: {
                  shadowBlur: 10,
                  shadowOffsetX: 0,
                  shadowColor: 'rgba(0, 0, 0, 0.5)'
                }
              }
            }
          ]
        }

        // 应用配置
        myChart.setOption(option)

        // 响应式调整
        window.addEventListener('resize', function() {
          myChart.resize()
        })

        // 组件卸载时清理
        onBeforeUnmount(() => {
          window.removeEventListener('resize', () => {})
          if (myChart) {
            myChart.dispose()
          }
        })
      }, 300)
    })

    return { numbers }
  }
}

4. 模板部分

<template>
  <div class="chart-container">
    <!-- 饼图容器 -->
    <div 
      id="attendanceChart" 
      style="width: 100%; height: 300px;"
    ></div>
    
    <!-- 导出按钮 -->
    <button @click="exportTextAndChartAsPDF" class="export-btn">
      导出报表
    </button>
  </div>
</template>

二、报表导出功能实现

1. 安装依赖

确保已安装html2canvas和jsPDF:

npm install html2canvas jspdf --save

2. 导入依赖

import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'

3. 完整导出函数实现

const personnelData = ref([
  { name: '张三', date: '2023-10-01', status: '出勤' },
  { name: '李四', date: '2023-10-01', status: '缺勤' },
  { name: '王五', date: '2023-10-02', status: '迟到' },
])

const exportTextAndChartAsPDF = async () => {
  // 创建PDF文档
  const pdf = new jsPDF('p', 'mm', 'a4') // 纵向A4
  const lineHeight = 10 // 行高
  let startY = 40 // 初始Y坐标

  // 1. 添加标题
  pdf.setFontSize(16).setTextColor(0, 0, 0)
  pdf.text('人员出勤报表', 105, 15, { align: 'center' })

  // 2. 添加表格标题行
  pdf.setFontSize(12)
  pdf.text('姓名', 20, 30)
  pdf.text('日期', 80, 30)
  pdf.text('状态', 140, 30)

  // 3. 添加数据行
  personnelData.value.forEach((item, index) => {
    const currentY = startY + index * lineHeight
    pdf.text(item.name, 20, currentY)
    pdf.text(item.date, 80, currentY)
    pdf.text(item.status, 140, currentY)
  })

  // 4. 截取饼图并添加到PDF
  const chartContainer = document.getElementById('attendanceChart')?.parentNode
  if (chartContainer) {
    try {
      // 截图饼图区域
      const canvas = await html2canvas(chartContainer, {
        scale: 2, // 提高分辨率
        logging: false,
        useCORS: true, // 允许跨域图片
        backgroundColor: '#FFFFFF', // 背景设为白色
        scrollY: -window.scrollY, // 解决滚动位置问题
        windowWidth: document.documentElement.scrollWidth, // 完整宽度
        windowHeight: document.documentElement.scrollHeight // 完整高度
      })

      // 计算饼图在PDF中的位置
      const imgProps = { width: 80, height: 80 } // 自定义饼图大小(mm)
      const imgX = 60 // X坐标(居中偏左)
      const imgY = startY + personnelData.value.length * lineHeight + 20 // Y坐标(表格下方留20mm间距)

      // 添加饼图到PDF
      pdf.addImage(
        canvas.toDataURL('image/png'),
        'PNG',
        imgX,
        imgY,
        imgProps.width,
        imgProps.height
      )
    } catch (error) {
      console.error('图表截图失败:', error)
      ElMessage.error('图表截图失败,请重试')
      return
    }
  }

  // 5. 保存PDF
  try {
    pdf.save('人员出勤报表.pdf')
    ElMessage.success('报表导出成功')
  } catch (error) {
    console.error('PDF保存失败:', error)
    ElMessage.error('报表导出失败')
  }
}

4. 样式优化

<style scoped>
.chart-container {
  position: relative;
  width: 100%;
  height: 100%;
  padding: 20px;
  background-color: #fff;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}

.export-btn {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 10;
  padding: 8px 15px;
  background-color: #409EFF;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s;
}

.export-btn:hover {
  background-color: #66b1ff;
  transform: translateY(-2px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.export-btn:active {
  transform: translateY(0);
}
</style>

三、完整组件实现

<template>
  <div class="chart-container">
    <!-- 饼图容器 -->
    <div 
      id="attendanceChart" 
      style="width: 100%; height: 300px;"
    ></div>
    
    <!-- 导出按钮 -->
    <button @click="exportTextAndChartAsPDF" class="export-btn">
      导出报表
    </button>
  </div>
</template>

<script>
import * as echarts from 'echarts'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
import { ElMessage } from 'element-plus'

export default {
  setup() {
    const numbers = ref(['加载中', '加载中'])
    const personnelData = ref([
      { name: '张三', date: '2023-10-01', status: '出勤' },
      { name: '李四', date: '2023-10-01', status: '缺勤' },
      { name: '王五', date: '2023-10-02', status: '迟到' },
    ])

    onMounted(() => {
      // 模拟数据加载
      setTimeout(() => {
        numbers.value = ['10', '30']
        
        const present = parseInt(numbers.value[0])
        const total = parseInt(numbers.value[1])
        const absent = total - present

        const chartDom = document.getElementById('attendanceChart')
        if (!chartDom) return
        
        const myChart = echarts.init(chartDom)

        const option = {
          tooltip: {
            trigger: 'item',
            formatter: '{a} <br/>{b}: {c} ({d}%)'
          },
          legend: {
            top: '0%',
            left: 'center',
            textStyle: {
              color: '#A6CAF4',
              fontSize: 14
            }
          },
          series: [
            {
              name: '出勤统计',
              type: 'pie',
              radius: '100%',
              top: '20%',
              data: [
                {
                  value: present,
                  name: '出勤',
                  itemStyle: {
                    color: '#91CC75'
                  }
                },
                {
                  value: absent,
                  name: '缺勤',
                  itemStyle: {
                    color: '#409EF0'
                  }
                }
              ],
              label: {
                show: false
              },
              labelLine: {
                show: false
              },
              emphasis: {
                label: {
                  show: true,
                  position: 'inside',
                  formatter: '{b}: {d}%',
                  color: '#fff',
                  fontSize: 14
                },
                itemStyle: {
                  shadowBlur: 10,
                  shadowOffsetX: 0,
                  shadowColor: 'rgba(0, 0, 0, 0.5)'
                }
              }
            }
          ]
        }

        myChart.setOption(option)

        window.addEventListener('resize', function() {
          myChart.resize()
        })

        onBeforeUnmount(() => {
          window.removeEventListener('resize', () => {})
          if (myChart) {
            myChart.dispose()
          }
        })
      }, 300)
    })

    const exportTextAndChartAsPDF = async () => {
      const pdf = new jsPDF('p', 'mm', 'a4')
      const lineHeight = 10
      let startY = 40

      pdf.setFontSize(16).setTextColor(0, 0, 0)
      pdf.text('人员出勤报表', 105, 15, { align: 'center' })

      pdf.setFontSize(12)
      pdf.text('姓名', 20, 30)
      pdf.text('日期', 80, 30)
      pdf.text('状态', 140, 30)

      personnelData.value.forEach((item, index) => {
        const currentY = startY + index * lineHeight
        pdf.text(item.name, 20, currentY)
        pdf.text(item.date, 80, currentY)
        pdf.text(item.status, 140, currentY)
      })

      const chartContainer = document.getElementById('attendanceChart')?.parentNode
      if (chartContainer) {
        try {
          const canvas = await html2canvas(chartContainer, {
            scale: 2,
            logging: false,
            useCORS: true,
            backgroundColor: '#FFFFFF',
            scrollY: -window.scrollY,
            windowWidth: document.documentElement.scrollWidth,
            windowHeight: document.documentElement.scrollHeight
          })

          const imgProps = { width: 80, height: 80 }
          const imgX = 60
          const imgY = startY + personnelData.value.length * lineHeight + 20

          pdf.addImage(
            canvas.toDataURL('image/png'),
            'PNG',
            imgX,
            imgY,
            imgProps.width,
            imgProps.height
          )
        } catch (error) {
          console.error('图表截图失败:', error)
          ElMessage.error('图表截图失败,请重试')
          return
        }
      }

      try {
        pdf.save('人员出勤报表.pdf')
        ElMessage.success('报表导出成功')
      } catch (error) {
        console.error('PDF保存失败:', error)
        ElMessage.error('报表导出失败')
      }
    }

    return {
      numbers,
      exportTextAndChartAsPDF
    }
  }
}
</script>

<style scoped>
.chart-container {
  position: relative;
  width: 100%;
  height: 100%;
  padding: 20px;
  background-color: #fff;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}

.export-btn {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 10;
  padding: 8px 15px;
  background-color: #409EFF;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s;
}

.export-btn:hover {
  background-color: #66b1ff;
  transform: translateY(-2px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.export-btn:active {
  transform: translateY(0);
}
</style>

四、常见问题与解决方案

1. 图表截图不完整或模糊

  • 原因:html2canvas默认截图分辨率较低
  • 解决方案
    const canvas = await html2canvas(chartContainer, {
      scale: 2, // 提高截图分辨率
      windowWidth: document.documentElement.scrollWidth,
      windowHeight: document.documentElement.scrollHeight
    })
    

2. 图表背景透明

  • 原因:未设置背景色
  • 解决方案
    backgroundColor: '#FFFFFF' // 在html2canvas配置中设置
    

3. 导出PDF中文乱码

  • 原因:jsPDF默认不支持中文
  • 解决方案
    • 使用支持中文的字体(如ctex插件)
    • 或者将图表转为图片后插入PDF(本文采用的方法)

4. 跨域图片问题

  • 原因:图表中使用了跨域图片
  • 解决方案
    useCORS: true // 在html2canvas配置中启用
    

五、性能优化建议

  1. 懒加载图表:只在需要导出时才渲染图表
  2. 虚拟滚动:对于大数据量的表格,使用虚拟滚动技术
  3. 分页导出:数据量很大时,考虑分页导出
  4. Web Worker:将截图和PDF生成放在Web Worker中执行,避免阻塞UI

六、总结

本文详细介绍了在若依Vue3框架中实现报表功能的完整方案,包括:

  1. 使用ECharts实现数据可视化
  2. 使用html2canvas实现图表截图
  3. 使用jsPDF生成PDF文档
  4. 完整的错误处理和用户体验优化

通过本文的方案,你可以快速实现包含表格和图表的报表导出功能,并根据实际需求进行扩展和优化。


网站公告

今日签到

点亮在社区的每一天
去签到