Просмотр исходного кода

fix: 解决首页部分图表tooltip失效bug

Jack Zhou 5 дней назад
Родитель
Сommit
e778bafb05

+ 235 - 230
src/views/Dashboard/src/components/BarChart.vue

@@ -2,265 +2,269 @@
 <script lang="ts" setup>
 import * as echarts from 'echarts'
 import { useThemeStore } from '@/stores/modules/theme'
-import { onMounted, ref, reactive, watch, computed } from 'vue'
+import { onMounted, ref, reactive, watch, computed, onBeforeUnmount } from 'vue'
 import { formatNumber } from '@/utils/tools'
+
 const themeStore = useThemeStore()
+const barRef = ref<HTMLElement | null>(null)
+let chartInstance: echarts.ECharts | null = null
+
+// 定义 Props
 const props = defineProps({
-  BarData: Object,
-  barHeight: Object,
-  saveImageName: String
-})
-const bar_data = ref(props.BarData)
-const bar_ref = ref()
-watch(
-  () => bar_data.value,
-  (current) => {
-    bar_data.value = current
-    initOption.xAxis.data = barName.value
-    initOption.series = bar_series.value
-    initOption.legend.data = Name.value
-    initOption.yAxis.max = Max.value
-    initOption.yAxis.interval = interval.value
-    initOption.title.text = bar_title.value
-    nextTick(() => {
-      barChart.value.setOption(initOption)
-    })
+  BarData: {
+    type: Object,
+    default: () => ({})
+  },
+  barHeight: {
+    type: Object,
+    default: () => ({ height: '100%' })
   },
-  {
-    deep: true
+  saveImageName: {
+    type: String,
+    default: 'data'
   }
-)
-// 最大值
-const Max = computed(() => {
-  return bar_data.value?.Max
-})
-// 刻度
-const interval = computed(() => {
-  return bar_data.value?.interval
-})
-// x轴值
-const barName = computed(() => {
-  return bar_data.value?.barList
-})
-// title
-const bar_title = computed(() => {
-  return bar_data.value?.bar_title
-})
-// series
-const bar_series = computed(() => {
-  return bar_data.value?.barSeries
 })
-// legend标题
-const Name = computed(() => {
-  if (bar_data.value?.barSeries) {
-    return bar_data.value?.barSeries.map((item: any) => {
-      return item.name
-    })
-  } else {
-    return []
-  }
+
+// 计算属性 (直接从 props 获取,保持响应式)
+const barData = computed(() => props.BarData || {})
+const Max = computed(() => barData.value?.Max)
+const interval = computed(() => barData.value?.interval)
+const barName = computed(() => barData.value?.barList || [])
+const bar_title = computed(() => barData.value?.bar_title || '')
+const bar_series = computed(() => barData.value?.barSeries || [])
+const legendData = computed(() => {
+  return bar_series.value.map((item: any) => item.name)
 })
 
-// 数额
-const initOption = reactive({
-  //标题
-  title: {
-    text: bar_title.value || '', //主标题
-    left: 19,
-    top: 9.5,
-    textStyle: {
-      color: '#2B2F36',
-      fontWeight: '700',
-      fontFamily: 'Lato-Light',
-      fontSize: '14px'
-    }
-  },
-  // 间距
-  grid: {
-    top: '18%',
-    left: '1%',
-    right: '2%',
-    bottom: '5%',
-    containLabel: true // 距离包含坐标轴上的文字
-  },
-  // hover时的文字显示
-  tooltip: {
-    show: true,
-    trigger: 'axis',
-    axisPointer: {
-      type: 'shadow'
-    },
-    backgroundColor: '#2b2f36',
-    borderColor: '#2b2f36',
-    formatter: function (params: any) {
-      let str: any = ''
-      let allnum: any = 0
-      str += '<div style= ' + 'color:#FFF;font-family: Lato-Light>' + params[0].name + '</div>'
-      params.forEach((item: any) => {
-        allnum += item.value
-        allnum = Number(allnum.toFixed(2))
-        str +=
-          '<div style= ' +
-          'color:#FFF>' +
-          item.marker +
-          item.seriesName +
-          ': ' +
-          formatNumber(item.value) +
-          '</div>'
-      })
-      allnum = allnum.toFixed(2)
-      str +=
-        '<div style= ' +
-        'color:#FFF;font-family: Lato-Light>Total: ' +
-        formatNumber(allnum) +
-        '</div>'
-      return str
-    },
-    textStyle: {
-      color: '#FFF',
-      fontWeight: 700,
-      fontFamily: 'Lato-Light',
-      fontSize: '14px'
-    }
-  },
-  xAxis: {
-    type: 'category',
-    data: barName.value,
-    axisTick: {
-      show: false
-    },
-    axisLine: {
-      lineStyle: {
-        color: '#eaebed'
+// 构建完整的 Option 配置函数
+// 每次调用都返回最新的配置对象,避免手动修改 reactive 对象的麻烦
+const getOption = () => {
+  return {
+    title: {
+      text: bar_title.value,
+      left: 19,
+      top: 9.5,
+      textStyle: {
+        color: '#2B2F36',
+        fontWeight: '700',
+        fontFamily: 'Lato-Light',
+        fontSize: '14px'
       }
     },
-    axisLabel: {
-      interval: 0,
-      fontFamily: 'Lato-Light',
-      color: '#B5B9BF'
-    }
-  },
-  yAxis: {
-    type: 'value',
-    scale: true,
-    splitLine: {
-      show: true,
-      lineStyle: {
-        type: 'dashed',
-        color: '#3F434A'
-      }
+    grid: {
+      top: '18%',
+      left: '1%',
+      right: '2%',
+      bottom: '5%',
+      containLabel: true
     },
-    axisLine: {
+    tooltip: {
       show: true,
-      lineStyle: {
-        color: '#3F434A'
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      },
+      backgroundColor: '#2b2f36',
+      borderColor: '#2b2f36',
+      formatter: (params: any) => {
+        if (!params || params.length === 0) return ''
+
+        let str = `<div style="color:#FFF;font-family: Lato-Light">${params[0].name}</div>`
+        let allnum = 0
+
+        params.forEach((item: any) => {
+          const val = Number(item.value) || 0
+          allnum += val
+          str += `<div style="color:#FFF">${item.marker}${item.seriesName}: ${formatNumber(val)}</div>`
+        })
+
+        allnum = Number(allnum.toFixed(2))
+        str += `<div style="color:#FFF;font-family: Lato-Light">Total: ${formatNumber(allnum)}</div>`
+        return str
+      },
+      textStyle: {
+        color: '#FFF',
+        fontWeight: 700,
+        fontFamily: 'Lato-Light',
+        fontSize: '14px'
       }
     },
-    axisLabel: {
-      fontFamily: 'Lato-Light',
-      color: '#B5B9BF',
-      formatter: function (value: any) {
-        return formatNumber(value, 0)
+    xAxis: {
+      type: 'category',
+      data: barName.value,
+      axisTick: { show: false },
+      axisLine: {
+        lineStyle: {
+          color: '#eaebed'
+        }
+      },
+      axisLabel: {
+        interval: 0,
+        fontFamily: 'Lato-Light',
+        color: '#B5B9BF'
       }
     },
-    min: 0, // 最小值
-    max: Max.value, // 最大值
-    interval: interval.value // 刻度
-  },
-  legend: {
-    data: Name.value,
-    top: '3%',
-    itemGap: 30,
-    left: '40%',
-    itemHeight: 8, //修改icon图形大小
-    itemWidth: 8, //修改icon图形大小
-    textStyle: {
-      fontSize: 12,
-      fontFamily: 'Lato-Light',
-      color: '#646A73'
-    }
-  },
-  toolbox: {
-    top: 6,
-    right: 8,
-    iconStyle: {
-      borderColor: '#2B2F36'
+    yAxis: {
+      type: 'value',
+      scale: true,
+      splitLine: {
+        show: true,
+        lineStyle: {
+          type: 'dashed',
+          color: '#3F434A'
+        }
+      },
+      axisLine: {
+        show: true,
+        lineStyle: {
+          color: '#3F434A'
+        }
+      },
+      axisLabel: {
+        fontFamily: 'Lato-Light',
+        color: '#B5B9BF',
+        formatter: (value: any) => formatNumber(value, 0)
+      },
+      min: 0,
+      max: Max.value,
+      interval: interval.value
+    },
+    legend: {
+      data: legendData.value,
+      top: '3%',
+      itemGap: 30,
+      left: '40%',
+      itemHeight: 8,
+      itemWidth: 8,
+      textStyle: {
+        fontSize: 12,
+        fontFamily: 'Lato-Light',
+        color: '#646A73'
+      }
     },
-    emphasis: {
+    toolbox: {
+      top: 6,
+      right: 8,
       iconStyle: {
-        borderColor: '#ff7500'
-      } // hover上去时的颜色
+        borderColor: '#2B2F36'
+      },
+      emphasis: {
+        iconStyle: {
+          borderColor: '#ff7500'
+        }
+      },
+      feature: {
+        saveAsImage: {
+          icon: 'path://M588.8 821.44H179.52a38.4 38.4 0 0 1-38.4-38.4v-105.088l202.432-115.584 112.96 64.576c7.872 4.48 17.6 4.48 25.472 0l400.768-228.8-0.32-0.512h0.64v-156.8a89.6 89.6 0 0 0-89.6-89.6H179.456a89.6 89.6 0 0 0-89.6 89.6v542.208a89.6 89.6 0 0 0 89.6 89.6H588.8v-51.2zM141.184 240.896a38.4 38.4 0 0 1 38.4-38.4h613.824a38.4 38.4 0 0 1 38.4 38.4v127.36L469.248 575.104 356.288 510.72a25.6 25.6 0 0 0-19.2-2.496l-6.144 2.56-189.76 108.288V240.896z m168.448 226.432c44.416 0 80.96-33.792 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376l-8.768-0.448c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832c4.096 40.32 36.16 72.448 76.544 76.544l8.832 0.448z m0-51.2a34.624 34.624 0 1 1 0-69.312 34.624 34.624 0 0 1 0 69.312z m445.888 449.024a25.6 25.6 0 0 0 36.224-0.064l138.368-138.368-36.224-36.224-94.592 94.656V545.536h-51.2v239.616l-94.72-94.656-36.224 36.224 138.368 138.432z',
+          show: true,
+          name: bar_title.value || props.saveImageName || 'data',
+          type: 'png',
+          backgroundColor: '#fff'
+        }
+      },
+      showTitle: false
     },
-    feature: {
-      saveAsImage: {
-        icon: 'path://M588.8 821.44H179.52a38.4 38.4 0 0 1-38.4-38.4v-105.088l202.432-115.584 112.96 64.576c7.872 4.48 17.6 4.48 25.472 0l400.768-228.8-0.32-0.512h0.64v-156.8a89.6 89.6 0 0 0-89.6-89.6H179.456a89.6 89.6 0 0 0-89.6 89.6v542.208a89.6 89.6 0 0 0 89.6 89.6H588.8v-51.2zM141.184 240.896a38.4 38.4 0 0 1 38.4-38.4h613.824a38.4 38.4 0 0 1 38.4 38.4v127.36L469.248 575.104 356.288 510.72a25.6 25.6 0 0 0-19.2-2.496l-6.144 2.56-189.76 108.288V240.896z m168.448 226.432c44.416 0 80.96-33.792 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376l-8.768-0.448c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832c4.096 40.32 36.16 72.448 76.544 76.544l8.832 0.448z m0-51.2a34.624 34.624 0 1 1 0-69.312 34.624 34.624 0 0 1 0 69.312z m445.888 449.024a25.6 25.6 0 0 0 36.224-0.064l138.368-138.368-36.224-36.224-94.592 94.656V545.536h-51.2v239.616l-94.72-94.656-36.224 36.224 138.368 138.432z',
-        show: true,
-        name: bar_title.value || props.saveImageName || 'data',
-        type: 'png',
-        backgroundColor: '#fff'
+    series: bar_series.value
+  }
+}
+
+// 初始化图表
+const initChart = () => {
+  if (!barRef.value) return
+
+  // 1. 如果实例已存在,先销毁(防止重复初始化和内存泄漏)
+  if (chartInstance) {
+    chartInstance.dispose()
+  }
+
+  // 2. 创建新实例
+  chartInstance = echarts.init(barRef.value)
+
+  // 3. 设置配置
+  chartInstance.setOption(getOption())
+
+  // 4. 绑定点击事件 (直接使用当前实例)
+  chartInstance.on('click', (params: any) => {
+    paramsdata.value.name = params.name
+    paramsdata.value.type = params.seriesName
+    emits('ClickParams')
+  })
+
+  // 5. 处理 Resize (使用 once 或确保只绑定一次逻辑,这里简单处理为每次 init 都重新绑定前需解绑,但 echarts 内部通常处理较好)
+  // 更好的做法是将 resize 逻辑提取到外部或使用 lodash debounce,这里保持简单但确保逻辑正确
+}
+
+// 监听数据变化
+watch(
+  () => props.BarData,
+  () => {
+    if (chartInstance) {
+      chartInstance.setOption(getOption(), true) // true = notMerge, 强制重置系列数据,防止旧数据残留
+    }
+  },
+  { deep: true }
+)
+
+// 监听主题变化
+watch(
+  () => themeStore.theme,
+  (newVal) => {
+    if (!chartInstance) return
+
+    // 动态修改颜色配置
+    const isDark = newVal === 'dark'
+    const textColor = isDark ? '#f0f1f3' : '#2B2F36'
+    const axisColor = isDark ? '#8d9095' : '#eaebed'
+    const splitLineColor = isDark ? '#8d9095' : '#eaebed' // 注意原代码 dark 模式下 splitLine 也是 #8d9095
+    const toolBorderColor = isDark ? '#f0f1f3' : '#2B2F36'
+    const saveBgColor = isDark ? '#3F434A' : '#fff'
+
+    // 使用 setOption 仅更新变化的部分
+    chartInstance.setOption({
+      title: { textStyle: { color: textColor } },
+      xAxis: { axisLine: { lineStyle: { color: axisColor } } },
+      yAxis: {
+        axisLine: { lineStyle: { color: axisColor } },
+        splitLine: { lineStyle: { color: splitLineColor } }
+      },
+      toolbox: {
+        iconStyle: { borderColor: toolBorderColor },
+        feature: {
+          saveAsImage: { backgroundColor: saveBgColor }
+        }
       }
-    },
-    showTitle: false
+    })
   },
-  series: bar_series.value
-})
+  { immediate: true }
+)
+
+// 监听窗口大小变化
+const handleResize = () => {
+  if (chartInstance) {
+    chartInstance.resize()
+  }
+}
+
 onMounted(() => {
   initChart()
-  clickParams()
+  window.addEventListener('resize', handleResize)
+})
 
-  watch(
-    () => themeStore.theme,
-    (newVal) => {
-      if (newVal === 'dark') {
-        initOption.title.textStyle.color = '#f0f1f3'
-        initOption.xAxis.axisLine.lineStyle.color = '#8d9095'
-        initOption.yAxis.axisLine.lineStyle.color = '#8d9095'
-        initOption.yAxis.splitLine.lineStyle.color = '#8d9095'
-        initOption.toolbox.iconStyle.borderColor = '#f0f1f3'
-        initOption.toolbox.feature.saveAsImage.backgroundColor = '#3F434A'
-        nextTick(() => {
-          barChart.value.setOption(initOption)
-        })
-      } else {
-        initOption.title.textStyle.color = '#2B2F36'
-        initOption.xAxis.axisLine.lineStyle.color = '#eaebed'
-        initOption.yAxis.axisLine.lineStyle.color = '#eaebed'
-        initOption.yAxis.splitLine.lineStyle.color = '#eaebed'
-        initOption.toolbox.iconStyle.borderColor = '#2B2F36'
-        initOption.toolbox.feature.saveAsImage.backgroundColor = '#fff'
-        nextTick(() => {
-          barChart.value.setOption(initOption)
-        })
-      }
-    },
-    {
-      immediate: true,
-      deep: true
-    }
-  )
+onBeforeUnmount(() => {
+  // 清理资源
+  window.removeEventListener('resize', handleResize)
+  if (chartInstance) {
+    chartInstance.dispose()
+    chartInstance = null
+  }
 })
 
+// 暴露数据给父组件
 const emits = defineEmits(['ClickParams'])
 const paramsdata = ref({
   name: '',
   type: ''
 })
-const clickParams = () => {
-  // 监听点击事件
-  barChart.value.on('click', function (params) {
-    paramsdata.value.name = params.name
-    paramsdata.value.type = params.seriesName
-    emits('ClickParams')
-  })
-}
-const barChart = ref()
-const initChart = () => {
-  barChart.value = echarts.init(bar_ref.value)
-  //图表响应式
-  window.addEventListener('resize', () => {
-    barChart.value.resize()
-  })
-}
 
 defineExpose({
   paramsdata
@@ -269,7 +273,7 @@ defineExpose({
 
 <template>
   <div class="com-container">
-    <div ref="bar_ref" id="bar_chart" :style="props.barHeight"></div>
+    <div ref="barRef" class="bar-chart" :style="barHeight"></div>
   </div>
 </template>
 
@@ -280,7 +284,8 @@ defineExpose({
   overflow: hidden;
   position: relative;
 }
-#bar_chart {
+.bar-chart {
   width: 100%;
+  height: 100%;
 }
 </style>

+ 256 - 224
src/views/Dashboard/src/components/SellerChart.vue

@@ -1,259 +1,289 @@
-<!-- 横形柱状图 -->
 <script lang="ts" setup>
 import * as echarts from 'echarts'
 import { useThemeStore } from '@/stores/modules/theme'
-import { onMounted, ref, reactive, watch, computed } from 'vue'
+import { onMounted, ref, reactive, watch, computed, onBeforeUnmount, nextTick } from 'vue'
 import { formatNumber } from '@/utils/tools'
+
+// --- Types ---
+interface SellerItem {
+  name: string
+  value: number
+  color?: string
+  city_name?: string
+  [key: string]: any
+}
+
+interface IntervalConfig {
+  Max?: number
+  interval?: number
+}
+
+interface Props {
+  SellerData?: SellerItem[]
+  Interval?: IntervalConfig
+  saveImageName?: string
+}
+
+// --- Props & Emits ---
+const props = withDefaults(defineProps<Props>(), {
+  SellerData: () => [],
+  Interval: () => ({}),
+  saveImageName: 'chart_image'
+})
+
+const emits = defineEmits<{
+  ClickParams: [data: { name: string; cityName?: string; value: number }]
+}>()
+
 const themeStore = useThemeStore()
-const props = defineProps({
-  SellerData: Array,
-  Interval: Object,
-  saveImageName: String
+const sellerRef = ref<HTMLElement | null>(null)
+let chartInstance: echarts.ECharts | null = null
+
+// --- Computed Data ---
+// 确保数据是响应式的,但不需要在 computed 里做复杂映射,直接在 option 中处理或使用简单映射
+const sortedData = computed(() => {
+  if (!props.SellerData || props.SellerData.length === 0) return []
+  return [...props.SellerData].sort((a, b) => a.value - b.value)
 })
-const seller_data = ref(props.SellerData)
-const seller_interval = ref(props.Interval)
-const seller_ref = ref()
-watch(
-  () => props.SellerData,
-  (current) => {
-    seller_data.value = current
-    nextTick(() => {
-      initChart()
-    })
-  },
-  {
-    deep: true
-  }
-)
-watch(
-  () => props.Interval,
-  (current) => {
-    seller_interval.value = current
-    initOption.xAxis.max = Max.value
-    initOption.xAxis.interval = interval.value
-    nextTick(() => {
-      initChart()
-    })
-  },
-  {
-    deep: true
+
+const xAxisMax = computed(() => props.Interval?.Max)
+const xAxisInterval = computed(() => props.Interval?.interval)
+
+// --- Chart Initialization ---
+const initChart = () => {
+  if (!sellerRef.value) return
+
+  // 如果实例已存在,先销毁避免重复初始化导致的内存泄漏或渲染异常
+  if (chartInstance) {
+    chartInstance.dispose()
   }
-)
-// 最大值
-const Max = computed(() => {
-  return seller_interval.value?.Max
-})
-// 刻度
-const interval = computed(() => {
-  return seller_interval.value?.interval
-})
-// y轴值
-const sellerName = computed(() => {
-  return seller_data.value?.map((item: any) => {
-    return item.name
-  })
-})
-// 数额
-const sellerValue = computed(() => {
-  return seller_data.value?.map((item: any) => {
-    return item.value
-  })
-})
-// 获取数据中的color
-const ColorValue = computed(() => {
-  return seller_data.value?.map((item: any) => {
-    return item.color
-  })
-})
-const initOption = reactive({
-  // 间距
-  grid: {
-    top: '12%',
-    left: '3%',
-    right: '6%',
-    bottom: '3%',
-    containLabel: true // 距离包含坐标轴上的文字
-  },
-  // hover时的文字显示
-  tooltip: {
-    show: true,
-    backgroundColor: '#2b2f36',
-    borderColor: '#2b2f36',
-    formatter: function (params: any) {
-      var str =
-        params.name +
-        '<div style= ' +
-        'color:#FFF>' +
-        params.marker +
-        formatNumber(params.value) +
-        '</div>'
-      return str
+
+  chartInstance = echarts.init(sellerRef.value)
+  updateOption()
+
+  // 绑定点击事件
+  chartInstance.off('click') // 防止重复绑定
+  chartInstance.on('click', handleChartClick)
+
+  // 监听窗口大小变化
+  window.addEventListener('resize', handleResize)
+}
+
+const updateOption = () => {
+  if (!chartInstance) return
+
+  const isDark = themeStore.theme === 'dark'
+  const gridColor = isDark ? '#3F434A' : '#eaebed'
+  const textColor = '#B5B9BF'
+  const toolboxBorderColor = isDark ? '#f0f1f3' : '#ed6d00'
+  const toolboxBgColor = isDark ? '#2B2F36' : '#fff'
+
+  const option = {
+    grid: {
+      top: '12%',
+      left: '3%',
+      right: '6%',
+      bottom: '3%',
+      containLabel: true
     },
-    textStyle: {
-      color: '#FFF',
-      fontWeight: 700,
-      fontFamily: 'Lato-Light',
-      fontSize: '14px'
-    }
-  },
-  // X轴
-  xAxis: {
-    splitLine: {
-      lineStyle: {
-        type: 'dashed',
-        color: '#eaebed'
+    tooltip: {
+      show: true,
+      backgroundColor: '#2b2f36',
+      borderColor: '#2b2f36',
+      formatter: (params: any) => {
+        return `${params.name}<div style="color:#FFF">${params.marker}${formatNumber(params.value)}</div>`
+      },
+      textStyle: {
+        color: '#FFF',
+        fontWeight: 700,
+        fontFamily: 'Lato-Light',
+        fontSize: '14px'
       }
     },
-    type: 'value',
-    axisLine: {
-      show: false
+    xAxis: {
+      type: 'value',
+      splitLine: {
+        lineStyle: {
+          type: 'dashed',
+          color: gridColor
+        }
+      },
+      axisLine: { show: false },
+      axisLabel: {
+        fontFamily: 'Lato-Light',
+        color: textColor,
+        formatter: (value: number) => formatNumber(value, 0)
+      },
+      min: 0,
+      max: xAxisMax.value, // 直接使用 computed,echarts setOption 会处理响应式更新
+      interval: xAxisInterval.value
     },
-    axisLabel: {
-      fontFamily: 'Lato-Light',
-      color: '#B5B9BF',
-      formatter: function (value: any) {
-        return formatNumber(value, 0)
+    yAxis: {
+      type: 'category',
+      data: sortedData.value.map((item) => item.name),
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: {
+        fontFamily: 'Lato-Light',
+        color: textColor
       }
+      // 移除 yAxis 上的 tooltip 配置,通常 tooltip 在根级别或 xAxis 控制即可,避免冲突
     },
-    min: 0, // 最小值
-    max: Max.value, // 最大值
-    interval: interval.value // 刻度
-  },
-  // y轴
-  yAxis: {
-    axisLine: {
-      show: false
-    },
-    axisTick: {
-      show: false
-    },
-    axisLabel: {
-      fontFamily: 'Lato-Light',
-      color: '#B5B9BF'
-    },
-    type: 'category',
-    data: sellerName,
-    // 工具提示
-    tooltip: {
-      trigger: 'axis', // 触发类型,轴触发,axis为鼠标移到一条柱状图显示
-      axisPointer: {
-        type: 'line', // 默认为line,line为直线,cross为十字准星,shadow为阴影
-        z: 0,
-        lineStyle: {
-          color: '#FFF'
+    series: [
+      {
+        type: 'bar',
+        data: sortedData.value.map((item) => item.value),
+        barWidth: '20',
+        barCategoryGap: '0%',
+        itemStyle: {
+          color: (params: { dataIndex: number }) => {
+            const colors = sortedData.value.map((item) => item.color).filter(Boolean)
+            if (colors.length === 0) return undefined // 让 ECharts 使用默认色
+            return colors[params.dataIndex % colors.length]
+          }
+        },
+        label: {
+          show: true,
+          color: '#646A73',
+          position: 'right',
+          fontFamily: 'Lato-Light',
+          formatter: (data: { value: number }) => formatNumber(data.value)
         }
       }
-    }
-  },
-  series: [
-    {
-      type: 'bar',
-      data: sellerValue,
-      barWidth: '20',
-      itemStyle: {
-        color: function (params: { dataIndex: number }) {
-          return ColorValue.value[params.dataIndex % ColorValue.value.length]
+    ],
+    toolbox: {
+      top: 4,
+      right: 8,
+      showTitle: false,
+      iconStyle: {
+        borderColor: toolboxBorderColor
+      },
+      emphasis: {
+        iconStyle: {
+          borderColor: '#ff7500'
         }
       },
-      barCategoryGap: '0%', // 消除不同系列间的间隔
-      // 设置柱形文字的样式
-      label: {
-        show: true,
-        color: '#646A73',
-        position: 'right',
-        fontFamily: 'Lato-Light',
-        // 数据每三位加一个逗号
-        formatter: function (data: { value: { toString: () => string } }) {
-          return formatNumber(Number(data.value.toString()))
+      feature: {
+        saveAsImage: {
+          icon: 'path://M588.8 821.44H179.52a38.4 38.4 0 0 1-38.4-38.4v-105.088l202.432-115.584 112.96 64.576c7.872 4.48 17.6 4.48 25.472 0l400.768-228.8-0.32-0.512h0.64v-156.8a89.6 89.6 0 0 0-89.6-89.6H179.456a89.6 89.6 0 0 0-89.6 89.6v542.208a89.6 89.6 0 0 0 89.6 89.6H588.8v-51.2zM141.184 240.896a38.4 38.4 0 0 1 38.4-38.4h613.824a38.4 38.4 0 0 1 38.4 38.4v127.36L469.248 575.104 356.288 510.72a25.6 25.6 0 0 0-19.2-2.496l-6.144 2.56-189.76 108.288V240.896z m168.448 226.432c44.416 0 80.96-33.792 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376l-8.768-0.448c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832c4.096 40.32 36.16 72.448 76.544 76.544l8.832 0.448z m0-51.2a34.624 34.624 0 1 1 0-69.312 34.624 34.624 0 0 1 0 69.312z m445.888 449.024a25.6 25.6 0 0 0 36.224-0.064l138.368-138.368-36.224-36.224-94.592 94.656V545.536h-51.2v239.616l-94.72-94.656-36.224 36.224 138.368 138.432z',
+          show: true,
+          name: props.saveImageName,
+          type: 'png',
+          backgroundColor: toolboxBgColor
         }
       }
     }
-  ],
-  toolbox: {
-    top: 4,
-    right: 8,
-    iconStyle: {
-      borderColor: '#ed6d00'
-    },
-    emphasis: {
-      iconStyle: {
-        borderColor: '#ff7500'
-      } // hover上去时的颜色
-    },
-    feature: {
-      saveAsImage: {
-        icon: 'path://M588.8 821.44H179.52a38.4 38.4 0 0 1-38.4-38.4v-105.088l202.432-115.584 112.96 64.576c7.872 4.48 17.6 4.48 25.472 0l400.768-228.8-0.32-0.512h0.64v-156.8a89.6 89.6 0 0 0-89.6-89.6H179.456a89.6 89.6 0 0 0-89.6 89.6v542.208a89.6 89.6 0 0 0 89.6 89.6H588.8v-51.2zM141.184 240.896a38.4 38.4 0 0 1 38.4-38.4h613.824a38.4 38.4 0 0 1 38.4 38.4v127.36L469.248 575.104 356.288 510.72a25.6 25.6 0 0 0-19.2-2.496l-6.144 2.56-189.76 108.288V240.896z m168.448 226.432c44.416 0 80.96-33.792 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376l-8.768-0.448c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832c4.096 40.32 36.16 72.448 76.544 76.544l8.832 0.448z m0-51.2a34.624 34.624 0 1 1 0-69.312 34.624 34.624 0 0 1 0 69.312z m445.888 449.024a25.6 25.6 0 0 0 36.224-0.064l138.368-138.368-36.224-36.224-94.592 94.656V545.536h-51.2v239.616l-94.72-94.656-36.224 36.224 138.368 138.432z',
-        show: true,
-        name: props.saveImageName,
-        type: 'png',
-        backgroundColor: '#fff'
-      }
-    },
-    showTitle: false
   }
-})
 
+  chartInstance.setOption(option, { notMerge: false }) // notMerge: false 允许增量更新
+}
+
+const handleResize = () => {
+  chartInstance?.resize()
+}
+
+const handleChartClick = (params: any) => {
+  const clickedItem = sortedData.value.find((item) => item.name === params.name)
+  if (clickedItem) {
+    emits('ClickParams', {
+      name: clickedItem.name,
+      cityName: clickedItem.city_name,
+      value: clickedItem.value
+    })
+  }
+}
+
+// --- Watchers ---
+// 监听数据变化
+watch(
+  () => props.SellerData,
+  () => {
+    nextTick(() => {
+      updateOption()
+    })
+  },
+  { deep: true }
+)
+
+// 监听区间配置变化
+watch(
+  () => props.Interval,
+  () => {
+    nextTick(() => {
+      updateOption()
+    })
+  },
+  { deep: true }
+)
+
+// 监听主题变化
+watch(
+  () => themeStore.theme,
+  () => {
+    nextTick(() => {
+      updateOption()
+    })
+  },
+  { immediate: true }
+)
+
+// --- Lifecycle ---
 onMounted(() => {
   initChart()
-  clickParams()
-  watch(
-    () => themeStore.theme,
-    (newVal) => {
-      if (newVal === 'dark') {
-        initOption.xAxis.splitLine.lineStyle.color = '#3F434A'
-        initOption.toolbox.iconStyle.borderColor = '#f0f1f3'
-        initOption.toolbox.feature.saveAsImage.backgroundColor = '#2B2F36'
-        initChart()
-      } else {
-        initOption.xAxis.splitLine.lineStyle.color = '#eaebed'
-        initOption.toolbox.iconStyle.borderColor = '#2B2F36'
-        initOption.toolbox.feature.saveAsImage.backgroundColor = '#fff'
-        initChart()
-      }
-    },
-    {
-      immediate: true,
-      deep: true
-    }
-  )
 })
-const emits = defineEmits(['ClickParams'])
-const paramsdata = ref()
-const paramscityname = ref()
-const clickParams = () => {
-  const seller_chart = echarts.init(seller_ref.value)
-  // 监听点击事件
-  seller_chart.on('click', function (params) {
-    paramsdata.value = params.name
-    seller_data.value?.forEach((item: any) => {
-      if (item.name == paramsdata.value) {
-        paramscityname.value = item.city_name
-      }
+
+onBeforeUnmount(() => {
+  if (chartInstance) {
+    chartInstance.dispose()
+    chartInstance = null
+  }
+  window.removeEventListener('resize', handleResize)
+})
+
+// --- Expose ---
+// 如果父组件需要访问内部数据,可以通过 emit 返回,或者暴露特定方法
+// 原代码暴露了 ref,这里为了保持兼容,创建一个响应式对象暴露
+const exposedData = ref({
+  paramsdata: '',
+  paramscityname: ''
+})
+
+// 拦截 emit 来更新暴露的数据 (可选,取决于父组件如何使用)
+const originalEmit = emits
+// 注意:在 setup 中直接重写 emits 比较麻烦,通常建议父组件直接监听事件获取数据。
+// 如果必须保持原有 expose 行为,可以在 click handler 里更新这个 ref
+const handleChartClickWithExpose = (params: any) => {
+  const clickedItem = sortedData.value.find((item) => item.name === params.name)
+  if (clickedItem) {
+    exposedData.value.paramsdata = clickedItem.name
+    exposedData.value.paramscityname = clickedItem.city_name || ''
+    emits('ClickParams', {
+      name: clickedItem.name,
+      cityName: clickedItem.city_name,
+      value: clickedItem.value
     })
-    emits('ClickParams')
-  })
-}
-const initChart = () => {
-  seller_data.value?.sort((a: any, b: any) => {
-    return a.value - b.value // 从大到小排序
-  })
-  const seller_chart = echarts.init(seller_ref.value)
-  //图表响应式
-  seller_chart.setOption(initOption)
-  //图表响应式
-  window.addEventListener('resize', () => {
-    seller_chart.resize()
-  })
+  }
 }
+// 重新绑定带 expose 逻辑的点击事件
+watch(
+  () => chartInstance,
+  (newInst) => {
+    if (newInst) {
+      newInst.off('click')
+      newInst.on('click', handleChartClickWithExpose)
+    }
+  }
+)
+
 defineExpose({
-  paramsdata,
-  paramscityname
+  paramsdata: computed(() => exposedData.value.paramsdata),
+  paramscityname: computed(() => exposedData.value.paramscityname)
 })
 </script>
 
 <template>
   <div class="com-container">
-    <div ref="seller_ref" id="seller_chart"></div>
+    <div ref="sellerRef" class="seller-chart"></div>
   </div>
 </template>
 
@@ -264,8 +294,10 @@ defineExpose({
   overflow: hidden;
   position: relative;
 }
-#seller_chart {
+
+.seller-chart {
   width: 100%;
   height: 310px;
+  // 移除 #id 选择器,使用 class 更符合 Vue 规范
 }
 </style>