news 2026/4/16 9:10:36

Vue2实现PC端高德地图选点功能

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue2实现PC端高德地图选点功能

效果图

一、功能概述

基于 Vue2 + 高德地图 JS API 2.0 实现 PC 端地址选点功能,支持定位当前位置、关键词搜索地址、地图点击选点、地址信息回显,采用父子组件分离设计,子组件封装地图核心能力,父组件通过弹窗调用并接收选点结果。

二、核心架构

1. 组件分层

组件类型作用核心交互
子组件(MapComponent)封装高德地图初始化、搜索、选点、定位等核心能力向父组件派发confirmAddress事件传递选点结果
父组件提供弹窗容器、地址临时存储、回显逻辑,触发选点流程通过center属性向子组件传递初始定位中心,监听confirmAddress接收选点结果

2. 技术依赖

  • 基础框架:Vue2
  • UI 组件:Element UI(弹窗、按钮、输入框等)
  • 地图能力:高德地图 JS API 2.0(需配置keysecurityJsCode
  • 核心能力:定位(Geolocation)、地址搜索(PlaceSearch)、逆地理编码(Geocoder)

三、核心功能实现

1. 地图初始化、搜索、选点

<template> <div class="map-container"> <!-- 搜索框 --> <div class="search-box"> <el-input v-model="searchKey" placeholder="输入地点关键词" @keyup.enter="handleSearch" > <template #append> <el-button icon="el-icon-search" @click="handleSearch" /> </template> </el-input> </div> <!-- 地图容器 --> <div id="map-container" style="height: 400px; width: 100%"></div> <!-- 选点信息 --> <div v-if="selectedLocation" class="location-info"> <div> <h3>选点位置信息</h3> <p>地址:{{ selectedLocation.address }}</p> <p>经纬度:{{ selectedLocation.position.join(', ') }}</p> </div> <el-button style="height: 40px;" size="mini" type="primary" @click.native="handleConfirm">确认选点</el-button> </div> <!-- 搜索结果列表 --> <div v-if="searchResults.length" class="result-list"> <div v-for="(item, index) in searchResults" :key="item.id" class="result-item" :class="{ 'selected-item': selectedResultIndex === index }" @click="handleResultClick(index)" > <div class="title">{{ item.name }}</div> <div class="address">{{ item.address }}</div> </div> </div> </div> </template> <script> export default { props: { center: { type: String, default: '' } }, data() { return { map: null, placeSearch: null, geocoder: null, searchKey: '', searchResults: [], markers: [], infoWindow: null, selectedLocation: null, selectedResultIndex: -1, clickMarker: null, chooseInfo:{ latitude: 0, longitude: 0, address: '' } }; }, mounted() { this.initMap(); }, methods: { initMap() { const key = '高德开放平台key'; window._AMapSecurityConfig = { securityJsCode: '高德开放平台密钥' }; const script = document.createElement('script'); script.src = `https://webapi.amap.com/maps?v=2.0&key=${key}`; script.onload = () => { this.$nextTick(() => { // 检查地图容器是否存在 const container = document.getElementById('map-container'); if (!container) { console.error('地图容器不存在'); return; } // 初始化地图 this.map = new AMap.Map('map-container', { zoom: 13, center: this.center ? this.center.split(',') : [] }); /** * 定位当前位置,并将地图中心点移动到定位点 */ console.log("地图中心",this.center); AMap.plugin('AMap.Geolocation', ()=> { var geolocation = new AMap.Geolocation({ enableHighAccuracy: true, // 是否使用高精度定位 timeout: 10000, // 定位超时时间 zoomToAccuracy: true, // 定位成功后自动调整地图视野 showMarker: true, // 定位成功后显示标记 showCircle: true, // 显示定位精度范围 panToLocation: true // 定位成功后将地图中心移动到定位点 }); this.map.addControl(geolocation); // 将定位控件添加到地图 geolocation.getCurrentPosition((status, result) => { if (status === 'complete') { console.log('定位成功', result); this.map.setCenter(result.position); } else { console.error('定位失败', result); } }); }); // 初始化地理编码服务 AMap.plugin('AMap.Geocoder', ()=> { this.geocoder = new AMap.Geocoder({ city: "全国", radius: 1000 }); }); // 初始化地点搜索 AMap.plugin(['AMap.PlaceSearch', 'AMap.AutoComplete'], () => { this.placeSearch = new AMap.PlaceSearch({ pageSize: 10, pageIndex: 1, city: '全国', //加上map即标记搜索结果 map: this.map, autoFitView: true, // 自动调整视野 children: 1, // 显示子级POI extensions: 'base' // 返回基本信息 }); // 绑定事件 AMap.Event.addListener(this.placeSearch, 'complete', this.handleSearchComplete); AMap.Event.addListener(this.placeSearch, 'error', this.handleSearchError); }); // 绑定地图点击事件 this.map.on('click', this.handleMapClick); }); }; document.head.appendChild(script); }, /** * 点击搜索后触发 */ handleSearch() { if (!this.searchKey.trim()) return; // 检查placeSearch是否已初始化 if (!this.placeSearch) { console.error('placeSearch未初始化'); this.$message.error('地图搜索功能未初始化完成,请稍后重试'); return; } this.clearMarkers(); this.selectedResultIndex = -1; try { const searchOptions = { pageSize: 10, pageIndex: 1, city: '全国', extensions: 'base' }; this.placeSearch.search(this.searchKey, searchOptions, (status, result) => { if (status === 'complete') { this.handleSearchComplete(result); } else if (status === 'error') { this.handleSearchError(result); } else { console.warn('未知搜索状态:', status); this.searchResults = []; } }); } catch (error) { console.error('搜索执行出错', error); this.$message.error('搜索执行失败,请检查控制台日志'); } }, handleSearchComplete(data) { if (data && data.poiList && data.poiList.pois) { this.searchResults = data.poiList.pois.map(poi => ({ id: poi.id, name: poi.name, address: poi.address, location: poi.location, distance: Math.round(poi.distance || 0) })); } else { this.searchResults = []; console.warn('搜索结果为空或格式不正确'); } }, handleSearchError(error) { console.error('搜索失败', error); console.error('错误详情:', { info: error.info, message: error.message, type: error.type }); this.searchResults = []; // 根据错误类型显示不同提示 if (error.info === 'INVALID_USER_SCODE') { this.$message.error('API密钥验证失败,请检查密钥配置'); } else if (error.info === 'INVALID_PARAMS') { this.$message.error('搜索参数错误,请重新输入'); } else if (error.info === 'TOO_FREQUENT') { this.$message.error('请求过于频繁,请稍后重试'); } else { this.$message.error(`搜索失败: ${error.info || '未知错误'}`); } }, handleResultClick(index) { this.selectedResultIndex = index; const target = this.searchResults[index]; this.map.setCenter(target.location); this.addMarker(target, true); this.showInfoWindow(target); console.log('选中的地点:', target); //触发地图点击事件 this.handleMapClick({lnglat:target.location}); }, handleMapClick(e) { this.clearMarkers(); this.selectedResultIndex = -1; this.selectedLocation = null; // 添加点击标记 this.addClickMarker(e.lnglat); // 逆地理编码 this.geocoder.getAddress(e.lnglat, (status, result) => { if (status === 'complete' && result.regeocode) { this.selectedLocation = { position: [e.lnglat.lng, e.lnglat.lat], address: result.regeocode.formattedAddress }; } this.map.setCenter([e.lnglat.lng, e.lnglat.lat]); }); }, addMarker(poi, isSearchResult = false) { const marker = new AMap.Marker({ position: poi.location, content: isSearchResult ? '<div class="result-marker">📍</div>' : '<div class="click-marker">📍</div>', offset: new AMap.Pixel(-10, -30) }); marker.on('click', () => { this.showInfoWindow(poi); }); this.map.add(marker); this.markers.push(marker); }, addClickMarker(lnglat) { this.clickMarker = new AMap.Marker({ position: lnglat, content: '<div class="click-marker">📍</div>', offset: new AMap.Pixel(-10, -30) }); this.map.add(this.clickMarker); this.markers.push(this.clickMarker); }, showInfoWindow(poi) { if (this.infoWindow) this.infoWindow.close(); this.infoWindow = new AMap.InfoWindow({ content: `<div class="info-window"> <h4>${poi.name}</h4> <p>${poi.address}</p> ${poi.distance ? `<p>距离:${poi.distance}米</p>` : ''} </div>`, offset: new AMap.Pixel(0, -30) }); this.infoWindow.open(this.map, poi.location); }, clearMarkers() { // 清除所有标记 this.markers.forEach(marker => this.map.remove(marker)); this.markers = []; if (this.clickMarker) { this.map.remove(this.clickMarker); this.clickMarker = null; } }, //确认选点 handleConfirm() { this.$emit('confirmAddress', { address: this.selectedLocation.address, lat: this.selectedLocation.position[1], lng: this.selectedLocation.position[0], }); } }, beforeDestroy() { if (this.map) { this.map.destroy(); this.map = null; } }, }; </script> <style scoped> .map-container { /* display: flex; flex-direction: column; */ } .search-box { padding: 10px 0 20px; background: #fff; z-index: 999; } #map-container { /* flex: 1; min-height: 400px; */ } .location-info { display: flex; align-items: center; justify-content: space-between; padding: 6px; background: #f8f9fa; border-top: 1px solid #eee; } .result-list { height: 300px; overflow-y: auto; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.05); } .result-item { padding: 6px; border-bottom: 1px solid #eee; cursor: pointer; transition: all 0.2s; } .result-item:hover { background: #f8f9fa; } .selected-item { background: #e6f7ff !important; border-left: 4px solid #1890ff; } .title { font-weight: 500; color: #333; margin-bottom: 4px; } .address { color: #666; font-size: 0.9em; margin-bottom: 4px; } .distance { color: #1890ff; font-size: 0.85em; } /* 信息窗口样式 */ .info-window { padding: 8px; min-width: 200px; } .info-window h4 { margin: 0 0 8px; color: #333; } .info-window p { margin: 4px 0; color: #666; } /* 标记样式 */ .result-marker { font-size: 24px; color: #1890ff; text-shadow: 0 2px 4px rgba(24,144,255,0.3); } .click-marker { font-size: 24px; color: #ff4d4f; text-shadow: 0 2px 4px rgba(255,77,79,0.3); } </style>

2. 父组件交互逻辑

  • 地图选择弹窗
<!-- 地图选择对话框 --> <el-dialog title="选择地址" :visible.sync="mapDialogVisible" width="800px" append-to-body :before-close="cancelMapSelect" :close-on-click-modal="false" class="map-dialog"> <div class="dialog-content"> <div class="selected-address" v-if="tempAddress.address"> <div class="address-details"> <div><strong>地址信息:</strong>{{ tempAddress.address }}</div> </div> </div> <div v-else class="no-address">请选择或搜索位置</div> <MapComponent v-if="mapDialogVisible" :center="mapCenter" @confirmAddress="handleMapChoose" /> </div> </el-dialog>
  • 数据初始化
data() { return { mapDialogVisible: false, currentItem: null, tempAddress: { address: '', lng: '', lat: '' } }; }, computed: { mapCenter() { if (this.currentItem) { const lng = this.currentItem.longitude || ''; const lat = this.currentItem.latitude || ''; return `${lng},${lat}`; } return ''; } },
  • 触发事件
methods: { // 类型选择 changeJumpType(e, item) { item.functionType = ""; item.content = null; item.appid = null; item.jumpUrl = null; item.jumpMethod = 0; item.jumpLinkType = 0; item.jumpLinkId = null; console.log(e, item); }, addAddress(item) { this.currentItem = item; if (item && item.address) { this.tempAddress = { address: item.content || item.address, lng: item.longitude || '', lat: item.latitude || '' }; } else { this.tempAddress = { address: '', lng: '', lat: '' }; } this.mapDialogVisible = true; }, // 处理地图选择(实时更新临时地址) handleMapChoose(mapData) { console.log('地图选择:', mapData); this.tempAddress = { address: mapData.address || '', lng: mapData.lng || '', lat: mapData.lat || '', url: mapData.url || '' }; // 更新当前项的数据 if (this.currentItem) { this.currentItem.content = this.tempAddress.address; this.currentItem.address = this.tempAddress.address; this.currentItem.longitude = this.tempAddress.lng; this.currentItem.latitude = this.tempAddress.lat; // 更新表单数据,确保响应式 this.$set(this.currentItem, 'content', this.tempAddress.address); this.$set(this.currentItem, 'address', this.tempAddress.address); this.$set(this.currentItem, 'longitude', this.tempAddress.lng); this.$set(this.currentItem, 'latitude', this.tempAddress.lat); console.log('地址已确认:', this.currentItem); this.$message.success('地址选择成功'); this.mapDialogVisible = false; // 触发表单验证更新 this.$forceUpdate(); } }, },
  • 样式
.dialog-content { .selected-address { padding: 10px; background-color: #f5f7fa; border-radius: 4px; margin-bottom: 10px; .address-details { div { margin-bottom: 5px; font-size: 14px; &:last-child { margin-bottom: 0; } } } } .no-address { padding: 20px; text-align: center; color: #999; background-color: #f5f7fa; border-radius: 4px; margin-bottom: 10px; } }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 0:36:27

5个Vim缓冲区管理技巧让你告别文件切换困扰

5个Vim缓冲区管理技巧让你告别文件切换困扰 【免费下载链接】vim-galore :mortar_board: All things Vim! 项目地址: https://gitcode.com/gh_mirrors/vi/vim-galore 作为一款功能强大的文本编辑器&#xff0c;Vim的核心优势在于其高效的缓冲区管理机制。在Vim中&#x…

作者头像 李华
网站建设 2026/4/15 11:29:45

Dubbo Spring Boot监控配置实战指南:从入门到精通

Dubbo Spring Boot监控配置实战指南&#xff1a;从入门到精通 【免费下载链接】dubbo Dubbo 是一款高性能、轻量级的分布式服务框架&#xff0c;旨在解决企业应用系统中服务治理的问题。轻量级的服务框架&#xff0c;支持多种通信协议和服务治理。适用分布式微服务架构下的服务…

作者头像 李华
网站建设 2026/4/14 3:30:02

PowerBI主题模板终极指南:30+专业模板一键美化数据报表

PowerBI主题模板终极指南&#xff1a;30专业模板一键美化数据报表 【免费下载链接】PowerBI-ThemeTemplates Snippets for assembling Power BI Themes 项目地址: https://gitcode.com/gh_mirrors/po/PowerBI-ThemeTemplates 想要让Power BI数据报表瞬间焕发专业魅力吗&…

作者头像 李华
网站建设 2026/4/6 20:06:02

NF恩乃普HSA4014 HSA4101功率放大器

HSA4014 是一款高速、宽频带的双极性功率放大器&#xff0c;由日本NF公司生产&#xff0c;属于HSA系列高性能产品。它采用四象限输出模式&#xff0c;能够处理从直流到最高10MHz的信号&#xff0c;支持电压和电流在正负方向上自由切换&#xff0c;适用于驱动电容性、电感性等复…

作者头像 李华
网站建设 2026/4/14 5:34:34

Base-Admin:企业级后台管理系统的现代化解决方案

Base-Admin是一套专为企业级应用设计的后台管理系统框架&#xff0c;通过现代化的技术架构和丰富的功能模块&#xff0c;为企业数字化转型提供强有力的技术支撑。本系统基于SpringBoot技术栈构建&#xff0c;集成了完整的权限管理、用户管理、菜单配置、系统监控等核心功能&…

作者头像 李华
网站建设 2026/4/14 13:16:35

LrcAPI:快速集成歌词服务的终极解决方案

LrcAPI&#xff1a;快速集成歌词服务的终极解决方案 【免费下载链接】LrcApi A Flask API For StreamMusic 项目地址: https://gitcode.com/gh_mirrors/lr/LrcApi 在音乐应用开发中&#xff0c;歌词同步显示功能往往成为技术实现的瓶颈。传统方案需要对接多个音乐平台AP…

作者头像 李华