高德地图骑行导航 API 介绍与使用方法

本文基于 Vue 3 + TypeScript 项目中的实际代码,介绍高德地图 JS API 2.0 中骑行导航(Riding)的核心用法,涵盖地图初始化、路径规划、自定义标记、区域遮罩、轮询刷新等完整实践。

一、概述

高德地图 JS API 2.0 提供了多种导航服务,包括:

导航方式 对应类 适用场景
步行导航 AMap.Walking 步行路线规划
骑行导航 AMap.Riding 自行车/电动自行车路线
驾车导航 AMap.Driving 机动车辆路线
公交导航 AMap.Transfer 公共交通换乘
货车导航 AMap.TruckDriving 货车路线规划

本文主要介绍 AMap.Riding(骑行导航),一个典型的应用场景是:在派单系统中,实时展示工人到电站的骑行路径和预计到达时间。

二、SDK 引入与配置

2.1 在 HTML 中引入 SDK

在 index.html 中通过

html 复制代码
<!-- 安全密钥配置(必须在使用地图前声明) -->
<script type="text/javascript">
  window._AMapSecurityConfig = {
    securityJsCode: "你的安全密钥"
  };
  window.global = window;
</script>

<!-- 加载 JS API,同时通过 plugin 参数加载骑行导航插件 -->
<script src="https://webapi.amap.com/maps?v=2.0&key=你的Key&plugin=AMap.Riding"></script>

关键参数说明:

  • v=2.0:使用 JS API 2.0 版本
  • key:在高德开放平台申请的 Web 端 Key
  • plugin=AMap.Riding:按需加载骑行导航插件,如果有多个插件用逗号分隔
  • window._AMapSecurityConfig.securityJsCode:安全密钥,配合 Key 使用(2021年底之后申请的 Key 必须配置)

2.2 安装 TypeScript 类型声明

bash 复制代码
npm install @types/amap-js-api

在 tsconfig.json 中确保类型被正确引用。

三、地图初始化

创建地图实例是使用所有功能的基础:

typescript 复制代码
const mapContainer = ref(null);

const mapInstance = new window.AMap.Map(mapContainer.value!, {
  center: [120.433904, 31.329341],  // 地图中心点 [经度, 纬度]
  zoom: 11,                          // 缩放级别(3-20)
  viewMode: "2D",                    // 视图模式:2D 或 3D
  mapStyle: "amap://styles/xxx",     // 自定义地图样式(可在高德控制台创建)
  scrollWheel: true,                 // 允许鼠标滚轮缩放
  resizeEnable: true                 // 容器大小变化时自动调整地图
});

常用配置项清单:

配置项 类型 说明
center lng, lat 地图中心点经纬度
zoom number 缩放级别(3-20)
viewMode "2D" "3D"
mapStyle string 自定义地图样式 URL
scrollWheel boolean 是否允许滚轮缩放
resizeEnable boolean 容器大小变化时自动调整
layers TileLayer\[\] 自定义图层
features string\[\] 地图要素显示控制

模板部分只需要一个容器元素:

html 复制代码
<template>
  <div ref="mapContainer" class="amap-container" />
</template>

<style scoped>
.amap-container {
  width: 100%;
  height: 100%;
}
</style>

四、骑行导航(Riding)核心用法

4.1 创建骑行导航实例

为每个需要导航的工人创建一个 Riding 实例:

typescript 复制代码
const riding = new (window.AMap as any).Riding({
  map: mapInstance,                                 // 绑定的地图实例
  policy: (window.AMap as any).RidingPolicy.ELECTRIC_BIKE,  // 骑行策略:电动车
  autoFitView: false,                               // 不自动调整视野
  showTraffic: false,                               // 不显示路况
  hideMarkers: true                                 // 隐藏默认的起终点标记(使用自定义标记)
});

构造函数参数详解:

参数 类型 说明
map AMap.Map 绑定地图实例,路径将绘制在此地图上
policy RidingPolicy 骑行策略,决定路线规划偏好
autoFitView boolean 是否自动调整地图视野以包含整条路线
showTraffic boolean 是否显示实时路况
hideMarkers boolean 是否隐藏默认的起点/终点标记(启用自定义标记时设为 true)

骑行策略(RidingPolicy)枚举:

说明
RidingPolicy.DEFAULT 默认模式(普通自行车)
RidingPolicy.ELECTRIC_BIKE 电动车模式
RidingPolicy.RECOMMENDED 推荐路线(最快到达)
RidingPolicy.SHORTEST_DISTANCE 最短距离

4.2 路径搜索

调用 search 方法进行路径规划,传入起点和终点坐标:

typescript 复制代码
riding.search(
  [Number(worker.lng), Number(worker.lat)],           // 起点:工人当前位置 [lng, lat]
  [Number(worker.station.lng), Number(worker.station.lat)], // 终点:电站位置 [lng, lat]
  (status: string, result: any) => {
    // status 取值:
    //   "complete" - 搜索成功
    //   "error"    - 搜索失败
    //   "no_data"  - 无结果
    if (status === "complete") {
      const routes = result.routes;       // 路线方案数组
      const firstRoute = routes[0];       // 第一条推荐路线
      const distance = firstRoute.distance;  // 总距离(米)
      const time = firstRoute.time;          // 预计耗时(秒)
      const steps = firstRoute.steps;        // 路线步骤详情
    }
  }
);

回调参数 result 结构:

result

├── info: string // 状态说明

├── origin: LngLat // 起点坐标

├── destination: LngLat // 终点坐标

├── start: Poi // 起点 POI 信息

├── end: Poi // 终点 POI 信息

└── routes: Route\[\] // 路线方案数组

└── 0

├── distance: number // 总距离(米)

├── time: number // 预计耗时(秒)

├── policy: string // 使用的策略

└── steps: Step\[\] // 每段路线步骤详情

4.3 批量路径搜索(并发优化)

在实际业务中,往往需要同时计算多个工人到各自电站的路径。使用 Promise.all 实现并发搜索,避免串行等待:

typescript 复制代码
const workerArray = Array.from(state.workers.values());

// 将每个 search 调用包装成 Promise
const pros = workerArray.map(
  worker =>
    new Promise<[any, any, WorkerTracing]>(resolve =>
      worker.riding.search(
        [Number(worker.lng), Number(worker.lat)],
        [Number(worker.station.lng), Number(worker.station.lat)],
        (status, result) => {
          resolve([status, result, worker]);
        }
      )
    )
);

// 并发执行所有搜索
const results = await Promise.all(pros);

// 提取预计到达时间并转换为分钟(保留一位小数)
const etimeMap = results.reduce<Map<ID, number>>((map, [status, result, worker]) => {
  const timeSeconds = result?.routes?.[0]?.time || 0;
  const timeMinutes = Math.round((timeSeconds / 60) * 10) / 10;
  map.set(worker.id, timeMinutes);
  return map;
}, new Map());

4.4 清除导航路径

当工人下线或不再需要某条路径时,调用 clear 方法清除地图上的路径线:

typescript 复制代码
// 清除单个导航实例的路径
worker.riding.clear();

// 批量清除所有路径
state.workers.forEach((worker) => worker.riding.clear());

五、轮询刷新机制

采用定时轮询的方式持续更新工人位置和导航路径,保证数据的实时性:

typescript 复制代码
let timer = true;  // 轮询控制标志

async function loadWorkers() {
  if (timer === false) return;  // 组件卸载时停止轮询

  try {
    const res = await APIMap.listByReceived();  // 从后端获取工人列表
    const works = new Map();

    res.data.forEach(worker => {
      const oldWorker = state.workers.get(worker.id);
      if (oldWorker && state.stations.has(oldWorker.station.id)) {
        // 复用已有实例,只更新数据
        works.set(worker.id, {
          ...state.workers.get(worker.id)!,
          ...worker
        });
        state.workers.delete(worker.id);
      } else {
        // 新工人,创建新的导航实例
        const station = state.stations.get(worker.stationId);
        if (!station) return;
        const w = {
          ...worker,
          station: station,
          riding: new (window.AMap as any).Riding({
            map: mapInstance,
            policy: (window.AMap as any).RidingPolicy.ELECTRIC_BIKE,
            autoFitView: false,
            showTraffic: false,
            hideMarkers: true
          })
        };
        works.set(w.id, w);
      }
    });

    // 清理失效的导航实例
    state.workers.forEach((worker) => worker.riding.clear());
    state.workers = works;

    // 并发路径搜索
    const workerArray = Array.from(state.workers.values());
    const pros = workerArray.map(/* ... 路径搜索 ... */);
    const results = await Promise.all(pros);

    // 更新地图标记
    workerMarkers.update(Array.from(state.workers.values()), etimeMap, mapInstance!);
  } catch (err) {
    console.error("获取接单人失败:", err);
  }

  setTimeout(loadWorkers, 3000);  // 每 3 秒轮询一次
}

轮询策略设计要点:

  1. 增量更新:对已存在的工人复用已有实例,只更新位置信息
  2. 清理失效数据:对不再出现在列表中的工人,调用 riding.clear() 清除路径
  3. 并发搜索:使用 Promise.all 同时发起所有路径请求,减少总等待时间
  4. 生命周期控制:组件卸载时通过 timer 标志位停止轮询

六、自定义标记(Marker)与路径联动

不使用高德地图默认的起终点标记,而是通过自定义 DOM 元素实现更丰富的标记效果。

6.1 通用标记管理器

封装一个通用的标记管理器,支持增量更新和 DOM 复用:

typescript 复制代码
type ID = number | string;

function markerManager() {
  const markers: Map<ID, AMap.Marker> = new Map();

  function update<Item extends { id: ID }>(
    items: Item[],
    mapInstance: AMap.Map,
    getPosition: (ite: Item) => AMap.LocationValue,
    createData: (item: Item, reuseContent?: HTMLElement | string) => { 
      content: HTMLElement | string; 
      title: string;
    },
    onClick?: (item: Item) => void
  ) {
    // 1. 更新已有标记或创建新标记
    items.forEach(item => {
      const position = getPosition(item);
      let marker = markers.get(item.id);

      if (marker) {
        // 更新已有标记的位置和内容
        marker.setPosition(position);
        const data = createData(item, marker.getContent());
        marker.setTitle(data.title);
        if (marker.getContent() !== data.content) {
          marker.setContent(data.content);
        }
      } else {
        // 创建新标记
        const data = createData(item);
        marker = new window.AMap.Marker({
          position: position,
          title: data.title,
          content: data.content,
          anchor: "center",
          zIndex: 100
        });
        if (onClick) {
          marker.on("click", () => onClick(item));
        }
        mapInstance.add(marker);
        markers.set(item.id, marker);
      }
    });

    // 2. 移除不在新列表中的标记
    const oldIds = Array.from(markers.keys());
    const removeIds = oldIds.filter(id => !items.some(inner => inner.id === id));
    const removeMarkers = removeIds.map(id => {
      const marker = markers.get(id)!;
      markers.delete(id);
      return marker;
    });
    mapInstance.remove(removeMarkers);
  }

  return { markers, update };
}

设计亮点:

  • 泛型约束:Item 必须包含 id 属性,确保标记可被追踪
  • DOM 复用:通过 reuseContent 参数复用已有 DOM 元素,避免频繁重建导致的闪烁
  • 差异更新:自动对比新旧列表,移除不再需要的标记,添加新增的标记

6.2 工人标记(含预计到达时间)

为每个工人创建带头像和预计到达时间的自定义标记:

typescript 复制代码
function useWorkerMarkers() {
  const MarkerManager = markerManager();

  function update(
    workers: WorkerTracing[],
    estimatedTimeSeconds: Map<ID, number>,
    mapInstance: AMap.Map
  ) {
    MarkerManager.update<WorkerTracing>(
      workers,
      mapInstance,
      item => [Number(item.lng), Number(item.lat)],
      (item, reuseContent) => {
        const time = Math.round((estimatedTimeSeconds.get(item.id)! / 60) * 10) / 10;
        const timeText = `${item.name}<br>预计${time}分钟`;

        // 复用已有 DOM 时只更新文字内容
        if (reuseContent !== undefined && typeof reuseContent === "object") {
          reuseContent.querySelector(".time-text")!.innerHTML = timeText;
          return { content: reuseContent, title: "" };
        }

        // 创建新的标记 DOM
        const startMarkerContent = document.createElement("div");
        startMarkerContent.className = "worker-marker-with-time";

        const avatarImg = document.createElement("img");
        avatarImg.src = item.avatar || workerImage;
        avatarImg.className = "worker-avatar";
        avatarImg.style.width = "32px";
        avatarImg.style.height = "32px";
        avatarImg.style.borderRadius = "50%";
        avatarImg.style.objectFit = "cover";
        avatarImg.style.border = "2px solid white";
        avatarImg.style.boxShadow = "0 0 4px rgba(0,0,0,0.3)";

        const timeDiv = document.createElement("div");
        timeDiv.className = "time-text";
        timeDiv.innerHTML = timeText;
        timeDiv.style.padding = "4px 8px";
        timeDiv.style.fontSize = "12px";
        timeDiv.style.color = "white";
        timeDiv.style.backgroundColor = "rgba(0, 0, 0, 0.7)";
        timeDiv.style.borderRadius = "10px";

        startMarkerContent.appendChild(avatarImg);
        startMarkerContent.appendChild(timeDiv);
        return { title: "", content: startMarkerContent };
      }
    );
  }

  return { markers: MarkerManager.markers, update };
}

6.3 电站标记(含状态波纹动画)

电站标记根据状态显示不同的视觉效果,报警状态有红色波纹动画:

typescript 复制代码
function useStationMarkers() {
  const MarkerManager = markerManager();

  function update(
    stations: PowerStation[],
    mapInstance: AMap.Map,
    onChange: (id: string | number) => void
  ) {
    MarkerManager.update<PowerStation>(
      stations,
      mapInstance,
      (item) => [Number(item.lng), Number(item.lat)],
      (item, reuseContent) => {
        const statusText = `${item.stationStatus}`;

        // 复用 DOM 时只更新状态属性
        if (reuseContent) {
          const rContent = reuseContent as HTMLElement;
          const attr = rContent.getAttribute("station-status");
          if (attr !== statusText) {
            rContent.setAttribute("station-status", statusText);
          }
          return { content: rContent, title: `电站${item.id}` };
        }

        // 创建新标记
        const markerContent = document.createElement("div");
        markerContent.classList.add("event-marker");
        markerContent.setAttribute("station-status", statusText);
        markerContent.innerHTML = `
          <div class="ripple-container status-2">
            <div class="ripple ripple-1"></div>
            <div class="ripple ripple-2"></div>
            <div class="ripple ripple-3"></div>
            <div class="center-dot"></div>
          </div>
          <div class="ripple-container status-1">
            <div class="center-dot-green"></div>
          </div>
        `;
        return { title: `电站${item.id}`, content: markerContent };
      },
      (item) => onChange(item.id)
    );
  }

  return { markers: MarkerManager.markers, update };
}

CSS 波纹动画实现:

css 复制代码
@keyframes ripple-animation {
  0% {
    width: 38px;
    height: 38px;
    opacity: 1;
  }
  100% {
    width: 95px;
    height: 95px;
    opacity: 0;
  }
}

.event-marker .ripple {
  position: absolute;
  top: 50%;
  left: 50%;
  border: 3.8px solid #f44;
  border-radius: 50%;
  opacity: 0;
  transform: translate(-50%, -50%);
}

.event-marker .ripple-1 {
  width: 38px;
  height: 38px;
  animation: ripple-animation 2s ease-out infinite;
}

.event-marker .ripple-2 {
  width: 38px;
  height: 38px;
  animation: ripple-animation 2s ease-out infinite 0.6s;
}

.event-marker .ripple-3 {
  width: 38px;
  height: 38px;
  animation: ripple-animation 2s ease-out infinite 1.2s;
}

通过 CSS 属性选择器控制不同状态下的显示:

css 复制代码
/* 默认隐藏所有波纹 */
.event-marker .ripple-container {
  display: none;
}

/* 正常状态显示绿色标记 */
.event-marker[station-status="1"] .ripple-container.status-1 {
  display: block;
}

/* 报警状态显示红色波纹 */
.event-marker[station-status="2"] .ripple-container.status-2 {
  display: block;
}

七、区域遮罩与高亮(Polygon)

使用 AMap.Polygon 实现地图区域遮罩和高亮效果,突出显示特定区域:

typescript 复制代码
export function createMask(mapInstance: AMap.Map) {
  // 全国范围的遮罩路径
  const maskPath: AMap.LocationValue[] = [
    [73.4992, 18.1489],
    [135.083, 18.1489],
    [135.083, 53.5609],
    [73.4992, 53.5609],
    [73.4992, 18.1489]
  ];

  // 大面积遮罩,挖空指定区域(多个区域同时高亮)
  const maskPolygon = new window.AMap.Polygon({
    path: [maskPath, areaPath2, yantaAreaPath],  // 第一个是外轮廓,后续是挖空区域
    strokeColor: "transparent",
    strokeWeight: 0,
    fillColor: "#000000",
    fillOpacity: 0.7
  });
  mapInstance.add(maskPolygon);

  // 高亮区域 1
  const polygonArea = new window.AMap.Polygon({
    path: areaPath2,
    strokeColor: "#00ffff",
    strokeWeight: 4,
    strokeOpacity: 1,
    fillColor: "rgba(0, 255, 255, 0.2)",
    fillOpacity: 0.2
  });
  mapInstance.add(polygonArea);

  // 高亮区域 2
  const yantaPolygon = new window.AMap.Polygon({
    path: yantaAreaPath,
    strokeColor: "#00ff9d",
    strokeWeight: 4,
    strokeOpacity: 1,
    fillColor: "rgba(0, 255, 157, 0.2)",
    fillOpacity: 0.2
  });
  mapInstance.add(yantaPolygon);
}

Polygon 关键属性:

属性 类型 说明
path LngLat\[\]\[\] 多边形路径,支持嵌套数组实现挖空(镂空)效果
strokeColor string 边框颜色
strokeWeight number 边框宽度(像素)
strokeOpacity number 边框透明度(0-1)
fillColor string 填充颜色
fillOpacity number 填充透明度(0-1)

遮罩实现原理:第一个 path 数组定义整个遮罩的区域,后续的 path 数组定义需要"挖空"显示的区域,实现类似聚光灯的效果。

八、交互与通信

8.1 点击电站切换

使用 BroadcastChannel API 实现地图组件与其他组件之间的通信:

typescript 复制代码
// 创建广播通道
const stationChannel = new BroadcastChannel("station-change");

// 点击电站标记时,广播切换消息
const changeStation = (stationId: string | number) => {
  stationChannel.postMessage({
    type: "CHANGE_STATION",
    stationId
  });
};

BroadcastChannel 的优势:

  • 同源页面/组件间通信,无需手动管理事件监听
  • 自动隔离不同频道,不会互相干扰
  • 比 postMessage 更简洁,不需要指定 targetOrigin

8.2 报警音效

当电站出现报警状态时,触发音效提醒:

typescript 复制代码
import sound from "@/assets/sound.mp3";

export function useAudioPlayer() {
  let audioPlayer: HTMLAudioElement | null = null;

  function play() {
    try {
      if (!audioPlayer) {
        audioPlayer = new Audio(sound);
      }
      audioPlayer.currentTime = 0;  // 从头播放
      audioPlayer.play().catch(error => {
        console.error("播放音效失败:", error);
      });
    } catch (error) {
      console.error("创建音频播放器失败:", error);
    }
  }

  function pause() {
    audioPlayer?.pause();
  }

  return { play, pause };
}

注意:现代浏览器要求用户交互后才能播放音频,因此 play() 需要在用户首次点击页面后调用才有效。如果是在自动轮询中触发,可能被浏览器拦截,需要做好错误处理。

九、生命周期管理

在 Vue 组件的 onBeforeUnmount 中妥善清理所有地图相关资源,避免内存泄漏:

typescript 复制代码
onBeforeUnmount(() => {
  timer = false;                              // 停止轮询定时器
  state.workers.forEach((worker) => worker.riding.clear());  // 清除所有导航路径
  AudioPlayer.pause();                       // 停止音效播放
  stationChannel.close();                    // 关闭广播通道
  mapInstance!.destroy();                    // 销毁地图实例,释放内存
});

清理顺序建议:

  1. 先停止数据轮询(timer = false),防止在销毁过程中产生新的异步操作
  2. 清除所有叠加物(路径、标记等)
  3. 关闭外部资源(音频、通信通道等)
  4. 最后销毁地图实例

十、常见问题与注意事项

10.1 安全密钥配置

2021年底之后申请的高德 Key 必须配置安全密钥,否则地图无法正常加载。在引入 SDK 的 script 标签之前声明:

html 复制代码
<script type="text/javascript">
  window._AMapSecurityConfig = {
    securityJsCode: "你的安全密钥"
  };
</script>

10.2 骑行导航的坐标格式

search 方法的起点和终点参数必须是 lng, lat 格式的数组,注意经纬度顺序,不要颠倒。

10.3 并发搜索限制

高德 API 对并发请求有频率限制(QPS)。如果同时需要计算大量路径,建议分批处理或加入请求间隔,避免触发限流。

10.4 TypeScript 类型处理

部分高德 API 的类型定义可能不完整(如 RidingPolicy),可以使用 (window.AMap as any) 进行类型断言,或自行补充类型声明。

10.5 隐藏版权信息

如果业务需要,可以通过 CSS 深度选择器隐藏高德地图的 logo 和版权信息:

css 复制代码
:deep(.amap-copyright),
:deep(.amap-logo) {
  display: none !important;
}

注意:请确认你的高德地图使用协议是否允许隐藏版权信息。

10.6 音频自动播放限制

浏览器通常禁止在没有用户交互的情况下自动播放音频,因此报警音效可能需要在用户首次点击页面后才能正常触发。建议在报警逻辑中做好 try-catch 错误处理。

十一、总结

本文完整介绍了高德地图骑行导航 API 在 Vue 3 项目中的实践,涵盖以下核心功能模块:

功能模块 使用的 API 核心要点
地图初始化 AMap.Map 配置中心点、缩放级别、自定义样式
骑行路径规划 AMap.Riding + search() 电动车模式、批量并发搜索
自定义标记 AMap.Marker + DOM 增量更新、DOM 复用、状态波纹动画
区域遮罩/高亮 AMap.Polygon 镂空遮罩实现聚光灯效果
实时轮询 setTimeout + Promise.all 每 3 秒刷新,并发路径搜索
跨组件通信 BroadcastChannel 点击电站切换联动
报警音效 HTMLAudioElement 浏览器自动播放限制处理

关键设计原则:

  1. 隐藏默认标记(hideMarkers: true),使用自定义 DOM 标记实现更丰富的视觉效果
  2. 批量并发搜索(Promise.all),大幅提升多路径计算性能
  3. 增量更新复用(DOM 复用 + 实例复用),避免重复创建和闪烁
  4. 完善的资源清理(onBeforeUnmount),防止内存泄漏

希望本文能帮助你在项目中更好地使用高德地图骑行导航 API!

相关推荐
AlienZHOU1 天前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc1 天前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师1 天前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen1 天前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒1 天前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow1 天前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林8181 天前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang800781 天前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明1 天前
第 6 章 组件进阶
前端·vue.js
江华森1 天前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端