echarts生成3D立体地图react组件

地图+散点图效果:

react项目中安装echarts、echarts-gl依赖:

复制代码
npm install echarts echarts-gl

文件目录结构:

地图组件map目录下文件代码,点击散点图圆点触发事件handleCityClick:

index.jsx:

javascript 复制代码
import { useRef, useEffect, useState, useCallback } from "react";
import * as echarts from "echarts";
import 'echarts-gl';
import customSettings from "@config/customSettings";
import styles from "./style.less";

const { PROVINCE_NAME, MAP_NAME, MAP_JSON } = customSettings;

const ProvinceMap = ({
  option,
  onCityClick,
  width = "100%",
  height = "100%",
}) => {
  const [selectedCity, setSelectedCity] = useState('');
  const chartRef = useRef(null);
  const mapInstanceRef = useRef(null); // 使用ref保存实例

  const handleCityClick = useCallback((name) => {
    console.log('点击参数:', name);
    if (name === selectedCity) {
      setSelectedCity(PROVINCE_NAME);
      onCityClick(PROVINCE_NAME);
    } else {
      setSelectedCity(name);
      onCityClick(name);
    }
  }, [selectedCity, onCityClick, PROVINCE_NAME]);

  useEffect(() => {
    if (!chartRef.current) return;

    // 初始化图表
    mapInstanceRef.current = echarts.init(chartRef.current);
    echarts.registerMap(MAP_NAME, MAP_JSON);

    // 设置选项
    mapInstanceRef.current.setOption(option);

    // 添加点击事件 - 3D地图需要特殊处理
    const handleClick = (params) => {
      console.log('完整点击参数:', params);

      // 3D地图点击参数可能的结构
      const cityName = params.name ||
        (params.data && params.data.name) ||
        (params.seriesName === 'map' && params.name);

      if (cityName) {
        handleCityClick(cityName);
      }
    };

    mapInstanceRef.current.on('click', handleClick);

    // 响应式调整
    const resizeObserver = new ResizeObserver(() => {
      mapInstanceRef.current?.resize();
    });
    resizeObserver.observe(chartRef.current.parentElement);

    // 清理函数
    return () => {
      mapInstanceRef.current?.off('click', handleClick);
      mapInstanceRef.current?.dispose();
      resizeObserver.disconnect();
    };
  }, [option, MAP_NAME, MAP_JSON, handleCityClick]);

  // 单独监听selectedCity变化时不重新渲染整个地图
  useEffect(() => {
    if (!mapInstanceRef.current) return;

    // 更新选中状态而不重新初始化
    mapInstanceRef.current.setOption({
      series: [{
        selectedMode: 'single',
        select: {
          itemStyle: {
            color: '#FF4500' // 选中颜色
          }
        }
      }]
    }, true);
  }, [selectedCity]);

  return (
    <div
      ref={chartRef}
      className={styles.mapClass}
      style={{ width, height }}
    />
  );
};

export default ProvinceMap;

如果想要点击地图区域触发点击事件可以将series的内容替换成下面的代码:

javascript 复制代码
series: [
    {
      type: 'map3D',
      map: customSettings.MAP_NAME,
      regionHeight: 3, // 区域高度
      itemStyle: {
        color: '#1E90FF',
        borderWidth: 1
      },
      emphasis: {
        itemStyle: {
          color: '#FFA500'
        }
      }
    }
  ]

mapOption.js:

javascript 复制代码
import customSettings from '@config/customSettings';

const backgroundColor = '#FFFFFF';
const borderColor = '#cbd1dc';

export const MAP_OPTION = {
  tooltip: {
    show: false,
    backgroundColor,
    borderColor, // 修改边框颜色
    triggerOn: "mousemove", // 鼠标移动时触发
    axisPointer: {
      type: "none",
    },
    position: (point, params, dom, rect, size) => {
      // 解决tooltip在最右侧时部分被遮挡
      let obj = {};
      if (point[0] > size.viewSize[0] / 2) {
        // 鼠标位置位于echarts容器的一半位置右侧时,提示框显示在左侧
        obj["left"] = point[0] - size.contentSize[0] - 20;
      } else {
        obj["right"] = size.viewSize[0] - size.contentSize[0] * 2;
      }
      if (point[1] > size.viewSize[1] / 2) {
        // 鼠标位置位于echarts容器的一半位置下侧时,提示框显示在上侧
        obj["top"] = point[1] - size.contentSize[1] - 20;
      } else {
        obj["bottom"] = size.viewSize[1] - size.contentSize[1] * 1;
      }
      return obj;
    },
    backgroundColor: "rgba(255,255,255,0.90)", // 提示标签背景颜色
    textStyle: { color: "#fff" }, // 提示标签字体颜色
  },
  visualMap: {
    show: false, // 是否显示 visualMap-continuous 组件。如果设置为 false,不会显示,但是数据映射的功能还存在
    type: "continuous", // 类型为连续型视觉映射
    calculable: false, // 是否显示拖拽用的手柄(手柄能拖拽调整选中范围)
    inRange: {
      // 定义在选中范围中的视觉元素
      color: ["#ffd289", "#ff9c45"],
    },
  },
  // 地理坐标系组件
  geo3D: {
    map: customSettings.MAP_NAME, // 地图名称,要和echarts注册的地图名称一致
    // 添加交互
    roam: false, // 设置为false,禁用地图的漫游功能
    // 添加立体效果
    boxHeight: 10, // 地图的高度,默认为0,即不显示立体效果
    regionHeight: 3, // 地图上每个区域的立体高度,默认为0,即不显示立体效果
    itemStyle: { // 地图区域的样式设置
      color: '#1E90FF20',
      opacity: 0.8,
      borderWidth: 0.5,
      borderColor: '#00FFFF'
    },
    emphasis: { // 鼠标悬停在地图区域上的样式设置
      itemStyle: {
        color: 'rgba(0, 153, 255, 0.3)'
      },
      label: { // 鼠标悬停在地图区域上显示的标签样式设置
        show: true,
        textStyle: {
          color: '#fff',
          fontSize: 12,
          backgroundColor: 'rgba(0, 0, 0, 0.7)',
          borderRadius: 3,
          padding: [4, 8]
        }
      }
    },
    light: { // 光照设置,用于增强立体效果
      main: { // 主光源设置
        intensity: 1.2, // 光源强度,默认为1.2
        shadow: true, // 是否显示阴影,默认为true
        shadowQuality: 'high', // 阴影质量,默认为'high'
        alpha: 30, // 固定俯仰角度(上下)
        beta: 40 // 固定方位角度(左右)
      },
      ambient: { // 环境光设置
        intensity: 0.3 // 环境光强度,默认为0.3
      }
    },
    viewControl: { // 视角控制设置,用于调整地图的旋转和缩放等操作
      distance: 120, // 视角距离地图的距离,默认为120
      alpha: 40, // 固定俯仰角度(上下)
      beta: 0,  // 固定方位角度(左右)
      autoRotate: false, // 设置为false
      rotateSensitivity: 0, // 禁用旋转
      zoomSensitivity: 0, // 0:禁用缩放
    }
  },
  series: [{
    type: 'scatter3D',
    coordinateSystem: 'geo3D',
    symbolSize: 12,
    encode: {
      tooltip: 2 // 第三个值(value[2])显示在tooltip中
    },
    itemStyle: {
      color: '#FF4500',
      opacity: 0.8
    },
    emphasis: {
      itemStyle: {
        color: '#00FFFF',
        borderWidth: 2,
        borderColor: '#FFF'
      },
      label: {
        show: true,
        formatter: '{b}',
        color: '#FFF',
        backgroundColor: 'rgba(0,0,0,0.7)',
        padding: [4, 8]
      }
    },
    data: [
      // 格式: { name: '地市', value: [经度, 纬度, 值] }
      { name: '郑州', value: [113.62, 34.75, 90] },      // 省会
      { name: '洛阳', value: [112.45, 34.62, 80] },      // 古都
      { name: '南阳', value: [112.53, 33.01, 75] },      // 豫西南
      { name: '新乡', value: [113.88, 35.30, 70] },      // 豫北
      { name: '商丘', value: [115.65, 34.45, 65] },      // 豫东
      { name: '安阳', value: [114.38, 36.10, 60] },      // 豫北
      { name: '开封', value: [114.31, 34.80, 55] },      // 古都
      { name: '焦作', value: [113.24, 35.22, 50] },      // 豫西北
      { name: '平顶山', value: [113.19, 33.77, 45] },    // 豫中
      { name: '信阳', value: [114.07, 32.13, 40] },      // 豫南
      { name: '周口', value: [114.65, 33.62, 35] },      // 豫东南
      { name: '驻马店', value: [114.02, 32.98, 30] },    // 豫南
      { name: '许昌', value: [113.81, 34.02, 25] },      // 豫中
      { name: '漯河', value: [114.02, 33.58, 20] },      // 豫中
      { name: '三门峡', value: [111.20, 34.78, 15] },    // 豫西
      { name: '鹤壁', value: [114.30, 35.75, 10] },      // 豫北
      { name: '濮阳', value: [115.03, 35.77, 8] },       // 豫东北
      { name: '济源', value: [112.60, 35.08, 5] }        // 省直辖
    ]
  }]
};

style.less:

css 复制代码
.mapClass {
  :global {
    .custom-tooltip-box {
      width: 306px;
      overflow: auto;
      color: #868686; // li文字颜色
      font-size: 13px;
      line-height: 22px;
      font-weight: 400;
      .title-text {
        font-family: PingFangSC-Semibold;
        font-size: 15px;
        color: #262626;
        text-align: center;
        font-weight: 600;
        padding-bottom: 3px;
        border-bottom: 1px dashed #d9d9d9;
      }
      ul {
        margin-top: 7px;
        padding-left: 5px;
        padding-right: 10px;
        li {
          list-style: none;
          line-height: 1.5;
          position: relative;
          .value-box {
            position: absolute;
            display: inline-block;
            right: 0;
            .num {
              color: #262626;
              font-family: PingFangSC-Semibold;
              font-size: 13px;
              font-weight: 600;
            }
          }
        }
      }
    }
  }
}

引用地图组件的父组件index.jsx:

javascript 复制代码
import React, { useState, useEffect } from 'react';
import { Progress, Table, Select, Drawer, Radio, DatePicker, Button, message, } from 'antd';
import { SettingOutlined } from '@ant-design/icons';
import styles from './style.less';
import ProvinceMap from './map/index';
import { MAP_OPTION } from './map/mapOption';
import { } from './service';
import customSettings from '@config/customSettings';

const { PROVINCE_NAME, CAPITAL_AREA_NAME } = customSettings;

export default function index() {

  const [mapIndexsObj, setMapIndexsObj] = useState([]); // 省地市指标值
  const [mapOption, setMapOption] = useState({}); // 地图option
  const [areaName, setAreaName] = useState(CAPITAL_AREA_NAME); // 地市名称

  // 获取地图option
  useEffect(() => {
    let option = { ...MAP_OPTION };
    // 鼠标移到图里面的浮动提示框
    option.tooltip.formatter = (params) => {
      const { name } = params;
      let shortName = '';
      if (PROVINCE_NAME.includes('内蒙')) {
        shortName = name.slice(0, name.length - 1);
      } else {
        shortName = name;
      }
      let tooltipHtml = `
      <div class="custom-tooltip-box">
        <div class="title-text">
          ${shortName}
        </div>
        <ul>`;
      // 使用 map 函数遍历数据并构建 HTML 字符串
      tooltipHtml += mapIndexsObj[shortName]?.map((item, index) => `
        <li>
          <span>${item.label}</span>
          <div class="value-box">
            <span class="num">${(item.value != undefined && item.value != null) ? item.value : '--'}</span>
            <span class="unit" style="display: ${(item.value != undefined && item.value != null) ? 'inline' : 'none'}">${item.unit}</span>
          </div>
        </li>
      `).join('') || ''; // .join('')去除map返回结果中的逗号
      tooltipHtml += `</ul></div>`
      return tooltipHtml;
    };
    setMapOption(option);
  }, [mapIndexsObj]);

  // 点击地图获取地市名称
  const handleCityClick = (cityName) => {
    setAreaName(cityName);
  };

  return (
    <div className={styles.container}>
      <ProvinceMap option={mapOption} onCityClick={handleCityClick} />
    </div>
  )
}

配置项文件customSettings:

javascript 复制代码
// 用于设置地图、查询条件中"地市"下拉菜单的初始值
import henanMap from "@/assets/map/henan.json";

export default {
  PROVINCE_ID: 37, // 与接口返回的类型保持一致
  PROVINCE_NAME: '河南省',
  CAPITAL_AREA_ID: 371, // 与接口返回的类型保持一致
  CAPITAL_AREA_NAME: '郑州市',
  MAP_JSON: henanMap,
  MAP_NAME: 'henan',
};
相关推荐
WindrunnerMax几秒前
深感一无所长,准备试着从零开始写个富文本编辑器
前端·javascript·github
緑水長流1 小时前
什么是Promise?什么是async和await?
前端·javascript·vue.js
Mintopia1 小时前
Three.js 相机(Camera)的使用详解
前端·javascript·three.js
Mintopia1 小时前
Node.js 中path模块的深度解析与实战应用
前端·javascript·node.js
webxin6661 小时前
微信端视频自动播放的兼容方案
前端·javascript
小钰能吃三碗饭1 小时前
第二篇:【前端进阶之道】现代 JavaScript 高级特性实战指南
前端·javascript·typescript
kovli1 小时前
红宝书第三讲:JavaScript 操作符与流程控制详解
前端·javascript
蓝色的猴子1 小时前
JS 中html的document
前端·javascript·html
伟笑2 小时前
elementui table禁用全选,一次限制勾选一项。
前端·javascript·elementui
介si啥呀~3 小时前
Vuex 的使用场景和使用方法
前端·javascript·vue.js·vuex