Vue3 + 高德地图 JS API 2.0 实战:打造多功能地址选择组件
在前端开发中地图组件是非常常见的需求尤其是地址选择、经纬度获取这类场景。本文将基于 Vue3 高德地图 JS API 2.0详细讲解如何封装一个功能完整、易用性强的地图地址选择组件包含地址搜索、地图点击选点、经纬度双向绑定等核心功能。组件核心功能概览基础地图初始化支持自定义初始经纬度、缩放级别地址搜索地理编码输入地址自动定位并标记地图点击选点点击地图任意位置获取经纬度并标记反地理编码可选扩展通过经纬度反查具体地址自定义标记图标支持自定义标记点的样式和尺寸经纬度双向通信向父组件实时传递选中的经纬度完善的异常处理和生命周期管理前置准备在使用该组件前你需要完成以下准备工作1.注册高德地图开发者账号获取Web 端开发者 Key和安全密钥securityJsCode2.安装依赖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 stylewidth: 100%; height: 600px; !-- 地图组件 -- MapSelector :initLng116.397428 :initLat39.90923 :initZoom11 coordChangehandleCoordChange / !-- 显示选中的经纬度 -- 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.397428initLat初始纬度默认 39.90923initZoom初始缩放级别默认 11markerIcon自定义标记图标地址可选markerSize标记图标尺寸默认 [40, 40]coordChange监听经纬度变化的事件地址搜索功能组件内置了地址搜索框使用方式非常简单在搜索框中输入地址 / 地点名称如 “北京市朝阳区天安门”点击 “搜索” 按钮组件会自动执行地理编码将地址转换为经纬度将地图中心定位到该位置并缩放至 15 级在该位置添加标记点通过coordChange事件向父组件传递经纬度若搜索失败如地址不存在会在搜索框下方显示错误提示点击 “清空” 按钮可清空搜索框内容和错误提示地图点击选点功能这是组件的核心交互功能之一点击地图上任意位置组件会自动获取点击位置的经纬度移除原有标记点在点击位置添加新标记通过coordChange事件向父组件实时传递新的经纬度在控制台打印点击位置的经纬度便于调试自定义标记图标如果你想替换默认的标记图标只需在父组件中传递markerIcon和markerSize参数MapSelector :initLng116.397428 :initLat39.90923 markerIcon/static/images/marker.png :markerSize[50, 50] coordChangehandleCoordChange /markerIcon传入图标文件的路径支持相对路径 / 绝对路径 / CDN 地址markerSize传入数组 [宽度高度]设置图标的显示尺寸调用组件暴露的方法高级用法组件通过defineExpose暴露了多个方法和数据你可以在父组件中通过ref调用template div stylewidth: 100%; height: 600px; MapSelector refmapRef :initLng116.397428 :initLat39.90923 coordChangehandleCoordChange / !-- 手动控制按钮 -- div stylemargin-top: 10px; button clicksetCustomCoord手动设置经纬度/button button clickreverseGeocodeTest反查地址/button button clickremoveMarker移除标记/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 clickchangeInitCoord修改初始经纬度/button MapSelector :initLnginitLng :initLatinitLat coordChangehandleCoordChange / /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 classmap-wrapper div classsearch-container input v-modelsearchKeyword typetext placeholder请输入地址/地点名称 classsearch-input / button clickhandleSearch classsearch-btn搜索/button button clickclearSearch classclear-btn清空/button /div div idcontainer classmap-container/div div v-ifsearchError classerror-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

相关新闻

Vue3 + TypeScript 封装 UEditor 富文本编辑器:一站式解决图片上传与格式控制

Vue3 + TypeScript 封装 UEditor 富文本编辑器:一站式解决图片上传与格式控制

UEditor(百度富文本编辑器)作为经典的富文本解决方案,在企业级项目中仍有广泛应用,但原生 UEditor 与 Vue3 TypeScript 结合时存在配置繁琐、图片上传逻辑不统一、样式控制难等问题。本文基于你提供的代码,详细讲解如…

2026/7/3 15:21:29 阅读更多 →
C++基于微服务脚手架的视频点播系统---客户端(2)

C++基于微服务脚手架的视频点播系统---客户端(2)

这是关于高性能即时通讯系统开发实战的续篇。在前文中,我们完成了系统架构设计的宏观规划、开发环境的精密部署以及版本控制策略的实施。本篇将深入客户端开发的微观层面,聚焦于应用程序启动流程的编排与主窗口视觉效果的深度定制。我们将探讨如何利用Qt…

2026/7/3 15:21:31 阅读更多 →
2026年网络安全行业新趋势:这5大方向,决定你明年的职业高薪

2026年网络安全行业新趋势:这5大方向,决定你明年的职业高薪

【收藏必看】2026网络安全五大核心方向全解析:人才缺口300万,薪资年年涨,助你实现职业跃迁 随着数字化进程加速,2026年网络安全人才缺口将持续扩大,突破300万。行业最值得关注的5大核心方向包括:AI安全应对…

2026/7/3 15:21:33 阅读更多 →

最新新闻

中外大模型能力对比分析

中外大模型能力对比分析

中外大模型能力差距:结构性成因的深度分析属性说明文档版本v1.0撰写日期2026-07-02文档类型技术战略分析分析视角机制解释,而非榜单罗列 摘要 「国产大模型不如国外」是一个过于粗糙的命题。截至 2026 年上半年,斯坦福 HAI《AI Index 2026》指…

2026/7/3 17:52:04 阅读更多 →
GHelper:如何用开源工具彻底解放你的华硕笔记本性能潜力?

GHelper:如何用开源工具彻底解放你的华硕笔记本性能潜力?

GHelper:如何用开源工具彻底解放你的华硕笔记本性能潜力? 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, Vivoboo…

2026/7/3 17:52:04 阅读更多 →
LENA-R8与PIC18LF45K40的嵌入式通信与精确定位方案

LENA-R8与PIC18LF45K40的嵌入式通信与精确定位方案

1. LENA-R8与PIC18LF45K40的硬件组合解析这个组合的核心价值在于将蜂窝通信与精确定位能力集成到嵌入式系统中。LENA-R8是u-blox推出的多模LTE Cat 1模块,支持14个LTE频段和4个GSM/GPRS频段,这意味着它能在全球绝大多数地区实现网络连接。其内置的u-blox…

2026/7/3 17:52:04 阅读更多 →
心电自监督分类论文分享(1)-read your heart

心电自监督分类论文分享(1)-read your heart

READING YOUR HEART 研究背景与动机 现有心电自监督学习分为对比学习、重构学习两类,但全部把心电当做普通时序信号,采用固定窗口、固定步长切割波形,存在两个核心缺陷: 丢失心电专属形态、节律特征破坏心跳间潜在语义关系 为…

2026/7/3 17:50:04 阅读更多 →
AI编程高效学习路径:从Python速成到文本分类实战

AI编程高效学习路径:从Python速成到文本分类实战

1. 为什么选择这条AI编程学习路径?我见过太多人被AI编程的学习门槛劝退。要么被复杂的数学公式吓跑,要么在环境配置阶段就耗尽耐心,还有人在工具选择上反复折腾却始终无法开始真正编码。经过三年多的AI教学实践,我总结出一条最适合…

2026/7/3 17:50:04 阅读更多 →
解锁NVIDIA显卡的色彩魔法:novideo_srgb让广色域显示器回归真实色彩

解锁NVIDIA显卡的色彩魔法:novideo_srgb让广色域显示器回归真实色彩

解锁NVIDIA显卡的色彩魔法:novideo_srgb让广色域显示器回归真实色彩 【免费下载链接】novideo_srgb Calibrate monitors to sRGB or other color spaces on NVIDIA GPUs, based on EDID data or ICC profiles 项目地址: https://gitcode.com/gh_mirrors/no/novide…

2026/7/3 17:48:03 阅读更多 →

日新闻

Nginx防御TLS重协商攻击实战:从原理到配置与监控

Nginx防御TLS重协商攻击实战:从原理到配置与监控

1. 项目概述:为什么TLS重协商攻击至今仍需警惕十多年前的CVE-2011-1473,一个关于TLS/SSL协议重协商机制的漏洞,现在提起来还有必要吗?很多运维和开发朋友可能会觉得,这都老掉牙了,现代服务器和客户端不都默…

2026/7/3 0:03:59 阅读更多 →
华为防火墙双通道远程管理实战:Web与SSH配置详解

华为防火墙双通道远程管理实战:Web与SSH配置详解

1. 项目概述:为什么需要双通道远程管理防火墙?在任何一个稍具规模的企业网络里,防火墙都是那个默默守护在边界的关键角色。作为网络工程师,我们不可能每次都跑到机房,插上console线去配置它。远程管理能力,…

2026/7/3 0:03:59 阅读更多 →
AD74413R与PIC18F65K40的高精度工业数据采集方案

AD74413R与PIC18F65K40的高精度工业数据采集方案

1. 项目概述:AD74413R与PIC18F65K40的协同工作在工业自动化和精密测量领域,同时实现高精度模数转换(ADC)和数模转换(DAC)功能是许多复杂系统的核心需求。AD74413R作为一款四通道可配置模拟输入/输出器件,与PIC18F65K40微控制器的组合&#xf…

2026/7/3 0:05:59 阅读更多 →

周新闻

月新闻