<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>

代码说明

  1. <template> 部分

    • 使用条件编译(#ifdef#ifndef)来区分不同平台的渲染逻辑。

    • 在 APP 和 H5 环境中,渲染 ECharts 容器;在其他环境中,显示不支持的提示。

  2. <script> 部分

    • 定义了组件的属性(props)、数据(data)、方法(methods)和监听器(watch)。

    • option 是 ECharts 的配置项,attackSourcesColor 是渐变色数组,用于图表的视觉效果。

    • onViewClick 方法用于处理点击事件,并将数据传递到父组件。

    • getData 方法用于接收外部数据并标准化处理。

    • refresh 方法用于强制重绘图表。

  3. <script module="echartsRender" lang="renderjs"> 部分

    • 使用 renderjs 模块来处理 ECharts 的初始化、更新和销毁。

    • safeInitEcharts 方法用于安全地初始化 ECharts,避免因容器未找到导致的初始化失败。

    • initEcharts 方法用于初始化 ECharts 实例,并设置默认配置和动画。

    • setupResizeObserver 方法用于监听窗口大小变化,并重新调整图表大小。

    • startAnimation 方法用于启动图表的动画效果。

    • updateEcharts 方法用于更新 ECharts 的配置。

    • clearTimerclearResources 方法用于清理定时器和释放资源。

    • 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,并实现点击事件的监听和处理。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐