俄罗斯Yandex地图实战

由于地域限制,一般在俄语区只能使用Yandex做地图展示,这里介绍yandex 常用的一些api

由于很多资料和ai都是2.1的版本,顾选择了较老的版本2.1做开发,最新更新到v3的版本

1.初始化

引入资源

js 复制代码
<script src="https://api-maps.yandex.ru/2.1/?lang=zh_CN&onload=initMap" defer></script>
 <div id="map"></div> 

初始化

  • 第一个参数为div的id
js 复制代码
     let map;
     
    // 初始化地图
    function initMap() {
      const mapCenter = [31.0, 114.0]; // 中国中南部,让多个城市可见
      map = new ymaps.Map("map", {
        center: mapCenter,
        zoom: 5
      });
    } 

封装初始化

为了方便维护,我们封装一下 yandex的初始化。

这里通过动态插入js方式初始化

注意原来的onload 通过这种方式无法正常触发。可以正常同步方法编写代码即可。如在:map = new (window as any).ymaps.Map(...)初始化后下面接着写需要初始化的逻辑

html 复制代码
<template> 
     <div class="main-container" ref="mapContainer" v-loading="loading">
  </div>
</template>
<script lang="ts" setup>
  import { ref,  onMounted, onUnmounted } from 'vue'   
  const props = defineProps({
    apiKey: {
      type: String,
      required: true,
    },
  })  
  const loading = ref(false)   
  let map: any | null = null   
  const mapContainer = ref<HTMLElement | null>(null) 

  // 检查Yandex Maps是否已加载
  const isYandexMapsLoaded = (): boolean => {
    return typeof (window as any).ymaps !== 'undefined'
  }
 
  const loadYandexMaps = (): Promise<void> => {
    return new Promise((resolve, reject) => {
      // 检查是否已经加载过
      if (isYandexMapsLoaded()) {
        resolve()
        return
      }

      // 创建script标签
      const script = document.createElement('script') 
      const apiKey = props.apiKey //  //
      script.src = `https://api-maps.yandex.ru/2.1/?apikey=${apiKey}&lang=ru_RU`
      script.type = 'text/javascript'
      script.defer = true

      // 加载完成回调
      script.onload = () => { 
        ;(window as any).ymaps.ready(() => {
          resolve()
        })
      } 
      script.onerror = (error) => {
        console.error('Failed to load Yandex Maps API', error)
        reject(error)
      } 
      document.head.appendChild(script)
    })
  } 
  
  // 初始化地图
  const initMap = () => {
    if (!mapContainer.value || !isYandexMapsLoaded()) return

    let initLat = 55.751244
    let initLng = 37.618423
    let initZoom = 15
 

    // 创建地图实例
    map = new (window as any).ymaps.Map(mapContainer.value, {
      center: [initLat, initLng], // 莫斯科坐标作为默认值
      zoom: initZoom,
      controls: ['fullscreenControl'] 
    }) 
    
    // 在这里正常写同步的代码
  }

  // 组件卸载时清理地图实例
  onUnmounted(() => {
    if (map) {
      map.destroy()
      map = null
    }
  })
 
  onMounted(async () => {
    loading.value = true
    try {
      await loadYandexMaps()  
      initMap() 
      loading.value = false
    } catch (error) {
      console.error('Error onMounted:', error)
      loading.value = false
    }
  })
 
  
</script>

<style lang="scss" scoped>
  .main-container {
    position: relative;
      width: 100%;
    height: calc(100vh - 70px);
  } 
  .loading {
    text-align: center;
    padding: 20px;
    color: #909399;
  }
 
</style>

2.标记

系统自带标记

  • 第一个参数为坐标
  • 第二个为弹框内容
js 复制代码
  const placemark = new ymaps.Placemark(
         [39.9042, 116.4074],
          { balloonContent: '<h3>北京站点</h3><p>这是首都核心站点,功能齐全。</p>' }, 
        );

点击效果

自定义标记图标

由于自带的图标好丑,一般使用美工设计的icon。

  • 通过参数iconLayout 指定渲染类型
  • 通过参数iconImageHref 设置对应的图片
js 复制代码
   myPlacemarkWithContent = new ymaps.Placemark(
           [39.9042, 116.4074],
            {
                hintContent: "A custom placemark icon with contents",
                balloonContent: "This one --- for Christmas", 
            },
            { 
                iconLayout: "default#imageWithContent", 
                iconImageHref: "dashboard/local1.png", // 自定义图片
                // The size of the placemark.
                iconImageSize: [40, 46], 
                iconImageOffset: [-24, -24], 
                iconContentOffset: [15, 15],  
            }
        );
        
    map.geoObjects.add(myPlacemarkWithContent);

图标追加信息显示

通过ymaps.templateLayoutFactory.createClass 我们可以在图标基础上追加信息

js 复制代码
        MyIconContentLayout = ymaps.templateLayoutFactory.createClass(
            '<div style="color: #FFFFFF; font-weight: bold;">$[properties.iconContent]</div>'
        ), 
        myPlacemarkWithContent = new ymaps.Placemark(
           [39.9042, 116.4074],
            {
                hintContent: "A custom placemark icon with contents",
                balloonContent: "This one --- for Christmas",
                iconContent: "M",
            },
            { 
                iconLayout: "default#imageWithContent",
                // Custom image for the placemark icon.
                iconImageHref: "dashboard/local1.png",
                // The size of the placemark.
                iconImageSize: [40, 46], 
                iconImageOffset: [-24, -24], 
                iconContentOffset: [15, 15], 
                iconContentLayout: MyIconContentLayout,
            }
        );
        map.geoObjects.add(myPlacemarkWithContent);

3.汇总显示

系统自带Clusterer

系统自动Clusterer

js 复制代码
var myMap = new ymaps.Map(
            "map",
            {
                center: [55.751574, 37.573856],
                zoom: 9,
            },
            {
                searchControlProvider: "yandex#search",
            }
        ),
        clusterer = new ymaps.Clusterer({
            preset: "islands#invertedVioletClusterIcons",
            clusterHideIconOnBalloonOpen: false,
            geoObjectHideIconOnBalloonOpen: false,
        });

 

    var getPointData = function (index) {
            return {
                balloonContentBody:
                    "placemark <strong>balloon " + index + "</strong>",
                clusterCaption: "placemark <strong>" + index + "</strong>",
            };
        },
        getPointOptions = function () {
            return {
                preset: "islands#violetIcon",
            };
        },
        points = [
            [55.831903, 37.411961],
            [55.763338, 37.565466],
            [55.763338, 37.565466],
            [55.744522, 37.616378],
            [55.780898, 37.642889],
            [55.793559, 37.435983],
            [55.800584, 37.675638],
            [55.716733, 37.589988],
            [55.775724, 37.56084],
            [55.822144, 37.433781],
            [55.87417, 37.669838],
            [55.71677, 37.482338],
            [55.78085, 37.75021],
            [55.810906, 37.654142],
            [55.865386, 37.713329],
            [55.847121, 37.525797],
            [55.778655, 37.710743],
            [55.623415, 37.717934],
            [55.863193, 37.737],
            [55.86677, 37.760113],
            [55.698261, 37.730838],
            [55.6338, 37.564769],
            [55.639996, 37.5394],
            [55.69023, 37.405853],
            [55.77597, 37.5129],
            [55.775777, 37.44218],
            [55.811814, 37.440448],
            [55.751841, 37.404853],
            [55.627303, 37.728976],
            [55.816515, 37.597163],
            [55.664352, 37.689397],
            [55.679195, 37.600961],
            [55.673873, 37.658425],
            [55.681006, 37.605126],
            [55.876327, 37.431744],
            [55.843363, 37.778445],
            [55.875445, 37.549348],
            [55.662903, 37.702087],
            [55.746099, 37.434113],
            [55.83866, 37.712326],
            [55.774838, 37.415725],
            [55.871539, 37.630223],
            [55.657037, 37.571271],
            [55.691046, 37.711026],
            [55.803972, 37.65961],
            [55.616448, 37.452759],
            [55.781329, 37.442781],
            [55.844708, 37.74887],
            [55.723123, 37.406067],
            [55.858585, 37.48498],
        ],
        geoObjects = [];

    for (var i = 0, len = points.length; i < len; i++) {
        geoObjects[i] = new ymaps.Placemark(
            points[i],
            getPointData(i),
            getPointOptions()
        );
    }

    clusterer.add(geoObjects);
    myMap.geoObjects.add(clusterer);

    myMap.setBounds(clusterer.getBounds(), {
        checkZoomRange: true,
    });

验证自带clusterer的瓶颈

通过随机方法创建4w节点

html 复制代码
<template> 
     <div class="main-container" ref="mapContainer" v-loading="loading">
  </div>
</template>
<script lang="ts" setup>
  import { ref,  onMounted, onUnmounted } from 'vue'   
  const props = defineProps({
    apiKey: {
      type: String,
      required: true,
    },
  })  
  const loading = ref(false)   
  let map: any | null = null   
  const mapContainer = ref<HTMLElement | null>(null) 

  // 检查Yandex Maps是否已加载
  const isYandexMapsLoaded = (): boolean => {
    return typeof (window as any).ymaps !== 'undefined'
  }
 
  const loadYandexMaps = (): Promise<void> => {
    return new Promise((resolve, reject) => {
      // 检查是否已经加载过
      if (isYandexMapsLoaded()) {
        resolve()
        return
      }

      // 创建script标签
      const script = document.createElement('script') 
      const apiKey = props.apiKey //  //
      script.src = `https://api-maps.yandex.ru/2.1/?apikey=${apiKey}&lang=ru_RU`
      script.type = 'text/javascript'
      script.defer = true

      // 加载完成回调
      script.onload = () => { 
        ;(window as any).ymaps.ready(() => {
          resolve()
        })
      } 
      script.onerror = (error) => {
        console.error('Failed to load Yandex Maps API', error)
        reject(error)
      } 
      document.head.appendChild(script)
    })
  } 
  
   const getRandomData = () => {
    const COUNT = 40000 // 300000
    const locations = [] 
    // 深圳市中心坐标范围(大致)
    const baseLat = 55.751244
    const baseLng = 37.618423
  
    for (let i = 0; i < COUNT; i++) {
      const lat = baseLat + (Math.random() - 0.5) * 30.5 // +-0.25范围
      const lng = baseLng + (Math.random() - 0.5) * 100.5
  
      locations.push([ parseFloat(lat.toFixed(6)),
        parseFloat(lng.toFixed(6))])
    }
    return locations
  }  // 初始化地图
  const initMap = () => {
    if (!mapContainer.value || !isYandexMapsLoaded()) return

    let initLat = 55.751244
    let initLng = 37.618423
    let initZoom = 15
 

    // 创建地图实例
    map = new (window as any).ymaps.Map(mapContainer.value, {
      center: [initLat, initLng], // 莫斯科坐标作为默认值
      zoom: initZoom,
      controls: ['fullscreenControl'] 
    }) 

       let clusterer =  new (window as any).ymaps.Clusterer({
            preset: "islands#invertedVioletClusterIcons",
            clusterHideIconOnBalloonOpen: false,
            geoObjectHideIconOnBalloonOpen: false,
        }); 
    let getPointData = function (index:number) {
            return {
                balloonContentBody:
                    "placemark <strong>balloon " + index + "</strong>",
                clusterCaption: "placemark <strong>" + index + "</strong>",
            };
        },
        getPointOptions = function () {
            return {
                preset: "islands#violetIcon",
            };
        };
    let    points = getRandomData()
    let    geoObjects = []

    for (var i = 0, len = points.length; i < len; i++) {
        geoObjects[i] = new ymaps.Placemark(
            points[i],
            getPointData(i),
            getPointOptions()
        );
    }

    clusterer.add(geoObjects);
    map.geoObjects.add(clusterer);

    map.setBounds(clusterer.getBounds(), {
        checkZoomRange: true,
    });

  }
  

  // 组件卸载时清理地图实例
  onUnmounted(() => {
    if (map) {
      map.destroy()
      map = null
    }
  })
 
  onMounted(async () => {
    loading.value = true
    try {
      await loadYandexMaps()  
      initMap() 
      loading.value = false
    } catch (error) {
      console.error('Error onMounted:', error)
      loading.value = false
    }
  })
 
  
</script>

<style lang="scss" scoped>
  .main-container {
    position: relative;
      width: 100%;
    height: calc(100vh - 70px);
  } 
 

  .loading {
    text-align: center;
    padding: 20px;
    color: #909399;
  }
 
</style>
 

在渲染时已经开始卡了。

这里可以参考之前谷歌地图的优化方案 juejin.cn/post/750111...

所以这里我们还是不直接使用自带的cluster,我们通过后台计算,前台自定义渲染返回的汇总数据

自定义渲染后台汇总数据

通过只绘制少量的汇总标记,来提升渲染效率

html 复制代码
<template> 
     <div class="main-container" ref="mapContainer" v-loading="loading">
  </div>
</template>
<script lang="ts" setup>
  import { ref,  onMounted, onUnmounted } from 'vue'   
  const props = defineProps({
    apiKey: {
      type: String,
      required: true,
    },
  })  
  const loading = ref(false)   
  let map: any | null = null   
  const mapContainer = ref<HTMLElement | null>(null) 

  // 检查Yandex Maps是否已加载
  const isYandexMapsLoaded = (): boolean => {
    return typeof (window as any).ymaps !== 'undefined'
  }
 
  const loadYandexMaps = (): Promise<void> => {
    return new Promise((resolve, reject) => {
      // 检查是否已经加载过
      if (isYandexMapsLoaded()) {
        resolve()
        return
      }

      // 创建script标签
      const script = document.createElement('script') 
      const apiKey = props.apiKey //  //
      script.src = `https://api-maps.yandex.ru/2.1/?apikey=${apiKey}&lang=ru_RU`
      script.type = 'text/javascript'
      script.defer = true

      // 加载完成回调
      script.onload = () => { 
        ;(window as any).ymaps.ready(() => {
          resolve()
        })
      } 
      script.onerror = (error) => {
        console.error('Failed to load Yandex Maps API', error)
        reject(error)
      } 
      document.head.appendChild(script)
    })
  } 
  
  const getRandomData = () => {
    const COUNT = 10 // 300000
    const locations = []
  
    // 深圳市中心坐标范围(大致)
    const baseLat = 55.751244
    const baseLng = 37.618423
 
  
    for (let i = 0; i < COUNT; i++) {
      const lat = baseLat + (Math.random() - 0.5) * 35 // +-0.25范围
      const lng = baseLng + (Math.random() - 0.5) * 35
  
      locations.push({
        id: `${i + 1}`,
        type: i % 2 === 0 ? 1 : 0,
        total: Math.random() * 100,
        lat: parseFloat(lat.toFixed(6)),
        lng: parseFloat(lng.toFixed(6))
      })
    }
    return locations
  }

    const createClusterIcon = (count: number, type: string, size: number = 40) => {
    const color = type === 'green' ? '#00BD24' : '#E0312C'
    const svg = `
    <svg width="${size}" height="${size}" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
      <g opacity="0.280000">
        <circle id="椭圆 201" cx="20" cy="20" r="20" fill="${color}" />
      </g>
      <g opacity="0.520000">
        <circle id="椭圆 202" cx="20" cy="20" r="17" fill="${color}"  />
      </g>
      <circle id="椭圆 203" cx="20" cy="20" r="14" fill="${color}" />
      <text x="20" y="25" font-size="10" fill="#fff" text-anchor="middle" dominant-baseline="top">
        ${count}
      </text>
    </svg>
  `
    return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
  }

  const createCluster = (title:string,own:number,total:number,lat:number,lng:number) => {
      const color = own === 1 ? 'green' : 'red'
      // 创建SVG格式的圆形图标
      const iconHref = createClusterIcon(total, color, 40)

      // 创建标记
      const placemark = new (window as any).ymaps.Placemark(
        [lat, lng],
        {
          hintContent: title, 
          // 保存完整的标记数据
          markerData: {
            title: title,
          },
        },
        {
          iconLayout: 'default#image', // 使用默认图片布局
          iconImageHref: iconHref, // 引用 SVG 的 Data URI
          iconImageSize: [40, 40], // 图标显示尺寸
          iconImageOffset: [-40 / 2, -40 / 2], // 偏移量(中心点对齐坐标)
        },
      ) 
      // 将标记添加到地图
      map.geoObjects.add(placemark)
      return placemark
  }

  const initMap = () => {
    if (!mapContainer.value || !isYandexMapsLoaded()) return

    let initLat = 55.751244
    let initLng = 37.618423
    let initZoom = 5 
    // 创建地图实例
    map = new (window as any).ymaps.Map(mapContainer.value, {
      center: [initLat, initLng], // 莫斯科坐标作为默认值
      zoom: initZoom,
      controls: ['fullscreenControl'] 
    }) 
 
    let points = getRandomData() 

    for (var i = 0, len = points.length; i < len; i++) {
      const obj = points[i]
      createCluster(obj.id,obj.type, obj.total, obj.lat,obj.lng) 
    }  
  }
  

  // 组件卸载时清理地图实例
  onUnmounted(() => {
    if (map) {
      map.destroy()
      map = null
    }
  })
 
  onMounted(async () => {
    loading.value = true
    try {
      await loadYandexMaps()  
      initMap() 
      loading.value = false
    } catch (error) {
      console.error('Error onMounted:', error)
      loading.value = false
    }
  })
 
  
</script>

<style lang="scss" scoped>
  .main-container {
    position: relative;
      width: 100%;
    height: calc(100vh - 70px);
  } 
 

  .loading {
    text-align: center;
    padding: 20px;
    color: #909399;
  }
 
</style>
 

可以看到现在汇总的只是一个标记,并没有完全在地图全部生成。

4.自定义弹框

  • 通过参数balloonLayout 自定义弹框内容
  • 通过placemark.options.set('iconImageHref','xxx') 实时修改原来自定义的标记图片
html 复制代码
<!DOCTYPE html>
<html lang="zh">

<head>
  <meta charset="UTF-8" />
  <style>
    body {
      font-family: Arial, sans-serif;
      margin: 20px;
    }

    #map {
      width: 100%;
      height: 600px;
      border: 1px solid #ccc;
    }

    /* 气泡容器 - 用于三角定位 */
    .balloon-wrapper {
      position: relative;
      overflow: visible;
      /* 确保三角不被裁剪 */
    }

    /* 气泡内容区域 */
    .custom-balloon {
      background: #fff;
      border-radius: 8px;
      box-shadow: 0 3px 14px rgba(0, 0, 0, 0.2);
      padding: 15px;
      width: 300px;
      position: relative;
      z-index: 2;
    }

    /* 左上角三角 - 指向左侧标记 */
    .balloon-triangle {
      position: absolute;
      top: 20px;
      /* 距离气泡顶部20px */
      left: -10px;
      /* 超出气泡左侧10px */
      width: 0;
      height: 0;
      /* 三角形指向左侧(右向三角形) */
      border-top: 8px solid transparent;
      border-bottom: 8px solid transparent;
      border-right: 10px solid #fff;
      /* 与气泡背景同色 */
      box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
      /* 右侧阴影增强立体感 */
      z-index: 1;
    }

    /* 加载状态样式 */
    .loading {
      text-align: center;
      padding: 20px;
    }

    /* 错误状态样式 */
    .error {
      color: #e74c3c;
      text-align: center;
      padding: 20px;
    }

    /* 气泡头部样式 */
    .balloon-header {
      margin-bottom: 10px;
      border-bottom: 1px solid #eee;
      padding-bottom: 10px;
    }

    .balloon-title {
      margin: 0 0 5px 0;
      color: #2c3e50;
    }

    /* 图片样式 */
    .balloon-image {
      width: 100%;
      border-radius: 4px;
      margin: 10px 0;
    }

    /* 统计信息样式 */
    .balloon-stats {
      display: flex;
      gap: 15px;
      margin: 10px 0;
      font-size: 14px;
    }
  </style>
</head>

<body>

  <div id="map"></div>

  <!-- 引入 Yandex Maps API -->
  <script src="https://api-maps.yandex.ru/2.1/?lang=zh_CN&onload=initMap" defer></script>

  <script>
    // 基础标记数据
    const markersData = [
      { coords: [39.9042, 116.4074], title: "北京", id: "bj001" },
      { coords: [31.2304, 121.4737], title: "上海", id: "sh002" },
      { coords: [23.1291, 113.2644], title: "广州", id: "gz003" },
      { coords: [22.3193, 114.1694], title: "深圳", id: "sz004" }
    ];

    let map;
    let placemarks = [];
    let dynamicBalloonLayout;
    let activePlacemark = null;
    const INITIAL_ZOOM = 5;
    const DEFAULT_COLOR = '#3498db'; // 默认标记颜色
    const ACTIVE_COLOR = '#e74c3c';  // 激活状态颜色

    // 生成带数字的圆形图标
    function generateCircleIconWithNumber(num, color = DEFAULT_COLOR) {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      const size = 50; // 图标大小
      canvas.width = size;
      canvas.height = size;

      // 白色填充
      ctx.fillStyle = '#ffffff';
      ctx.beginPath();
      ctx.arc(size / 2, size / 2, size / 2 - 2, 0, 2 * Math.PI);
      ctx.fill();

      // 边框颜色
      ctx.strokeStyle = color;
      ctx.lineWidth = 3;
      ctx.stroke();

      // 数字颜色
      ctx.fillStyle = color;
      ctx.font = 'bold 16px Arial';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(num.toString(), size / 2, size / 2);

      return canvas.toDataURL();
    }

    // 模拟API请求获取详细数据
    async function fetchMarkerDetails(markerId) {
      return new Promise((resolve) => {
        setTimeout(() => {
          const mockData = {
            "bj001": {
              description: "北京是中国的首都,政治、文化和国际交流中心。",
              population: "约2154万",
              area: "16,410.54平方公里",
              image: "https://picsum.photos/id/1016/400/200",
              temperature: "18°C",
              updateTime: "2023-10-01 12:00"
            },
            "sh002": {
              description: "上海是中国的经济、金融中心,国际化大都市。",
              population: "约2487万",
              area: "6,340.5平方公里",
              image: "https://picsum.photos/id/1015/400/200",
              temperature: "20°C",
              updateTime: "2023-10-01 12:10"
            },
            "gz003": {
              description: "广州是华南地区的经济中心和交通枢纽。",
              population: "约1530万",
              area: "7,434.4平方公里",
              image: "https://picsum.photos/id/1018/400/200",
              temperature: "25°C",
              updateTime: "2023-10-01 12:05"
            },
            "sz004": {
              description: "深圳是中国的科技创新中心,新兴一线城市。",
              population: "约1303万",
              area: "1,997.47平方公里",
              image: "https://picsum.photos/id/1019/400/200",
              temperature: "24°C",
              updateTime: "2023-10-01 12:15"
            }
          };
          resolve(mockData[markerId]);
        }, 800);
      });
    }

    // 创建自定义气泡布局(左上角三角)
    function createDynamicBalloonLayout() {
      return ymaps.templateLayoutFactory.createClass(
        // 气泡结构:外层容器 -> 内容区 + 左上角三角
        '<div class="balloon-wrapper">' +
        '<div id="dynamic-balloon-content" class="custom-balloon"></div>' +
        '<div class="balloon-triangle"></div>' + // 左上角三角
        '</div>',
        {
          // 构建气泡时触发
          build: function () {
            this.constructor.superclass.build.call(this);

            const properties = this.getData().properties;
            const contentContainer = document.getElementById('dynamic-balloon-content');

            // 显示加载状态
            contentContainer.innerHTML = `
              <div class="loading">
                <p>正在加载${properties.get('title')}的详细信息...</p>
                <div style="margin-top:10px;">⏳</div>
              </div>
            `;

            // 加载并渲染数据
            this.loadAndRenderData(properties.get('id'), contentContainer);
          },

          // 加载数据并渲染内容
          async loadAndRenderData(markerId, container) {
            try {
              const details = await fetchMarkerDetails(markerId);
              const baseInfo = markersData.find(item => item.id === markerId);

              // 渲染气泡内容
              container.innerHTML = `
                <div class="balloon-header">
                  <h3 class="balloon-title">${baseInfo.title}</h3>
                  <small>数据更新于: ${details.updateTime}</small>
                </div>
                <img src="${details.image}" class="balloon-image" />
                <p>${details.description}</p>
                <div class="balloon-stats">
                  <div>人口: ${details.population}</div>
                  <div>面积: ${details.area}</div>
                </div>
                <div>当前温度: ${details.temperature}</div>
                <button onclick="handleBalloonAction('${markerId}')" style="margin-top:15px;padding:6px 12px;background:#3498db;color:white;border:none;border-radius:4px;cursor:pointer;">
                  查看更多信息
                </button>
              `;
            } catch (error) {
              // 错误状态
              container.innerHTML = `
                <div class="error">
                  <p>加载失败,请稍后重试</p> 
                </div>
              `;
            }
          },

          // 清除气泡时触发
          clear: function () {
            this.constructor.superclass.clear.call(this);
          }
        }
      );
    }

    // 重置所有标记为默认样式
    function resetAllMarkers() {
      placemarks.forEach((placemark, index) => {
        placemark.options.set('iconImageHref',
          generateCircleIconWithNumber(index + 1, DEFAULT_COLOR));
      });
      activePlacemark = null;
    }

    // 设置标记为激活状态(红色)
    function setMarkerActive(placemark, index) {
      resetAllMarkers();
      placemark.options.set('iconImageHref',
        generateCircleIconWithNumber(index + 1, ACTIVE_COLOR));
      activePlacemark = placemark;
    }

    // 重试加载数据 

    // 气泡内按钮点击事件
    window.handleBalloonAction = function (markerId) {
      const marker = markersData.find(item => item.id === markerId);
      alert(`您点击了${marker.title}的"查看更多信息"按钮,ID: ${markerId}`);
    };

    // 关闭所有气泡
    function closeAllBalloons() {
      placemarks.forEach(placemark => {
        if (placemark.balloon.isOpen()) {
          placemark.balloon.close();
        }
      });
      resetAllMarkers();
    }

    // 初始化地图
    function initMap() {
      const mapCenter = [31.0, 114.0];
      map = new ymaps.Map("map", {
        center: mapCenter,
        zoom: INITIAL_ZOOM
      });

      // 点击地图空白处关闭气泡
      map.events.add('click', function (e) {
        const target = e.get('target');
        if (target === map) {
          closeAllBalloons();
        }
      });

      createCustomMarkers();
    }

    // 创建自定义标记
    function createCustomMarkers() {
      dynamicBalloonLayout = createDynamicBalloonLayout();

      markersData.forEach((data, index) => {
        const placemark = new ymaps.Placemark(
          data.coords,
          {
            title: data.title,
            id: data.id
          },
          {
            iconLayout: 'default#image',
            iconImageHref: generateCircleIconWithNumber(index + 1),
            iconImageSize: [50, 50],
            iconImageOffset: [-25, -25], // 图标中心点对准坐标点
            balloonLayout: dynamicBalloonLayout,
            // 气泡偏移量:向右移动100px,向上微调20px(确保三角指向标记)
            balloonOffset: [20, -20],
            balloonShadow: false, // 禁用默认阴影,使用自定义阴影
            hideIconOnBalloonOpen: false
          }
        );

        // 标记点击事件
        placemark.events.add('click', function () {
          setMarkerActive(placemark, index);
        });

        map.geoObjects.add(placemark);
        placemarks.push(placemark);
      });
    }

  </script>

</body>

</html>

5.踩坑事项

缩放与移动统一事件

map.events.add('boundschange' 可以用来统一监听缩放与移动逻辑,不建议事情其他自带防止冲突

js 复制代码
    map.events.add('boundschange', (event: any) => { 
      if (event.get('newZoom') !== event.get('oldZoom')) { // 缩放了
        //  console.log('进入缩放事件', event)
        
      } else { // 只是移动 
        //  console.log('进入移动事件', event)  
      }
    })

浏览器宽度变化,地图不能识别

google可以自己自适应,yandex没有,需要实时监听容器变化

js 复制代码
    // 创建 ResizeObserver 监听容器尺寸变化
    const resizeObserver = new ResizeObserver(
      debounce((entries: any) => {
        if(!mapContainer.value) {
          return
        }
        for (let entry of entries) { 
          if(mapContainer.value === entry.target) { 
            map.container.fitToViewport()
            break;
          } 
        }
      }, 1000),
    )

    // 开始监听容器
    resizeObserver.observe(mapContainer.value)

动态修改 marker.options.set('iconImageSize','xx'),再次访问异常

yandex当通过marker.options.set('iconImageSize','xx')动态修改元素的物理渲染信息时候,会修改原来对象的引用,不能通过 全局引用的对象保留,需要重新遍历索引。

js 复制代码
  const selectedMarker = ref<any | null>(null) // 如全局记录了上次选中对象
  
  selectedMarker = xxx // 点击选中赋值
  selectedMarker.value.events.fire('click', {})  // 这里会报错
  
    if (selectedMarker && selectedMarker.value) {
      const storeId = getStoreIdByMarker(selectedMarker.value) 
      let marker = findMarkerByStoreId(storeId)  // 每次重新再查找一遍
      if (marker) {
          marker.events.fire('click', {}) 
      }
    }
 

移动和放大时候与boundschange事件冲突

系统自带map.panTo()来实现移动和放大,但是会导致触发boundschange事件的事件。可以通过全局状态位来控制逻辑

js 复制代码
  let isPanToNow = false
  const myPanTo = (targetCoords: any) => {
    return new Promise((resolve, reject) => {
      isPanToNow = true
      // 平滑移动
      map.panTo(targetCoords, { duration: 500 , flying: true }).then(() => {
        // 再平滑移动
        isPanToNow = false
        resolve(null)
      })
    })
  }
  
 // 添加缩放 + 拖拽 事件监听
    map.events.add('boundschange', (event: any) => { 
      if (isPanToNow) {
        console.log('isPanToNow 为 true', isPanToNow)
        return
      }
     
    })
  

主动触发标记点击事件

通过marker.events.fire('click', {}) 主动触发,注意标记必须是渲染完成的,目前没找到稳定的回调渲染方法钩子,暂时加settimout再下一次事件循环里再触发确保已经渲染

js 复制代码
 marker.events.fire('click', {}) 

也可以通过提前加载图片,确保渲染标记时候不会阻塞

js 复制代码
const customIconUrl1 = "/dashboard/local1.png"; 
const customIconUrl2 = "/dashboard/local2.png"; 
// 预先加载图标图片的函数
function loadIconImage(url:string) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.src = url;
    img.onload = () => resolve(url); // 图片加载完成
    img.onerror = () => reject("图标加载失败");
  });
}
  onMounted(async () => {
    loading.value = true
    try {
      await loadYandexMaps() 
       // 等待图标加载完成
      await loadIconImage(customIconUrl1);
      await loadIconImage(customIconUrl2);

      initMap() 
      loading.value = false
    } catch (error) {
      console.error('Error onMounted:', error)
      mapLoading.value = false // 发生错误时也关闭加载状态
    }
  })

6.源码地址

github.com/mjsong07/ya...

7.资料参考

官方api文档 v2.1 yandex.com/dev/jsapi-v...

例子 yandex.com/dev/jsapi-v...

官方api文档 v3 yandex.com/maps-api/do...

相关推荐
daols882 小时前
vxe-table 配置 ajax 加载列表数据,配置分页和查询搜索表单
vue.js·ajax·table·vxe-table
~无忧花开~3 小时前
Vue.config.js配置全攻略
开发语言·前端·javascript·vue.js
默默乄行走4 小时前
wangEditor5在vue中自定义菜单栏--格式刷,上传图片,视频功能
vue.js
G***66914 小时前
前端框架选型:React vs Vue深度对比
vue.js·react.js·前端框架
局i6 小时前
vue简介
前端·javascript·vue.js
yqcoder6 小时前
vue2 和 vue3 中,列表中的 key 值作用
前端·javascript·vue.js
梵得儿SHI6 小时前
Vue 指令系统:事件处理与表单绑定全解析,从入门到精通
前端·javascript·vue.js·v-model·v-on·表单数据绑定·表单双向绑定
lcc1876 小时前
Vue props
前端·vue.js
秋天的一阵风7 小时前
😱一行代码引发的血案:展开运算符(...)竟让图表功能直接崩了!
前端·javascript·vue.js