From bfdf57cd72b97c066b58128d86e7629b642a23aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Fri, 5 Jun 2026 19:12:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9C=B0=E5=9B=BE=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E9=97=AA=E7=83=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- meshmap_frontend/src/components/MeshMap.vue | 76 +++++++++++++++++---- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/meshmap_frontend/src/components/MeshMap.vue b/meshmap_frontend/src/components/MeshMap.vue index 7d72181..fa43bf9 100644 --- a/meshmap_frontend/src/components/MeshMap.vue +++ b/meshmap_frontend/src/components/MeshMap.vue @@ -30,6 +30,7 @@ const menuY = ref(0) const lastRaisedNodeId = ref(null) let map: L.Map | null = null let markerLayer: L.LayerGroup | null = null +const markersByKey = new Map() let hasFitBounds = false const minMapZoom = 3 @@ -77,6 +78,7 @@ onBeforeUnmount(() => { map?.remove() map = null markerLayer = null + markersByKey.clear() }) watch( @@ -145,30 +147,60 @@ function renderMarkers(forceFit: boolean) { if (!map || !markerLayer) { return } - markerLayer.clearLayers() const bounds = L.latLngBounds([]) + const visibleMarkerKeys = new Set() for (const item of props.items) { + const markerKey = mapMarkerKey(item) + visibleMarkerKeys.add(markerKey) + if (item.type === 'cluster') { - const marker = buildClusterMarker(item) - marker.addTo(markerLayer) + const existingMarker = markersByKey.get(markerKey) + if (!existingMarker) { + const marker = buildClusterMarker(item) + marker.addTo(markerLayer) + markersByKey.set(markerKey, marker) + } bounds.extend([item.latitude, item.longitude]) continue } + const node = item const selected = node.node_id === props.selectedNodeId const raised = selected || node.node_id === lastRaisedNodeId.value - const marker = L.marker([node.latitude, node.longitude], { - icon: L.divIcon({ - className: `node-marker${selected ? ' selected' : ''}`, - html: `${escapeHTML(node.label || 'N')}`, - iconSize: [34, 22], - iconAnchor: [17, 11], - }), - title: node.label, - zIndexOffset: raised ? 1000 : 0, + const nodeIcon = L.divIcon({ + className: `node-marker${selected ? ' selected' : ''}`, + html: `${escapeHTML(node.label || 'N')}`, + iconSize: [34, 22], + iconAnchor: [17, 11], }) - marker.bindPopup(buildNodePopupHTML(node), { maxWidth: 320, className: 'node-detail-popup' }) + let marker = markersByKey.get(markerKey) + + if (!marker) { + marker = L.marker([node.latitude, node.longitude], { + icon: nodeIcon, + title: node.label, + zIndexOffset: raised ? 1000 : 0, + }) + marker.bindPopup(buildNodePopupHTML(node), { maxWidth: 320, className: 'node-detail-popup' }) + marker.addTo(markerLayer) + markersByKey.set(markerKey, marker) + } else { + marker.setLatLng([node.latitude, node.longitude]) + marker.setIcon(nodeIcon) + marker.setZIndexOffset(raised ? 1000 : 0) + marker.options.title = node.label + marker.getElement()?.setAttribute('title', node.label) + const popup = marker.getPopup() + if (popup) { + popup.setContent(buildNodePopupHTML(node)) + } else { + marker.bindPopup(buildNodePopupHTML(node), { maxWidth: 320, className: 'node-detail-popup' }) + } + } + + marker.off('click') + marker.off('contextmenu') marker.on('click', (event) => { L.DomEvent.stopPropagation(event) lastRaisedNodeId.value = node.node_id @@ -176,19 +208,33 @@ function renderMarkers(forceFit: boolean) { emit('select-node', node.node_id) }) marker.on('contextmenu', (event) => openNodeMenu(node, event)) - marker.addTo(markerLayer) - if (selected) { + + if (selected && !marker.getPopup()?.isOpen()) { marker.openPopup() } bounds.extend([node.latitude, node.longitude]) } + for (const [markerKey, marker] of markersByKey) { + if (!visibleMarkerKeys.has(markerKey)) { + markerLayer.removeLayer(marker) + markersByKey.delete(markerKey) + } + } + if (props.autoFit && props.items.length > 0 && (forceFit || !hasFitBounds)) { map.fitBounds(bounds, { padding: [24, 24], maxZoom: 13 }) hasFitBounds = true } } +function mapMarkerKey(item: MapRenderable): string { + if (item.type === 'cluster') { + return `cluster:${item.latitude}:${item.longitude}:${item.count}` + } + return `node:${item.node_id}` +} + function buildClusterMarker(cluster: MapClusterNode): L.Marker { const size = clusterIconSize(cluster.count) const marker = L.marker([cluster.latitude, cluster.longitude], {