文章目录
前言
在若依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配置中启用
五、性能优化建议
- 懒加载图表:只在需要导出时才渲染图表
- 虚拟滚动:对于大数据量的表格,使用虚拟滚动技术
- 分页导出:数据量很大时,考虑分页导出
- Web Worker:将截图和PDF生成放在Web Worker中执行,避免阻塞UI
六、总结
本文详细介绍了在若依Vue3框架中实现报表功能的完整方案,包括:
- 使用ECharts实现数据可视化
- 使用html2canvas实现图表截图
- 使用jsPDF生成PDF文档
- 完整的错误处理和用户体验优化
通过本文的方案,你可以快速实现包含表格和图表的报表导出功能,并根据实际需求进行扩展和优化。