一、地图组件封装
定义地图容器 DOM 元素,并动态设置其高度
<template>
<div class="map-container" ref="mapRef" :style="{ height: props.height ? `${props.height}px` : '400px' }"></div>
</template>
按需异步加载高德地图 JS API(包括核心库、插件等)
import AMapLoader from '@amap/amap-jsapi-loader'
定义组件的 props 和 emits 类型及用法。
const props = defineProps<{
zoom?: number // 可选参数,控制地图缩放级别
height?: number // 可选参数,控制地图容器高度
}>()
const emit = defineEmits<{
'fence-drawn': [fenceData: FenceData]
'draw-complete': [fenceData: FenceData]
}>()
声明变量
const mapRef = ref<HTMLElement | null>(null)
let map: AMap.Map | null = null
let mouseTool: AMap.MouseTool | null = null
let currentPolygon: AMap.Polygon | null = null
let currentCircle: AMap.Circle | null = null
let currentRectangle: AMap.Rectangle | null = null
let userInteracted = false // ✅ 是否用户手动交互过地图
加载地图
const initMap = async () => {
try {
console.log('开始加载高德地图...')
const AMap = await AMapLoader.load({
key: '你的key',//💡创建时服务平台请勾选Web端(JS API)
version: '2.0',
plugins: ['AMap.MouseTool', 'AMap.Polygon', 'AMap.Circle', 'AMap.Rectangle']
})
console.log('高德地图加载成功:', AMap)
if (mapRef.value) {
// @ts-ignore
map = new AMap.Map(mapRef.value, {
zoom: props.zoom || 12,
viewMode: '2D',
})
console.log('地图实例创建成功:', map)
// ✅ 监听用户交互
map.on('zoomstart', () => userInteracted = true)
map.on('dragstart', () => userInteracted = true)
// 创建鼠标工具
// @ts-ignore
mouseTool = new AMap.MouseTool(map)
console.log('MouseTool 已创建:', mouseTool)
console.log('MouseTool 方法:', Object.getOwnPropertyNames(mouseTool))
// 注册一次统一的绘制事件监听器
mouseTool.on('draw', handleDraw)
}
} catch (error) {
console.error('地图初始化失败:', error)
}
}
封装统一方法👉 处理高德地图绘制完成后的图形数据,并将其标准化为统一格式传给父组件。
const handleDraw = async (event: any) => {
const shape = event.obj
console.log('🎈 CLASS_NAME:', shape.CLASS_NAME)
const className = shape.CLASS_NAME
let fenceData: FenceData | null = null
if (className?.includes('Circle')) {
currentCircle = shape
const center = shape.getCenter()
const radius = shape.getRadius()
fenceData = {
type: 'circle',
coordinates: [[center.lng, center.lat]],
center: [center.lng, center.lat],
radius,
}
} else if (className?.includes('Polygon')) {
currentPolygon = shape
const path = shape.getPath()
const coordinates = path.map((p: any) => [p.lng, p.lat])
const center = currentPolygon.getBounds().getCenter()
const radius = getApproximateRadius(path)
fenceData = {
type: 'polygon',
coordinates,
center,
radius
}
} else if (className?.includes('Rectangle')) {
currentRectangle = shape
const bounds = shape.getBounds()
const coordinates = [
[bounds.getSouthWest().lng, bounds.getSouthWest().lat],
[bounds.getNorthEast().lng, bounds.getSouthWest().lat],
[bounds.getNorthEast().lng, bounds.getNorthEast().lat],
[bounds.getSouthWest().lng, bounds.getNorthEast().lat]
]
const center = bounds.getCenter()
const radius = getRectangleRadius(bounds)
fenceData = {
type: 'rectangle',
coordinates,
center,
radius
}
}
if (fenceData) {
emit('fence-drawn', fenceData)
emit('draw-complete', fenceData)
}
mouseTool?.close()
}
开始绘制多边形
const startDrawPolygon = () => {
if (!mouseTool || !map) return
clearCurrentShape()
mouseTool?.off('draw')
mouseTool?.on('draw', handleDraw)
mouseTool.polygon({
strokeColor: '#FF0000',
strokeWeight: 2,
strokeOpacity: 0.8,
fillColor: '#FF0000',
fillOpacity: 0.2,
strokeStyle: 'solid'
})
}
开始绘制圆形
const startDrawCircle = () => {
if (!mouseTool || !map) return
clearCurrentShape()
console.log('准备监听 draw 事件...')
mouseTool?.off('draw')
mouseTool.on('draw', (event: any) => {
console.log('🎯 draw 事件触发了!', event)
handleDraw(event)
})
mouseTool.circle({
strokeColor: '#FF0000',
strokeWeight: 2,
strokeOpacity: 0.8,
fillColor: '#FF0000',
fillOpacity: 0.2,
strokeStyle: 'solid'
})
}
开始绘制矩形
const startDrawRectangle = () => {
if (!mouseTool || !map) return
clearCurrentShape()
mouseTool?.off('draw')
mouseTool?.on('draw', handleDraw)
mouseTool.rectangle({
strokeColor: '#FF0000',
strokeWeight: 2,
strokeOpacity: 0.8,
fillColor: '#FF0000',
fillOpacity: 0.2,
strokeStyle: 'solid'
})
}
停止绘制
const stopDraw = () => {
if (mouseTool) {
mouseTool.close()
}
}
清除绘制的图形
const clearFence = () => {
clearCurrentShape()
if (mouseTool) {
mouseTool.close() // 关闭绘制状态
}
}
const clearCurrentShape = () => {
currentCircle?.setMap(null)
currentPolygon?.setMap(null)
currentRectangle?.setMap(null)
currentCircle = null
currentPolygon = null
currentRectangle = null
}
🧠 根据业务需求,增加回显围栏图形的处理
const showFence = (fenceData: FenceData) => {
if (!map) return
clearCurrentShape()
if (fenceData.type === 'polygon') {
const coordinates = fenceData.coordinates as number[][]
// @ts-ignore
currentPolygon = new AMap.Polygon({
path: coordinates,
strokeColor: '#FF0000',
strokeWeight: 2,
fillColor: '#FF0000',
fillOpacity: 0.2,
})
map.add(currentPolygon)
map.setFitView([currentPolygon])
} else if (fenceData.type === 'circle') {
const { center, radius } = fenceData
// @ts-ignore
currentCircle = new AMap.Circle({
center,
radius,
strokeColor: '#FF0000',
strokeWeight: 2,
fillColor: '#FF0000',
fillOpacity: 0.2,
})
map.add(currentCircle)
map.setFitView([currentCircle])
} else if (fenceData.type === 'rectangle') {
const bounds = fenceData.coordinates
if (bounds.length < 2) return
const southWest = bounds[0]
const northEast = bounds[2]
// @ts-ignore
currentRectangle = new AMap.Rectangle({
// @ts-ignore
bounds: new AMap.Bounds(southWest, northEast),
strokeColor: '#FF0000',
strokeWeight: 2,
fillColor: '#FF0000',
fillOpacity: 0.2,
})
map.add(currentRectangle)
map.setFitView([currentRectangle])
}
}
暴露方法给父组件
defineExpose({
startDrawPolygon,
startDrawCircle,
startDrawRectangle,
stopDraw,
clearFence,
showFence
})
🧠 扩展
/**
* 获取多边形的“等效半径”,即中心点到最远顶点的距离
* @param path 多边形坐标数组(AMap.LngLat 类型数组或 [lng, lat] 数组)
* @returns 半径(单位:米)
*/
export function getApproximateRadius(path: (AMap.LngLat | [number, number])[]): number {
if (!path.length) return 0
// 1. 转换成 AMap.LngLat 类型
// @ts-ignore
const lngLats = path.map(p => Array.isArray(p) ? new AMap.LngLat(p[0], p[1]) : p)
// 2. 计算中心点
let avgLng = 0, avgLat = 0
lngLats.forEach(p => {
avgLng += p.getLng()
avgLat += p.getLat()
})
avgLng /= lngLats.length
avgLat /= lngLats.length
const center = new AMap.LngLat(avgLng, avgLat)
// 3. 计算中心点到每个顶点的距离,取最大值
let maxDistance = 0
lngLats.forEach(p => {
const distance = center.distance(p)
if (distance > maxDistance) {
maxDistance = distance
}
})
return maxDistance
}
矩形对角线一半作为半径(仅适用于矩形)
export function getRectangleRadius(bounds: AMap.Bounds): number {
const sw = bounds.getSouthWest() // 左下角
const ne = bounds.getNorthEast() // 右上角
return sw.distance(ne) / 2
}
二、父组件使用
<a-button @click="clearFence" style="margin-right:8px" :disabled="!currentFenceData">清除绘制</a-button>
<a-button @click="showDrawOptions = true" :disabled="isDrawing" type="primary">绘制围栏</a-button>
<!-- 地图 -->
<GaoDeMap ref="mapRef" :zoom="12" @fence-drawn="onFenceDrawn" @draw-complete="onDrawComplete" />
<!-- 绘制选择弹窗 -->
<a-modal v-model:open="showDrawOptions" title="选择绘制方式" @ok="startDraw" cancelText="取消" okText="开始绘制"
:okButtonProps="{ disabled: !selectedDrawType }">
<div class="draw-options">
<a-radio-group v-model:value="selectedDrawType">
<a-radio value="polygon">
<div class="draw-option">
<div class="draw-icon polygon-icon">◢</div>
<div class="draw-text">
<div class="draw-title">多边形围栏</div>
<div class="draw-desc">点击地图绘制多边形区域,双击完成绘制</div>
</div>
</div>
</a-radio>
<a-radio value="circle">
<div class="draw-option">
<div class="draw-icon circle-icon">●</div>
<div class="draw-text">
<div class="draw-title">圆形围栏</div>
<div class="draw-desc">点击地图中心点,拖动鼠标绘制圆形区域</div>
</div>
</div>
</a-radio>
<a-radio value="rectangle">
<div class="draw-option">
<div class="draw-icon rectangle-icon">■</div>
<div class="draw-text">
<div class="draw-title">矩形围栏</div>
<div class="draw-desc">点击地图左上角和右下角绘制矩形区域</div>
</div>
</div>
</a-radio>
</a-radio-group>
</div>
</a-modal>
export interface FenceData {
/**
* 围栏类型,可选:
* - 'polygon' 多边形围栏
* - 'circle' 圆形围栏
* - 'rectangle' 矩形围栏
*/
type: 'polygon' | 'circle' | 'rectangle'
/**
* 围栏的边界坐标数组(经纬度),格式为 [lng, lat]
* - 对于 polygon:表示各个顶点
* - 对于 rectangle:4 个顶点按顺序排列
* - 对于 circle:只包含圆心 [lng, lat]
*/
coordinates: number[][]
/**
* 围栏中心点的经纬度(可选)
* - 对于 circle 为圆心
* - 对于 polygon/rectangle 为中心点
*/
center?: number[]
/**
* 围栏的半径(单位:米,仅对 circle 有效)
* - polygon 和 rectangle 可用近似值
*/
radius?: number
}
const showDrawOptions = ref(false)
const selectedDrawType = ref<'polygon' | 'circle' | 'rectangle' | null>(null)
const isDrawing = ref(false)
const currentFenceData = ref<FenceData | null>(null)
const mapRef = ref()
// 开始绘制
const startDraw = () => {
if (!selectedDrawType.value) return
showDrawOptions.value = false
isDrawing.value = true
if (selectedDrawType.value === 'polygon') {
mapRef.value?.startDrawPolygon()
} else if (selectedDrawType.value === 'circle') {
mapRef.value?.startDrawCircle()
} else if (selectedDrawType.value === 'rectangle') {
mapRef.value?.startDrawRectangle()
}
// 围栏绘制中
const onFenceDrawn = (fenceData: FenceData) => {
currentFenceData.value = fenceData
}
// 围栏绘制完成
const onDrawComplete = (fenceData: FenceData) => {
console.log('onDrawComplete 被调用', fenceData)
currentFenceData.value = fenceData
isDrawing.value = false
selectedDrawType.value = null
console.log('currentFenceData 已更新:', currentFenceData.value)
}
// 清除围栏
const clearFence = () => {
mapRef.value?.clearFence()
currentFenceData.value = null
}