
安装就不讲了,直接上代码(填上自己的密钥、Key就可以使用)
父级调用
javascript
<el-col :span="24" class="p-4 text-align-center">
<el-form-item label=" ">
<el-button type="primary" @click="openMapLocation">
<SvgIcon size="small" iconClass="el-icon-location"></SvgIcon>
<span>获取经纬度</span>
</el-button>
</el-form-item>
<map-location ref="mapLocationRef" @confirm="handleLocationConfirm"></map-location>
<script setup name="formData">
import MapLocation from '@/components/Map/index.vue' // 引入
// 打开弹窗获取经纬度
const mapLocationRef = ref(null);
const openMapLocation = () => {
mapLocationRef.value.open();
}
// 经纬度确认
const handleLocationConfirm = (locationData) => {
formData.value.longitude = locationData.lng
formData.value.latitude = locationData.lat
}
</script>
地图封装
javascript
<template>
<el-dialog
v-model="mapDialogFlg"
title="选择位置与影响范围"
width="70vw"
:before-close="handleClose"
:close-on-click-modal="false"
draggable
destroy-on-close
style="border-radius: 5px;"
align-center
@open="handleDialogOpen"
>
<!-- 内容结构保持不变 -->
<!-- 1. 功能切换与搜索区域 -->
<div class="top-container mb-4">
<el-radio-group v-model="selectMode" class="mb-3" @change="handleModeChange">
<el-radio label="point">单点选址(经纬度)</el-radio>
<el-radio label="circle">圆形范围(中心+半径)</el-radio>
<el-radio label="polygon">多边形范围(顶点坐标)</el-radio>
</el-radio-group>
<div class="search-container flex gap-2">
<el-input
v-model="searchKeyword"
placeholder="请输入地址搜索"
clearable
class="flex-1"
@keyup.enter="handleSearch"
:disabled="searchDisabled"
>
<template #append>
<el-button
type="primary"
@click="handleSearch"
:disabled="searchDisabled || !searchKeyword.trim()"
>
搜索
</el-button>
</template>
</el-input>
<el-button
v-if="selectMode !== 'point' && (selectedRange || rangeShapes.length > 0)"
type="text"
color="danger"
@click="clearRange"
>
清除范围
</el-button>
</div>
</div>
<!-- 2. 搜索次数超限提示 -->
<el-alert
v-if="showLimitAlert"
title="今日搜索次数已达上限,请明日再试或联系管理员升级服务"
type="error"
show-icon
class="mb-4"
/>
<!-- 3. 地图容器 + 绘图提示 -->
<div class="map-wrapper">
<div id="container"></div>
<el-tooltip
v-if="selectMode === 'circle'"
content="点击地图确定圆心,拖拽鼠标调整半径(双击结束)"
placement="top"
effect="light"
class="map-tip"
>
<el-tag size="small">圆形绘制提示</el-tag>
</el-tooltip>
<el-tooltip
v-if="selectMode === 'polygon'"
content="点击地图添加顶点(至少3个),双击结束绘制"
placement="top"
effect="light"
class="map-tip"
>
<el-tag size="small">多边形绘制提示</el-tag>
</el-tooltip>
</div>
<!-- 4. 选中数据显示区域 -->
<div class="selected-info mt-4">
<el-descriptions :column="1" border>
<el-descriptions-item label="选中经纬度">
<span v-if="selectedLngLat">{{ selectedLngLat.lng }}, {{ selectedLngLat.lat }}</span>
<span v-else class="text-gray-400">未选择位置</span>
</el-descriptions-item>
<el-descriptions-item label="行政区编码">
<span v-if="selectedDistrict.adcode">{{ selectedDistrict.adcode }}</span>
<span v-else class="text-gray-400">未获取到编码</span>
</el-descriptions-item>
<!-- 新增:行政区信息显示 -->
<el-descriptions-item label="所属行政区">
<span v-if="selectedDistrict.district">
{{ selectedDistrict.province }} > {{ selectedDistrict.city }} > {{ selectedDistrict.district }}
</span>
<span v-else class="text-gray-400">未获取到行政区信息</span>
</el-descriptions-item>
<el-descriptions-item
v-if="selectMode === 'circle' && selectedRange"
label="圆形范围信息"
>
<div>中心经纬度:{{ selectedRange.center.lng }}, {{ selectedRange.center.lat }}</div>
<div>半径:{{ (selectedRange.radius / 1000).toFixed(2) }} 公里({{ selectedRange.radius }} 米)</div>
</el-descriptions-item>
<el-descriptions-item
v-if="selectMode === 'polygon' && selectedRange"
label="多边形范围信息"
>
<div>顶点数量:{{ selectedRange.path.length }} 个</div>
<div>顶点坐标:
<el-tag
v-for="(point, idx) in selectedRange.path"
:key="idx"
size="mini"
class="mr-1 mb-1"
>
{{ point.lng.toFixed(6) }},{{ point.lat.toFixed(6) }}
</el-tag>
</div>
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 5. 底部按钮 -->
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
@click="confirmSelection"
:disabled="!selectedLngLat || (selectMode !== 'point' && !selectedRange)"
>
确认选择
</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, defineExpose, defineEmits, onUnmounted } from 'vue'
import AMapLoader from '@amap/amap-jsapi-loader'
import { ElMessage, ElTag } from 'element-plus'
const emit = defineEmits(['confirm', 'update:visible'])
// 响应式变量
const mapDialogFlg = ref(false)
const searchKeyword = ref('')
const selectedLngLat = ref(null)
const selectedRange = ref(null)
const selectMode = ref('point')
const map = ref(null)
const marker = ref(null)
const mouseTool = ref(null)
const placeSearch = ref(null)
const searchDisabled = ref(false)
const showLimitAlert = ref(false)
const rangeShapes = ref([])
// 新增:存储行政区信息(省/市/区/县)
const selectedDistrict = ref({
province: '', // 省份
city: '', // 城市
district: '', // 区/县
adcode: '' // 新增:行政区编码
})
// 图标配置 - 增加备用图标和默认样式
const markerIconConfig = {
// 主图标地址(使用HTTPS确保兼容性)
mainUrl: 'https://a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png',
// 备用图标地址(如果主图标失效)
fallbackUrl: 'https://a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-red.png',
// 图标尺寸
size: [36, 42],
// 图标偏移
offset: [-18, -38]
}
// 检查图标是否可访问的工具函数
const checkIconUrl = (url) => {
return new Promise((resolve) => {
const img = new Image()
img.src = url
img.onload = () => resolve(true)
img.onerror = () => resolve(false)
})
}
// 弹窗关闭逻辑
const handleClose = () => {
mapDialogFlg.value = false
searchKeyword.value = ''
selectedDistrict.value = {
province: '',
city: '',
district: '',
adcode: ''
}
if (mouseTool.value) {
mouseTool.value.close()
mouseTool.value = null
}
clearRangeShapes()
}
// 打开弹窗
const open = (initData = {}) => {
mapDialogFlg.value = true
if (initData.lng && initData.lat) {
selectedLngLat.value = { lng: initData.lng, lat: initData.lat }
}
if (initData.range) {
selectedRange.value = initData.range
selectMode.value = initData.range.type
}
}
// 模式切换处理
const handleModeChange = (newMode) => {
if (mouseTool.value) {
mouseTool.value.close()
mouseTool.value = null
}
selectedRange.value = null
clearRangeShapes()
if (newMode !== 'point' && map.value) {
initMouseTool(newMode)
}
}
// 初始化绘图工具
const initMouseTool = (mode) => {
AMapLoader.load({
key: '4e4fc1871d4ec68a716f29fd52436bda',
version: '2.0',
plugins: ['AMap.MouseTool']
}).then((AMap) => {
mouseTool.value = new AMap.MouseTool(map.value)
mouseTool.value.on('draw', (e) => {
const { obj } = e
rangeShapes.value.push(obj)
if (mode === 'circle') {
selectedRange.value = {
type: 'circle',
center: { lng: obj.getCenter().lng, lat: obj.getCenter().lat },
radius: obj.getRadius(),
path: []
}
selectedLngLat.value = selectedRange.value.center
addMarker([selectedLngLat.value.lng, selectedLngLat.value.lat])
// 新增:调用逆地理编码
getDistrictByLngLat(selectedLngLat.value.lng, selectedLngLat.value.lat)
} else if (mode === 'polygon') {
const path = obj.getPath().map((lnglat) => ({
lng: lnglat.lng,
lat: lnglat.lat
}))
selectedRange.value = {
type: 'polygon',
path: path,
center: {},
radius: 0
}
// 计算多边形中心点(优化方案)
const center = calculatePolygonCenter(path)
selectedLngLat.value = center
addMarker([center.lng, center.lat])
// 新增:调用逆地理编码
getDistrictByLngLat(center.lng, center.lat)
}
mouseTool.value.close()
})
if (mode === 'circle') {
mouseTool.value.circle({
strokeColor: '#2f54eb',
fillColor: 'rgba(47, 84, 235, 0.2)',
strokeWeight: 2
})
} else if (mode === 'polygon') {
mouseTool.value.polygon({
strokeColor: '#2f54eb',
fillColor: 'rgba(47, 84, 235, 0.2)',
strokeWeight: 2
})
}
}).catch((e) => {
console.error('绘图工具初始化失败:', e)
ElMessage.error('范围绘制功能加载失败,请稍后重试')
})
}
// 计算多边形几何中心点
const calculatePolygonCenter = (path) => {
let totalX = 0
let totalY = 0
for (const point of path) {
totalX += point.lng
totalY += point.lat
}
return {
lng: totalX / path.length,
lat: totalY / path.length
}
}
// 清除范围
const clearRange = () => {
selectedRange.value = null
clearRangeShapes()
if (mouseTool.value) {
mouseTool.value.close()
mouseTool.value = null
initMouseTool(selectMode.value)
}
}
// 清除地图上所有范围图形
const clearRangeShapes = () => {
if (map.value && rangeShapes.value.length > 0) {
map.value.remove(rangeShapes.value)
rangeShapes.value = []
}
}
// 地址搜索逻辑
const handleSearch = () => {
if (!searchKeyword.value.trim() || !placeSearch.value || searchDisabled.value) return
placeSearch.value.search(searchKeyword.value, (status, result) => {
if (status === 'complete') {
if (result.info === 'OK') {
if (result.poiList?.pois.length > 0) {
const firstResult = result.poiList.pois[0]
const { lng, lat } = firstResult.location
map.value.setCenter([lng, lat])
addMarker([lng, lat])
selectedLngLat.value = { lng, lat }
if (selectMode.value === 'circle' && !selectedRange) {
clearRangeShapes()
const AMap = window.AMap
const circle = new AMap.Circle({
center: [lng, lat],
radius: 1000,
strokeColor: '#2f54eb',
fillColor: 'rgba(47, 84, 235, 0.2)',
strokeWeight: 2,
map: map.value
})
rangeShapes.value.push(circle)
selectedRange.value = {
type: 'circle',
center: { lng, lat },
radius: 1000,
path: []
}
}
} else {
ElMessage.warning('未找到匹配的地址,请尝试其他关键词')
}
} else if (result.info === 'DAILY_QUERY_OVER_LIMIT') {
handleSearchLimit()
} else {
ElMessage.error(`搜索失败: ${result.info}`)
}
} else if (status === 'error') {
result.code === 10002 ? handleSearchLimit() : ElMessage.error(`搜索出错: ${result?.message || '未知错误'}`)
}
})
}
// 搜索超限处理
const handleSearchLimit = () => {
searchDisabled.value = true
showLimitAlert.value = true
ElMessage.error('今日搜索次数已达上限,请明日再试')
}
// 修复的单点标记方法 - 增加图标检查和容错
const addMarker = async (position) => {
// 先移除旧标记
if (marker.value) {
map.value.remove(marker.value)
marker.value = null
}
if (!window.AMap) return
// 检查图标可用性,优先使用可用图标
let iconUrl = markerIconConfig.mainUrl
const isMainIconValid = await checkIconUrl(markerIconConfig.mainUrl)
if (!isMainIconValid) {
console.warn('主标记图标不可用,尝试使用备用图标')
const isFallbackValid = await checkIconUrl(markerIconConfig.fallbackUrl)
if (isFallbackValid) {
iconUrl = markerIconConfig.fallbackUrl
} else {
console.warn('备用图标也不可用,将使用默认样式')
// 如果所有图标都不可用,使用默认样式
marker.value = new window.AMap.Marker({
position: position,
map: map.value,
// 使用默认样式
color: '#2f54eb',
size: new window.AMap.Size(16, 16),
shape: 'circle'
})
return
}
}
// 创建图标并添加标记
marker.value = new AMap.Marker({
position: position,
map: map.value,
icon: new AMap.Icon({
size: new AMap.Size(36, 42),
image: '//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png',
imageSize: new AMap.Size(36, 42),
imageOffset: new AMap.Pixel(0, 0)
}),
offset: new AMap.Pixel(-18, -38) // 图标偏移,使图标底部指向坐标点
})
}
// 地图初始化
const handleDialogOpen = () => {
window._AMapSecurityConfig = {
securityJsCode: '' // 密钥
}
AMapLoader.load({
key: '', // 高德key
version: '2.0',
plugins: [
'AMap.Scale', 'AMap.OverView', 'AMap.Geolocation',
'AMap.PlaceSearch', 'AMap.ToolBar', 'AMap.MouseTool', 'AMap.Geocoder'
]
}).then((AMap) => {
window.AMap = AMap
map.value = new AMap.Map('container', {
mapStyle: 'amap://styles/macaron',
viewMode: '3D',
zoom: 13,
center: selectedLngLat.value ? [selectedLngLat.value.lng, selectedLngLat.value.lat] : [117.000923, 36.651242]
})
map.value.addControl(new AMap.ToolBar({ position: 'RB' }))
map.value.addControl(new AMap.Scale())
placeSearch.value = new AMap.PlaceSearch({
pageSize: 10,
pageIndex: 1,
city: '',
map: map.value,
panel: ''
})
// 回显已选单点标记
if (selectedLngLat.value) {
addMarker([selectedLngLat.value.lng, selectedLngLat.value.lat])
}
// 回显已选范围
if (selectedRange.value) {
renderRange(selectedRange.value)
}
// 单点模式:地图点击选点
map.value.on('click', (e) => {
if (selectMode.value !== 'point') return
const { lng, lat } = e.lnglat
addMarker([lng, lat])
selectedLngLat.value = { lng, lat }
// 新增:点击后获取行政区信息
getDistrictByLngLat(lng, lat)
})
// 范围模式:初始化绘图工具
if (selectMode.value !== 'point') {
initMouseTool(selectMode.value)
}
}).catch((e) => {
console.error('地图加载失败:', e)
ElMessage.error('地图加载失败,请稍后重试')
})
}
// 回显已保存的范围
const renderRange = (rangeData) => {
const AMap = window.AMap
if (!AMap || !map.value) return
clearRangeShapes()
let shape
if (rangeData.type === 'circle') {
shape = new AMap.Circle({
center: [rangeData.center.lng, rangeData.center.lat],
radius: rangeData.radius,
strokeColor: '#2f54eb',
fillColor: 'rgba(47, 84, 235, 0.2)',
strokeWeight: 2,
map: map.value
})
}
if (rangeData.type === 'polygon') {
shape = new AMap.Polygon({
path: rangeData.path.map((p) => [p.lng, p.lat]),
strokeColor: '#2f54eb',
fillColor: 'rgba(47, 84, 235, 0.2)',
strokeWeight: 2,
map: map.value
})
}
if (shape) {
rangeShapes.value.push(shape)
}
const center = rangeData.center || rangeData.path[0]
map.value.setCenter([center.lng, center.lat])
}
/**
* 通过经纬度获取行政区信息
* @param {Number} lng - 经度
* @param {Number} lat - 纬度
*/
// 修改后的 getDistrictByLngLat 函数
const getDistrictByLngLat = async (lng, lat) => {
if (!window.AMap || !lng || !lat) return;
// 初始化逆地理编码实例(新增 extensions: 'all')
const geocoder = new window.AMap.Geocoder({
radius: 1000, // 搜索半径(米)
extensions: 'all' // 返回详细地址信息(含 adcode)
});
geocoder.getAddress([lng, lat], (status, result) => {
if (status === 'complete' && result.info === 'OK') {
const addressComponent = result.regeocode.addressComponent;
// 解析行政区信息和编码
selectedDistrict.value = {
province: addressComponent.province || '',
city: addressComponent.city || addressComponent.province,
district: addressComponent.district || '',
adcode: addressComponent.adcode // 新增:获取行政区编码
};
console.log('行政区编码:', selectedDistrict.value.adcode);
} else {
// 处理失败逻辑
selectedDistrict.value = { province: '', city: '', district: '', adcode: '' };
}
});
};
// 确认选择
const confirmSelection = () => {
if (!selectedLngLat) return
const result = {
lng: selectedLngLat.value.lng,
lat: selectedLngLat.value.lat,
range: selectedRange.value,
region: selectedDistrict.value
}
emit('confirm', result)
handleClose()
}
// 组件卸载
onUnmounted(() => {
if (map.value) {
map.value.destroy()
map.value = null
}
if (mouseTool.value) {
mouseTool.value.close()
mouseTool.value = null
}
window.AMap = null
rangeShapes.value = []
})
// 暴露方法给父组件
defineExpose({
open
})
</script>
<style scoped lang="scss">
#container {
width: 100%;
height: 500px;
border-radius: 4px;
overflow: hidden;
}
.top-container {
.search-container {
display: flex;
align-items: center;
}
}
.map-wrapper {
position: relative;
.map-tip {
position: absolute;
top: 10px;
left: 10px;
z-index: 100;
}
}
.selected-info {
font-size: 14px;
.el-descriptions-item__content {
line-height: 1.8;
}
}
:deep(.amap-logo) {
display: none !important;
}
:deep(.amap-copyright) {
opacity: 0 !important;
}
:deep(.el-input.is-disabled .el-input__inner) {
background-color: #f5f7fa;
cursor: not-allowed;
}
</style>