1. 项目概述
这个基于Vue.js和AMap地图组件开发的航线管理系统,是我最近完成的一个实际项目。系统主要实现了两个核心功能:航线管理和围栏管理。在开发过程中,我发现同时展示折线图和多边形矢量图在地图上是一个常见但又有一定技术挑战的需求,特别是在需要保持两者独立操作的情况下。
系统采用了现代化的前端技术栈,包括Vue 3的组合式API、TypeScript类型系统,以及AMap地图SDK。界面设计采用了暗色主题,与地图的卫星图层风格相得益彰。最让我满意的是系统实现了航点与围栏的完全独立管理,两者可以同时显示但互不干扰,这在无人机航线规划等场景中非常实用。
2. 技术架构与核心设计
2.1 技术选型解析
选择Vue 3作为前端框架主要考虑其优秀的响应式系统和组合式API,这对于地图应用的状态管理特别重要。AMap作为国内领先的地图服务,提供了丰富的API和稳定的服务,特别适合需要展示复杂地理信息的应用。
TypeScript的引入大大提升了代码的可靠性,特别是在处理经纬度坐标、地图覆盖物等复杂数据结构时,类型检查可以避免很多低级错误。Element UI虽然在这个项目中用得不多,但它的基础组件为快速搭建UI提供了便利。
2.2 核心数据结构设计
系统中最关键的两个数据结构是航点信息和围栏信息:
typescript复制// 航点数据结构
interface Waypoint {
lng: number;
lat: number;
longitude?: number; // 兼容不同命名
latitude?: number;
height: number;
waypoint_speed: number;
gimbal_pitch_angle: number;
filesuffix: string;
action_group: Array<{
action: Array<{
actionActuatorFunc: string;
}>;
}>;
}
// 围栏数据结构
interface FenceFormState {
id: string;
name: string;
desc: string;
resource: {
type: number;
content: {
type: string;
properties: {
color: string;
clampToGround: boolean;
};
geometry: {
type: 'Polygon' | 'Circle';
coordinates: number[][][] | number[];
radius: number;
};
};
};
}
这种设计充分考虑了业务需求,航点信息包含了飞行动作组,围栏信息则区分了多边形和圆形两种类型,为后续功能扩展预留了空间。
3. 核心功能实现细节
3.1 地图初始化与基础配置
地图初始化是整个系统的基础,我们采用了卫星图层和路网图层的组合:
typescript复制const init = () => {
workSpacesCurrent({}).then(res => {
map.value = new AMap.Map('g-container', {
resizeEnable: true,
center: res.data.longitude ? [res.data.longitude, res.data.latitude] : [118.830601, 32.324487],
layers: [
new AMap.TileLayer.Satellite(),
new AMap.TileLayer.RoadNet(),
],
zoom: 14,
zooms: [0, 22],
});
initRouteFromProps();
initFenceFromProps();
});
};
这里有几个关键点:
resizeEnable: true确保地图在容器尺寸变化时能自适应- 默认中心点设置了后备值,避免空数据导致的问题
- 同时加载卫星图和路网图,既美观又实用
- 缩放范围设为0-22,覆盖从全球视图到街道级别的所有缩放需求
3.2 航点与航线管理实现
航点管理采用了自定义标记(Marker)和折线(Polyline)的组合:
typescript复制const inMake = (length: any) => {
// 清除旧标记
markers.value.forEach((marker: any) => {
marker.off('click');
map.value.remove(marker);
});
markers.value = [];
// 创建新标记
table.value.ruleForm.from.forEach((i: any, index) => {
const markerContent = `<div class="red-circle"><span>${index + 1}</span></div>`;
const position = [i.lng || i.longitude, i.lat || i.latitude];
const marker = new AMap.Marker({
position,
content: markerContent,
offset: new AMap.Pixel(-19, -49),
draggable: false,
});
// 点击事件处理
marker.on('click', function() {
const markerIndex = markers.value.indexOf(this);
if(markerIndex !== -1) {
updateActiveMarker(markerIndex);
showWaypointDetails(markerIndex);
}
});
map.value.add(marker);
markers.value.push(marker);
});
if(isMaker.value) {
map.value.setFitView();
isMaker.value = false;
}
};
航线绘制则相对简单,但要注意性能优化:
typescript复制const inLine = (map: any) => {
if(polyline.value) {
polyline.value.setMap(null);
polyline.value = null;
}
polyline.value = new AMap.Polyline({
path: table.value.ruleForm.LngLat,
isOutline: true,
outlineColor: '#ffeeff',
borderWeight: 3,
strokeColor: '#3366FF',
strokeOpacity: 1,
strokeWeight: 6,
strokeStyle: 'solid',
lineJoin: 'round',
lineCap: 'round',
zIndex: 50,
draggable: false,
});
polyline.value.setMap(map);
};
提示:在频繁更新航线时,一定要先移除旧的Polyline对象,否则会导致内存泄漏和性能下降。
3.3 电子围栏管理实现
围栏管理支持两种类型:矩形和圆形。矩形围栏的实现较为复杂,需要计算四个顶点:
typescript复制const createRectangleFence = () => {
clearMapFence();
if(!map.value) return;
const mapCenter = map.value.getCenter();
squareVertices.value = calculateSquareVertices(mapCenter.lat, mapCenter.lng, CONSTANTS.FENCE_SIDE_LENGTH);
renderRectangleFence();
};
const calculateSquareVertices = (centerLat: number, centerLng: number, sideLength: number): FenceVertex[] => {
const deltaLat = (sideLength / 111300) * 1.1;
const deltaLng = (sideLength / (111300 * Math.cos(centerLat * Math.PI / 180))) * 1.1;
return [
{ lat: centerLat + deltaLat, lng: centerLng - deltaLng },
{ lat: centerLat - deltaLat, lng: centerLng - deltaLng },
{ lat: centerLat - deltaLat, lng: centerLng + deltaLng },
{ lat: centerLat + deltaLat, lng: centerLng + deltaLng }
];
};
圆形围栏的实现相对简单,但要注意半径的单位是米:
typescript复制const createCircleFence = () => {
clearMapFence();
if(!map.value) return;
const mapCenter = map.value.getCenter();
const initialCenter: FenceVertex = { lat: mapCenter.lat, lng: mapCenter.lng };
currentFence.value = { instance: null, type: 'circle' };
updateCircleFence(initialCenter);
};
const updateCircleFence = (newCenter: FenceVertex, customRadius?: number) => {
if(!map.value) return;
if(currentFence.value.instance) {
map.value.remove(currentFence.value.instance);
}
const circleRadius = customRadius || CONSTANTS.FENCE_CIRCLE_RADIUS;
circleCenter.value = newCenter;
const newCircle = new AMap.Circle({
center: [newCenter.lng, newCenter.lat],
radius: circleRadius,
...CONSTANTS.CIRCLE_STYLE
});
map.value.add(newCircle);
currentFence.value = { instance: newCircle, type: 'circle' };
};
4. 关键技术与经验分享
4.1 性能优化实践
在地图应用中,性能优化至关重要。以下是几个有效的优化策略:
- 标记聚合:当航点数量过多时(超过50个),应考虑使用标记聚合(MarkerCluster)技术
- 事件解绑:在移除标记前一定要解绑事件,避免内存泄漏
- 节流处理:对地图的zoomend、moveend等频繁触发的事件应进行节流处理
- 图层管理:合理使用map.add和map.remove,避免不必要的图层重绘
4.2 响应式数据同步
系统采用了watch来监听props变化,实现数据的动态更新:
typescript复制// 航线props变化 → 只更新航线
watch(() => props.messageFather, (newVal) => {
if(newVal && map.value) {
initRouteFromProps();
}
}, { immediate: true, deep: true });
// 围栏props变化 → 只更新围栏
watch(() => props.fenceMessage, (newVal) => {
if(newVal && map.value) {
initFenceFromProps();
}
}, { immediate: true, deep: true });
这种设计保证了父组件数据变化时,地图展示能及时更新,同时避免了不必要的全量刷新。
4.3 自定义样式技巧
航点标记采用了自定义HTML内容,通过CSS实现了独特的视觉效果:
css复制.red-circle {
width: 39px;
height: 52px;
background-image: url("data:image/png;base64,...");
/* 其他样式 */
}
.red-circle-active {
/* 激活状态样式 */
}
这种方式的优点是:
- 完全自定义外观,不受地图SDK默认样式的限制
- 使用Base64编码图片,避免额外的HTTP请求
- 可以通过CSS轻松实现各种状态效果
5. 常见问题与解决方案
5.1 地图加载问题
问题:有时地图无法加载或显示空白
解决方案:
- 检查AMap JS API的加载顺序,确保在Vue组件挂载前完成加载
- 确认容器元素存在且尺寸正确
- 添加错误处理逻辑,如初始化失败后重试
5.2 内存泄漏问题
问题:长时间使用后页面变卡顿
解决方案:
- 在组件卸载时正确清理地图资源
- 使用WeakMap存储标记引用
- 定期检查内存使用情况
typescript复制onUnmounted(() => {
if(map.value) {
markers.value.forEach(m => {
m.off('click');
map.value.remove(m);
});
markers.value = [];
if(polyline.value) map.value.remove(polyline.value);
clearMapFence();
map.value.destroy();
map.value = null;
}
});
5.3 坐标转换问题
问题:不同数据源的坐标格式不一致
解决方案:
- 统一内部使用[lng, lat]格式
- 在数据入口处进行格式转换
- 添加类型守卫函数确保数据正确性
typescript复制function normalizePosition(pos: any): [number, number] {
if(Array.isArray(pos)) {
return [pos[0], pos[1]];
}
if(pos.lng !== undefined && pos.lat !== undefined) {
return [pos.lng, pos.lat];
}
if(pos.longitude !== undefined && pos.latitude !== undefined) {
return [pos.longitude, pos.latitude];
}
throw new Error('Invalid position format');
}
6. 扩展与优化方向
在实际使用中,我发现这个系统还有几个可以进一步优化的方向:
- 撤销/重做功能:实现操作历史记录,方便用户回退
- 围栏碰撞检测:在添加航点时自动检测是否与围栏冲突
- 导入导出:支持标准格式如KML、GPX的导入导出
- 性能监控:添加性能指标收集和分析功能
- 3D视图:集成AMap的3D功能,提供更直观的展示
在实现这些扩展功能时,建议采用插件化架构,保持核心功能的简洁性,通过扩展点来增加新功能。