在前端开发中,地图组件是非常常见的需求,尤其是地址选择、经纬度获取这类场景。本文将基于 Vue3 + 高德地图 JS API 2.0,详细讲解如何封装一个功能完整、易用性强的地图地址选择组件,包含地址搜索、地图点击选点、经纬度双向绑定等核心功能。
组件核心功能概览
- 基础地图初始化(支持自定义初始经纬度、缩放级别)
- 地址搜索(地理编码):输入地址自动定位并标记
- 地图点击选点:点击地图任意位置获取经纬度并标记
- 反地理编码(可选扩展):通过经纬度反查具体地址
- 自定义标记图标:支持自定义标记点的样式和尺寸
- 经纬度双向通信:向父组件实时传递选中的经纬度
- 完善的异常处理和生命周期管理
前置准备
在使用该组件前,你需要完成以下准备工作:
1.注册高德地图开发者账号,获取Web 端开发者 Key和安全密钥(securityJsCode)
2.安装依赖:
npm install @amap/amap-jsapi-loader --save3.将代码中的mapConfig里的key和securityJsCode替换为你自己的:
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:监听经纬度变化的事件
地址搜索功能
组件内置了地址搜索框,使用方式非常简单:
- 在搜索框中输入地址 / 地点名称(如 “北京市朝阳区天安门”)
- 点击 “搜索” 按钮,组件会自动:
- 执行地理编码,将地址转换为经纬度
- 将地图中心定位到该位置,并缩放至 15 级
- 在该位置添加标记点
- 通过
coordChange事件向父组件传递经纬度
- 若搜索失败(如地址不存在),会在搜索框下方显示错误提示
- 点击 “清空” 按钮可清空搜索框内容和错误提示
地图点击选点功能
这是组件的核心交互功能之一:
- 点击地图上任意位置
- 组件会自动:
- 获取点击位置的经纬度
- 移除原有标记点,在点击位置添加新标记
- 通过
coordChange事件向父组件实时传递新的经纬度 - 在控制台打印点击位置的经纬度(便于调试)
自定义标记图标
如果你想替换默认的标记图标,只需在父组件中传递markerIcon和markerSize参数:
<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>组件核心优化点说明
- 生命周期管理:在
onUnmounted中销毁地图实例、移除事件监听,避免内存泄漏 - 异常处理:所有核心方法都有 try/catch 包裹,地图加载完成后才执行操作
- 标记点管理:添加新标记前先移除旧标记,避免重复标记
- 兼容性:使用 2D 地图模式,兼容更多浏览器和设备
- 事件防抖:搜索和清空按钮添加了事件阻止,避免冒泡
总结
- 该组件基于 Vue3 + 高德地图 JS API 2.0 开发,封装了地址搜索、地图选点、经纬度传递等核心功能,开箱即用。
- 使用时需先替换高德地图 Key 和安全密钥,父组件可通过 Props 配置初始参数,通过
coordChange事件接收经纬度。 - 组件暴露了丰富的方法(如
setCoord、reverseGeocode),支持高级自定义操作,满足不同场景需求。
源码
<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>