echart 柱状图悬浮X轴显示tooltip 效果如图悬浮X轴刻度显示tooltipecharts版本5.4.2关键代码是// 绑定柱状图的鼠标悬浮事件 _bindChartHoverEvents() { if (!this.chartInstance) return this.chartInstance.on(mouseover, (params) { if (params.componentType xAxis params.targetType axisLabel) { this.chartInstance.setOption({ tooltip: { triggerOn: none } }) this.chartInstance.dispatchAction({ type: showTip, seriesIndex: 0, dataIndex: params.dataIndex }) } }) this.chartInstance.on(mouseout, (params) { if (params.componentType xAxis) { this.chartInstance.dispatchAction({ type: hideTip }) this.chartInstance.setOption({ tooltip: { triggerOn: mousemove|click } }) } }) },完整代码template div classbar-chart-container refbar-chart v-loadingloading/div /template script import echarts from /plugin/echarts export default { name: BarChart, props: { // 加载状态 loading: { type: Boolean, default: false }, // 目标条件数据 targetData: { type: Array, default: () [] }, // 对比项数据列表 comparisonDataList: { type: Array, default: () [] }, // 目标条件参数 targetCondition: { type: Object, default: () ({}) }, // 对比项列表 comparisonList: { type: Array, default: () [] }, // 目标条件颜色 targetColor: { type: String, default: #ff0000 }, // 可用颜色列表 availableColors: { type: Array, default: () [] }, // 枚举映射配置 enumMaps: { type: Object, default: () ({}) }, // 国家列表 countryList: { type: Array, default: () [] }, // 渠道选项列表 channelOptions: { type: Array, default: () [] }, // 渠道多选哨兵值 channelAllMultiUi: { type: String, default: __POPO_DATA_ANALYSIS_ALL_CHANNELS__ }, // 目标条件的指标数据 targetMetrics: { type: Object, default: () ({ income: 0, payUser: 0, arppu: 0, arpu: 0, roi: 0 }) }, // 对比项的指标数据列表 comparisonMetrics: { type: Array, default: () [] } }, data() { return { chartInstance: null } }, watch: { // 监听数据变化,重新绘制图表 targetData: { handler() { this.drawChart() }, deep: true }, comparisonDataList: { handler() { this.drawChart() }, deep: true }, targetMetrics: { handler() { this.drawChart() }, deep: true }, comparisonMetrics: { handler() { this.drawChart() }, deep: true } }, mounted() { // 延迟初始化确保容器完全渲染且可见 this.$nextTick(() { this.initChart() this.drawChart() }) // 添加窗口 resize 监听 window.addEventListener(resize, this.handleResize) }, beforeDestroy() { window.removeEventListener(resize, this.handleResize) if (this.chartInstance) { this.chartInstance.dispose() this.chartInstance null } }, methods: { // 格式化人数显示(大于1000显示为1K) formatPersonCount(num) { const number Number(num) if (isNaN(number)) return 0 if (number 1000) { return (number / 1000).toFixed(1).replace(/\.0$/, ) K } return String(Math.round(number)) }, // 根据 countryIds 获取名称列表 getCountryNames(countryIds) { if (!countryIds || !Array.isArray(countryIds) || countryIds.length 0) { return [] } const names [] countryIds.forEach(id { const country this.countryList.find(c c.countryId id) if (country) { const displayName country.memo names.push(displayName) } }) return names }, // 格式化渠道显示文本(统一处理逻辑) formatChannelsText(channels) { if (!channels || !Array.isArray(channels) || channels.length 0) { return 渠道:所有渠道 } // 过滤掉哨兵值 const realChannels channels.filter(v v ! this.channelAllMultiUi) if (realChannels.length 0) { return 渠道:所有渠道 } // 查找渠道名称 const channelLabels realChannels.map(channelValue { const channel this.channelOptions.find(opt opt.value channelValue) return channel ? channel.label : channelValue }) return 渠道:${channelLabels.join(, )} }, // 获取条件的各项显示文本(公共方法) getConditionDisplayTexts(conditionData) { if (!conditionData) { return { channelText: , platformLabel: , dbTypeLabel: , countryText: } } // 获取渠道文本 const channelText this.formatChannelsText(conditionData.channels).replace(渠道:, ) // 获取注册平台文本 const platform conditionData.platform ! undefined ? conditionData.platform : const platformLabel this.enumMaps.platform?.[platform] || 全部平台 // 获取数据库类型文本 const dbTypeLabel this.enumMaps.dbType?.[conditionData.dbType] || 使用ES // 获取详细数据文本 - 显示国家名称 let countryText 全部 if (conditionData.countryIds conditionData.countryIds.length 0) { const names this.getCountryNames(conditionData.countryIds) if (names.length 0) { if (names.length 6) { countryText names.join(, ) } else { countryText names.slice(0, 6).join(, ) ... } } else { countryText 全部 } } return { channelText, platformLabel, dbTypeLabel, countryText } }, // 构建完整的条件文本(公共方法) formatConditionText(condition, prefix ) { const { channelText, platformLabel, dbTypeLabel, countryText } this.getConditionDisplayTexts(condition) let conditionText prefix if (condition.begin condition.end) { conditionText ${condition.begin} ~ ${condition.end} } else if (condition.dateRange condition.dateRange.length 2) { conditionText ${condition.dateRange[0]} ~ ${condition.dateRange[1]} } conditionText 渠道: ${channelText} conditionText 注册平台: ${platformLabel} conditionText 数据库: ${dbTypeLabel} conditionText 详细数据:${countryText} return conditionText }, // 初始化图表实例 initChart() { const chartDom this.$refs[bar-chart] if (chartDom) { if (this.chartInstance) { this.chartInstance.dispose() } this.chartInstance echarts.init(chartDom) this._bindChartHoverEvents() } }, // 绑定柱状图的鼠标悬浮事件 _bindChartHoverEvents() { if (!this.chartInstance) return this.chartInstance.on(mouseover, (params) { if (params.componentType xAxis params.targetType axisLabel) { this.chartInstance.setOption({ tooltip: { triggerOn: none } }) this.chartInstance.dispatchAction({ type: showTip, seriesIndex: 0, dataIndex: params.dataIndex }) } }) this.chartInstance.on(mouseout, (params) { if (params.componentType xAxis) { this.chartInstance.dispatchAction({ type: hideTip }) this.chartInstance.setOption({ tooltip: { triggerOn: mousemove|click } }) } }) }, // 窗口 resize 处理 handleResize() { if (this.chartInstance) { this.chartInstance.resize() } }, // 公共方法重新调整图表大小并重绘 resize() { if (this.chartInstance) { this.chartInstance.resize() // 重新绘制以更新动态计算的宽度 this.drawChart() } }, // 绘制柱状图 async drawChart() { if (!this.chartInstance) return // 检查容器是否可见宽度大于 0 const chartDom this.$refs[bar-chart] if (!chartDom || chartDom.clientWidth 0) { // 图表容器不可见跳过绘制 // console.warn(图表容器不可见跳过绘制) return } // 柱状图检查指标数据是否存在 if (!this.targetMetrics) { console.warn(柱状图指标数据不存在跳过绘制) return } // 调试日志查看传入的数据 // console.log(柱状图绘制 - targetMetrics:, this.targetMetrics) // console.log(柱状图绘制 - comparisonMetrics:, this.comparisonMetrics) // console.log(柱状图绘制 - comparisonList:, this.comparisonList) try { // 构建所有条件列表:目标条件 对比项列表 const allConditions [ { ...this.targetCondition, label: 目标条件, colorIndex: -1 }, ...this.comparisonList.map((item, index) ({ ...item, begin: item.dateRange?.[0] || item.begin, end: item.dateRange?.[1] || item.end, label: 对比项${index 1}, colorIndex: item.colorIndex })) ] // 使用传入的指标数据 const allMetrics [this.targetMetrics, ...this.comparisonMetrics] // 直接使用接口返回的汇总数据不再自己计算 const summaryData allConditions.map((condition, index) { const metrics allMetrics[index] || { income: 0, payUser: 0, arppu: 0, arpu: 0, roi: 0 } // 直接使用后端返回的字段 return { label: condition.label, condition: condition, income: Number(metrics.income || 0), // 充值金额 payUser: Number(metrics.payUser || 0), // 充值人数 arppu: Number(metrics.arppu || 0), // ARPPU arpu: Number(metrics.arpu || 0), // ARPU roi: Number(metrics.roi || 0) // ROI } }) // 准备X轴数据(条件标签,省略显示) const xAxisData summaryData.map((_, index) { if (index 0) return 目标条件... return 对比${index}... }) // 准备各指标的数据 const incomeData summaryData.map(item item.income) const payUserData summaryData.map(item item.payUser) const arppuData summaryData.map(item item.arppu) const arpuData summaryData.map(item item.arpu) const roiData summaryData.map(item item.roi) // 动态计算柱宽与类目间距 const SERIES_COUNT 5 const FIXED_GROUP_GAP 20 const RESERVED_LEFT 60 const RESERVED_RIGHT 260 const containerW (this.$refs[bar-chart] this.$refs[bar-chart].clientWidth) || 1200 const usableW Math.max(200, containerW - RESERVED_LEFT - RESERVED_RIGHT) const categoryW usableW / summaryData.length const rawBarW Math.floor((categoryW - FIXED_GROUP_GAP) / SERIES_COUNT) const barWidth Math.max(4, Math.min(30, rawBarW)) const barCategoryGap Math.min(80, Math.max(10, Math.round((FIXED_GROUP_GAP / categoryW) * 100))) % // 统一的颜色配置 const COLORS { recharge: #f00, person: #ff9903, arppu: #67C23A, arpu: #409EFF, roi: #9B59B6 } const option { color: [COLORS.recharge, COLORS.person, COLORS.arppu, COLORS.arpu, COLORS.roi], legend: { data: [充值金额, 充值人数, ARPPU, ARPU, ROI], top: 10, textStyle: { fontSize: 12 } }, tooltip: { trigger: axis, triggerOn: mousemove|click, axisPointer: { type: shadow, label: { show: true, backgroundColor: rgba(50, 50, 50, 0.8), borderColor: unset, borderWidth: 1, color: #fff, formatter: (params) { const seriesData params.seriesData if (!seriesData || seriesData.length 0) return params.value const dataIndex seriesData[0].dataIndex const summary summaryData[dataIndex] if (!summary) return params.value const condition summary.condition return this.formatConditionText(condition, ${summary.label} ).replace(/:/g, :) } } }, backgroundColor: rgba(255, 255, 255, 0.95), borderColor: #ddd, borderWidth: 1, textStyle: { color: #333 }, formatter: (params) { if (!params || params.length 0) return const dataIndex params[0].dataIndex const summary summaryData[dataIndex] const condition summary.condition const conditionText this.formatConditionText(condition) let result div stylepadding: 10px; min-width: 350px; max-width: 620px; word-wrap: break-word; div stylemargin-bottom: 10px; font-size: 12px; color: #666; line-height: 1.6; word-wrap: break-word; white-space: normal; word-break: break-all; ${summary.label} ${conditionText} /divdiv styleborder-top: 1px solid #eee; padding-top: 8px; params.forEach((item) { if (item.data null || item.data undefined) return let valueText item.data if (item.seriesName ROI) { valueText ${item.data}% } else if ([充值金额, ARPPU, ARPU].includes(item.seriesName)) { valueText ${item.data} USD } result div styledisplay: flex; align-items: center; margin-bottom: 6px; span styledisplay: inline-block; width: 10px; height: 10px; border-radius: 50%; background-color: ${item.color}; margin-right: 8px;/span span${item.seriesName}: ${valueText}/span /div }) result /div/div return result } }, grid: { left: 0px, right: 100px, bottom: 3%, containLabel: true }, xAxis: { type: category, data: xAxisData, triggerEvent: true, barCategoryGap, axisPointer: { type: shadow, label: { show: true } }, axisLabel: { interval: 0, rotate: 0, color: (_, index) { if (index 0) { return this.targetColor } else { const compIndex index - 1 if (compIndex this.comparisonList.length) { return this.availableColors[this.comparisonList[compIndex].colorIndex] } return #333 } }, fontSize: 12, fontWeight: bold } }, yAxis: [ { type: value, name: 金额($), position: left, nameTextStyle: { color: COLORS.recharge }, axisLine: { show: true, lineStyle: { color: COLORS.recharge } }, axisLabel: { color: COLORS.recharge }, splitLine: { show: true } }, { type: value, name: 人数, position: right, offset: 0, nameTextStyle: { color: COLORS.person }, axisLine: { show: true, lineStyle: { color: COLORS.person } }, axisLabel: { formatter: (value) this.formatPersonCount(value), color: COLORS.person }, splitLine: { show: false } }, { type: value, name: ARPPU, position: right, offset: 55, nameTextStyle: { color: COLORS.arppu }, axisLine: { show: true, lineStyle: { color: COLORS.arppu } }, axisLabel: { color: COLORS.arppu }, splitLine: { show: false } }, { type: value, name: ARPU, position: right, offset: 110, nameTextStyle: { color: COLORS.arpu }, axisLine: { show: true, lineStyle: { color: COLORS.arpu } }, axisLabel: { color: COLORS.arpu }, splitLine: { show: false } }, { type: value, name: ROI, position: right, offset: 165, nameTextStyle: { color: COLORS.roi }, axisLine: { show: true, lineStyle: { color: COLORS.roi } }, axisLabel: { color: COLORS.roi }, splitLine: { show: false } } ], series: [ { name: 充值金额, type: bar, data: incomeData, yAxisIndex: 0, barWidth, itemStyle: { color: COLORS.recharge }, label: { show: true, position: top, formatter: {c}, fontSize: 10 } }, { name: 充值人数, type: bar, data: payUserData, yAxisIndex: 1, barWidth, itemStyle: { color: COLORS.person }, label: { show: true, position: top, formatter: (params) this.formatPersonCount(params.value), fontSize: 10 } }, { name: ARPPU, type: bar, data: arppuData, yAxisIndex: 2, barWidth, itemStyle: { color: COLORS.arppu }, label: { show: true, position: top, formatter: {c}, fontSize: 10 } }, { name: ARPU, type: bar, data: arpuData, yAxisIndex: 3, barWidth, itemStyle: { color: COLORS.arpu }, label: { show: true, position: top, formatter: {c}, fontSize: 10 } }, { name: ROI, type: bar, data: roiData, yAxisIndex: 4, barWidth, itemStyle: { color: COLORS.roi }, label: { show: true, position: top, formatter: {c}%, fontSize: 10 } } ] } this.chartInstance.setOption(option, true) } catch (error) { console.error(绘制柱状图失败:, error) } } } } /script style scoped langscss .bar-chart-container { min-height: 400px; height: 400px; width: 100%; margin-top: 10px; } /style