news 2026/5/10 4:39:38

Vue3 + 高德地图 JS API 2.0 实战:打造多功能地址选择组件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3 + 高德地图 JS API 2.0 实战:打造多功能地址选择组件

在前端开发中,地图组件是非常常见的需求,尤其是地址选择、经纬度获取这类场景。本文将基于 Vue3 + 高德地图 JS API 2.0,详细讲解如何封装一个功能完整、易用性强的地图地址选择组件,包含地址搜索、地图点击选点、经纬度双向绑定等核心功能。

组件核心功能概览

  • 基础地图初始化(支持自定义初始经纬度、缩放级别)
  • 地址搜索(地理编码):输入地址自动定位并标记
  • 地图点击选点:点击地图任意位置获取经纬度并标记
  • 反地理编码(可选扩展):通过经纬度反查具体地址
  • 自定义标记图标:支持自定义标记点的样式和尺寸
  • 经纬度双向通信:向父组件实时传递选中的经纬度
  • 完善的异常处理和生命周期管理

前置准备

在使用该组件前,你需要完成以下准备工作:

1.注册高德地图开发者账号,获取Web 端开发者 Key安全密钥(securityJsCode)

2.安装依赖:

npm install @amap/amap-jsapi-loader --save

3.将代码中的mapConfig里的keysecurityJsCode替换为你自己的:

const mapConfig = { key: "你的高德Key", securityJsCode: "你的安全密钥", version: "2.0", plugins: ["AMap.Scale", "AMap.Geocoder", "AMap.Marker"] };

功能详解与使用方法

组件基础使用(父组件中引入)

这是最基础的使用方式,直接引入组件并设置初始参数:

<template> <div style="width: 100%; height: 600px;"> <!-- 地图组件 --> <MapSelector :initLng="116.397428" :initLat="39.90923" :initZoom="11" @coordChange="handleCoordChange" /> <!-- 显示选中的经纬度 --> <div> 选中的经纬度:{{ selectedLng }}, {{ selectedLat }} </div> </div> </template> <script setup> import { ref } from "vue"; import MapSelector from "./components/MapSelector.vue"; const selectedLng = ref(116.397428); const selectedLat = ref(39.90923); // 接收子组件传递的经纬度 const handleCoordChange = (coord) => { selectedLng.value = coord.lng; selectedLat.value = coord.lat; console.log("当前选中的经纬度:", coord); }; </script>

参数说明

  • initLng:初始经度(默认 116.397428)
  • initLat:初始纬度(默认 39.90923)
  • initZoom:初始缩放级别(默认 11)
  • markerIcon:自定义标记图标地址(可选)
  • markerSize:标记图标尺寸(默认 [40, 40])
  • @coordChange:监听经纬度变化的事件

地址搜索功能

组件内置了地址搜索框,使用方式非常简单:

  1. 在搜索框中输入地址 / 地点名称(如 “北京市朝阳区天安门”)
  2. 点击 “搜索” 按钮,组件会自动:
    • 执行地理编码,将地址转换为经纬度
    • 将地图中心定位到该位置,并缩放至 15 级
    • 在该位置添加标记点
    • 通过coordChange事件向父组件传递经纬度
  3. 若搜索失败(如地址不存在),会在搜索框下方显示错误提示
  4. 点击 “清空” 按钮可清空搜索框内容和错误提示

地图点击选点功能

这是组件的核心交互功能之一:

  1. 点击地图上任意位置
  2. 组件会自动:
    • 获取点击位置的经纬度
    • 移除原有标记点,在点击位置添加新标记
    • 通过coordChange事件向父组件实时传递新的经纬度
    • 在控制台打印点击位置的经纬度(便于调试)

自定义标记图标

如果你想替换默认的标记图标,只需在父组件中传递markerIconmarkerSize参数:

<MapSelector :initLng="116.397428" :initLat="39.90923" markerIcon="/static/images/marker.png" :markerSize="[50, 50]" @coordChange="handleCoordChange" />
  • markerIcon:传入图标文件的路径(支持相对路径 / 绝对路径 / CDN 地址)
  • markerSize:传入数组 [宽度,高度],设置图标的显示尺寸

调用组件暴露的方法(高级用法)

组件通过defineExpose暴露了多个方法和数据,你可以在父组件中通过ref调用:

<template> <div style="width: 100%; height: 600px;"> <MapSelector ref="mapRef" :initLng="116.397428" :initLat="39.90923" @coordChange="handleCoordChange" /> <!-- 手动控制按钮 --> <div style="margin-top: 10px;"> <button @click="setCustomCoord">手动设置经纬度</button> <button @click="reverseGeocodeTest">反查地址</button> <button @click="removeMarker">移除标记</button> </div> </div> </template> <script setup> import { ref } from "vue"; import MapSelector from "./components/MapSelector.vue"; const mapRef = ref(null); // 1. 手动设置经纬度 const setCustomCoord = async () => { // 等待地图加载完成 await new Promise(resolve => setTimeout(resolve, 500)); // 调用组件暴露的 setCoord 方法 mapRef.value.setCoord(120.123456, 30.654321); }; // 2. 反地理编码:通过经纬度查地址 const reverseGeocodeTest = async () => { await new Promise(resolve => setTimeout(resolve, 500)); const result = await mapRef.value.reverseGeocode(116.397428, 39.90923); console.log("反查地址结果:", result); // 包含省市区等详细信息 }; // 3. 移除标记点 const removeMarker = () => { mapRef.value.removeMarker(); }; const handleCoordChange = (coord) => { console.log("经纬度变化:", coord); }; </script>

常用暴露方法说明

  • setCoord(lng, lat):手动设置经纬度并添加标记
  • geocode(address):手动执行地理编码(地址转经纬度)
  • reverseGeocode(lng, lat):手动执行反地理编码(经纬度转地址)
  • addMarker(lng, lat, title):添加标记点
  • removeMarker():移除标记点
  • selectedLng/selectedLat:获取当前选中的经纬度

监听初始参数变化

组件内置了watch监听,当父组件修改initLng/initLat时,地图会自动更新标记位置和中心:

<template> <div> <button @click="changeInitCoord">修改初始经纬度</button> <MapSelector :initLng="initLng" :initLat="initLat" @coordChange="handleCoordChange" /> </div> </template> <script setup> import { ref } from "vue"; import MapSelector from "./components/MapSelector.vue"; const initLng = ref(116.397428); const initLat = ref(39.90923); // 修改初始经纬度 const changeInitCoord = () => { initLng.value = 121.473701; initLat.value = 31.230416; // 上海经纬度 }; const handleCoordChange = (coord) => { console.log(coord); }; </script>

组件核心优化点说明

  1. 生命周期管理:在onUnmounted中销毁地图实例、移除事件监听,避免内存泄漏
  2. 异常处理:所有核心方法都有 try/catch 包裹,地图加载完成后才执行操作
  3. 标记点管理:添加新标记前先移除旧标记,避免重复标记
  4. 兼容性:使用 2D 地图模式,兼容更多浏览器和设备
  5. 事件防抖:搜索和清空按钮添加了事件阻止,避免冒泡

总结

  • 该组件基于 Vue3 + 高德地图 JS API 2.0 开发,封装了地址搜索、地图选点、经纬度传递等核心功能,开箱即用。
  • 使用时需先替换高德地图 Key 和安全密钥,父组件可通过 Props 配置初始参数,通过coordChange事件接收经纬度。
  • 组件暴露了丰富的方法(如setCoordreverseGeocode),支持高级自定义操作,满足不同场景需求。

源码

<template> <div class="map-wrapper"> <div class="search-container"> <input v-model="searchKeyword" type="text" placeholder="请输入地址/地点名称" class="search-input" /> <button @click="handleSearch" class="search-btn">搜索</button> <button @click="clearSearch" class="clear-btn">清空</button> </div> <div id="container" class="map-container"></div> <div v-if="searchError" class="error-tip">{{ searchError }}</div> </div> </template> <script setup> import { ref, onMounted, onUnmounted, defineExpose, defineProps, computed, watch, defineEmits } from "vue"; import AMapLoader from "@amap/amap-jsapi-loader"; // 1. 定义自定义事件,用于向父组件传递经纬度 const emit = defineEmits(['coordChange']); // 定义Props const props = defineProps({ initLng: { type: Number, default: 116.397428 }, initLat: { type: Number, default: 39.90923 }, initZoom: { type: Number, default: 11 }, markerIcon: { type: String, default: "" }, markerSize: { type: Array, default: () => [40, 40] } }); // 计算初始中心点 const initCenter = computed(() => [props.initLng, props.initLat]); // 响应式数据 const map = ref(null); const geocoder = ref(null); const marker = ref(null); const mapLoaded = ref(false); const searchKeyword = ref(""); const searchResult = ref(null); const searchError = ref(""); // 新增:存储选中的经纬度 const selectedLng = ref(props.initLng); const selectedLat = ref(props.initLat); // 地图配置 const mapConfig = { key: "", // 替换为你的key securityJsCode: "", // 替换为你的安全密钥 version: "2.0", plugins: ["AMap.Scale", "AMap.Geocoder", "AMap.Marker"] // 显式声明Marker插件 }; // 移除标记 const removeMarker = () => { if (marker.value && map.value) { map.value.remove(marker.value); marker.value = null; } }; // 添加标记(核心修复) const addMarker = (lng, lat, title = "标记点") => { // 前置校验:确保地图和AMap对象存在 if (!map.value || !window.AMap) { console.error("地图实例或AMap未初始化"); return null; } // 先移除已有标记 removeMarker(); try { // 标记基础配置 const markerOptions = { position: new window.AMap.LngLat(lng, lat), // 显式创建LngLat对象 title: title, anchor: "bottom-center", zIndex: 9999, // 强制最高层级 offset: new window.AMap.Pixel(0, 3) // 调整锚点偏移,避免标记被遮挡 }; // 自定义图标(兼容默认图标) if (props.markerIcon) { markerOptions.icon = new window.AMap.Icon({ size: new window.AMap.Size(...props.markerSize), image: props.markerIcon, imageSize: new window.AMap.Size(...props.markerSize) }); } else { // 强制使用高德默认图标(兜底) markerOptions.icon = new window.AMap.Icon({ size: new window.AMap.Size(32, 32), image: "https://a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png", imageSize: new window.AMap.Size(32, 32) }); } // 创建并添加标记 marker.value = new window.AMap.Marker(markerOptions); map.value.add(marker.value); // 调试信息 console.log("标记添加成功:", { lng, lat, title }); return marker.value; } catch (e) { console.error("创建标记失败:", e); return null; } }; // 2. 新增:处理地图点击事件,获取经纬度 const handleMapClick = (e) => { if (!mapLoaded.value) return; // 获取点击位置的经纬度 const lng = e.lnglat.getLng(); const lat = e.lnglat.getLat(); // 更新选中的经纬度 selectedLng.value = lng; selectedLat.value = lat; // 在点击位置添加标记 addMarker(lng, lat, "选中位置"); // 触发自定义事件,向父组件传递经纬度 emit('coordChange', { lng, lat }); console.log("地图点击位置:", { lng, lat }); }; // 初始化地图(核心修复) const initMap = async () => { try { // 1. 设置安全密钥 window._AMapSecurityConfig = { securityJsCode: mapConfig.securityJsCode }; // 2. 加载AMap核心 const AMap = await AMapLoader.load({ key: mapConfig.key, version: mapConfig.version, plugins: mapConfig.plugins }); // 3. 挂载到window(关键) window.AMap = AMap; // 4. 创建地图实例 map.value = new AMap.Map("container", { viewMode: "2D", // 先改用2D模式,3D可能有兼容性问题 zoom: props.initZoom, center: initCenter.value, resizeEnable: true // 开启自适应 }); // 5. 添加比例尺 map.value.addControl(new AMap.Scale()); // 6. 创建地理编码实例 geocoder.value = new AMap.Geocoder({ radius: 1000, extensions: "all" }); // 7. 监听地图加载完成事件(关键) map.value.on("complete", () => { mapLoaded.value = true; // 地图加载完成后立即添加初始标记 addMarker(props.initLng, props.initLat, "初始位置"); console.log("地图加载完成,初始标记已添加"); // 8. 新增:绑定地图点击事件 map.value.on('click', handleMapClick); }); } catch (e) { console.error("地图初始化失败:", e); } }; // 地理编码 const geocode = async (address) => { if (!mapLoaded.value || !geocoder.value) throw new Error("地图未加载完成"); return new Promise((resolve, reject) => { geocoder.value.getLocation(address, (status, result) => { if (status === "complete" && result.geocodes.length > 0) { const { lng, lat } = result.geocodes[0].location; const formattedAddress = result.geocodes[0].formattedAddress; // 更新选中的经纬度 selectedLng.value = lng; selectedLat.value = lat; map.value.setCenter([lng, lat]); map.value.setZoom(15); addMarker(lng, lat, formattedAddress); // 搜索结果添加标记 // 触发自定义事件 emit('coordChange', { lng, lat }); resolve({ lng, lat, address: formattedAddress }); } else { reject(new Error(`地理编码失败:${result.info || "地址不存在"}`)); } }); }); }; // 反地理编码 const reverseGeocode = async (lng, lat) => { if (!mapLoaded.value || !geocoder.value) throw new Error("地图未加载完成"); return new Promise((resolve, reject) => { geocoder.value.getAddress([lng, lat], (status, result) => { if (status === "complete" && result.regeocode) { const { formatted_address: address, addressComponent } = result.regeocode; // 更新选中的经纬度 selectedLng.value = lng; selectedLat.value = lat; map.value.setCenter([lng, lat]); map.value.setZoom(15); addMarker(lng, lat, address); // 反编码结果添加标记 // 触发自定义事件 emit('coordChange', { lng, lat }); resolve({ address, province: addressComponent.province, city: addressComponent.city, district: addressComponent.district }); } else { reject(new Error(`反地理编码失败:${result.info || "坐标无效"}`)); } }); }); }; // 搜索/清空逻辑 const handleSearch = async () => { if (window.event) { window.event.preventDefault(); window.event.stopPropagation(); } searchError.value = ""; if (!searchKeyword.value.trim()) { searchError.value = "请输入地址"; return; } try { searchResult.value = await geocode(searchKeyword.value.trim()); } catch (error) { searchError.value = error.message; } }; const clearSearch = () => { if (window.event) { window.event.preventDefault(); window.event.stopPropagation(); } searchKeyword.value = ""; searchResult.value = null; searchError.value = ""; }; // 监听props变化,更新标记位置 watch([() => props.initLng, () => props.initLat], ([newLng, newLat]) => { if (mapLoaded.value) { selectedLng.value = newLng; selectedLat.value = newLat; addMarker(newLng, newLat, "初始位置"); map.value.setCenter([newLng, newLat]); // 触发自定义事件 emit('coordChange', { lng: newLng, lat: newLat }); } }); // 生命周期 onMounted(() => { // 确保DOM渲染完成后初始化地图 setTimeout(initMap, 100); }); onUnmounted(() => { // 移除点击事件监听 if (map.value) { map.value.off('click', handleMapClick); } removeMarker(); if (map.value) map.value.destroy(); map.value = null; geocoder.value = null; marker.value = null; mapLoaded.value = false; }); // 暴露方法和数据 defineExpose({ geocode, reverseGeocode, addMarker, removeMarker, map, mapLoaded, // 暴露选中的经纬度 selectedLng, selectedLat, // 暴露手动设置经纬度的方法 setCoord: (lng, lat) => { selectedLng.value = lng; selectedLat.value = lat; addMarker(lng, lat, "手动设置位置"); emit('coordChange', { lng, lat }); } }); </script> <style scoped> .map-wrapper { width: 100%; height: 100%; position: relative; box-sizing: border-box; } .search-container { z-index: 1000; position: absolute; top: 10px; left: 10px; display: flex; gap: 10px; background: #fff; padding: 8px; border-radius: 4px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .search-input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; outline: none; width: 200px; } .search-btn { background: #1677ff; color: white; border: none; padding: 0 12px; border-radius: 4px; cursor: pointer; } .clear-btn { background: #f5f5f5; color: #666; border: none; padding: 0 12px; border-radius: 4px; cursor: pointer; } /* 新增:经纬度信息显示样式 */ .coord-info { z-index: 1000; position: absolute; top: 10px; right: 10px; background: #fff; padding: 6px 12px; border-radius: 4px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); font-size: 12px; color: #333; } .map-container { width: 100%; height: 100%; z-index: 1; } .error-tip { position: absolute; top: 70px; left: 10px; z-index: 1000; background: #fff2f0; color: #f5222d; padding: 8px 12px; border-radius: 4px; border: 1px solid #ffccc7; font-size: 12px; } </style>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/10 8:17:16

第八届传智杯环保包装设计挑战赛练习题库(一)

1.[单选] 环保包装设计结合AIGC&#xff0c;可使包装 &#xff08; 1分 &#xff09; 得分&#xff1a;0分 更具科技感与环保性未选 更昂贵 更难生产 更易损坏 正确答案更具科技感与环保性 答案解析结合AIGC可融入科技元素且优化环保设计。不一定更昂贵、难生产或易损坏。…

作者头像 李华
网站建设 2026/5/1 9:11:06

【BiFormer】BiFormer: Vision Transformer with Bi-Level Routing Attention 译读笔记

BiFormer: Vision Transformer with Bi-Level Routing Attention 摘要 作为视觉变换器的核心构建模块&#xff0c;注意力机制是一种强大的工具&#xff0c;用于捕获长距离依赖关系。然而&#xff0c;这种强大的功能是有代价的&#xff1a;它会导致巨大的计算负担和沉重的内存…

作者头像 李华
网站建设 2026/5/3 12:38:50

知识图谱的智能跃迁:大模型环境下的架构革命

本文提出上下文图谱概念&#xff0c;通过四元组或n元组结构融入时效性、来源和决策逻辑等元数据&#xff0c;解决传统知识图谱的局限性。结合CGR3&#xff08;检索-排名-推理&#xff09;范式&#xff0c;利用大语言模型提升知识图谱补全和问答任务性能。实验显示&#xff0c;在…

作者头像 李华