Vue项目中天地图动态标注的添加与删除实践
1. 天地图与Vue结合的基础准备
在Vue项目中使用天地图API前,需要先完成基础的环境配置。我推荐使用npm安装天地图JavaScript API的方式,这样能更好地与现代前端工程化开发流程结合。首先在项目中执行:
npm install tdt-map安装完成后,在main.js中全局引入天地图资源。这里有个小技巧,建议把天地图密钥放在环境变量中管理:
import T from 'tdt-map' import '../node_modules/tdt-map/dist/tdt.min.css' Vue.prototype.$T = T Vue.prototype.$tdtKey = process.env.VUE_APP_TDT_KEY初始化地图组件时,我习惯把地图实例挂载到Vue实例上,方便全局调用。在组件mounted钩子中这样写:
mounted() { this.map = new this.$T.Map('mapContainer', { center: new this.$T.LngLat(116.404, 39.915), zoom: 11, minZoom: 5, maxZoom: 18 }) }这里有个容易踩的坑:天地图容器需要设置明确的高度。我建议在CSS中使用vh单位,确保在不同设备上都能正常显示:
#mapContainer { width: 100%; height: 80vh; margin: 0; padding: 0; }2. 动态标注的创建与管理
2.1 标注的批量创建
实际项目中,我们通常需要批量添加标注点。我封装了一个可复用的方法,支持自定义图标和点击事件:
addMarkers(points, iconConfig, clickHandler) { const markers = [] points.forEach(item => { const marker = new this.$T.Marker( new this.$T.LngLat(item.lng, item.lat), { icon: new this.$T.Icon({ iconUrl: iconConfig.url, iconSize: new this.$T.Point(iconConfig.width, iconConfig.height) }) } ) marker.customId = item.id // 自定义标识 this.map.addOverLay(marker) if (clickHandler) { marker.addEventListener('click', () => { clickHandler(item, marker) }) } markers.push(marker) }) return markers }使用时可以这样调用:
const hospitals = this.addMarkers( [ {id: 'h1', lng: 116.404, lat: 39.915, name: '协和医院'}, {id: 'h2', lng: 116.428, lat: 39.903, name: '301医院'} ], { url: require('@/assets/hospital.png'), width: 32, height: 32 }, (item, marker) => { this.showHospitalInfo(item) } )2.2 标注的自定义属性
为了后续能精准控制特定标注,我们需要给标注添加自定义属性。除了上面代码中的customId,还可以扩展更多元数据:
marker.metaData = { type: 'hospital', district: '海淀区', level: '三甲' }这样在后续操作时,我们不仅能通过ID查找标注,还能按类型、区域等属性进行筛选。
3. 精准删除标注的实现方案
3.1 基于标识的删除方法
原始文章展示了通过遍历删除的方式,这里我优化了一个更高效的版本:
removeMarkersByIds(ids) { const overlays = this.map.getOverlays() overlays.forEach(overlay => { if (overlay.customId && ids.includes(overlay.customId)) { this.map.removeOverLay(overlay) } }) }这个方法支持批量删除,调用时只需传入要删除的标注ID数组:
// 删除ID为h1和h2的标注 this.removeMarkersByIds(['h1', 'h2'])3.2 基于条件的动态筛选
更灵活的做法是根据条件动态筛选要删除的标注:
removeMarkersByCondition(conditionFn) { const overlays = this.map.getOverlays() overlays.forEach(overlay => { if (conditionFn(overlay)) { this.map.removeOverLay(overlay) } }) }使用示例:
// 删除所有类型为医院的标注 this.removeMarkersByCondition( overlay => overlay.metaData?.type === 'hospital' ) // 删除海淀区的三甲医院 this.removeMarkersByCondition( overlay => overlay.metaData?.district === '海淀区' && overlay.metaData?.level === '三甲' )4. 高级应用:标注的状态管理
4.1 使用Vuex管理标注状态
在复杂项目中,建议使用Vuex集中管理标注状态。首先定义store:
// store/modules/map.js const state = { markers: [], visibleMarkerIds: [] } const mutations = { ADD_MARKERS(state, markers) { state.markers = [...state.markers, ...markers] }, TOGGLE_MARKERS(state, {ids, visible}) { if (visible) { state.visibleMarkerIds = [...new Set([...state.visibleMarkerIds, ...ids])] } else { state.visibleMarkerIds = state.visibleMarkerIds.filter(id => !ids.includes(id)) } } }然后在组件中使用watch监听状态变化:
watch: { 'mapStore/visibleMarkerIds': { handler(newVal, oldVal) { // 找出需要新增的标注 const toAdd = this.mapStore.markers.filter( m => newVal.includes(m.customId) && !oldVal.includes(m.customId) ) // 找出需要删除的标注 const toRemove = oldVal.filter(id => !newVal.includes(id)) // 执行更新 if (toAdd.length) this.addMarkers(toAdd) if (toRemove.length) this.removeMarkersByIds(toRemove) }, deep: true } }4.2 标注的动画效果
为提升用户体验,可以为标注添加显隐动画。这里实现一个淡入淡出效果:
async fadeMarker(marker, show = true, duration = 500) { const step = 20 const delta = 1 / (duration / step) if (show) { marker.setOpacity(0) this.map.addOverLay(marker) for (let op = 0; op <= 1; op += delta) { await new Promise(resolve => setTimeout(resolve, step)) marker.setOpacity(op) } } else { for (let op = 1; op >= 0; op -= delta) { await new Promise(resolve => setTimeout(resolve, step)) marker.setOpacity(op) } this.map.removeOverLay(marker) } }使用时可以这样调用:
// 淡入显示 this.fadeMarker(marker, true) // 淡出隐藏 this.fadeMarker(marker, false)5. 性能优化实践
5.1 标注的聚类显示
当地图缩放级别较小时,可以使用标注聚类来提升性能。先安装聚类插件:
npm install tdt-map-cluster然后在项目中这样使用:
import MarkerCluster from 'tdt-map-cluster' // 创建聚类实例 const cluster = new MarkerCluster(this.map, { gridSize: 80, maxZoom: 15, styles: [{ url: require('@/assets/cluster.png'), size: new this.$T.Point(40, 40), textColor: '#fff' }] }) // 添加标注到聚类管理器 cluster.addMarkers(markers) // 需要删除时 cluster.removeMarkers(markersToRemove)5.2 可视区域优化
当地图标注很多时,可以只渲染可视区域内的标注:
updateVisibleMarkers() { const bounds = this.map.getBounds() this.allMarkers.forEach(marker => { const position = marker.getLngLat() if (bounds.contains(position)) { if (!this.map.getOverlays().includes(marker)) { this.map.addOverLay(marker) } } else { this.map.removeOverLay(marker) } }) } // 监听地图移动事件 this.map.addEventListener('moveend', this.updateVisibleMarkers)6. 常见问题解决方案
在实际开发中,我遇到过几个典型问题值得分享。首先是天地图标注点击事件冒泡问题,当地图上既有标注点击又有地图点击时,会出现事件冲突。解决方案是:
marker.addEventListener('click', e => { e.stopPropagation() // 处理标注点击逻辑 })其次是标注z-index的控制问题。天地图的标注默认是按添加顺序决定叠放层次,如果需要手动控制:
// 将标注置顶 marker.setTop(true) // 设置具体z-index marker.setZIndex(100)还有一个常见问题是内存泄漏。在Vue组件销毁时,务必清理所有标注和事件监听:
beforeDestroy() { // 移除所有标注 this.map.clearOverlays() // 移除事件监听 this.map.removeEventListener('moveend', this.updateVisibleMarkers) // 如果有聚类实例 if (this.cluster) { this.cluster.clearMarkers() } }7. 完整示例代码
最后分享一个完整的Vue单文件组件示例,实现了标注的增删改查:
<template> <div> <div id="mapContainer"></div> <div class="control-panel"> <button @click="addHospitalMarkers">添加医院</button> <button @click="removeAllMarkers">清除所有</button> <div v-for="marker in markers" :key="marker.id"> <input type="checkbox" v-model="marker.visible" @change="toggleMarker(marker)" > {{ marker.name }} </div> </div> </div> </template> <script> export default { data() { return { map: null, markers: [ {id: 'h1', lng: 116.404, lat: 39.915, name: '协和医院', visible: true}, {id: 'h2', lng: 116.428, lat: 39.903, name: '301医院', visible: true} ], markerInstances: {} } }, mounted() { this.initMap() this.addHospitalMarkers() }, methods: { initMap() { this.map = new this.$T.Map('mapContainer', { center: new this.$T.LngLat(116.404, 39.915), zoom: 12 }) }, addHospitalMarkers() { this.markers.forEach(item => { if (item.visible && !this.markerInstances[item.id]) { const marker = new this.$T.Marker( new this.$T.LngLat(item.lng, item.lat), { icon: new this.$T.Icon({ iconUrl: require('@/assets/hospital.png'), iconSize: new this.$T.Point(32, 32) }) } ) marker.customId = item.id this.map.addOverLay(marker) this.markerInstances[item.id] = marker } }) }, toggleMarker(marker) { if (marker.visible) { this.addMarker(marker) } else { this.removeMarker(marker.id) } }, addMarker(markerData) { if (this.markerInstances[markerData.id]) return const marker = new this.$T.Marker( new this.$T.LngLat(markerData.lng, markerData.lat), { icon: new this.$T.Icon({ iconUrl: require('@/assets/hospital.png'), iconSize: new this.$T.Point(32, 32) }) } ) marker.customId = markerData.id this.map.addOverLay(marker) this.markerInstances[markerData.id] = marker }, removeMarker(markerId) { if (!this.markerInstances[markerId]) return this.map.removeOverLay(this.markerInstances[markerId]) delete this.markerInstances[markerId] }, removeAllMarkers() { Object.values(this.markerInstances).forEach(marker => { this.map.removeOverLay(marker) }) this.markerInstances = {} this.markers.forEach(m => m.visible = false) } }, beforeDestroy() { this.map.clearOverlays() } } </script> <style> #mapContainer { width: 100%; height: 80vh; } .control-panel { position: absolute; top: 20px; right: 20px; background: white; padding: 10px; z-index: 1000; } </style>