1. 电子围栏功能的核心价值与应用场景

电子围栏是地理围栏(Geo-fencing)技术在小程序中的具体实现,它通过在地图上划定虚拟边界,当用户设备进入或离开该区域时触发预设动作。在实际项目中,我经常用它来做门店服务范围限定、共享单车停放区管理、物流配送范围控制等场景。

比如去年帮一个连锁奶茶品牌做的小程序,就用圆形围栏实现了"3公里内免费配送"的功能。当用户打开小程序时,系统会自动检测当前位置是否在门店配送范围内,这个体验比传统文字提示"XX路到XX路之间配送"直观太多了。多边形围栏则更适合不规则区域,比如景区电子导览、工业园区安全监控等。

技术本质上,微信小程序通过map组件的polygons(多边形)和circles(圆形)属性实现围栏绘制。这两个参数都接受数组格式,意味着可以同时绘制多个围栏。不过要注意的是,这些围栏本质上只是视觉呈现,真正的区域判断逻辑需要开发者自行实现——这点和原生地图SDK有所不同。

2. 多边形围栏的完整实现流程

2.1 基础页面结构搭建

先来看WXML部分的核心代码。建议使用flex布局确保地图全屏显示,特别是安卓设备上要注意高度计算:

<view class="container">
  <map 
    id="map"
    longitude="{{longitude}}" 
    latitude="{{latitude}}"
    scale="16"
    bindtap="handleMapTap"
    markers="{{markers}}"
    polygons="{{polygons}}"
    style="width:100%; height:80vh">
  </map>
  <view class="toolbar">
    <button size="mini" bindtap="clearAll">清空围栏</button>
    <button size="mini" type="primary" bindtap="saveFence">保存围栏</button>
  </view>
</view>

这里有几个实用技巧:

  1. 给map设置固定高度(如80vh)比百分比更可靠
  2. 初始scale建议设为16,这个缩放级别最适合围栏操作
  3. toolbar采用flex布局并固定在底部,操作更方便

2.2 核心JS逻辑实现

初始化时需要设置几个关键数据:

Page({
  data: {
    longitude: 116.404,
    latitude: 39.915,
    markers: [],
    polygons: [{
      points: [],
      strokeWidth: 2,
      strokeColor: '#FF0000',
      fillColor: '#FF000033'
    }]
  },
  
  handleMapTap(e) {
    const { markers, polygons } = this.data
    const newMarker = {
      id: markers.length,
      latitude: e.detail.latitude,
      longitude: e.detail.longitude,
      iconPath: '/assets/location.png',
      width: 24,
      height: 24
    }
    
    this.setData({
      markers: [...markers, newMarker],
      [`polygons[0].points`]: [...polygons[0].points, {
        latitude: e.detail.latitude,
        longitude: e.detail.longitude
      }]
    })
  },
  
  clearAll() {
    this.setData({
      markers: [],
      [`polygons[0].points`]: []
    })
  }
})

踩坑提醒:polygons的points数组必须包含至少3个点才会显示。在实际项目中,我通常会加个提示:

if (this.data.polygons[0].points.length < 3) {
  wx.showToast({
    title: '至少需要3个点才能形成围栏',
    icon: 'none'
  })
  return
}

3. 圆形围栏的特殊处理技巧

3.1 两点确定圆形范围

圆形围栏的实现逻辑与多边形不同,需要两个关键点:

  1. 圆心(第一个点击点)
  2. 圆周上的点(第二个点击点)
Page({
  data: {
    circles: [],
    tempPoints: []
  },
  
  handleMapTap(e) {
    const { tempPoints, circles } = this.data
    
    if (tempPoints.length < 2) {
      this.setData({
        tempPoints: [...tempPoints, {
          latitude: e.detail.latitude,
          longitude: e.detail.longitude
        }]
      })
      
      if (tempPoints.length === 1) {
        wx.showToast({
          title: '请点击确定圆形半径',
          icon: 'none'
        })
      }
    }
    
    if (tempPoints.length === 2) {
      const radius = this.calculateDistance(
        tempPoints[0].latitude,
        tempPoints[0].longitude,
        tempPoints[1].latitude,
        tempPoints[1].longitude
      )
      
      this.setData({
        circles: [{
          latitude: tempPoints[0].latitude,
          longitude: tempPoints[0].longitude,
          radius,
          fillColor: '#7cb5ec88',
          strokeColor: '#1a7bf7',
          strokeWidth: 2
        }]
      })
    }
  },
  
  calculateDistance(lat1, lng1, lat2, lng2) {
    // 使用Haversine公式计算两点间距离
    const rad = x => x * Math.PI / 180
    const R = 6378137 // 地球半径(米)
    const dLat = rad(lat2 - lat1)
    const dLng = rad(lng2 - lng1)
    const a = Math.sin(dLat/2)*Math.sin(dLat/2) + 
              Math.cos(rad(lat1))*Math.cos(rad(lat2))*
              Math.sin(dLng/2)*Math.sin(dLng/2)
    return Math.round(R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)))
  }
})

3.2 圆形围栏的视觉优化

微信地图的圆形围栏有个已知问题:z-index属性在某些机型上不生效。经过多次测试,我发现通过调整fillColor的透明度是最可靠的解决方案:

// 推荐的颜色配置
{
  fillColor: '#7cb5ec88', // 最后两位控制透明度
  strokeColor: '#1a7bf7',
  strokeWidth: 2
}

如果围栏区域很大,建议把strokeWidth设为3-5,这样在缩放时边界更明显。实测发现,当radius超过5000米时,默认的1px边框几乎不可见。

4. 常见问题与性能优化

4.1 围栏闪烁问题处理

在低端安卓设备上,频繁更新polygons数据可能导致地图闪烁。解决方案是使用wx.nextTick延迟渲染:

wx.nextTick(() => {
  this.setData({
    [`polygons[0].points`]: newPoints
  })
})

另一个技巧是使用transform过渡动画:

map {
  transition: all 0.3s ease;
}

4.2 大数据量性能优化

当需要显示上百个围栏时,建议:

  1. 使用include-points属性只渲染可视区域
  2. 对polygons数据进行分片加载
  3. 在非WiFi环境下降低渲染质量
this.setData({
  polygons: largeData.slice(0, 20), // 首屏只加载20个
  isWifi: true // 根据网络状态调整
})

4.3 坐标偏移校正

国内地图需要处理GCJ-02坐标系偏移。推荐使用第三方库如wxmp-google-maps进行转换:

import { transformFromGCJToWGS } from 'wxmp-google-maps'

const corrected = transformFromGCJToWGS(latitude, longitude)

如果不想引入库,可以用这个简化版校正函数:

function correctCoord(lat, lng) {
  // 简单校正参数
  const a = 6378245.0
  const ee = 0.00669342162296594323
  
  function transformLat(x, y) {
    let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x))
    ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0
    ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0
    ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320 * Math.sin(y * Math.PI / 30.0)) * 2.0 / 3.0
    return ret
  }
  
  // 具体转换逻辑...
}

5. 进阶功能实现

5.1 围栏拖拽编辑

实现可拖拽的围栏需要结合marker的callout属性:

markers.push({
  // ...其他参数
  callout: {
    content: '可拖动调整',
    color: '#fff',
    bgColor: '#07C160',
    display: 'ALWAYS'
  },
  draggable: true
})

然后监听marker的drag事件:

<map bindmarkertap="onMarkerTap" bindcontroltap="onControlTap" bindregionchange="onRegionChange" 
     bindmarkerdrag="onMarkerDrag" bindmarkerdragend="onMarkerDragEnd">
</map>

5.2 围栏数据持久化

建议将围栏数据转换为GeoJSON格式存储:

function toGeoJSON(points) {
  return {
    type: "Feature",
    geometry: {
      type: "Polygon",
      coordinates: [points.map(p => [p.longitude, p.latitude])]
    },
    properties: {}
  }
}

微信云开发数据库特别适合存储这类地理数据,支持地理位置查询:

db.collection('fences').add({
  data: {
    geo: db.Geo.Point(longitude, latitude),
    radius,
    createTime: db.serverDate()
  }
})

5.3 围栏碰撞检测

判断点是否在多边形内使用射线法:

function isPointInPolygon(point, polygon) {
  const x = point.longitude, y = point.latitude
  let inside = false
  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
    const xi = polygon[i].longitude, yi = polygon[i].latitude
    const xj = polygon[j].longitude, yj = polygon[j].latitude
    
    const intersect = ((yi > y) !== (yj > y))
      && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)
    if (intersect) inside = !inside
  }
  return inside
}

对于圆形围栏更简单:

function isPointInCircle(point, center, radius) {
  const dx = point.longitude - center.longitude
  const dy = point.latitude - center.latitude
  return dx*dx + dy*dy <= radius*radius
}

在实际项目中,我通常会结合微信的onLocationChange事件实现实时位置监控:

wx.startLocationUpdate({
  success: res => {
    wx.onLocationChange(location => {
      const isInside = checkAllFences(location)
      if (isInside) {
        wx.showModal({
          title: '进入围栏区域',
          content: '已触发预设动作'
        })
      }
    })
  }
})

更多推荐