高德地图骑行导航 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!

相关推荐
kyriewen2 小时前
我扒了 GPT-5.6 的全部编程跑分——有一项 OpenAI 没敢放出来
前端·gpt·ai编程
前端H3 小时前
微前端 v3 架构升级:从加载隔离到状态协同
前端·架构·状态模式
陆枫Larry4 小时前
小程序包体积优化:用 SVG 图片替换 iconfont,并保留 CSS 控色能力
前端
退休倒计时4 小时前
【每日一题】LeetCode 437. 路径总和 III TypeScript
算法·leetcode·typescript
环境栈笔记5 小时前
多账号浏览器选型检查清单:Profile、权限、Session 和任务日志怎么评估
前端·人工智能·后端·自动化
前端炒粉5 小时前
手写promise
java·前端·javascript
LaughingZhu5 小时前
智能体经典范式构建:ReAct、Plan-and-Solve 与 Reflection
前端·react.js·前端框架
jsonbro5 小时前
手把手带你实现发布订阅模式
前端·vue.js·设计模式
一只猫的梦5 小时前
自动化模块 (Automation Module)
前端·chrome
熊猫钓鱼>_>6 小时前
Electron:当 Web 技术统治桌面
大数据·前端·javascript·人工智能·架构·electron·agent