Vue 3 + TypeScript 项目中集成 高德地图 并实现绘制电子围栏( 多边形、圆形、矩形围栏)

 

 

 

 

一、地图组件封装

 定义地图容器 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
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小九今天不码代码

感谢支持,一起进步~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值