一、前言
GIS开发过程中,经常需要绘制marker,这些marker很大概率会有坐标相同导致的叠加问题,这种情况下会降低使用体验感。所以我们可以将叠加的marker的popup做一个分页效果,可以切换显示的marker。
二、技术要点
我们以leaflet为例,我们可以使用leaflet的popup展示marker的详细信息,简单的信息展示我们可以直接拼接html字符串就能解决,但是我们需要切换信息,这样就会涉及到dom的事件监听还有信息内容的动态更新,如果用js+html这样的方式实现起来非常复杂,于是我们看看能不能直接用vue3的组件作为popup的content,bindPopup(<String|HTMLElement|Function|Popup>content,<Popup options>options?) 这个是marker绑定popup的函数,可以看到可以传HTMLElement、String也可以是一个Function,于是可以写一个Function返回Vue3的组件$el。
三、步骤
1、创建Popup的内容Vue3组件
/**
* WarningSignalPopup.vue 地图marker的popup内容组件
* @Author ZhangJun
* @Date 2025/3/14 9:37
**/
<template>
<div class="w-[300px]">
<div>
<div class="font-bold text-lg mb-2">{{ currentMarkerContent?.headline }}</div>
<div class="flex items-start gap-2 mb-2">
<img class="h-[65px] w-min-[60px]" :src="currentMarkerContent?.iconUrl" :alt="levelNames[currentMarkerContent?.severity]" />
<div>
<div>预警等级:{{ levelNames[currentMarkerContent?.severity] }}</div>
<div>所属区域:{{ currentMarkerContent?.regionName }}</div>
<div>发布时间:{{ currentMarkerContent?.sendtime }}</div>
<div>发布单位:{{ currentMarkerContent?.sender }}</div>
</div>
</div>
<div class="w-full">
{{ currentMarkerContent?.description }}
</div>
</div>
<el-space size="8px" class="mt-4" v-show="popupContentList.length > 1">
<el-button type="primary" size="small" @click="handleClickPrev" :icon="ArrowLeft"></el-button>
<div>{{ markerIndex + 1 }} / {{ popupContentList.length }}</div>
<el-button type="primary" size="small" @click="handleClickNext" :icon="ArrowRight"></el-button>
</el-space>
</div>
</template>
<script setup>
import { ref, defineExpose, watchEffect, onUnmounted } from 'vue'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
//marker的popup内容列表
let popupContentList = ref([])
//marker对象
let targetMarker = ref(null)
//当前marker的索引
let markerIndex = ref(0)
const levelNames = {
Blue: '蓝色',
Yellow: '黄色',
Orange: '橙色',
Red: '红色',
}
//当前需要显示的marker内容
let currentMarkerContent = ref({})
/**
* 上一个
*/
const handleClickPrev = () => {
if (markerIndex.value > 0) {
markerIndex.value = (markerIndex.value - 1) % popupContentList.value.length
}
}
/**
* 下一个
*/
const handleClickNext = () => {
if (markerIndex.value < popupContentList.value.length - 1) {
markerIndex.value = (markerIndex.value + 1) % popupContentList.value.length
}
}
/**
* 重置
*/
const reset = () => {
popupContentList.value = []
targetMarker.value = null
markerIndex.value = 0
currentMarkerContent.value = {}
}
//这里就是将marker的icon更新,跟popup显示的image一致
watchEffect(() => {
//marker不能为null
if (targetMarker.value) {
//获取marker要显示的popup内容
currentMarkerContent.value = popupContentList.value?.[markerIndex.value]
//获取popup内容的标记图片url,这里跟marker的iconUrl是一样的地址
let iconUrl = currentMarkerContent.value?.iconUrl
//得到marker的icon对象
let icon = targetMarker.value?.getIcon()
//判断iconUrl是否与marker的iconUrl不同,如果不同,则更新marker的iconUrl
if (iconUrl && iconUrl !== icon.options.iconUrl) {
icon.options.iconUrl = iconUrl
targetMarker.value.setIcon(icon)
}
}
})
defineExpose({
targetMarker,
popupContentList,
markerIndex,
reset,
})
</script>
<style scoped lang="scss"></style>
2、用于生成marker的hook
/**
* @ClassName UseWarningSignal.js
* @Description 预警信号展示hook
* @Author ZhangJun
* @Date 2025/3/12 17:36
**/
import { ref } from 'vue'
import { getAction } from '@/utils/manage'
import moment from 'moment'
/**
* 预警信号hook
* @param warningSignalPopupRef 预警信号popup弹窗实例
* @returns {{getWarningSignalList: getWarningSignalList, warningSignalList: Ref<UnwrapRef<[]>, UnwrapRef<[]> | []>}}
*/
export function useWarningSignal(warningSignalPopupRef) {
//预警信号数据列表
let warningSignalList = ref([])
let warningIconLayer = null
/**
* 获取预警信号列表
* @param timeKey 时间标识
*/
const getWarningSignalList = (timeKey = moment().format('YYYY-MM-DD')) => {
//todo: 这里应该是从后端获取数据,这里只是模拟数据
let endTime = moment('2025-01-05')
let startTime = endTime.clone().subtract(2, 'days').format('YYYY-MM-DD')
getAction('productInfo/getProductInfoTxt', { startTime, endTime: endTime.format('YYYY-MM-DD') }).then(res => {
res[1].severity = 'Red'
warningSignalList.value = res
drawWarningSignalMarkers(res)
})
}
/**
* 绘制预警信号标记
* @param warningSignalList
*/
const drawWarningSignalMarkers = (warningSignalList = warningSignalList.value) => {
//因为返回的数据里面没有灾害名称,所以只能这种操作
let districtTypeList = ['冰雹', '台风', '大雾', '大风', '寒潮', '山洪', '干旱', '暴雨', '暴雪', '森林火险', '道路结冰', '雷电', '雷雨大风', '霜冻', '高温']
let warningLevelList = ['', 'Blue', 'Yellow', 'Orange', 'Red']
//先清除之前的标记
if (warningIconLayer) {
warningIconLayer.clearLayers()
}
//没有行政区数据,所以只能通过行政区名称来获取
wizMap.getJson('/json/area.json', data => {
let { features } = data || {}
let allRegionData = {}
features?.forEach(
({
properties: {
name,
center: [lon, lat],
},
}) => {
name = name.replace('省', '').replace('市', '').replace('区', '').replace('县', '')
allRegionData[name] = { lon, lat }
},
)
let markersDict = {}
warningSignalList.forEach(item => {
let { headline, description, sender, sendtime, severity } = item
let distressType = districtTypeList.find(name => headline?.includes(name))
let level = warningLevelList.findIndex(val => val === severity)
let regionName = ''
if (sender.includes('县')) {
regionName = sender.split('县')?.[0]
} else if (sender.includes('区')) {
regionName = sender.split('区')?.[0]
} else if (sender.includes('市')) {
regionName = sender.split('市')?.[0]
} else if (sender.includes('省')) {
regionName = sender.split('省')?.[0]
}
//由于没有行政区的数据,所以只能通过行政区名称来判断
if (regionName) {
let iconUrl = `/icons/warningIcon/${distressType}/${level}.png`
let myIcon = L.icon({
iconUrl: iconUrl,
iconSize: [44 * 1.5, 36 * 1.5],
iconAnchor: [22 * 1.5, 18 * 1.5],
})
//坐标
let { lat, lon } = allRegionData[regionName]
let marker = L.marker([lat, lon], { icon: myIcon })
let markerIndex = markersDict?.[regionName]?.length - 1
//提示框内容,保存到markersDict中
marker.attributes = {
...item,
iconUrl,
}
//添加到图标集合中
if (!markersDict?.[regionName]) {
markersDict[regionName] = [marker]
} else {
markersDict[regionName]?.push(marker)
}
}
})
let markers = Object.entries(markersDict).map(([key, markerObjects]) => {
let [markerObject] = markerObjects
markerObject
.bindPopup(
e => {
//先重置popup内容
warningSignalPopupRef.value.reset()
//设置图标对象
warningSignalPopupRef.value.targetMarker = markerObject
//设置弹窗内容列表
warningSignalPopupRef.value.popupContentList = markerObjects.map(({ attributes }) => attributes)
//返回弹窗组件实例,这里是vue组件实例,所以需要用$el来获取dom
return warningSignalPopupRef.value.$el
},
{ maxWidth: 300 },
)
.on('popupclose', () => {
//关闭弹窗时,清除图标对象
warningSignalPopupRef.value.markerIndex = 0
})
return markerObject
})
if (markers.length) {
warningIconLayer = L.layerGroup(markers).addTo(window.wizMap.map)
}
})
}
getWarningSignalList()
return {
warningSignalList,
getWarningSignalList,
}
}
代码还有可以优化的地方,受数据条件的影响,获取marker坐标时只能通过json文件查询后填充。