index.vue 12.7 KB
<template>
  <!-- 原生方式,没有使用vue-echarts -->
  <n-space vertical>
    <n-spin :show="show">
      <div :style="`width:${w}px;height:${h}px;`" ref="map3DRef"></div>
    </n-spin>
  </n-space>
</template>

<script setup lang="ts">
import { onMounted, ref, nextTick, PropType, toRefs, watch, reactive } from 'vue'
import 'echarts-gl'
import { registerMap, init } from 'echarts/core'
import type { EChartsType } from 'echarts/core'
import config, {
  areaEnum,
  dataPointI,
  optionType,
  historyParentType,
  backMapLevel,
  levelFunc,
  regionMapParentArea,
  specialTreatmentAnhui,
  setScale
} from './config'
import { getGeoJsonMap } from '@/api/external/common'
import { ThreeMapEnum } from '@/enums/external/mapEnum'
import cloneDeep from 'lodash/cloneDeep'

const props = defineProps({
  chartConfig: {
    type: Object as PropType<config>,
    required: true
  }
})

const backIcon =
  'path://M853.333333 245.333333H245.333333l93.866667-93.866666c12.8-12.8 12.8-34.133333 0-46.933334-12.8-12.8-34.133333-12.8-46.933333 0l-145.066667 145.066667c-12.8 12.8-12.8 34.133333 0 46.933333l145.066667 145.066667c6.4 6.4 14.933333 10.666667 23.466666 10.666667s17.066667-4.266667 23.466667-10.666667c12.8-12.8 12.8-34.133333 0-46.933333L256 311.466667h597.333333c6.4 0 10.666667 4.266667 10.666667 10.666666v426.666667c0 6.4-4.266667 10.666667-10.666667 10.666667H170.666667c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h682.666666c40.533333 0 74.666667-34.133333 74.666667-74.666667V320c0-40.533333-34.133333-74.666667-74.666667-74.666667z'

const { w, h } = toRefs(props.chartConfig.attr)

const { mapRegion, dataset, drillingIn, saveClickRegion, geo3D, series } = toRefs(props.chartConfig.option)

const map3DRef = ref<Nullable<HTMLElement>>()

const show = ref(true)

const chartInstance = ref<Nullable<EChartsType>>()

const toolBoxOption = ref({
  show: true,
  right: 110,
  top: 20,
  feature: {
    myFullButton: {
      show: false,
      title: '返回',
      icon: backIcon,
      iconStyle: {
        color: ''
      },
      onclick: () => handleBack()
    }
  }
})

const excludeCountryLevels = ['PROVINCE', 'CITY'] //如果从右侧配置选择全中国

const includeCityLevels = ['CITY'] //如果从右侧配置选择省份

//元组 优化if elseif else分支 隐藏返回图标
const backIconMappingLevels: levelFunc[][] = [
  [
    (levelStr: string) => levelStr === areaEnum.COUNTRY,
    (level: string) => excludeCountryLevels.includes(level),
    () => (toolBoxOption.value.feature.myFullButton.show = true),
    () => (toolBoxOption.value.feature.myFullButton.show = false)
  ],
  [
    (levelStr: string) => levelStr === areaEnum.PROVINCE,
    (level: string) => includeCityLevels.includes(level),
    () => (toolBoxOption.value.feature.myFullButton.show = true),
    () => (toolBoxOption.value.feature.myFullButton.show = false)
  ]
]

watch(
  () => props.chartConfig.option,
  (newData: optionType) => {
    const { iconColor, iconDistanceRight, iconDistanceTop, mapRegion } = newData
    const { saveSelect } = mapRegion
    const { levelStr } = saveSelect
    const findBackLevel = backIconMappingLevels.find((backLevelItem: levelFunc[]) =>
      backLevelItem[0](levelStr)
    ) as backMapLevel[]
    if (findBackLevel) {
      if (findBackLevel[0]) {
        const findLevel = findBackLevel[1](saveLevelStr.level)
        if (findLevel) findBackLevel[2]()
        else findBackLevel[3]()
      }
    }
    toolBoxOption.value.feature.myFullButton.iconStyle.color = iconColor //返回图标颜色
    toolBoxOption.value.right = iconDistanceRight
    toolBoxOption.value.top = iconDistanceTop
  },
  {
    deep: true
  }
)

//追加echarts右上角自带toolbox
props.chartConfig.option = {
  ...props.chartConfig.option,
  ...{ toolbox: toolBoxOption.value }
}

//地图点击返回
const handleBack = async () => {
  stopWatch()
  if (drillingIn.value) {
    //如果是从右边配置里设置的,比如点击四川省,然后点击返回
    const savePopParent = saveHistoryParent.value.pop()
    let saveAdcode = savePopParent?.adcode as string | number
    saveLevelStr.level = savePopParent?.level as string
    if (!savePopParent) {
      saveAdcode = getParentAdcode(mapRegion.value.adcode).adcodeNum
      saveLevelStr.level = (regionMapParentArea as Recordable)[mapRegion.value.saveSelect.levelStr]
    }
    if (saveAdcode === 0) {
      saveAdcode = 'china'
      saveLevelStr.level = areaEnum.COUNTRY
    }
    const exist = await getGeojson(saveAdcode)
    const adcode = saveAdcode === 100000 ? 'china' : saveAdcode
    saveClickRegion.value.level = saveLevelStr.level
    if (exist) {
      mapRegion.value.areaName = getParentAdcode(mapRegion.value.adcode).areaName
      //fix 解决点击下钻返回后页面为空问题
      mapRegion.value.adcode = adcode
    }
  }
}

//地图点击
const handleMap3DClick = async (params: Recordable) => {
  if (drillingIn.value) {
    const { name } = params
    const geoJson = JSON.parse(saveGeojson.value?.geoJson)
    geoJson?.features.forEach((item: Recordable) => {
      if (item.properties.name === name) {
        const level = item.properties.level.toUpperCase()
        const adcode = item.properties.adcode
        const areaName = item.properties.name
        if (level === 'DISTRICT') return //下钻暂且不支持地区
        if (String(adcode).startsWith('15') && level === areaEnum.CITY) return //特殊处理地区码15开头的
        mapRegion.value.adcode = adcode
        mapRegion.value.areaName = areaName
        saveClickRegion.value.level = level
        saveLevelStr.level = level
        saveHistoryParent.value.push({
          adcode: specialTreatmentAnhui.includes(item.properties.name)
            ? JSON.parse(item.properties.parent)?.adcode
            : item.properties.parent.adcode,
          level: (regionMapParentArea as Recordable)[level],
          areaName: saveGeojson.value.name
        })
      }
    })
  }
}

const saveGeojson: Recordable = ref({}) // 保存一份服务端返回的geojson

const chinaDefaultRegionId = ref(100000) //如果是china则adcode为100000

const saveLevelStr = reactive<{ level: historyParentType['level'] }>({
  // 地区级别
  level: ''
})

const saveHistoryParent = ref<historyParentType[]>([])

//动态注册地图
const getGeojson = (regionId: number | string) => {
  try {
    return new Promise<boolean>(resolve => {
      const { levelStr } = mapRegion.value.saveSelect //右侧配置项获取的行政级别
      getGeoJsonMap(
        regionId === 'china' ? chinaDefaultRegionId.value : regionId,
        !saveLevelStr.level ? levelStr : saveLevelStr.level //没有则获取右侧配置的行政级别
      ).then(res => {
        saveGeojson.value = res.data //保存一份服务端返回的数据
        const { geoJson, name, level, code } = res.data
        const geoJsonFile = JSON.parse(geoJson)
        if (!geoJsonFile) return
        const nameChina = name === '中国' ? 'china' : name //为中国的话,registerMap第一个必须是china,否则显示不出来
        /**
         * 主要注意的点,registerMap中的第一个参数需要和series中的map匹配,否则渲染不出地图,
         * 比如map: '北京市' echarts.registerMap('北京市', beijingGeoJSON);
         */
        registerMap(level === areaEnum.COUNTRY ? nameChina : !mapRegion.value.areaName ? code : name, {
          geoJSON: geoJsonFile,
          specialAreas: {}
        }) //注册geoJSON
        resolve(true)
        show.value = false
        changeOption.value = true
      })
    })
  } catch (error) {
    show.value = false
    console.error('注册三维地图出错,出错原因->', error)
    //注册出错则注册空的,不然在选择正确的adcode,则视图无法更新
    registerMap(mapRegion.value.adcode, { geoJSON: {} as any, specialAreas: {} })
  }
}

//异步时先注册空的 保证初始化不报错
registerMap(mapRegion.value.adcode, { geoJSON: {} as any, specialAreas: {} })

//传adcode 获取上级
const getParentAdcode = (adcode: number) => {
  let adcodeNum = 100000
  let areaName = ''
  const geoJson = JSON.parse(saveGeojson.value?.geoJson)
  geoJson.features.forEach((item: Recordable) => {
    if (item.properties.adcode === adcode) {
      adcodeNum = item.properties.parent.adcode
      areaName = saveGeojson.value.name
    }
  })
  return { adcodeNum, areaName }
}

// 初始化三维地图
const initMap3D = async () => {
  chartInstance.value = init(map3DRef.value as HTMLElement) as any as Nullable<EChartsType>
  await nextTick()
  await getGeojson(mapRegion.value.adcode)
  await nextTick().then(() => {
    handleRegisterMapNameAndData(mapRegion.value.adcode, dataset.value, 'china')
  })
  chartInstance.value?.on('click', (e: Recordable) => {
    if (!e) return
    handleMap3DClick(e)
  })
}

onMounted(() => initMap3D())

// 动态注册 series中的map必须和registerMap的第一个参数匹配,否则渲染不出
const handleRegisterMapNameAndData = (adcode: string | number, data: Recordable, areaName: string) => {
  geo3D.value.map = !areaName ? adcode : areaName // coordinateSystem使用了geo3D,不能删除这一行
  series.value.forEach((item: Recordable) => {
    if (item.type === ThreeMapEnum.MAP3D) {
      item.map = !areaName ? adcode : areaName
      item.data = data[ThreeMapEnum.MAP3D]
    }
    if (item.type === ThreeMapEnum.BAR3D) {
      item.data = data[ThreeMapEnum.BAR3D]
    }
  })
}

// 动态触发渲染
const handleSetOption = (instance: EChartsType, option: Recordable) => {
  if (!instance) return
  try {
    instance && instance.clear()
    instance && instance.setOption(option)
  } catch (error) {
    console.error('动态触发渲染出错,出错原因->', error)
  }
}

watch(
  () => [w.value, h.value],
  async (newValue: number[]) => {
    await nextTick()
    chartInstance.value?.resize({
      width: newValue.at(-2) + 'px',
      height: newValue.at(-1) + 'px'
    } as Recordable)
  }
)

//处理数据标点
const handleDataPoint = (newData: string | number, areaName: string) => {
  if (newData === 'china') {
    // 全国则展示所有的标点
    handleRegisterMapNameAndData(newData, dataset.value, 'china')
  } else {
    // 展示对应区域的标点
    series.value.forEach((item: Recordable) => {
      if (item.type === ThreeMapEnum.MAP3D) {
        item.map = !areaName ? newData : areaName
        item.data = dataset.value[ThreeMapEnum.MAP3D].filter((dataItem: dataPointI) => {
          if (String(dataItem.adcode) === String(!areaName ? newData : areaName)) {
            return dataItem
          } else if (dataItem.name === String(!areaName ? newData : areaName)) {
            return dataItem
          }
        })
        const cloneDeepData = cloneDeep(item.data)
        cloneDeepData.forEach((item: dataPointI) => {
          item.name = item.city_name
        })
        item.data = cloneDeepData.filter((item: Recordable) => item.name !== null) || []
      }
      if (item.type === ThreeMapEnum.BAR3D) {
        item.data = dataset.value[ThreeMapEnum.BAR3D]
      }
    })
  }
}
const changeOption = ref(false)

// 监听地图展示区域发生变化
watch(
  () => `${props.chartConfig.option.mapRegion.adcode}`,
  async (newData: number | string) => {
    try {
      await getGeojson(newData)
      const { distance} = setScale(String(newData))
      const option = props.chartConfig.option
      // 修复缩放
      const { series,geo3D } = option || {}
      
      series?.forEach((item: Recordable) => {
        if (item.type === 'map3D') {
          item.viewControl = {
            ...item?.viewControl,
            distance,
          }
        }
      })
      handleRegisterMapNameAndData(newData, dataset.value, mapRegion.value.areaName)
      handleDataPoint(newData, mapRegion.value.areaName)
      changeOption.value = true
      handleSetOption(chartInstance.value!, { ...option, series ,geo3D:{...geo3D,viewControl:{distance:distance}}})
    } catch (error) {
      console.error('展示区域发生变化出错,出错原因->', error)
    }
  },
  {
    immediate: true,
    deep: true
  }
)

// 实时监听地图右侧配置项变化
const stopWatch = watch(
  props.chartConfig.option,
  async newData => {
    try {
      if (changeOption.value) {
        // handleSetOption(chartInstance.value!, newData)
      }
    } catch (error) {
      console.error('监听地图右侧配置项变化,出错原因->', error)
    }
  },
  {
    deep: true
  }
)
watch(()=>props.chartConfig.option.isShowExecute,async()=>{
  const {adcode,areaName} = props.chartConfig.option.mapRegion || {}
  handleRegisterMapNameAndData(adcode,dataset.value,mapRegion.value.areaName)
  handleDataPoint(adcode,areaName)

  const { distance } = setScale(String(adcode))
  const option = props.chartConfig.option
  const { series,geo3D } = option || {}
  handleSetOption(chartInstance.value!, { ...option, series ,geo3D:{...geo3D,viewControl:{distance:distance}}})
})
</script>