如果你也在用 React + Mapbox 做可编辑围栏(画多边形、拖顶点、弹属性面板),下面这几个现象可能眼熟:
- 两块围栏名字一样,点 A 再点 B,右边属性面板还是 A 的内容
- 切到另一套坐标系的地图时,多边形突然被撕开、经纬度飞到天边
- 分时 / 等级那种小图标正好压在编辑手柄上,绿点点不到
1. 同名围栏一切换,属性面板串了
咋回事
面板大概长这样:
tsx
<InfoModal
selectedPolygon={selectedPolygon}
key={
selectedPolygon.index +
selectedPolygon.polygon.properties.draw_type +
selectedPolygon.polygon.properties.name
}
/>
看起来挺合理:换一个就换 key,组件重建。
问题是 name 可以重复。两块围栏同名、同类型时,拼出来的 key 一模一样,React 觉得还是同一个实例,内部表单状态就不会清。
咋改
key 跟业务主键走,别用展示字段拼:
tsx
key={String(selectedPolygon.polygon.id)}
还有一个容易忽略的:选中切换是异步的。属性 atom 更新时,地图上「当前选中」可能已经是下一块了,id 对不上还继续 setFeatureProperty,等于把 A 的字段写进 B。回写前加一句守卫就行:
tsx
if (String(polygonAttributes.id) !== String(lastSelectedPolygon.id)) return
顺带提一嘴:两套地图来回切的时候,如果上一张已经算好了当前视口,下一张就别再无脑 flyTo 多边形中心。用户刚看好的位置被抢走,体验很差。有切换视口就保留,没有再居中。
2. 坐标转换之后,多边形形状爆炸
这个最离谱,也最值得单独说。
现象
编辑侧几何看着正常,一切到展示用的 lng/lat 地图,整块围栏偶发「撕开」。不是每次都炸,反复拖、反复裁更容易撞上。
一开始会怀疑投影超界、接口转错、裁剪把几何裁坏了。把数据坐标 dump 出来看,其实一直是对的------炸在展示转换。
真正的原因
GeoJSON 多边形是闭合环,很多人(包括 turf 裁完再 nudge)会这么写:
ts
ring[ring.length - 1] = ring[0] // 首尾绑成同一个数组引用
然后你调用库里的「整 Feature 转换」,比如类似:
ts
crsControl.toLngLatByFeature(feature)
内部常见实现是:cloneDeep 一下,再对每个点 splice 原地改坐标。
坑在这:cloneDeep 只复制结构,不断开别名 。
ring[0] === ring[last] 在 clone 之后还是 true。
遍历时:
- 第一个点:投影 → 展示坐标,比如
[-118, 83] - 轮到末尾:还是同一个数组,把已经转好的展示坐标再当投影喂进去 → 变成
[-221, 2.6]这种鬼东西
经度冲出 ±180,Mapbox 再一环绕,肉眼就是形状爆炸。
中间顶点都正常,只有首尾被转了两遍------对照日志打一下就很明显。
咋改
别信「内部已经 clone 了」的整 Feature API。逐点转,每次 new 一个新数组:
ts
export const featureToLngLat = (features, crsControl) => {
return features.map((feature) => {
const srcRings = feature.geometry.coordinates
return {
...feature,
geometry: {
...feature.geometry,
type: 'Polygon',
coordinates: srcRings.map((ring) =>
ring.map((pos) => crsControl.toLngLat([pos[0], pos[1]])),
),
},
}
})
}
反向转 UTM 同理。从机制上杜绝二次转换,不依赖库内部 clone 行为。
如果暂时必须原地改,至少先把首尾断引用:
ts
ring[ring.length - 1] = [...ring[0]]
以后凡是对 GeoJSON 做逐点变换,我都会先看一眼:ring[0] === ring[last] 是不是 true。是的话就危险。
3. 小图标挡住了编辑绿点
现象
顶点上要挂一个小 icon(分时、风险等级之类)。坐标落在 ring[ring.length - 2],Mapbox 默认 icon-anchor 是中心------图片正中对准顶点,编辑时的绿色拖拽手柄经常被盖住。
试过又改回去的方案
第一反应是把 icon 挪到外接矩形中心。绿点是腾出来了,但 icon 离围栏角点太远,看起来不像「属于这块围栏」。
更干净的做法
坐标还放顶点,改 layout:
ts
layout: {
'icon-allow-overlap': true, // 角点挤在一起别被碰撞检测藏掉
'icon-ignore-placement': true,
'icon-anchor': 'bottom', // 底边对齐坐标,图片浮在顶点上方
}
Mapbox symbol 的「锚点坐标」和「你眼睛看到的中心」不是一回事。很多遮挡问题,调 icon-anchor / icon-offset 比硬改几何点舒服。
写在最后
这几件事乍一看像地图玄学,掰开都是很普通的前端问题:
- React 拿展示字段当
key→ 同名复用错实例 - 异步状态回写不做 id 校验 → 串写
- 闭合环首尾同引用 + 原地坐标变换 → 点被转两遍
- symbol 默认居中锚点 → 挡住交互手柄
做围栏编辑的同学,坐标转换那条建议自己 dump 一次:同一份 Feature,中间点和首尾点分别打转换前后的值,再打一下 ring[0] === ring[last]。很多「偶现撕开」其实一眼就能钉死。