Vue3+TS项目中高德地图组件封装(集成关键词搜索、输入提示、标记点、信息弹窗和数据回显)

目录标题

1、前置准备:获取Key和密钥以及安装依赖

访问高德开放平台注册账号并创建应用,选择应用类型为"Web端(JS API)"。创建成功后获取Key和安全密钥。

在Vue3项目中安装@amap/amap-jsapi-loader依赖,用于异步加载高德地图API。(代码示例使用的版本是: "@amap/amap-jsapi-loader": "^1.0.1")

bash 复制代码
npm install @amap/amap-jsapi-loader --save
# 或
pnpm add @amap/amap-jsapi-loader

2、核心代码实现流程解析

组件初始化与地图加载

设置安全密钥至全局变量 window._AMapSecurityConfig,确保在加载JS API脚本前完成配置。

通过 AMapLoader.load 异步加载地图核心库及插件(如地点搜索、输入提示)。在回调中创建地图实例,配置初始缩放级别和中心点。

地点搜索与输入提示

输入提示功能通过绑定 AMap.AutoComplete 实例实现,用户选择提示项时自动触发搜索。

搜索功能调用 placeSearch.search 清理旧标记后发起请求,成功返回POI列表后,遍历添加标记点并调整地图视野以展示所有结果。

标记点与信息窗口交互

标记点使用自定义图标创建,附带地点数据(名称、地址、经纬度)以便后续交互。

点击标记点触发事件处理函数,提取位置信息并保存至响应式变量,同时通过事件传递数据至父组件。

信息窗口通过 AMap.InfoWindow 展示,内容为自定义HTML模板,包含地址和经纬度信息。

数据回显功能

初始化时检查父组件传入的 selectedLocation 属性,若有效则作为初始位置,否则使用默认坐标。

通过监听属性变化实现外部数据更新时同步地图显示,确保标记点和信息窗口动态更新。

3、完整组件示例

整合以上功能,提供一个完整的高德地图组件示例。

复制代码
<script setup lang="ts">
import {
  ref,
  reactive,
  onMounted,
  onUnmounted,
  watch,
  getCurrentInstance,
  nextTick
} from "vue";
import AMapLoader from "@amap/amap-jsapi-loader";

import { message } from "@/utils/message";

// --------------------------------
// 地图相关实例和变量定义
// --------------------------------

// 地图实例
let map: any = null;
// 地点搜索实例
let placeSearch: any = null;
// 自动完成实例
let autoComplete: any = null;
// 信息窗口实例
let infoWindow: any = null;
// 所有标记点数组,用于管理地图上的标记点
let markers: any[] = [];

interface sLocation {
  lng?: number;
  lat?: number;
  address?: string;
}

// 当前选中的位置信息,响应式变量
const selectedLocation = ref<sLocation | null>(null);

// 默认坐标(北京天安门)
const DEFAULT_CENTER = [116.397428, 39.90923];
// 默认位置信息
const DEFAULT_LOCATION = {
  lng: 116.397428,
  lat: 39.90923,
  address: "北京市东城区天安门"
};

// --------------------------------
// 组件属性定义
// --------------------------------

// 定义组件属性,父组件可以传递选中的位置
const props = defineProps<{
  selectedLocation?: sLocation;
}>();

// --------------------------------
// 组件事件定义
// --------------------------------

// 定义组件事件,向父组件传递选中的位置信息
const emit = defineEmits<{
  (
    e: "selectLocation",
    location: {
      lng: number;
      lat: number;
      address: string;
    }
  ): void;
}>();

// --------------------------------
// 响应式变量定义
// --------------------------------

// 地图容器引用
const mapContainer = ref<HTMLElement | null>(null);
// 搜索结果列表
const searchResults = ref<any[]>([]);
// 地图加载状态
const loading = ref<boolean>(true);
// 搜索关键词
const keyword = ref<string>("");
// 搜索输入框的DOM引用
const searchInputRef = ref<HTMLInputElement | null>(null);

// --------------------------------
// 辅助函数和工具函数
// --------------------------------

/**
 * 获取更美观的标记点图标
 * 使用更小尺寸(20x30像素)的红色标记点图标
 * @returns AMap.Icon 图标实例
 */
const getMarkerIcon = (AMap: any) => {
  return new AMap.Icon({
    size: new AMap.Size(20, 30), // 图标尺寸:宽20px,高30px
    image: "https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png", // 红色标记点图标
    imageSize: new AMap.Size(20, 30) // 图标显示尺寸
  });
};

/**
 * 获取当前组件实例
 */
const instance = getCurrentInstance();

// --------------------------------
// 地图初始化函数
// --------------------------------

/**
 * 初始化地图和相关插件
 * 这是组件的核心初始化函数,负责:
 * 1. 加载高德地图API
 * 2. 创建地图实例
 * 3. 初始化搜索和自动完成插件
 * 4. 设置事件监听
 * 5. 处理数据回显
 */
const initMap = () => {
  // 安全检查:确保组件实例和地图容器存在
  if (!instance || !mapContainer.value) return;

  // 从全局配置中获取地图配置
  const { MapConfigure } = instance.appContext.config.globalProperties.$config;

  console.log("地图配置信息:", MapConfigure);

  // --------------------------------
  // 步骤1: 设置安全密钥(必须在加载JS API之前设置)
  // --------------------------------
  window._AMapSecurityConfig = {
    securityJsCode: MapConfigure.securityJsCode
  };

  // --------------------------------
  // 步骤2: 加载高德地图API和插件
  // --------------------------------
  AMapLoader.load({
    key: MapConfigure.amapKey, // 地图API密钥
    version: "2.0", // API版本
    plugins: [
      "AMap.ToolBar", // 工具条插件:提供缩放、平移等控件
      "AMap.PlaceSearch", // 地点搜索插件:提供关键词搜索功能
      "AMap.AutoComplete" // 自动完成插件:提供输入提示功能
    ]
  })
    .then(AMap => {
      console.log("✅ 高德地图API加载成功");

      // --------------------------------
      // 步骤3: 创建地图实例
      // --------------------------------
      map = new AMap.Map(mapContainer.value, {
        zoom: 13, // 初始缩放级别
        center: DEFAULT_CENTER, // 初始中心点坐标
        resizeEnable: true // 允许地图自适应窗口大小
      });

      // --------------------------------
      // 步骤4: 添加地图控件
      // --------------------------------
      // 添加工具条控件(缩放、平移按钮)
      map.addControl(new AMap.ToolBar());

      // --------------------------------
      // 步骤5: 创建地点搜索实例
      // --------------------------------
      // 注意:这里不传递map参数,避免自动添加标记点
      placeSearch = new AMap.PlaceSearch({
        pageSize: 20, // 每页显示结果数量
        pageIndex: 1, // 当前页码
        city: "全国", // 搜索城市范围
        //map:map, 注意:这里不设置map参数,避免自动添加标记点
        autoFitView: true // 搜索结果后自动调整地图视野
      });

      // --------------------------------
      // 步骤6: 创建信息窗口实例
      // --------------------------------
      infoWindow = new AMap.InfoWindow({
        offset: new AMap.Pixel(0, 0), // 信息窗口偏移量
        closeWhenClickMap: true // 点击地图时关闭信息窗口
      });

      // --------------------------------
      // 步骤7: 初始化自动完成插件(输入提示)
      // --------------------------------
      // 等待DOM渲染完成后初始化
      nextTick(() => {
        if (searchInputRef.value) {
          // 创建自动完成实例,直接绑定到输入框
          autoComplete = new AMap.AutoComplete({
            input: searchInputRef.value, // 绑定到输入框
            city: "全国" // 搜索城市范围
          });

          // --------------------------------
          // 步骤8: 监听自动完成选择事件
          // --------------------------------
          autoComplete.on("select", (e: any) => {
            console.log("用户选择了自动完成建议:", e.poi);
            // 当用户选择搜索建议时,执行搜索
            keyword.value = e.poi.name;
            searchByKeyword(e.poi.name);
          });
        }
      });

      // --------------------------------
      // 步骤9: 监听地图点击事件
      // --------------------------------
      map.on("click", () => {
        // 点击地图时关闭信息窗口
        if (infoWindow) {
          infoWindow.close();
        }
      });

      // --------------------------------
      // 步骤10: 监听地图加载完成事件
      // --------------------------------
      map.on("complete", () => {
        console.log("✅ 地图加载完成");
        loading.value = false;
      });

      // --------------------------------
      // 步骤11: 数据回显处理
      // --------------------------------
      // 检查父组件是否传递了选中位置
      if (props.selectedLocation?.lng && props.selectedLocation?.lat) {
        console.log("收到父组件传递的位置:", props.selectedLocation);
        // 使用父组件传递的位置
        const location = {
          lng: props.selectedLocation.lng,
          lat: props.selectedLocation.lat,
          address: props.selectedLocation.address
        };
        // 将地图中心移动到该位置
        map.setCenter([location.lng, location.lat]);
        // 添加标记点
        addMarker([location.lng, location.lat], location);
        // 显示信息窗口
        showInfoWindow(location);
      } else {
        // 如果没有传递位置,使用默认位置
        console.log("使用默认位置:", DEFAULT_LOCATION);
        map.setCenter(DEFAULT_CENTER);
        addMarker(DEFAULT_CENTER, DEFAULT_LOCATION);
        showInfoWindow(DEFAULT_LOCATION);
      }
    })
    .catch(error => {
      console.error("❌ 地图加载失败:", error);
      loading.value = false;
    });
};

// --------------------------------
// 搜索功能相关函数
// --------------------------------

/**
 * 执行关键词搜索
 * @param keyword - 搜索关键词
 * 功能:
 * 1. 清空之前的标记点
 * 2. 执行搜索
 * 3. 处理搜索结果
 * 4. 在地图上添加标记点
 */
const searchByKeyword = (keyword: string) => {
  // 安全检查
  if (!placeSearch || !keyword.trim()) return;

  console.log("执行搜索,关键词:", keyword);

  // 清空之前的标记点
  clearMarkers();

  // 执行搜索
  placeSearch.search(keyword, (status: string, result: any) => {
    if (status === "complete" && result.poiList && result.poiList.pois) {
      console.log("搜索成功:", result);
      console.log("搜索成功,找到结果:", result.poiList.pois.length);
      // 保存搜索结果
      searchResults.value = result.poiList.pois;

      // 在地图上为每个搜索结果添加标记点
      result.poiList.pois.forEach((poi: any) => {
        console.log(poi, "poi");
        const location = {
          lng: poi.location.lng,
          lat: poi.location.lat,
          address: `${poi.name} (${poi.address})`,
          fullInfo: poi // 保存完整信息用于InfoWindow显示
        };
        addMarker([location.lng, location.lat], location);
      });

      // 自动调整地图视野以显示所有标记点
      map.setFitView();
    } else {
      console.log("搜索失败或无结果");
      message("搜索失败或无结果", { type: "warning" });
      searchResults.value = [];
    }
  });
};

// --------------------------------
// 标记点管理相关函数
// --------------------------------

/**
 * 在地图上添加标记点
 * @param position - 标记点位置 [经度, 纬度]
 * @param data - 标记点相关数据
 * 功能:
 * 1. 创建标记点实例
 * 2. 设置标记点样式
 * 3. 绑定点击事件
 * 4. 将标记点添加到地图
 */
const addMarker = (position: [number, number], data: any) => {
  if (!map) return;

  // 创建标记点实例
  const marker = new AMap.Marker({
    position: position, // 标记点位置
    title: data.address, // 鼠标悬停时显示的标题
    icon: getMarkerIcon(window.AMap), // 使用自定义图标
    extData: data // 将数据附加到标记点上,便于后续获取
  });

  // --------------------------------
  // 监听标记点点击事件
  // --------------------------------
  marker.on("click", () => {
    console.log("点击了标记点:", data);
    handleMarkerClick(data);
  });

  // 将标记点添加到数组和地图
  markers.push(marker);
  map.add(marker);
};

/**
 * 处理标记点点击事件
 * @param data - 标记点相关数据
 * 功能:
 * 1. 保存并显示位置信息
 * 2. 将地图中心移动到标记点位置
 */
const handleMarkerClick = (data: any) => {
  // 保存并显示位置信息
  saveAndShowLocation(data);

  // 将地图中心移动到标记点位置
  const position = [data.lng, data.lat];
  map.setCenter(position);
};

// --------------------------------
// 位置信息管理相关函数
// --------------------------------

/**
 * 保存并显示位置信息
 * @param data - 位置相关数据
 * 功能:
 * 1. 构建位置信息对象
 * 2. 保存到响应式变量
 * 3. 触发事件传递给父组件
 * 4. 显示信息窗口
 */
const saveAndShowLocation = (data: any) => {
  console.log(data, "saveAndShowLocation");
  // 构建位置信息对象
  const location = {
    lng: data.lng,
    lat: data.lat,
    address: data.address
  };

  console.log("保存位置信息:", location);

  // 保存到选中位置变量
  selectedLocation.value = location;

  // 触发事件给父组件(只传递必需的信息)
  emit("selectLocation", {
    lng: location.lng,
    lat: location.lat,
    address: location.address
  });

  console.log("触发事件给父组件(只传递必需的信息):", location);

  // 显示信息窗口
  showInfoWindow(location);
};

/**
 * 显示信息窗口
 * @param location - 位置信息
 * 功能:
 * 1. 构建信息窗口内容
 * 2. 关闭之前的信息窗口
 * 3. 打开新的信息窗口
 */
const showInfoWindow = (location: any) => {
  if (!infoWindow || !map) return;

  console.log(location, "location");
  // 构建信息窗口内容
  const content = `
    <div style="padding: 10px; min-width: 250px; max-width: 300px;  color: #666; font-size: 12px; line-height: 1.5;">
      <div style="margin-bottom: 8px;">
        <strong>已选位置:</strong> ${location.address}
      </div>
      <div style="margin-top: 8px;">
        <strong>经纬度:</strong> ${location.lng.toFixed(6)}, ${location.lat.toFixed(6)}
      </div>
    </div>
  `;

  // 关闭之前的信息窗口
  infoWindow.close();

  // 打开新的信息窗口
  infoWindow.setContent(content);
  infoWindow.open(map, [location.lng, location.lat]);
};

// --------------------------------
// 清理和工具函数
// --------------------------------

/**
 * 清空所有标记点
 * 功能:
 * 1. 从地图中移除所有标记点
 * 2. 清空标记点数组
 * 3. 清空搜索结果
 * 4. 关闭信息窗口
 */
const clearMarkers = () => {
  if (!map) return;

  console.log("清空标记点");

  // 从地图中移除所有标记点
  markers.forEach(marker => {
    map.remove(marker);
  });

  // 清空标记点数组
  markers = [];

  // 清空搜索结果列表
  searchResults.value = [];

  // 清空搜索关键词
  // keyword.value = "";

  // 关闭信息窗口
  if (infoWindow) {
    infoWindow.close();
  }
};

/**
 * 处理搜索按钮点击
 * 功能:执行关键词搜索
 */
const handleSearch = () => {
  if (keyword.value.trim()) {
    console.log("进行搜索,关键词:", keyword.value);
    searchByKeyword(keyword.value);
  }
};

/**
 * 处理键盘事件
 * @param e - 键盘事件对象
 * 功能:监听Enter键执行搜索
 */
const handleKeydown = (e: KeyboardEvent) => {
  if (e.key === "Enter") {
    console.log("按下Enter键执行搜索");
    handleSearch();
  }
};

// --------------------------------
// 监听父组件传递的选中位置变化
// --------------------------------

/**
 * 监听props.selectedLocation变化
 * 功能:当父组件传递的位置变化时,更新地图显示
 */
watch(
  () => props.selectedLocation,
  newVal => {
    if (newVal && newVal.lng && newVal.lat && map) {
      console.log("selectedLocation变化", newVal);
    }
  },
  { deep: true }
);

// --------------------------------
// 生命周期钩子
// --------------------------------

/**
 * 组件挂载时初始化地图
 */
onMounted(() => {
  console.log("地图组件挂载");
  // 确保DOM已经渲染
  nextTick(() => {
    initMap();
  });
});

/**
 * 组件卸载时清理资源
 */
onUnmounted(() => {
  console.log("地图组件卸载");
  if (map) {
    map.destroy();
    map = null;
  }
});
</script>

<template>
  <div class="map-container">
    <!-- 地图实例容器 -->
    <div ref="mapContainer" class="map-view" :class="{ loading }">
      <!-- 搜索框 -->
      <div class="simple-search-box">
        <input
          ref="searchInputRef"
          v-model="keyword"
          type="text"
          placeholder="请输入关键字搜索"
          class="simple-search-input"
          @blur="handleKeydown"
        />
      </div>

      <!-- 地图加载遮罩 -->
      <div v-if="loading" class="loading-mask">地图加载中...</div>
    </div>
  </div>
</template>

<style>
.map-container {
  width: 100%;
  height: 300px;
  display: flex;
  flex-direction: column;
  position: relative;
  overflow: hidden;
}

.map-view {
  flex: 1;
  position: relative;
  background: #f5f7fa;
}

.loading-mask {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.9);
  font-size: 16px;
  color: #606266;
  z-index: 999;
}

/* 搜索框样式 */
.simple-search-box {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 1000;
  width: 300px;
  background: white;
  border-radius: 2px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
  display: flex;
}

.simple-search-input {
  flex: 1;
  height: 36px;
  padding: 0 12px;
  border: none;
  border-right: 1px solid #ddd;
  font-size: 14px;
  color: #333;
  outline: none;
}

.amap-sug-result {
  position: absolute;
  z-index: 9999 !important;
}
</style>




4、关键实现技巧与优化建议

安全地管理密钥

避免将key和securityJsCode硬编码在源码中。生产环境中推荐使用环境变量(如.env文件)存储,通过import.meta.env动态读取,确保敏感信息的安全性。

资源销毁

在组件的onUnmounted生命周期钩子中调用map.destroy(),主动销毁地图实例。这一操作能有效释放内存,防止因实例未清理导致的内存泄漏问题。

优化标记点图标

默认的红色标记点图标可替换为自定义图片URL。通过调整图标样式与项目UI设计保持一致,提升视觉统一性。支持动态修改图标属性,适应不同场景需求。

增强错误处理

基础示例已包含搜索无结果的提示。实际项目中需扩展更多容错机制,例如网络请求失败、API加载超时等场景。建议增加用户友好的反馈界面,结合日志记录辅助排查问题。

扩展功能方向

当前实现聚焦搜索、标记和信息弹窗功能。后续可集成地理编码服务转换地址坐标,添加路线绘制或区域覆盖物(如多边形)等高级功能。通过组合高德地图API与Vue3响应式特性,灵活扩展业务逻辑。

5、常见问题与解决方案

  • z-index问题的根源: 当组件在弹窗或对话框中使用时,高德地图的自动完成下拉框(如.amap-sug-result)可能被其他元素的样式覆盖。这主要是由于以下原因:①弹窗通常具有较高的z-index值。②高德地图生成的DOM元素无法直接受Vue组件的scoped样式影响 。
  • 安全密钥设置:在调用AMapLoader.load()方法之前,必须先设置window._AMapSecurityConfig。
  • 域名白名单:在高德控制台中,将本地开发域名(例如localhost和127.0.0.1)添加到白名单。
  • key与安全密钥匹配:确认使用的key和安全密钥来自同一高德应用。
  • 网络问题:检查是否能正常访问高德地图的API服务,确保网络连接畅通。

如何获取技术支持

如果遇到无法解决的问题,可以通过以下方式寻求帮助:

相关推荐
Light601 天前
Vue 高阶优化术:v-bind 与 v-on 的实战妙用与思维跃迁
前端·低代码·vue3·v-bind·组件封装·v-on·ai辅助开发
Jeking2175 天前
进阶流程图绘制工具 Unione Flow Editor-- 巧用Event事件机制,破解复杂业务交互难题
流程图·vue3·workflow·unione flow·flow editor·unione cloud
一只小阿乐6 天前
前端vue3 web端中实现拖拽功能实现列表排序
前端·vue.js·elementui·vue3·前端拖拽
AAA阿giao6 天前
从“操纵绳子“到“指挥木偶“:Vue3 Composition API 如何彻底改变前端开发范式
开发语言·前端·javascript·vue.js·前端框架·vue3·compositionapi
૮・ﻌ・6 天前
Vue3:组合式API、Vue3.3新特性、Pinia
前端·javascript·vue3
凯小默9 天前
37-实现地图配置项(完结)
echarts·vue3
凯小默10 天前
36-引入地图
echarts·vue3
凯小默10 天前
【TypeScript+Vue3+Vite+Vue-router+Vuex+Mock 进行 WEB 前端项目实战】学习笔记共 89 篇(完结)
typescript·echarts·mock·vue3·vite·vuex·vue-router
凯小默11 天前
34-监听数据渲染饼图以及饼图配置
vue3