Browse Source

fix🐛: 优化地图

gitboyzcf 2 weeks ago
parent
commit
7e4205b457
3 changed files with 298 additions and 169 deletions
  1. 104 69
      src/components/ECharts/ECharts.vue
  2. 4 0
      src/components/ECharts/optionsConfig.js
  3. 190 100
      src/layout/HomeMap/HomeMap.vue

+ 104 - 69
src/components/ECharts/ECharts.vue

@@ -1,11 +1,16 @@
 <template>
-  <div :id="id" :class="className" :style="{ height, width }" />
+  <div :id="id" :class="['echart-container', className]" :style="{ height, width }" />
 </template>
+
 <script setup>
+  import { ref, shallowRef, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
   import * as echarts from 'echarts'
-  import 'echarts-liquidfill'
+  // 如果需要 3D 效果,请确保也引入了 echarts-gl
+  // import 'echarts-liquidfill'
 
-  let myChart = shallowRef(null)
+  // 1. 使用 shallowRef:这是性能优化的关键,防止 Vue 对巨大的 ECharts 实例进行深度响应式追踪
+  const chartRef = ref(null)
+  const myChart = shallowRef(null)
 
   const props = defineProps({
     // 区分chart
@@ -14,88 +19,118 @@
       default: 'e-chart',
       required: true
     },
-    // 用于添加样式
-    className: {
-      type: String,
-      default: ''
-    },
-    // echarts 画布宽高
-    width: {
-      type: String,
-      default: '100%'
-    },
-    height: {
-      type: String,
-      default: '300px'
-    },
-    // echarts loading
-    loading: {
-      type: Boolean,
-      default: true
-    },
-    // echarts需要得 options以及其他配置
-    fullOptions: {
-      type: Object,
-      default: () => ({}),
-      required: true
-    }
+    className: { type: String, default: '' },
+    width: { type: String, default: '100%' },
+    height: { type: String, default: '300px' },
+    loading: { type: Boolean, default: false },
+    // 建议直接传递 options,结构更清晰
+    fullOptions: { type: Object, default: () => ({}), required: true },
+    // 渲染器选择:3D 或复杂动画建议用 canvas,普通图表建议用 svg
+    renderer: { type: String, default: 'canvas' },
+    theme: { type: String, default: null }
   })
 
-  //重绘图表函数
-  const resizeHandler = () => {
-    myChart.value.resize()
+  // 2. 优化防抖逻辑:使用标准闭包或引入 lodash-es
+  const debounce = (fn, delay) => {
+    let timer = null
+    return (...args) => {
+      if (timer) clearTimeout(timer)
+      timer = setTimeout(() => fn.apply(this, args), delay)
+    }
   }
-  //设置防抖,保证无论拖动窗口大小,只执行一次获取浏览器宽高的方法
-  const debounce = (fun, delay) => {
-    let timer
-    return function () {
-      if (timer) {
-        clearTimeout(timer)
-      }
-      timer = setTimeout(() => {
-        fun()
-      }, delay)
+
+  const handleResize = debounce(() => {
+    myChart.value?.resize({ animation: { duration: 300 } })
+  }, 100)
+
+  const destroyed = () => {
+    myChart.value.clear() // 清空当前画布内容
+    myChart.value.dispose() // 销毁实例
+    myChart.value = null // 释放引用
+  }
+
+  // 3. 初始化图表
+  const initChart = () => {
+    myChart.value = echarts.init(document.getElementById(props.id), props.theme, {
+      renderer: props.renderer,
+      // 增加设备像素比优化,防止高分屏模糊
+      devicePixelRatio: window.devicePixelRatio
+    })
+
+    if (props.loading) {
+      showChartLoading()
+    } else {
+      renderChart()
     }
   }
-  const cancalDebounce = debounce(resizeHandler, 50)
 
-  //页面成功渲染,开始绘制图表
-  onMounted(() => {
-    //配置为 svg 形式,预防页面缩放而出现模糊问题;图表过于复杂时建议使用 Canvas
-    myChart.value = echarts.init(document.getElementById(props.id), { renderer: 'svg' })
-    myChart.value.showLoading({
-      text: '',
+  const showChartLoading = () => {
+    myChart.value?.showLoading({
+      text: '加载中...',
       color: '#409eff',
-      textColor: '#000',
-      maskColor: '#041027',
+      maskColor: 'rgba(255, 255, 255, 0)',
       zlevel: 0
     })
+  }
 
-    //自适应不同屏幕时改变图表尺寸
-    window.addEventListener('resize', cancalDebounce)
-  })
-  //页面销毁前,销毁事件和实例
-  onBeforeUnmount(() => {
-    window.removeEventListener('resize', cancalDebounce)
-    myChart.value.dispose()
-  })
-  //监听图表数据时候变化,重新渲染图表
+  const renderChart = () => {
+    if (!myChart.value) return
+    myChart.value.hideLoading()
+    // useDirtyRect: true 可进一步优化频繁更新时的性能
+    myChart.value.setOption(props.fullOptions.options, { notMerge: true })
+  }
+
+  // 4. 监听器优化
   watch(
-    () => [props.fullOptions.options, props.loading],
-    () => {
-      if (!props.loading) {
-        myChart.value.hideLoading()
-        myChart.value.setOption(props.fullOptions.options, true)
-        nextTick(() => {
-          cancalDebounce()
-        })
+    () => props.fullOptions.options,
+    (newOptions) => {
+      if (newOptions && myChart.value) {
+        renderChart()
       }
     },
     { deep: true }
   )
 
+  watch(
+    () => props.loading,
+    (val) => {
+      if (val) showChartLoading()
+      else myChart.value?.hideLoading()
+    }
+  )
+
+  onMounted(() => {
+    initChart()
+    window.addEventListener('resize', handleResize)
+
+    // 额外增加:监听容器大小变化(不仅仅是窗口变化)
+    // 解决侧边栏折叠、局部区域缩放导致的图表不自适应问题
+    if (window.ResizeObserver) {
+      const ro = new ResizeObserver(() => handleResize())
+      ro.observe(document.getElementById(props.id))
+    }
+  })
+
+  onBeforeUnmount(() => {
+    window.removeEventListener('resize', handleResize)
+    if (myChart.value) {
+      myChart.value.dispose()
+      myChart.value = null
+    }
+  })
+
+  // 暴露给父组件的方法
   defineExpose({
-    myChart
+    initChart,
+    myChart,
+    destroyed,
+    resize: () => handleResize()
   })
 </script>
-<style scoped lang="scss"></style>
+
+<style scoped>
+  .echart-container {
+    /* 确保容器有基本渲染能力 */
+    min-height: 100%;
+  }
+</style>

File diff suppressed because it is too large
+ 4 - 0
src/components/ECharts/optionsConfig.js


+ 190 - 100
src/layout/HomeMap/HomeMap.vue

@@ -86,7 +86,7 @@
       lonlat: [91.132212, 29.660361]
     }
   ]
-  const fullOptions = ref({ options: {} })
+  const fullOptions = shallowRef({ options: {} })
   // 判断当前经纬度是否在规定区域内
   function isPointInMultiPolygon(point, multiPolygons) {
     for (let polygon of multiPolygons) {
@@ -140,65 +140,139 @@
   let mapType = 0
   let currentMap = { name: 'china', code: 100000 }
   let optionConfig = null
+  // const setData = async (code = 100000, name = 'china') => {
+  //   const geoJSON = await API_GET_GEO_JSON_GET({ code })
+  //   const data = []
+  //   loading.value = false
+  //   const newGeojson = formatJson(geoJSON)
+  //   newGeojson.features.forEach((item) => {
+  //     data.push({
+  //       name: item.properties.name,
+  //       adcode: item.properties.adcode,
+  //       level: item.properties.level,
+  //       lonlat: item.properties.center
+  //     })
+  //   })
+
+  //   // 过滤出当前地图的点位,过滤掉不属于改地图区域的点位
+  //   // let currentData = toolTipData.filter((v) => {
+  //   //   return isPointInMultiPolygon([v.lonlat[0], v.lonlat[1]], newGeojson.features)
+  //   // })
+
+  //   // echartsRef.value.destroyed()
+  //   optionConfig = chartOptions.setMapOption(
+  //     data,
+  //     geoJSON,
+  //     code === 100000 ? toolTipData : [],
+  //     name
+  //   )
+  //   optionConfig.option.geo.layoutSize = name == 'china' ? '140%' : '100%'
+  //   currentMap = { name, code }
+  //   const series =
+  //     mapType === 0
+  //       ? optionConfig.getSeries1()
+  //       : mapType === 1
+  //         ? optionConfig.getSeries2()
+  //         : optionConfig.getSeries1()
+
+  //   console.log(optionConfig)
+  //   console.log(series)
+
+  //   optionConfig.option.series.splice(1, 0, ...series)
+  //   // fullOptions.value.options = optionConfig.option
+  //   echartsRef.value.myChart.setOption(optionConfig.option, { notMerge: false })
+  //   // echartsRef.value.myChart.on('georoam', function (params) {
+  //   //   var option = echartsRef.value.myChart.getOption() //获得option对象
+  //   //   if (params.zoom != null && params.zoom != undefined) {
+  //   //     //捕捉到缩放时
+  //   //     option.geo[1].zoom = option.geo[0].zoom //下层geo的缩放等级跟着上层的geo一起改变
+  //   //     option.geo[1].center = option.geo[0].center //下层的geo的中心位置随着上层geo一起改变
+  //   //   } else {
+  //   //     //捕捉到拖曳时
+  //   //     option.geo[1].center = option.geo[0].center //下层的geo的中心位置随着上层geo一起改变
+  //   //   }
+  //   //   echartsRef.value.myChart.dispatchAction({
+  //   //     type: 'restore'
+  //   //   })
+  //   //   echartsRef.value.myChart.setOption(option) //设置option
+  //   // })
+  // }
+
+  // 保存事件监听器的引用,以便在组件销毁时移除
+
   const setData = async (code = 100000, name = 'china') => {
-    const geoJSON = await API_GET_GEO_JSON_GET({ code })
-    const data = []
-    loading.value = false
-    const newGeojson = formatJson(geoJSON)
-    newGeojson.features.forEach((item) => {
-      data.push({
-        name: item.properties.name,
-        adcode: item.properties.adcode,
-        level: item.properties.level,
-        lonlat: item.properties.center
+    if (echartsRef.value?.myChart) {
+      // 如果是从 3D 饼图切回地图,或者地图 adcode 变化大
+      // 强制清理一次画布可以缓解 WebGL 压力
+      echartsRef.value.myChart.clear()
+    }
+    try {
+      // 1. 开启 Loading 并锁定逻辑,防止重复触发
+      loading.value = true
+
+      const geoJSON = await API_GET_GEO_JSON_GET({ code })
+      if (!geoJSON) return
+
+      // 2. 格式化数据
+      const data = []
+      const newGeojson = formatJson(geoJSON)
+      newGeojson.features.forEach((item) => {
+        data.push({
+          name: item.properties.name,
+          adcode: item.properties.adcode,
+          level: item.properties.level,
+          lonlat: item.properties.center
+        })
       })
-    })
 
-    // 过滤出当前地图的点位,过滤掉不属于改地图区域的点位
-    // let currentData = toolTipData.filter((v) => {
-    //   return isPointInMultiPolygon([v.lonlat[0], v.lonlat[1]], newGeojson.features)
-    // })
+      // 3. 释放旧的 optionConfig 引用
+      let optionConfig = chartOptions.setMapOption(
+        data,
+        geoJSON,
+        code === 100000 ? toolTipData : [],
+        name
+      )
 
-    echartsRef.value.myChart?.clear()
-    optionConfig = chartOptions.setMapOption(
-      data,
-      geoJSON,
-      code === 100000 ? toolTipData : [],
-      name
-    )
-    optionConfig.option.geo.forEach((v) => {
-      v.layoutSize = name == 'china' ? '140%' : '100%'
-    })
-    currentMap = { name, code }
-    optionConfig.option.series.splice(
-      1,
-      0,
-      ...(mapType === 0
-        ? optionConfig.getSeries1()
-        : mapType === 1
-          ? optionConfig.getSeries2()
-          : optionConfig.getSeries1())
-    )
-    fullOptions.value.options = optionConfig.option
-    // echartsRef.value.myChart.on('georoam', function (params) {
-    //   var option = echartsRef.value.myChart.getOption() //获得option对象
-    //   if (params.zoom != null && params.zoom != undefined) {
-    //     //捕捉到缩放时
-    //     option.geo[1].zoom = option.geo[0].zoom //下层geo的缩放等级跟着上层的geo一起改变
-    //     option.geo[1].center = option.geo[0].center //下层的geo的中心位置随着上层geo一起改变
-    //   } else {
-    //     //捕捉到拖曳时
-    //     option.geo[1].center = option.geo[0].center //下层的geo的中心位置随着上层geo一起改变
-    //   }
-    //   echartsRef.value.myChart.dispatchAction({
-    //     type: 'restore'
-    //   })
-    //   echartsRef.value.myChart.setOption(option) //设置option
-    // })
+      // 4. 适配不同层级的缩放
+      optionConfig.option.geo.forEach((g) => {
+        g.layoutSize = name === 'china' ? '140%' : '100%'
+        // 关键:强制关闭缩放,防止多层 geo 错位
+        g.roam = false
+      })
+
+      currentMap = { name, code }
+
+      // 5. 获取 Series 逻辑
+      const seriesData = mapType === 1 ? optionConfig.getSeries2() : optionConfig.getSeries1()
+
+      // 清理并重新组合 series
+      // 建议:不要用 splice,直接重新赋值以保证数据的纯净
+      optionConfig.option.series = [optionConfig.option.series[0], ...seriesData]
+
+      // 6. 执行渲染
+      if (echartsRef.value && echartsRef.value.myChart) {
+        // 关键优化:使用 notMerge: true 彻底释放旧地图显存
+        echartsRef.value.myChart.setOption(optionConfig.option, {
+          notMerge: true,
+          lazyUpdate: false
+        })
+      }
+    } catch (error) {
+      console.error('地图数据加载失败', error)
+    } finally {
+      loading.value = false
+    }
+  }
+  const eventListeners = {
+    dblclick: null,
+    click: null
   }
+
   const init = async () => {
     setData()
-    echartsRef.value.myChart.on('dblclick', async function (params) {
+
+    // 保存dblclick事件监听器
+    eventListeners.dblclick = async function (params) {
       if (params.data) {
         const { adcode, name, level } = params.data
         if (level === 'district' || params.componentSubType != 'map') {
@@ -207,66 +281,71 @@
         useHomeStore.setCode({ code: adcode, name })
         setData(adcode, name)
       }
-    })
-    echartsRef.value.myChart.on('click', async function (params) {
+    }
+    echartsRef.value.myChart.on('dblclick', eventListeners.dblclick)
+
+    // 保存click事件监听器
+    eventListeners.click = async function (params) {
       console.log(params)
       if (params.seriesType === 'scatter') {
         visible.value = true
       }
-    })
+    }
+    echartsRef.value.myChart.on('click', eventListeners.click)
 
+    // mitt事件监听器
     $mitt.on('onPreLevel', (item) => {
       if (useHomeStore.codes[useHomeStore.codes.length - 1].code === item.code) return
       useHomeStore.codes = useHomeStore.codes.slice(
         0,
         useHomeStore.codes.findIndex((v) => v.name === item.name) + 1
       )
+
       setData(item.code, item.name === '中国' ? 'china' : item.name)
     })
-    const toolsIndex = (i) => {
-      const series = fullOptions.value.options.series
-      let newSeries = [series[0]]
-      const index = series.findIndex((v) => v.name === '热力图')
-      if (typeof i === 'number') {
-        mapType = i
-        if (i === 0) {
-          if (index != -1) {
-            newSeries = [...newSeries, series[index], ...optionConfig.getSeries1()]
-          } else {
-            newSeries = [...newSeries, ...optionConfig.getSeries1()]
-          }
-        } else if (i === 1) {
-          if (index != -1) {
-            newSeries = [...newSeries, series[index], ...optionConfig.getSeries2()]
-          } else {
-            newSeries = [...newSeries, ...optionConfig.getSeries2()]
-          }
-        }
-        fullOptions.value.options.series = newSeries
-      } else {
-        if (i.type === 'rlt') {
-          if (i.selected) {
-            series.push(...optionConfig.getSeries3())
-          } else {
-            if (index == -1) return
-            delete fullOptions.value.options.visualMap
-            series.splice(
-              series.findIndex((v) => v.name === '热力图'),
-              1
-            )
-          }
-        }
-        //  else if (i.type === 'xs') {
-        //   if (i.selected) {
-        //     toolsIndex(mapType)
-        //     toolsIndex({ type: 'rlt' })
-        //   } else {
-        //     fullOptions.value.options.series = [series[0]]
-        //   }
-        // }
-      }
-    }
-    $mitt.on('onToolsIndex', toolsIndex)
+    // const toolsIndex = (i) => {
+    //   const series = fullOptions.value.options.series
+    //   let newSeries = [series[0]]
+    //   const index = series.findIndex((v) => v.name === '热力图')
+    //   if (typeof i === 'number') {
+    //     mapType = i
+    //     if (i === 0) {
+    //       if (index != -1) {
+    //         newSeries = [...newSeries, series[index], ...optionConfig.getSeries1()]
+    //       } else {
+    //         newSeries = [...newSeries, ...optionConfig.getSeries1()]
+    //       }
+    //     } else if (i === 1) {
+    //       if (index != -1) {
+    //         newSeries = [...newSeries, series[index], ...optionConfig.getSeries2()]
+    //       } else {
+    //         newSeries = [...newSeries, ...optionConfig.getSeries2()]
+    //       }
+    //     }
+    //     fullOptions.value.options.series = newSeries
+    //   } else {
+    //     if (i.type === 'rlt') {
+    //       if (i.selected) {
+    //         series.push(...optionConfig.getSeries3())
+    //       } else {
+    //         if (index == -1) return
+    //         delete fullOptions.value.options.visualMap
+    //         series.splice(
+    //           series.findIndex((v) => v.name === '热力图'),
+    //           1
+    //         )
+    //       }
+    //     } else if (i.type === 'xs') {
+    //       // if (i.selected) {
+    //       //   toolsIndex(mapType)
+    //       //   toolsIndex({ type: 'rlt' })
+    //       // } else {
+    //       //   fullOptions.value.options.series = [series[0]]
+    //       // }
+    //     }
+    //   }
+    // }
+    // $mitt.on('onToolsIndex', toolsIndex)
   }
 
   watch(
@@ -277,9 +356,20 @@
       }
     }
   )
+
   onMounted(() => {
     init()
   })
+
+  onUnmounted(() => {
+    // 移除echarts事件监听器
+    if (echartsRef.value?.myChart) {
+      echartsRef.value.myChart.off('dblclick', eventListeners.dblclick)
+      echartsRef.value.myChart.off('click', eventListeners.click)
+    }
+    // 移除mitt事件监听器
+    $mitt.off('onPreLevel')
+  })
 </script>
 
 <style lang="scss">

Some files were not shown because too many files changed in this diff