第十二节 Vue3集成高德地图并设计电子围栏

亮子 | 2026-02-26 13:47:44 | 54 | 0 | 0 | 0

1、安装依赖

npm install @amap/amap-jsapi-loader --save
# 或
pnpm add @amap/amap-jsapi-loader --save

2、添加类型声明

创建 src/types/global.d.ts 文件:

// 扩展Window类型,支持高德地图安全配置
interface Window {
  _AMapSecurityConfig: {
    securityJsCode: string;
  };
}

// 高德地图类型声明(根据需要补充)
declare namespace AMap {
  class Map {
    constructor(container: string, options?: any);
    destroy(): void;
    addControl(control: any): void;
    setCenter(center: [number, number]): void;
    // ... 其他方法
  }
  
  class Polygon {
    constructor(options?: any);
    setMap(map: Map): void;
    getPath(): any;
    contains(point: [number, number]): boolean;
    on(event: string, handler: Function): void;
  }
  
  class Circle {
    constructor(options?: any);
    setMap(map: Map): void;
    contains(point: [number, number]): boolean;
  }
  
  // 其他类型...
}

3. 创建电子围栏组件

src/components/ElectronicFence.vue

<template>
  <div class="fence-container">
    <!-- 地图容器 -->
    <div id="mapContainer" class="map-wrapper"></div>
    
    <!-- 控制面板 -->
    <div class="control-panel">
      <h3>电子围栏管理</h3>
      
      <!-- 围栏类型选择 -->
      <div class="form-item">
        <label>围栏类型:</label>
        <select v-model="fenceType">
          <option value="polygon">多边形围栏</option>
          <option value="circle">圆形围栏</option>
          <option value="rectangle">矩形围栏</option>
        </select>
      </div>
      
      <!-- 绘制控制 -->
      <div class="form-item">
        <button @click="startDrawing" :disabled="isDrawing">
          {{ isDrawing ? '绘制中...' : '开始绘制' }}
        </button>
        <button @click="clearFence" :disabled="!currentFence">清除围栏</button>
        <button @click="saveFence" :disabled="!currentFence">保存围栏</button>
      </div>
      
      <!-- 模拟位置测试 -->
      <div class="form-item" v-if="currentFence">
        <label>测试位置:</label>
        <div class="coord-input">
          <input type="number" v-model.number="testLng" placeholder="经度" step="0.000001">
          <input type="number" v-model.number="testLat" placeholder="纬度" step="0.000001">
        </div>
        <button @click="testPosition">检测位置</button>
        <div class="test-result" :class="resultClass">
          {{ testResult }}
        </div>
      </div>
      
      <!-- 报警记录 -->
      <div class="alarm-log" v-if="alarmLogs.length > 0">
        <h4>报警记录</h4>
        <ul>
          <li v-for="(log, index) in alarmLogs" :key="index">
            {{ log.time }} - {{ log.message }}
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref, reactive, computed } from 'vue';
import AMapLoader from '@amap/amap-jsapi-loader';

// 配置参数(请替换为你的实际key)
const AMAP_KEY = '你的高德地图Key';
const SECURITY_CODE = '你的安全密钥';

// 状态变量
const map = ref<any>(null);
const currentFence = ref<any>(null);
const fenceType = ref<'polygon' | 'circle' | 'rectangle'>('polygon');
const isDrawing = ref(false);
const testLng = ref(116.397428);
const testLat = ref(39.90923);
const testResult = ref('');
const alarmLogs = ref<Array<{ time: string; message: string }>>([]);
const lastInsideState = ref<boolean | null>(null); // 记录上次状态,避免重复报警

// 配置安全密钥
window._AMapSecurityConfig = {
  securityJsCode: SECURITY_CODE,
};

// 初始化地图
onMounted(() => {
  initMap();
});

// 组件销毁前清理
onBeforeUnmount(() => {
  if (map.value) {
    map.value.destroy();
  }
});

// 初始化地图
const initMap = async () => {
  try {
    const AMap = await AMapLoader.load({
      key: AMAP_KEY,
      version: '2.0',
      plugins: [
        'AMap.ToolBar',
        'AMap.Scale',
        'AMap.PolygonEditor',
        'AMap.CircleEditor'
      ],
    });

    // 创建地图实例
    map.value = new AMap.Map('mapContainer', {
      zoom: 12,
      center: [116.397428, 39.90923],
      mapStyle: 'amap://styles/normal',
      viewMode: '2D',
    });

    // 添加控件
    map.value.addControl(new AMap.ToolBar());
    map.value.addControl(new AMap.Scale());

    console.log('地图初始化成功');
  } catch (error) {
    console.error('地图加载失败:', error);
  }
};

// 开始绘制围栏
const startDrawing = () => {
  if (!map.value) return;
  
  isDrawing.value = true;
  
  // 根据类型创建不同围栏
  if (fenceType.value === 'polygon') {
    drawPolygon();
  } else if (fenceType.value === 'circle') {
    drawCircle();
  } else if (fenceType.value === 'rectangle') {
    drawRectangle();
  }
};

// 绘制多边形围栏
const drawPolygon = () => {
  const AMap = (window as any).AMap;
  
  // 创建多边形
  const polygon = new AMap.Polygon({
    path: [
      [116.403322, 39.920255],
      [116.410703, 39.897555],
      [116.402292, 39.892353],
      [116.389732, 39.898256],
      [116.387671, 39.912482]
    ],
    strokeColor: '#FF33FF',
    strokeWeight: 6,
    strokeOpacity: 0.8,
    fillColor: '#1791fc',
    fillOpacity: 0.4,
    strokeStyle: 'dashed',
    draggable: true,
  });
  
  polygon.setMap(map.value);
  currentFence.value = polygon;
  
  // 添加编辑工具
  const polygonEditor = new AMap.PolygonEditor(map.value, polygon);
  polygonEditor.open();
  
  // 监听绘制完成
  setTimeout(() => {
    isDrawing.value = false;
  }, 500);
};

// 绘制圆形围栏
const drawCircle = () => {
  const AMap = (window as any).AMap;
  
  const circle = new AMap.Circle({
    center: [116.397428, 39.90923],
    radius: 1000, // 半径(米)
    strokeColor: '#FF33FF',
    strokeWeight: 6,
    fillColor: '#1791fc',
    fillOpacity: 0.4,
    draggable: true,
  });
  
  circle.setMap(map.value);
  currentFence.value = circle;
  
  const circleEditor = new AMap.CircleEditor(map.value, circle);
  circleEditor.open();
  
  setTimeout(() => {
    isDrawing.value = false;
  }, 500);
};

// 绘制矩形围栏(特殊的多边形)
const drawRectangle = () => {
  const AMap = (window as any).AMap;
  
  const rectangle = new AMap.Polygon({
    path: [
      [116.368724, 39.926489],
      [116.427081, 39.926489],
      [116.427081, 39.882619],
      [116.368724, 39.882619]
    ],
    strokeColor: '#FF33FF',
    strokeWeight: 6,
    fillColor: '#1791fc',
    fillOpacity: 0.4,
    draggable: true,
  });
  
  rectangle.setMap(map.value);
  currentFence.value = rectangle;
  
  const polygonEditor = new AMap.PolygonEditor(map.value, rectangle);
  polygonEditor.open();
  
  setTimeout(() => {
    isDrawing.value = false;
  }, 500);
};

// 清除围栏
const clearFence = () => {
  if (currentFence.value) {
    currentFence.value.setMap(null);
    currentFence.value = null;
    testResult.value = '';
    lastInsideState.value = null;
  }
};

// 保存围栏(可扩展为保存到后端)
const saveFence = () => {
  if (!currentFence.value) return;
  
  let fenceData = null;
  
  if (fenceType.value === 'circle') {
    fenceData = {
      type: 'circle',
      center: currentFence.value.getCenter(),
      radius: currentFence.value.getRadius(),
    };
  } else {
    fenceData = {
      type: fenceType.value,
      path: currentFence.value.getPath(),
    };
  }
  
  console.log('保存围栏数据:', fenceData);
  alert('围栏已保存(请查看控制台数据)');
  
  // TODO: 发送到后端
};

// 测试位置是否在围栏内
const testPosition = () => {
  if (!currentFence.value || !map.value) {
    testResult.value = '请先绘制围栏';
    return;
  }
  
  const point: [number, number] = [testLng.value, testLat.value];
  const isInside = currentFence.value.contains(point);
  
  const currentTime = new Date().toLocaleTimeString();
  const insideState = isInside;
  
  // 判断状态变化,避免重复报警
  if (lastInsideState.value !== null && lastInsideState.value !== insideState) {
    // 状态发生变化,触发报警
    const alarmMessage = insideState 
      ? '⚠️ 车辆进入围栏' 
      : '⚠️ 车辆离开围栏';
    
    alarmLogs.value.unshift({
      time: currentTime,
      message: alarmMessage,
    });
    
    // 只保留最近10条记录
    if (alarmLogs.value.length > 10) {
      alarmLogs.value.pop();
    }
  }
  
  lastInsideState.value = insideState;
  
  testResult.value = isInside 
    ? '✅ 当前位置在围栏内部' 
    : '❌ 当前位置在围栏外部';
    
  // 在地图上添加临时标记点
  addTempMarker(point);
};

// 添加临时标记点
const addTempMarker = (position: [number, number]) => {
  const AMap = (window as any).AMap;
  
  // 移除之前的标记
  const prevMarker = document.querySelector('.temp-marker');
  if (prevMarker) {
    (prevMarker as any)._marker?.setMap(null);
  }
  
  const marker = new AMap.Marker({
    position: position,
    map: map.value,
    icon: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png',
    offset: new AMap.Pixel(-10, -30),
    className: 'temp-marker',
  });
  
  (marker as any)._marker = marker;
  
  // 3秒后移除标记
  setTimeout(() => {
    marker.setMap(null);
  }, 3000);
};

// 计算结果样式
const resultClass = computed(() => {
  if (testResult.value.includes('内部')) return 'inside';
  if (testResult.value.includes('外部')) return 'outside';
  return '';
});
</script>

<style scoped>
.fence-container {
  position: relative;
  width: 100%;
  height: 100vh;
  display: flex;
}

.map-wrapper {
  flex: 1;
  height: 100%;
}

.control-panel {
  width: 300px;
  background: white;
  padding: 20px;
  box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
  overflow-y: auto;
  z-index: 10;
}

.control-panel h3 {
  margin-top: 0;
  margin-bottom: 20px;
  padding-bottom: 10px;
  border-bottom: 1px solid #eee;
}

.form-item {
  margin-bottom: 15px;
}

.form-item label {
  display: block;
  margin-bottom: 5px;
  font-weight: 500;
  font-size: 14px;
}

.form-item select,
.form-item input {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}

.form-item select:focus,
.form-item input:focus {
  outline: none;
  border-color: #1791fc;
}

.form-item button {
  padding: 8px 12px;
  margin-right: 8px;
  border: none;
  border-radius: 4px;
  background: #1791fc;
  color: white;
  cursor: pointer;
  font-size: 14px;
  transition: background 0.3s;
}

.form-item button:hover {
  background: #0e6ab8;
}

.form-item button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.coord-input {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
}

.coord-input input {
  width: 50%;
}

.test-result {
  margin-top: 10px;
  padding: 10px;
  border-radius: 4px;
  font-weight: 500;
  text-align: center;
}

.test-result.inside {
  background: #e6f7e6;
  color: #2e7d32;
  border: 1px solid #a5d6a7;
}

.test-result.outside {
  background: #ffebee;
  color: #c62828;
  border: 1px solid #ef9a9a;
}

.alarm-log {
  margin-top: 20px;
  border-top: 1px solid #eee;
}

.alarm-log h4 {
  margin: 15px 0 10px;
  font-size: 16px;
}

.alarm-log ul {
  list-style: none;
  padding: 0;
  margin: 0;
  max-height: 200px;
  overflow-y: auto;
}

.alarm-log li {
  padding: 8px 10px;
  background: #fff3e0;
  margin-bottom: 5px;
  border-radius: 4px;
  font-size: 13px;
  color: #e65100;
  border-left: 3px solid #ff9800;
}
</style>

4. 使用组件

在页面中使用:

<template>
  <ElectronicFence />
</template>

<script setup lang="ts">
import ElectronicFence from '@/components/ElectronicFence.vue';
</script>

<style>
/* 确保父容器有高度 */
html, body, #app {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}
</style>

5、演示效果

image.png

📌 核心功能说明

1. 地图初始化

  • 使用 @amap/amap-jsapi-loader 按需加载
  • onMounted 生命周期中进行,确保DOM已渲染
  • 配置安全密钥防止盗用

2. 围栏绘制

  • 多边形围栏:通过 AMap.Polygon 实现,支持任意形状
  • 圆形围栏:通过 AMap.Circle 实现,设置圆心和半径
  • 矩形围栏:作为特殊的多边形实现
  • 使用 PolygonEditor/CircleEditor 支持拖拽编辑

3. 位置检测

  • 调用围栏对象的 contains() 方法判断点是否在内部
  • 支持实时测试输入坐标

4. 报警机制

  • 记录上一次状态,避免重复报警
  • 状态变化时(进/出)触发报警
  • 报警日志滚动显示

5. TypeScript支持

  • 扩展 Window 类型添加安全配置
  • 声明 AMap 命名空间类型

扩展建议

1. 集成真实定位

// 使用Geolocation插件获取真实位置
AMap.plugin('AMap.Geolocation', () => {
  const geolocation = new AMap.Geolocation();
  geolocation.getCurrentPosition((status, result) => {
    if (status === 'complete') {
      const { position } = result;
      testLng.value = position.lng;
      testLat.value = position.lat;
      testPosition();
    }
  });
});

2. 后端集成

  • 保存围栏数据时,将坐标发送到后端
  • 设备实时位置上报,由后端判断进出事件
  • 或使用高德猎鹰服务(Falcon Track Service)

3. 样式定制

  • 支持多种围栏颜色和透明度
  • 可配置报警音效
  • 可扩展多种报警模式(进入报警/离开报警/双向报警)