这类需求我一年能接到十几回:后台管理系统里要画地块、画区域、做标注,画完还得能改、能存、能还原。标题里的“高德地图JS API地块多样式多图形绘制编辑导出导入删除”基本就是一套完整的地图绘制模块需求。我最初以为只是调几个API就行,后来发现高德开放平台的文档只提供了散装能力,把它们串成一套完整业务工具需要自己处理大量细节。这篇文章就把我实际落地的一套方案拆开讲,覆盖图形绘制、多样式管理、编辑交互、数据导入导出、批量删除和常见坑,适合做GIS项目、地块管理后台、园区招商系统的前端同学参考。
1. 整体设计思路:先把“地块编辑器”拆成四个能力
1.1 这类需求到底在解决什么问题
很多业务场景都需要在地图上画“地块”:园区管理系统里标注入驻企业占地,招商系统里划分可售楼栋和地块,农业平台圈定种植区域,甚至物业系统里画停车场和绿化边界。纯展示地图满足不了这类需求,用户要求的是能交互:新增一个地块、编辑边界、换个颜色代表不同用途、把数据保存下来下次打开还能看到。
“多样式”这个词很关键。地块不是画出来就完事,不同性质的地块得用不同颜色区分,否则一片全绿谁分得清哪块是住宅、哪块是商业?这里样式的背后其实是业务属性。“多图形”则是说,除了多边形地块,可能还要用到圆、矩形、线、点标记,比如圆形表示半径范围,矩形做快速圈选,折线画道路或轨迹,标记点做定位。“绘制编辑导出导入删除”连在一起,才是完整闭环。
1.2 技术选型:高德JS API加自研交互层
高德JS API本身提供覆盖物对象,包括AMap.Polygon、AMap.Circle、AMap.Rectangle、AMap.Polyline、AMap.Marker。绘制方面有AMap.MouseTool插件,这个插件封装了鼠标在地图上画图的能力。但这里有一个问题:MouseTool只管画,画完之后你拿到一个覆盖物对象,后面要编辑、要管理场景数据、要序列化保存,它一概不管。
我的做法是把高德覆盖物当成“渲染层”,业务上再包一层统一数据结构。每个图形在业务层是一个对象,包含图形类型、业务属性、样式配置、图形坐标数据,同时内部持有对应的高德覆盖物实例。地图上所有的删除、编辑、导入导出操作,都是先操作业务数据,再同步刷新覆盖物。这样代码结构清晰,也方便对接后端的保存接口。
1.3 数据模型和整体数据流
我定义的核心数据模型大致如下:
javascript复制// 每个地块或图形对应一个 item
{
id: 'plot_1697412345678',
type: 'polygon', // polygon | circle | rectangle | polyline | marker
name: 'A-01 号楼地块',
bizType: 'commercial', // 业务类型,决定默认样式
style: {
strokeColor: '#5470c6',
strokeWeight: 3,
fillColor: '#5470c6',
fillOpacity: 0.2,
lineDash: null,
// marker 样式
icon: 'https://...',
size: [26, 32]
},
// 坐标数据
path: [[lng, lat], [lng, lat]], // polygon / polyline / rectangle 用
center: [lng, lat], // circle、marker 用
radius: 500, // circle 用
extData: {}, // 自定义业务数据
overlay: null // 高德覆盖物实例,运行时不序列化
}
整体数据流是:用户操作 -> 更新这个item -> 调用渲染函数同步到地图覆盖物;后端保存时,把item数组里的overlay字段剥离,序列化成JSON传给接口;页面重新加载时,从接口拿JSON,先重建item,再逐个覆盖物渲染上地图。所有功能绕这个核心转,代码就不会散。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 绘制模块实现:多样式多图形的接入细节
2.1 接入AMap.MouseTool完成基础绘制
AMap.MouseTool是高德地图提供的鼠标绘图工具,支持画标记、折线、多边形、矩形、圆。用起来很简单,但有个容易踩的坑:插件加载方式是异步插件,需要在AMap.plugin回调里使用。我封装了一个绘制管理器,通过调用draw(type, styleOptions)来启动不同类型绘制:
javascript复制async function loadMouseTool() {
return new Promise((resolve) => {
AMap.plugin('AMap.MouseTool', () => {
resolve(true);
});
});
}
function startDraw(map, drawType, styleOptions) {
// 先关闭上一个绘制工具,避免叠加
if (window._mouseTool) {
window._mouseTool.close(true);
}
const mouseTool = new AMap.MouseTool(map);
window._mouseTool = mouseTool;
const drawMap = {
polygon: () => mouseTool.polygon(styleOptions),
rectangle: () => mouseTool.rectangle(styleOptions),
circle: () => mouseTool.circle(styleOptions),
polyline: () => mouseTool.polyline(styleOptions),
marker: () => mouseTool.marker(styleOptions)
};
drawMap[drawType]?.();
// 监听绘制完成事件,只挂一次
mouseTool.off('draw', onDrawDone);
mouseTool.on('draw', onDrawDone);
}
这里我特别解释下mouseTool.close(true)的作用。如果不主动关闭,上一个开启的绘制模式会一直生效,用户再次点击地图可能触发上一次的绘制回调,造成重复创建图形。true参数表示同时移除当前工具绘制过程中产生的辅助图形,比如画了一半的虚线段。不清理的话,用户取消绘制后地图上会残留一条虚线,非常恼人。
MouseTool的draw事件回调参数里,e.obj就是绘制完成的覆盖物实例,可以直接拿到坐标数据。对不同类型要分别处理坐标获取,这点细节较多,见下节。
javascript复制function onDrawDone(e) {
const overlay = e.obj;
const drawType = overlay.className; // 'AMap.Polygon' / ...
// 根据实际类型提取坐标,生成 item
const newItem = createItemFromOverlay(overlay, typeMapping[drawType]);
items.push(newItem);
bindOverlayEvents(newItem);
handleStyleChange(newItem, currentStylePreset);
}
overlay.className在高德覆盖物文档里不一定被强调,但实例上确实存在这个属性,可以用于判断当前图形类型。稳妥的做法是绘制前自己记住类型,或者用instanceof判断。我在项目里是绘制前传入类型,在回调里直接使用。
2.2 多样式实现:绘制瞬间就定色调
多样式最基础的做法是给不同业务类型预设不同颜色。例如地块分为住宅、商业、教育、绿地、工业时,样式预设可以先做成一个配置表:
javascript复制const bizStyleMap = {
residential: {
fillColor: '#3b82f6',
strokeColor: '#1d4ed8',
fillOpacity: 0.15,
label: '住宅用地'
},
commercial: {
fillColor: '#f59e0b',
strokeColor: '#b45309',
fillOpacity: 0.15
},
education: {
fillColor: '#10b981',
strokeColor: '#047857',
fillOpacity: 0.15
},
industrial: {
fillColor: '#ef4444',
strokeColor: '#b91c1c',
fillOpacity: 0.15
},
greenSpace: {
fillColor: '#84cc16',
strokeColor: '#4d7c0f',
fillOpacity: 0.2
}
};
在业务界面里,用户绘制前会先从下拉框选择“当前绘制类型”。绘制开始前我们把类型存到临时变量,绘制完成后用该类型去查bizStyleMap,得到样式并更新覆盖物。这样不同用途的地块一画出来就是不同颜色,用户不需要再手动去配,直观且不易出错。
图形画完后样式可以随时改。高德覆盖物支持统一的setOptions方法,传参中包含描边色、填充色、透明度、线宽等信息。所以后续在地块列表里切换业务类型,只需要调用overlay.setOptions(styles)以及更新item里的bizType和style字段即可:
javascript复制function applyBizType(item, newBizType) {
const oldBizType = item.bizType;
item.bizType = newBizType;
const targetStyle = bizStyleMap[newBizType];
if (targetStyle) {
item.style = {
...item.style,
// 这里只覆盖颜色相关属性,保留其他手工微调项
fillColor: targetStyle.fillColor,
strokeColor: targetStyle.strokeColor,
fillOpacity: targetStyle.fillOpacity
};
item.overlay.setOptions(item.style);
}
refreshItemListUI();
}
样式保持一致性另一个关键点在于初始值。比如Polygon绘制时如果填充透明度是0.5,多个地块叠在一起会严重遮挡底图,透明度要控制在0.1~0.3之间,方便看清边界同时不遮住标注。网络热词里很多人搜“高德地图底图纹理地址”“地图瓦片样式”,本质也是在调底图表现,要注意业务图层和底图的视觉权重分配。
2.3 绘制完成后的对象管理与事件绑定
覆盖物绘制出来后,如果只是把它丢到地图上,不保存引用,很可能会被垃圾回收掉,或者无法在后续做选中、删改等操作。我采用一个items数组统一维护所有图形对象,绘制完成后立刻入数组。同时给每个覆盖物绑定点击事件、鼠标悬浮事件和右键事件,用于交互反馈。
javascript复制function bindOverlayEvents(item) {
const overlay = item.overlay;
overlay.on('click', (e) => {
e.originEvent && e.originEvent.stopPropagation();
selectItem(item);
});
overlay.on('mouseover', () => {
// 视觉反馈,比如调高透明度表示可选中
});
overlay.on('mouseout', () => {
if (!item.isSelected) {
// 还原透明度
}
});
// Marker 的画完可以自动弹信息窗体,但不要每次点击都弹
}
必须处理一个关键问题:地图点击和多边形点击事件是共存的。如果用户点击的是某个地块区域,只想选中它,但地图的click事件也会触发,可能导致误判为“点击空白处、取消选中”。解决办法是在多边形事件里先阻止原生事件冒泡,再处理选中逻辑。我在实现里加了e.originEvent.stopPropagation(),这个在高德覆盖物事件里有效,否则常常出“点地块却把地图click也触发了”的bug。
3. 编辑模块设计与踩坑记录
3.1 为什么没有用高德自带的Editor插件
高德JS API确实提供了覆盖物编辑器相关的插件,比如AMap.PolyEditor能编辑多边形。网上不少教程就是直接new AMap.PolyEditor(map, polygon)然后调open(),但我最终没有采用这个方案,原因有三:
第一,管理多个覆盖物时,每个编辑器实例得独立维护,切换编辑目标时需要显式调用close()关闭上一个编辑器,否则会同时操作两个图形,产生数据混乱。第二,高德编辑器的外观是内置的,样式不统一,顶点在深色底图上显示不清晰,且控制点数量有时会出现异常。第三,编辑器直接操作的是覆盖物对象,但它是否同步更新我们业务层item里的坐标数据?需要再监听事件手动同步,实际上一样要开发代码,那不如直接从底层设计自己的编辑交互。
所以我选择自研“顶点手柄”方案:进入编辑状态时,根据图形顶点坐标生成一些可拖拽的Marker作为手柄,拖动Marker时实时更新覆盖物路径,拖完再将新坐标同步回item。这个方案代码量稍微多一些,但整个交互完全自己控制,后期扩展旋转、缩放也容易。
3.2 顶点控制点方案的核心实现
顶点控制点方案的本质就是“覆盖物坐标数据驱动”,不直接操作覆盖物的几何对象。编辑多边形的流程如下:
- 找到当前选中Polygon的路径数组
polygon.getPath(),得到一个AMap.LngLat数组。 - 为每个顶点创建一个
AMap.Marker,指定Icon为小圆点,调整偏移让它对准坐标点,设置draggable: true。 - 把VertexMarker存到临时数组
activeHandles里,同时启动dragging事件监听。 - 拖动Marker过程中,实时获取该Marker当前位置,替换原路径数组中对应索引的坐标,然后重新
setPath()更新主Polygon。 - Marker拖动结束,同步更新item里的path数据。
javascript复制function enablePolygonEditor(item) {
// 先清除所有旧手柄
clearHandles();
const polygon = item.overlay;
const path = polygon.getPath().map((lnglat) => [lnglat.lng, lnglat.lat]);
item.editingPath = path;
// 记录每个顶点对应的marker和顶点索引
path.forEach((point, index) => {
const marker = new AMap.Marker({
position: point,
map: map,
anchor: 'center',
draggable: true,
cursor: 'pointer',
content: createHandleDom('#fff', '#3b82f6', 6)
});
marker.on('dragging', (e) => {
// 拖拽中的实时更新保留,待dragend统一刷新
});
marker.on('dragend', (e) => {
const newPos = e.target.getPosition();
item.editingPath[index] = [newPos.lng, newPos.lat];
refreshPolygonFromPath(item);
});
activeHandles.push({ marker, index });
});
}
function refreshPolygonFromPath(item) {
const lngLatPath = item.editingPath.map((p) => new AMap.LngLat(p[0], p[1], true));
item.overlay.setPath(lngLatPath);
item.path = item.editingPath.map((p) => [...p]);
}
这里有个高德坐标体系细节:new AMap.LngLat(x, y, true)第三个参数noAutofix要传true。不传的话,当你传入不在当前地图范围内的经纬度时,高德会将其转换到-180到180范围内,可能造成坐标被意外修改。我在跟踪一个离屏坐标的bug时发现的问题,不传true会导致边界图形自动跳变。
顶点控制点的表现层有个可以优化的小地方:拖动时Marker的层级要设置得高于Polygon,否则Marker会被图形填充盖住。我给控制点设置高zIndex,并关闭Polygon的鼠标事件穿透性,让用户永远能抓到手柄。
如果绘制的是矩形,虽然AMap.Rectangle内部维护的是一个矩形边界,本质上它也是继承多边形,可以先rectangle.getBounds()拿东北、西南两个顶点,但这样编辑自由度低。实际项目中我把Rect对象转换为Polygon路径再编辑,矩形就变成了四条边四个顶点的普通多边形。如果业务上要求矩形必须是矩形,编辑东北角或西南角即可,不过这种场景少,多数矩形只是用户“粗略圈一下”,转Polygon完全够用。
圆形编辑稍微不一样,顶点手柄只需要一个控制点来控制半径。点击图形进入编辑时,我在圆的正北方向放一个Marker,拖动Marker时实时计算它到圆心AMap.GeometryUtil.distance(center, markerPos)的距离,通过circle.setRadius(distance)更新圆:
javascript复制function enableCircleEditor(item) {
clearHandles();
const circle = item.overlay;
const center = circle.getCenter();
const radius = circle.getRadius();
// 圆心到半径边缘的方向向量,取正北作为控制点初始位置
const handlePos = [center.lng, center.lat + radius / 111319.9];
const marker = new AMap.Marker({
position: handlePos,
map: map,
draggable: true,
anchor: 'center',
content: createHandleDom(...)
});
marker.on('dragend', (e) => {
const pos = e.target.getPosition();
const dist = AMap.GeometryUtil.distance([center.lng, center.lat], [pos.lng, pos.lat]);
circle.setRadius(dist);
item.radius = Math.round(dist * 100) / 100;
item.center = [center.lng, center.lat];
});
activeHandles.push({ marker });
}
这里还要注意一个地理常识:纬度每度大概对应111公里,但经度每度对应的实际距离随纬度变化而不同。我上面把半径转成纬度增量作为控制点初始位置,是一种近似做法,在高纬度区域会有偏差。更稳妥的做法是用AMap.GeometryUtil.offset传入方向和距离,或者直接用圆的getBounds()计算边的中心点位置来生成手柄,我最终版本采用的是后者。
3.3 整体拖拽、图形选中与交互增强
编辑不只是拖顶点,用户经常需要整体移动一个地块。高德覆盖物自带draggable属性,对象创建时或之后设overlay.setDraggable(true)即可拖动整个图形。但要注意,图形整体拖动后不会自动修改我们item里的坐标数据,所以要监听dragend事件同步更新。
javascript复制overlay.on('dragend', (e) => {
const overlay = e.target;
if (item.type === 'circle') {
const center = overlay.getCenter();
item.center = [center.lng, center.lat];
} else {
const path = overlay.getPath();
item.path = path.map((p) => [p.lng, p.lat]);
}
});
选中态处理也是体验细节最密集的地方。我遇到过最典型的问题:图形数量一多,用户根本点不中目标多边形,因为填充色透明区域太小或者地块非常狭窄,鼠标很难精准命中。解决方法是新增一个“地块列表”面板,每个item在面板中有一行,点击列表行即执行map.setFitView([overlay])并选中该图形。这个思路在许多项目里比纯地图点选更好用,尤其是手机上操作地图,点选范围本来就小,列表选择能大幅增加可用性。
选中一个图形后,顶部工具栏才启用“编辑”、“删除”、“更换样式”等操作按钮。这样交互逻辑非常清晰:不选中任何图形时,工具栏的编辑和删除按钮置灰,防止用户误操作。操作按钮绑定当前选中的item,完成后清空选中状态。这套逻辑适合管理类后台,而不是编辑器类的连续操作场景。
4. 导入导出功能的关键实现
4.1 数据格式设计:向后端和其他系统看齐
既然有保存需求,就要考虑数据格式。项目落地时我对比了两种方案:
第一种是严格GeoJSON格式。好处是标准、通用,ArcGIS、QGIS这些GIS工具都能直接吃,后端如果要接入地图服务也方便。坏处是高德的Circle数据没有标准GeoJSON类型对应,你得自定义扩展字段,或者把圆拟合成多边形存储。
第二种是自用JSON结构,字段完全跟内部item对应,保存和还原最省事。缺点是不通用,别的系统拿到这个JSON不能直接在地图上渲染。
我的实际选择是:导出文件时区分场景。如果用户明确说要导出给ArcGIS或别的平台用,就导出GeoJSON,圆转成带半径属性的Point或者直接转成近似圆多边形;如果只是系统自身的备份、恢复、编辑保存,就导出内部JSON,保留所有层级细节。
以下是GeoJSON导出结构示例,一个地块对应一个Feature:
json复制{
"type": "FeatureCollection",
"name": "园区地块导出",
"features": [
{
"type": "Feature",
"properties": {
"id": "plot_001",
"name": "A-01 办公楼",
"bizType": "commercial",
"strokeColor": "#f59e0b",
"strokeWeight": 3,
"fillColor": "#f59e0b",
"fillOpacity": 0.2
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[116.473587, 39.993828],
[116.474235, 39.993826],
[116.474277, 39.993571],
[116.473613, 39.993548],
[116.473587, 39.993828]
]
]
}
}
]
}
4.2 导出逻辑:从item到GeoJSON的转换函数
导出时最要命的一点是坐标转换顺序。高德Polygon的getPath()返回的经纬度数组是纬度在前还是经度在前?实际上AMap.LngLat实例有.lng和.lat两个属性,我们重新整理数组时要注意GeoJSON规范要求坐标顺序为[lng, lat],高德内部大多数方法也是经纬度顺序,但如果你不小心把数组原样塞给GeoJSON,轻则符号不变只是后端读错坐标,重则在地图上直接画到海里去。我写了一个统一的转换工具函数,所有多边形导出都走它:
javascript复制function buildGeoJSONFeature(item) {
let geometry = null;
switch (item.type) {
case 'polygon':
case 'rectangle': {
const rings = [item.path.map((p) => [p[0], p[1]])];
// 简单闭合:确保首尾相同,某些GIS工具要求
if (rings[0][0][0] !== rings[0][rings[0].length - 1][0] ||
rings[0][0][1] !== rings[0][rings[0].length - 1][1]) {
rings[0].push([...rings[0][0]]);
}
geometry = { type: 'Polygon', coordinates: rings };
break;
}
case 'polyline':
geometry = {
type: 'LineString',
coordinates: item.path.map((p) => [p[0], p[1]])
};
break;
case 'point':
case 'marker':
geometry = {
type: 'Point',
coordinates: [item.center[0], item.center[1]]
};
break;
case 'circle':
// GeoJSON没有Circle,保留为Point并在properties放radius
geometry = {
type: 'Point',
coordinates: [item.center[0], item.center[1]]
};
break;
}
return {
type: 'Feature',
properties: {
...item.style,
id: item.id,
name: item.name,
bizType: item.bizType,
drawType: item.type,
radius: item.radius || null
},
geometry
};
}
封装成完整导出函数时,会生成一个Blob并触发下载。这一步看起来小,但很多项目都需要注意文件名不要用中文直接拼时间戳,某些Windows环境下会出现文件名乱码。我一般用模板字符串生成:地块标注_${new Date().toISOString().slice(0,10)}.json。
4.3 导入逻辑:能把数据重新还原到地图上
导入是导出的逆过程,但不只是简单的反向循环。需要处理用户如何上传JSON文件、如何解析、如何容错。我的实现是提供一个input[type=file]隐藏按钮,用户点击导入时触发文件选择。用FileReader读取后JSON.parse,然后逐条还原。
还原过程有几个业务判断要做:数据里是GeoJSON Feature还是内部item结构,这里主要通过顶层字段区分。还原GeoJSON时,遍历features数组,对每个feature先读geometry.type:
javascript复制async function importFromGeoJSON(geojson) {
const items = geojson.features || [];
for (const feature of items) {
const props = feature.properties || {};
const geometry = feature.geometry;
let newItem;
if (geometry.type === 'Polygon' || (geometry.type === 'MultiPolygon')) {
const coords = geometry.type === 'Polygon'
? geometry.coordinates[0]
: geometry.coordinates[0][0];
newItem = createPolygonItem(coords, props);
} else if (geometry.type === 'LineString') {
newItem = createPolylineItem(geometry.coordinates, props);
} else if (geometry.type === 'Point') {
if (props.drawType === 'circle') {
newItem = createCircleItem(geometry.coordinates, props.radius, props);
} else {
newItem = createMarkerItem(geometry.coordinates, props);
}
}
if (newItem) {
renderItemOnMap(newItem);
items.push(newItem);
}
}
// 还原后,把视野缩放到全部图形范围
fitAllItems();
}
导入时一个非常容易出的bug是:使用同一个底图容器,导入后Polygon的路径坐标如果与当前地图中心差距过大,视野里找不到图形,用户以为导入失败。所以导入成功后一定要map.setFitView([...allOverlays], false, [80, 80, 80, 80]),第二个参数控制是否显示动画,第三个参数设置留白边距,避免图形贴边被工具栏遮挡。
导入如果涉及样式,比如GeoJSON的properties里虽带有strokeColor等字段,但这些字段是自定义的,标准的GeoJSON只保证geometry和properties存在。因此导入逻辑中要做默认值兜底:如果properties里没有颜色相关字段,就直接从当前选中的业务类型预设取。否则画出来一堆黑色默认多边形,体验会非常差。
5. 删除交互、性能打磨与常见问题排查
5.1 删除功能的设计:单个、批量、联动
删除功能看似最不起眼,但踩坑不少。最基础的是从数组里删除item,同时移除地图覆盖物:
javascript复制function removeItem(item) {
// 如果该item正在编辑中,先关闭编辑手柄
if (currentEditingId === item.id) {
clearHandles();
}
// 移除地图覆盖物
if (item.overlay) {
map.remove(item.overlay);
}
// 从数组里删除
const index = items.findIndex((it) => it.id === item.id);
if (index > -1) {
items.splice(index, 1);
}
refreshItemListUI();
}
高德的map.remove()方法和数组操作没有冲突,但如果你用items = items.filter(...)这种新数组替代旧数组,要留意所有对items的引用是否同步更新了。如果没有把items保存在一个统一store或全局变量里,组件里多个地方引用旧数组,会留下记忆化bug,删除一个后UI还显示旧数据。所以我在项目里总是把items放在一个单一状态管理对象中,用发布订阅模式通知列表和地图刷新。
批量删除功能要额外处理一个业务确认场景。用户在列表里勾选多个地块后点“删除”,如果直接把地块删了,一旦误操作所有数据都没了。我的建议是删除前弹确认框,同时给“已编辑未保存”的提示。如果真的没做后端保存,还可以加一层“撤销删除”能力:把删除的item先放进一个history栈,点击撤销时重新渲染。实现不复杂,体验提升很多,尤其适合非专业GIS操作人员的后台系统。
如果和网络热词里用户提到的“大地块、多层图形叠加”结合,删除时还有一层叠加区域清理的问题:如果两个地块有重叠区域,删除一个后,另一个地块底下的残留图形可能会被误删?这倒不会,高德overlay独立管理,地图remove只删除传入的覆盖物实例。但你要小心循环删除时数组的迭代问题:
javascript复制// 错误写法:边遍历边删除会跳过元素
items.forEach((item, index) => {
if (selectedIds.includes(item.id)) {
items.splice(index, 1); // 删除后后面元素前移,索引会错位
}
});
// 正确写法:拷贝一份后循环
[...items].forEach((item) => {
if (selectedIds.includes(item.id)) {
removeItem(item);
}
});
5.2 大量地块地图性能问题
高德地图对覆盖物的承载能力并没有那么神。本地测试中,页面挂载500个Polygon时,缩放地图已经能感觉到轻微卡顿,如果每个Polygon还绑定了多个事件、设置了阴影和圆角等额外效果,帧率会进一步下降。1000个以上直接卡到不舒服。
我的优化经验有几点:
第一,视口裁剪。高德原生覆盖物如果直接添加到map上,不管是否在当前视野都会参与渲染。大数据量时,监听map.on('moveend')事件,遍历items中的overlay,判断其bounds是否和当前视野bounds相交,不相交则从地图上map.remove(overlay),相交再添加。这个技巧能把渲染量降到实际可见的数量级,收益很高。
javascript复制map.on('moveend', throttle(() => {
const bounds = map.getBounds();
items.forEach((item) => {
const overlay = item.overlay;
const overlayBounds = getOverlayBounds(overlay);
const visible = bounds.intersects(overlayBounds) || bounds.contains(overlayBounds);
if (visible && !overlay.getMap()) {
overlay.setMap(map);
} else if (!visible && overlay.getMap()) {
overlay.setMap(null);
}
});
}, 200));
第二,非编辑状态下不给Polygon做高亮拖拽效果。只有选中时才动态调整阴影与透明度,其他地块尽量保持简单样式,减少浏览器样式重绘成本。
第三,Marker数量多时优先用AMap.MarkerCluster插件聚合。比如地块中心点标注如果数量大,直接铺上会严重拖慢移动端,用聚合策略能减少96%以上的标记数量。
还有网络热词里有人问“Vue弹窗高德地图空白”,其实很多是地图容器初始化时容器尺寸为0或隐藏导致地图无法正确计算尺寸。如果你的地块编辑模块是在弹窗里打开的,一定要等弹窗完全可见后再调用map.resize()。我在编辑组件中通过nextTick加延迟调用map.resize()解决了这个问题,否则底图只在弹窗打开的一瞬间是灰色或没瓦片。
5.3 一份“经验避坑清单”,照着做少走三天弯路
把整个项目踩过的高频坑整理成一张速查表,方便后续维护者接手:
| 现象 | 根因 | 处理方案 |
|---|---|---|
| 绘制完成后图形丢失 | 覆盖物对象没有被保存到items集合,被回收或引用丢失 | 绘制回调后立刻存入数组并保持引用 |
| 点地块触发地图取消选中 | 地图click事件冒泡 | 覆盖物事件里调用e.originEvent.stopPropagation() |
| 导入后看不见图形 | 没有setFitView,图形位置太远 | 导入完成后计算所有overlay的bounds并fitView |
| 拖拽多边形后保存坐标是旧位置 | dragend事件未同步item坐标 | 统一监听dragend同步坐标数据 |
| 连续绘制时出现重复图形 | MouseTool未关闭,draw事件重复绑定 | startDraw前close,off旧事件再on |
| 圆型半径控制点在北方不准 | 直接用纬度换算距离,未考虑投影 | 用圆的bounds顶点做手柄或使用GeometryUtil |
| 高德坐标和GPS坐标对不上 | 不同坐标系之间未经转换 | 明确约定位数据和后端存储的坐标系,必要时先转换再显示 |
| 视频弹窗内地图高度不对 | 弹窗动画还没结束就init | 延迟或监听弹窗动画结束后resize() |
最后说个小技巧。列表里地块名称重名或数据较多时,我习惯把地块ID当成extData塞进覆盖物实例中,后期做点击命中统计、重新加载恢复时都不需要额外维护映射表。高德覆盖物的extData属性就是专门干这个的,顶层结构简单,传递也方便。
如果你也要从零做类似的地块编辑后台,别急着堆功能。先把“绘制-选中-编辑-保存-还原”这条最小闭环跑通,再逐步加多样式、批量删除、导入导出。点击穿帮、坐标偏差、数组引用这些坑,大概率还是会在实际测试里遇到,但上面这些经验能让你排查时快很多。
