微信小程序地图实战:手把手教你实现电子围栏(圆形+多边形)
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>
这里有几个实用技巧:
- 给map设置固定高度(如80vh)比百分比更可靠
- 初始scale建议设为16,这个缩放级别最适合围栏操作
- 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 两点确定圆形范围
圆形围栏的实现逻辑与多边形不同,需要两个关键点:
- 圆心(第一个点击点)
- 圆周上的点(第二个点击点)
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 大数据量性能优化
当需要显示上百个围栏时,建议:
- 使用include-points属性只渲染可视区域
- 对polygons数据进行分片加载
- 在非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: '已触发预设动作'
})
}
})
}
})
更多推荐


所有评论(0)