原因是:echats 實例,不能夠用響應式變量去接收。
<template><div class="attendance-chart"><div v-if="loading" class="loading">加載中...</div><div v-else-if="error" class="error">數據加載失敗: {{ error }}<button @click="fetchData">重新加載</button></div><div v-else><div ref="chart" style="width: 100%; height: 350px;"></div><div v-if="!chartData.attendanceData.length" class="no-data">暫無數據</div></div></div>
</template><script>
import * as echarts from 'echarts';
import homeService from '@api/home/home';export default {name: 'AttendanceChart',data() {return {chartInstance: null, //這里是響應式chartData: {projectList: [],dateList: [],attendanceData: [],},loading: false,error: null,};},mounted() {this.fetchData();window.addEventListener('resize', this.resizeChart);},beforeDestroy() {window.removeEventListener('resize', this.resizeChart);this.destroyChart();},methods: {async fetchData() {this.loading = true;this.error = null;try {const response = await homeService.getData();if (this.validateData(response)) {this.chartData = response;this.$nextTick(() => this.initChart());} else {throw new Error('數據格式不正確');}} catch (err) {this.error = err.message || '請求失敗';console.error('獲取數據失敗:', err);} finally {this.loading = false;}},validateData(data) {return (Array.isArray(data.projectList) &&Array.isArray(data.dateList) &&Array.isArray(data.attendanceData) &&data.attendanceData.every(item => item.projectName && Array.isArray(item.data) &&item.data.every(val => typeof val === 'number')));},initChart(attempt = 0) {if (attempt > 3) {this.error = '圖表初始化超時';return;}if (!this.$refs.chart) {setTimeout(() => this.initChart(attempt + 1), 200);return;}this.destroyChart();try {this.chartInstance = echarts.init(this.$refs.chart);this.updateChart();} catch (err) {console.error('圖表初始化失敗:', err);this.error = '圖表初始化失敗';}},updateChart() {if (!this.chartInstance) return;try {const option = {tooltip: {trigger: 'axis',backgroundColor: 'rgba(0,0,0,0.7)',borderWidth: 0,padding: 10,textStyle: {color: '#fff',fontSize: 12},},legend: {data: this.chartData.projectList,bottom: 10,textStyle: {fontSize: 12},},grid: {left: '3%',right: '4%',bottom: '15%',top: '5%',containLabel: true},xAxis: {type: 'category',boundaryGap: false,data: this.chartData.dateList,axisLabel: {rotate: 45,fontSize: 12}},yAxis: {type: 'value',name: '人數',nameTextStyle: {fontSize: 12}},series: this.chartData.attendanceData.map(item => ({name: item.projectName,type: 'line',data: item.data,symbol: 'circle',symbolSize: 6,showSymbol: false,lineStyle: {width: 2},emphasis: {lineStyle: {width: 3}}}))};this.chartInstance.setOption(option, true);} catch (err) {console.error('圖表配置失敗:', err);this.error = '圖表渲染失敗';}},resizeChart() {if (this.chartInstance) {this.chartInstance.resize();}},destroyChart() {if (this.chartInstance) {this.chartInstance.dispose();this.chartInstance = null;}}}
};
</script><style scoped>
.attendance-chart {width: 100%;height: 350px;padding: 12px;background: #fff;border-radius: 8px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);position: relative;
}.loading,
.error,
.no-data {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);text-align: center;color: #666;
}.error {color: #f56c6c;
}.error button {margin-top: 8px;padding: 4px 12px;background: #f56c6c;color: white;border: none;border-radius: 4px;cursor: pointer;
}.no-data {color: #999;
}
</style>
修正后的代碼:
<template><div class="attendance-chart"><div v-if="loading" class="loading">加載中...</div><div v-else-if="error" class="error">數據加載失敗: {{ error }}<button @click="fetchData">重新加載</button></div><div v-else><div ref="chart" style="width: 100%; height: 350px;"></div><div v-if="!chartData.attendanceData.length" class="no-data">暫無數據</div></div></div>
</template><script>
import * as echarts from 'echarts';
import homeService from '../../../api/home/home';export default {name: 'AttendanceChart',data() {return {// 不要將 chartInstance 放在 data 中chartData: {projectList: [],dateList: [],attendanceData: [],},loading: false,error: null,};},// 將 chartInstance 作為組件實例屬性chartInstance: null,mounted() {this.fetchData();window.addEventListener('resize', this.resizeChart);},beforeDestroy() {window.removeEventListener('resize', this.resizeChart);this.destroyChart();},methods: {async fetchData() {this.loading = true;this.error = null;try {const response = await homeService.getData();if (this.validateData(response)) {this.chartData = response;this.$nextTick(() => this.initChart());} else {throw new Error('數據格式不正確');}} catch (err) {this.error = err.message || '請求失敗';console.error('獲取數據失敗:', err);} finally {this.loading = false;}},validateData(data) {return (Array.isArray(data.projectList) &&Array.isArray(data.dateList) &&Array.isArray(data.attendanceData) &&data.attendanceData.every(item => item.projectName && Array.isArray(item.data) &&item.data.every(val => typeof val === 'number')));},initChart(attempt = 0) {if (attempt > 3) {this.error = '圖表初始化超時';return;}if (!this.$refs.chart) {setTimeout(() => this.initChart(attempt + 1), 200);return;}this.destroyChart();try {// 直接賦值給組件實例屬性,而不是響應式數據this.$options.chartInstance = echarts.init(this.$refs.chart);this.updateChart();} catch (err) {console.error('圖表初始化失敗:', err);this.error = '圖表初始化失敗';}},updateChart() {// 從組件實例屬性獲取const chartInstance = this.$options.chartInstance;if (!chartInstance) return;try {const option = {tooltip: {trigger: 'axis',backgroundColor: 'rgba(0,0,0,0.7)',borderWidth: 0,padding: 10,textStyle: {color: '#fff',fontSize: 12},},legend: {data: this.chartData.projectList,bottom: 10,textStyle: {fontSize: 12},},grid: {left: '3%',right: '4%',bottom: '15%',top: '5%',containLabel: true},xAxis: {type: 'category',boundaryGap: false,data: this.chartData.dateList,axisLabel: {rotate: 45,fontSize: 12}},yAxis: {type: 'value',name: '人數',nameTextStyle: {fontSize: 12}},series: this.chartData.attendanceData.map(item => ({name: item.projectName,type: 'line',data: item.data,symbol: 'circle',symbolSize: 6,showSymbol: false,lineStyle: {width: 2},emphasis: {lineStyle: {width: 3}}}))};chartInstance.setOption(option, true);} catch (err) {console.error('圖表配置失敗:', err);this.error = '圖表渲染失敗';}},resizeChart() {const chartInstance = this.$options.chartInstance;if (chartInstance) {chartInstance.resize();}},destroyChart() {const chartInstance = this.$options.chartInstance;if (chartInstance) {chartInstance.dispose();this.$options.chartInstance = null;}}}
};
</script><style scoped>
.attendance-chart {width: 100%;height: 350px;padding: 12px;background: #fff;border-radius: 8px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);position: relative;
}.loading,
.error,
.no-data {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);text-align: center;color: #666;
}.error {color: #f56c6c;
}.error button {margin-top: 8px;padding: 4px 12px;background: #f56c6c;color: white;border: none;border-radius: 4px;cursor: pointer;
}.no-data {color: #999;
}
</style>
ECharts 在 Vue 中的正確使用方式
關鍵修改點
1. 移除響應式的 chartInstance
- ? 不再在
data()
中聲明chartInstance
- ? 改為使用
this.$options.chartInstance
存儲 ECharts 實例
2. 統一實例訪問方式
- 所有方法中通過
this.$options.chartInstance
訪問圖表實例 - 確保不會觸發 Vue 的響應式系統
3. 完善銷毀機制
destroyChart() {const chartInstance = this.$options.chartInstanceif (chartInstance) {chartInstance.dispose()this.$options.chartInstance = null}
}
為什么不能使用響應式變量?
ECharts 實例被 Vue 響應式代理后會導致:
?? ECharts 內部方法調用異常
?? 圖例點擊等交互事件失效
?? 可能引發內存泄漏
?? 性能下降替代方案
// 方案1:使用組件選項
this.$options.chartInstance = echarts.init()// 方案2:在 created 中定義非響應式屬性
created() {this.chartInstance = null
}
注意事項
確保容器存在
mounted() {this.$nextTick(() => {this.fetchData()})
}
處理動態數據更新
watch: {'chartData.attendanceData': {handler() {this.updateChart()},deep: true}
}
添加加載狀態
updateChart() {if (this.$options.chartInstance) {this.$options.chartInstance.showLoading()// ...設置option...this.$options.chartInstance.hideLoading()}
}
最終效果:經過這些修改后,圖表的所有交互功能(包括圖例點擊)都將正常工作。