效果图
一、功能概述
基于 Vue2 + 高德地图 JS API 2.0 实现 PC 端地址选点功能,支持定位当前位置、关键词搜索地址、地图点击选点、地址信息回显,采用父子组件分离设计,子组件封装地图核心能力,父组件通过弹窗调用并接收选点结果。
二、核心架构
1. 组件分层
| 组件类型 | 作用 | 核心交互 |
|---|---|---|
| 子组件(MapComponent) | 封装高德地图初始化、搜索、选点、定位等核心能力 | 向父组件派发confirmAddress事件传递选点结果 |
| 父组件 | 提供弹窗容器、地址临时存储、回显逻辑,触发选点流程 | 通过center属性向子组件传递初始定位中心,监听confirmAddress接收选点结果 |
2. 技术依赖
- 基础框架:Vue2
- UI 组件:Element UI(弹窗、按钮、输入框等)
- 地图能力:高德地图 JS API 2.0(需配置
key和securityJsCode) - 核心能力:定位(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; } }