app封装通用echarts
通过以上代码和注释,你可以更清楚地理解如何在 uni-app 中使用 ECharts,并实现点击事件的监听和处理。方法用于安全地初始化 ECharts,避免因容器未找到导致的初始化失败。方法用于初始化 ECharts 实例,并设置默认配置和动画。方法用于监听点击事件,并将点击的数据传递到父组件。方法用于监听窗口大小变化,并重新调整图表大小。方法用于处理点击事件,并将数据传递到父组件。方法用于处理初
<template>
<view class="span-24 c-h-100p" :style="{ height: height }">
<!-- #ifdef APP-PLUS || H5 -->
<!-- @click="echartsRender.onClick" -->
<view ref="chart" :prop="option" :change:prop="echartsRender.updateEcharts" :id="'commonEcharts-' + _uid"
class="span-24 c-h-100p"></view>
<!-- #endif -->
<!-- #ifndef APP-PLUS || H5 -->
<view>非 APP、H5 环境不支持</view>
<!-- #endif -->
</view>
</template>
<script>
import echarts from "echarts";
export default {
props: {
timerID: {
type: Number,
default: 0,
validator: value => value >= 0
},
height: {
type: String,
default: '400px',
validator: value => {
const valid = /^\d+(px|rpx|%|vh|vw)$/.test(value);
if (!valid) {
console.warn('height 必须是有效的 CSS 长度值');
}
return valid;
}
},
// 新增 prop 用于强制重绘
forceUpdate: {
type: Boolean,
default: false
}
},
data() {
return {
option: {},
// 用于跟踪组件内部状态
internalState: {
isInitialized: false,
lastDataTime: 0
},
};
},
watch: {
forceUpdate(newVal) {
if (newVal) {
this.$nextTick(() => {
this.$refs.chart && this.echartsRender.safeInitEcharts();
});
}
}
},
methods: {
onViewClick(params) {
console.log('onViewClick', params);
this.$emit('chart-click', params);
},
getData(echartData) {
if (!echartData) {
console.error('getData 必须传入有效数据');
return;
}
console.log('getData', echartData);
this.option = this.normalizeOptions(echartData);
this.internalState.lastDataTime = Date.now();
},
normalizeOptions(echartData) {
// 数据标准化处理
return echartData
},
// 提供给外部调用的刷新方法
refresh() {
this.$nextTick(() => {
this.echartsRender.safeInitEcharts();
});
}
}
}
</script>
<script module="echartsRender" lang="renderjs">
import echarts from 'echarts';
// 动画帧节流
const requestAnimationFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function(callback) {
setTimeout(callback, 16);
};
export default {
data() {
return {
myChart: null,
timer: null,
currentIndex: -1,
isMounted: false,
initRetryCount: 0,
maxInitRetries: 3,
animationInterval: 3000,
resizeObserver: null
};
},
mounted() {
this.isMounted = true;
this.safeInitEcharts();
this.setupResizeObserver();
},
destroyed() {
this.clearResources();
},
methods: {
safeInitEcharts() {
if (!this.isMounted) return;
requestAnimationFrame(() => {
const chartId = 'commonEcharts-' + this._uid;
const chartDom = document.getElementById(chartId);
if (!chartDom) {
if (this.initRetryCount < this.maxInitRetries) {
this.initRetryCount++;
console.warn(
`ECharts 容器 ${chartId} 未找到,重试 ${this.initRetryCount}/${this.maxInitRetries}`
);
setTimeout(() => this.safeInitEcharts(), 100 * this.initRetryCount);
} else {
console.error(`无法初始化 ECharts,容器 ${chartId} 未找到`);
}
return;
}
this.initEcharts(chartDom);
});
},
initEcharts(chartDom) {
try {
// 清理旧实例
if (this.myChart) {
this.myChart.dispose();
}
// 修正环境变量
echarts.env.touchEventsSupported = false;
echarts.env.wxa = false;
// 初始化新实例
this.myChart = echarts.init(chartDom);
this.initRetryCount = 0;
// 设置默认配置
const defaultOptions = {
animation: true,
animationDuration: 1000,
animationEasing: 'cubicOut'
};
// 合并配置
const finalOptions = Object.assign({}, defaultOptions, this.option);
this.myChart.setOption(finalOptions, true);
this.myChart.on('click',(params)=>{
// 传递给父组件
this.$emit('chart-click', params.name);
})
// 启动动画
if (this.timerID !== 0) {
this.startAnimation();
}
// 监听窗口变化
window.addEventListener('resize', this.handleResize);
} catch (e) {
console.error('ECharts 初始化失败:', e);
this.handleInitError(e);
}
},
setupResizeObserver() {
if (typeof ResizeObserver !== 'undefined') {
this.resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
if (entry.target.id === 'commonEcharts-' + this._uid) {
this.handleResize();
}
}
});
const chartDom = document.getElementById('commonEcharts-' + this._uid);
if (chartDom) {
this.resizeObserver.observe(chartDom);
}
}
},
handleResize() {
if (this.myChart) {
try {
this.myChart.resize();
} catch (e) {
console.warn('图表 resize 失败:', e);
}
}
},
startAnimation() {
this.clearTimer();
if (!this.myChart) return;
const option = this.myChart.getOption();
if (!option || !option.series || option.series.length === 0) {
console.warn('无法启动动画,图表数据未准备好');
return;
}
this.timer = setInterval(() => {
if (!this.myChart) return;
try {
const currentOption = this.myChart.getOption();
const dataLength = currentOption.series[0].data.length;
// 取消之前高亮的图形
this.myChart.dispatchAction({
type: "downplay",
seriesIndex: 0,
dataIndex: this.currentIndex,
});
// 更新当前索引
this.currentIndex = (this.currentIndex + 1) % dataLength;
// 高亮当前图形
this.myChart.dispatchAction({
type: "highlight",
seriesIndex: 0,
dataIndex: this.currentIndex,
});
// 显示 tooltip
this.myChart.dispatchAction({
type: "showTip",
seriesIndex: 0,
dataIndex: this.currentIndex,
});
} catch (e) {
console.warn('图表动画执行出错:', e);
this.clearTimer();
}
}, this.animationInterval);
},
updateEcharts(newValue, oldValue, ownerInstance, instance) {
if (!newValue || !this.isMounted) return;
requestAnimationFrame(() => {
if (!this.myChart) {
this.safeInitEcharts();
return;
}
try {
// 深度比较避免不必要的更新
if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
this.myChart.setOption(newValue, {
notMerge: true,
lazyUpdate: false
});
// 重置动画
if (this.timerID !== 0) {
this.startAnimation();
}
}
} catch (e) {
console.error('图表更新失败:', e);
}
});
},
clearTimer() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.currentIndex = -1;
},
clearResources() {
this.isMounted = false;
this.clearTimer();
if (this.myChart) {
try {
window.removeEventListener('resize', this.handleResize);
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
this.myChart.dispose();
} catch (e) {
console.warn('ECharts 销毁时出错:', e);
}
this.myChart = null;
}
},
handleInitError(error) {
console.error('图表初始化错误:', error);
// 可以在这里添加错误上报逻辑
if (this.initRetryCount < this.maxInitRetries) {
this.initRetryCount++;
setTimeout(() => this.safeInitEcharts(), 500 * this.initRetryCount);
}
},
// 在 renderjs 模块中修改 onClick 方法
// 在 renderjs 模块中修改 onClick 方法
onClick(event, ownerInstance) {
if (!this.myChart) return;
try {
// 获取点击的图表数据
const point = [event.detail.x, event.detail.y];
// 1. 获取当前图表配置
const currentOption = this.myChart.getOption();
const series = currentOption.series[0];
const seriesType = series ? series.type : null;
// 2. 根据图表类型处理点击
let dataIndex = -1;
let itemData = null;
let categoryValue = null;
let clickedData = null;
if (seriesType === 'bar') {
// 检查是否是横向柱状图
const isHorizontal = currentOption.xAxis[0] &&
currentOption.xAxis[0].type &&
currentOption.xAxis[0].type === 'value';
// 获取坐标轴
const yAxis = currentOption.yAxis[0];
// 横向柱状图的处理
if (isHorizontal || (currentOption.xAxis[0] && currentOption.xAxis[0].type === 'value' && currentOption.yAxis && currentOption.yAxis[0].type === 'category')) {
// 对于横向柱状图,y轴是类目轴
try {
} catch (e) {
console.warn('横向柱状图点击处理失败:', e);
// 回退到遍历查找最近点
let minDistance = Infinity;
series.data.forEach((item, index) => {
// 对于横向柱状图,数据点坐标是 [value, index]
const dataCoord = this.myChart.convertToPixel({
seriesIndex: 0
}, [item, index]);
if (Array.isArray(dataCoord)) {
// 对于横向柱状图,主要考虑y轴方向的距离
const distance = Math.abs(point[1] - dataCoord[1]);
if (distance < minDistance) {
minDistance = distance;
dataIndex = index;
}
}
});
if (dataIndex !== -1) {
itemData = series.data[dataIndex];
if (yAxis && yAxis.data) {
categoryValue = yAxis.data[dataIndex];
}
clickedData = [itemData, dataIndex];
}
}
}
// 构造安全的可序列化对象
const safeEvent = {
type: 'click',
data: clickedData, // 原始坐标数据
dataIndex: dataIndex, // 数据索引
itemData: itemData, // 完整数据项
categoryValue: categoryValue, // 类别值
seriesType: seriesType, // 图表类型
seriesName: series ? series.name : null, // 系列名称
chartId: 'commonEcharts-' + this._uid,
timestamp: Date.now(),
offsetX: event.offsetX,
offsetY: event.offsetY,
isHorizontalBar: seriesType === 'bar' &&
((series.encode && series.encode.x && series.encode.x[0] === 'value') ||
(currentOption.xAxis && currentOption.xAxis[0] && currentOption.xAxis[0].type === 'value' &&
currentOption.yAxis && currentOption.yAxis[0] && currentOption.yAxis[0].type === 'category'))
};
console.log('图表点击数据:', safeEvent);
// 传递给父组件
ownerInstance.callMethod('onViewClick', safeEvent);
// 高亮点击的数据点
if (dataIndex !== -1) {
this.myChart.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: dataIndex
});
// 显示提示框
this.myChart.dispatchAction({
type: 'showTip',
seriesIndex: 0,
dataIndex: dataIndex
});
}
}} catch (e) {
console.warn('图表点击处理失败:', e);
ownerInstance.callMethod('onViewClick', {
error: e.message,
type: 'error'
});
}
}
}
}
</script>
代码说明
-
<template>部分:-
使用条件编译(
#ifdef和#ifndef)来区分不同平台的渲染逻辑。 -
在 APP 和 H5 环境中,渲染 ECharts 容器;在其他环境中,显示不支持的提示。
-
-
<script>部分:-
定义了组件的属性(
props)、数据(data)、方法(methods)和监听器(watch)。 -
option是 ECharts 的配置项,attackSourcesColor是渐变色数组,用于图表的视觉效果。 -
onViewClick方法用于处理点击事件,并将数据传递到父组件。 -
getData方法用于接收外部数据并标准化处理。 -
refresh方法用于强制重绘图表。
-
-
<script module="echartsRender" lang="renderjs">部分:-
使用
renderjs模块来处理 ECharts 的初始化、更新和销毁。 -
safeInitEcharts方法用于安全地初始化 ECharts,避免因容器未找到导致的初始化失败。 -
initEcharts方法用于初始化 ECharts 实例,并设置默认配置和动画。 -
setupResizeObserver方法用于监听窗口大小变化,并重新调整图表大小。 -
startAnimation方法用于启动图表的动画效果。 -
updateEcharts方法用于更新 ECharts 的配置。 -
clearTimer和clearResources方法用于清理定时器和释放资源。 -
handleInitError方法用于处理初始化错误,并尝试重新初始化。 -
onClick方法用于监听点击事件,并将点击的数据传递到父组件。使用在父组件中引入并使用该组件:
<template> <view> <common-echarts :height="height" :option="option" @chart-click="handleChartClick" ></common-echarts> </view> </template> <script> import CommonEcharts from './CommonEcharts.vue'; export default { components: { CommonEcharts }, data() { return { height: '400px', option: { xAxis: { type: 'category', data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'] }, yAxis: { type: 'value' }, series: [ { name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20] } ] } }; }, methods: { handleChartClick(name) { console.log('点击了:', name); } } }; </script> -
点击图表时,
handleChartClick方法会被触发,并接收点击的数据。 -
通过以上代码和注释,你可以更清楚地理解如何在 uni-app 中使用 ECharts,并实现点击事件的监听和处理。
-
更多推荐


所有评论(0)