没想到小小一个地图组件,我翻来覆去重构了好几回,总算实现了所需要的基础功能:可拖拽的标记点+信息浮窗+地理编码+逆地理编码+搜索+只读回显/编辑+获取详细地址去掉省市区。
最开始找了很多文章参考,现在成型的组件也是参考了大佬们的结构,但是一直报错,后来发现是我没搞清AMap(组件内命名为AMapObj) 跟 new AMap.Map(组件内命名为amap) 导致的。
源码:
<script setup name="GaoDeMapDemo">
import { ref, shallowRef, onMounted, onUnmounted, watch, computed, toRefs } from 'vue'
import AMapLoader from '@amap/amap-jsapi-loader'
import { ElMessage } from 'element-plus'
import { debounce } from 'lodash'
// 2021年起 ,key 必须跟 密钥搭配使用
window._AMapSecurityConfig = {
securityJsCode: '密钥'
}
const props = defineProps({
modelValue: { // 坐标点数据,包括坐标/地址/省市区等
type: Object,
default: () => ({})
},
isRead: { // 只读时禁止搜索,禁止修改坐标
type: Boolean,
default: false
}
})
const { modelValue, isRead } = toRefs((props))
const emit = defineEmits(['update:modelValue', 'on-select'])
const map = shallowRef(null)
// 地点
const location = computed({
get: () => modelValue.value,
set: (val) => {
emit('update:modelValue', val)
}
})
watch(location, (val) => {
if (val.longitude && val.latitude && AMapObj) {
drawMarker()
}
})
// 搜索地点
const keyword = ref('')
// 搜索,AMap,标记点,地理编码,new AMap.Map()实例
let placeSearch, AMapObj, marker, geocoder, amap
const initMap = () => {
AMapLoader.load({
key: 'key,需要先申请', // 申请好的Web端Key,首次调用 load 时必填
version: '2.0',
plugins: [
'AMap.Scale', //工具条,控制地图的缩放、平移等
'AMap.ToolBar', //比例尺,显示当前地图中心的比例尺
'AMap.PlaceSearch', // POI搜索插件
'AMap.Geocoder' //定位
] // 需要使用的的插件列表,如比例尺'AMap.Scale'等
}).then(AMap => {
// 方便其他方法引用 AMap
AMapObj = AMap
let map = new AMap.Map('container', {
zoom: 16, //初始化地图层级
zooms: [6, 20], //缩放范围
resizeEnable: true, //如果没有传入center参数,将自动定位到当前城市
viewMode: '2D', //是否为3D地图模式
center: [113.17, 23.8] //初始化地图中心点位置
})
// 方便其他方法引用 map实例
amap = map
//异步同时加载多个插件
map.addControl(new AMap.Scale())
map.addControl(new AMap.ToolBar())
// 搜索地点
placeSearch = new AMap.PlaceSearch({
map: map.value,
city: '',
pageSize: 30, // 但也显示结果条数
pageIndex: 1,// 页码
citylimit: false, // 是否强制限制在设置的城市内搜索
autoFitView: true
})
// 存在坐标时,给地图添加标记
if (location?.value?.longitude && location?.value?.latitude) {
drawMarker()
}
// 地理编码,用于点击事件
geocoder = new AMap.Geocoder({ city: '全国' })
// 添加点击事件,只读时不可点击
if (!isRead.value) {
map.on('click', onMapClick)
}
}).catch(e => {
ElMessage.error({ message: e })
})
}
// 远程搜索搜索地图
const handleSearch = (queryString, cb) => {
let _result=queryString
if(location?.value?.zone?.length){
// 补全城市,减少输入
_result = `${location.value.zone[1]}${ queryString }`
}
placeSearch.search(_result, (status, result) => {
if (result && typeof result === 'object' && result.poiList) {
const list = result.poiList.pois
list.forEach(item => {
item.value = item.name
item.label = item.name
})
cb(list)
} else {
cb([])
}
})
}
// 点击地图 获取坐标信息
const onMapClick = (item) => {
const { lng, lat } = item.lnglat
// 逆地理编码
geocoder.getAddress([lng, lat], (status, result) => {
if (status === 'complete' && result.info === 'OK') {
const { addressComponent, formattedAddress } = result.regeocode
// township 镇/街道办 street街 streetNumber门牌号
let { city, province, district, township, street, streetNumber } = addressComponent
// 去掉省市区,同时插入街道+门牌号,作为详细地址
let _address = township + street + streetNumber + formattedAddress.replace(province, '').replace(city, '').replace(district, '').replace(township, '')
if (!city) {
// 直辖市
city = province
}
location.value = {
longitude: lng,
latitude: lat,
address: _address,
zone: [province, city, district]
}
}
})
}
// 点击搜索项
const handleSelect = (item) => {
const _pos = {
lnglat: {
lng: item.location.lng,
lat: item.location.lat
}
}
onMapClick(_pos)
amap.setZoomAndCenter(16, [_pos.lnglat.lng, _pos.lnglat.lat])
}
// 绘制地点marker
const drawMarker = () => {
let { longitude, latitude } = location.value
if (marker) {
marker.setMap(null)
}
marker = new AMapObj.Marker({
map: amap,
position: new AMapObj.LngLat(longitude, latitude),
anchor: 'bottom-center',
draggable: isRead.value ? false : true
})
marker.on('dragging', onDragging)
amap.add(marker)
setInfoWindow()
amap.setZoomAndCenter(16, [longitude, latitude])
}
// 信息浮窗 参数用于onDragging时减少偏移
let infoPopper
const setInfoWindow = (lng = null, lat = null, offTop = -44) => {
let longitude,
latitude,
address = location.value.address
if (lng && lat) {
longitude = lng
latitude = lat
} else {
longitude = location.value.longitude
latitude = location.value.latitude
}
mapData.keyword = address
if (infoPopper) {
infoPopper.setMap(null)
}
infoPopper = new AMapObj.InfoWindow({
position: new AMapObj.LngLat(longitude, latitude),
anchor: 'bottom-center',
isCustom: true,
content: `<div class="marker-info-popper">
<p>${ address }</p>
</div>`,
offset: new AMapObj.Pixel(0, offTop) // 位置偏移
})
amap.add(infoPopper)
}
// 拖动标记点时获取偏移后的坐标数据
const onDragging = debounce(item => {
const { lng, lat } = item.lnglat
location.value.longitude = lng
location.value.latitude = lat
setInfoWindow(lng, lat, -24)
}, 350)
// 调用地图初始化函数
onMounted(() => {
initMap()
//初始化时,获取关键字
if (modelValue?.value?.address) {
keyword.value = modelValue.value?.address
}
})
onUnmounted(() => {
if (amap) {
amap.destroy()
}
})
</script>
<template>
<div class="gao-de-map-search">
<div class="mr-1 gao-de-map-search__label">详细地址</div>
<ElAutocomplete
class="flex-1"
v-model="keyword"
:fetch-suggestions="handleSearch"
style="width:100%"
:trigger-on-focus="false"
@select="handleSelect"
clearable
:disabled="isRead"
placeholder="输入城市+关键字搜索"
>
<template #prepend>
<span v-if="location.zone.length">
{{ location.zone[0] }}/{{ location.zone[1] }}/{{ location.zone[2] }}
</span>
<span v-else>省/市/区</span>
</template>
</ElAutocomplete>
</div>
<div class="gao-de-map" id="container" ref="map"></div>
</template>
<style lang="scss">
.gao-de-map {
margin: 10px auto;
width: 100%;
height: 400px;
}
.gao-de-map-search {
display: flex;
.gao-de-map-search__label {
width: 120px;
line-height: 30px;
text-align: right;
color: var(--el-text-color-regular);
}
}
</style>