From e6a8826a8f100d76a71dff890d48e3be66536904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=A0=E9=97=BB=E9=A3=8E?= Date: Tue, 16 Jun 2026 19:45:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=98=BE=E7=A4=BA=E8=BD=A8=E8=BF=B9=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E5=9D=90=E6=A0=87=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- .../src/components/NodeTrajectoryMap.vue | 61 +++++++++++++++++-- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/meshmap_frontend/src/components/NodeTrajectoryMap.vue b/meshmap_frontend/src/components/NodeTrajectoryMap.vue index 27761d1..864dee8 100644 --- a/meshmap_frontend/src/components/NodeTrajectoryMap.vue +++ b/meshmap_frontend/src/components/NodeTrajectoryMap.vue @@ -40,15 +40,58 @@ function applyTileLayer() { }).addTo(map) } +function pointStyle(isStart: boolean, isEnd: boolean): L.CircleMarkerOptions { + if (isStart) { + return { radius: 7, color: '#7f9183', fillColor: '#9aaa95', fillOpacity: 0.88, weight: 2 } + } + if (isEnd) { + return { radius: 7, color: '#b4877f', fillColor: '#c59b93', fillOpacity: 0.88, weight: 2 } + } + return { radius: 3, color: '#7d8f9a', fillColor: '#9ab3c2', fillOpacity: 0.72, weight: 1 } +} + +function buildPositionPopupHTML(position: PositionRecord, isStart: boolean, isEnd: boolean): string { + const title = isStart ? '起点' : isEnd ? '终点' : '轨迹点' + const latitude = position.latitude == null ? '-' : position.latitude.toFixed(6) + const longitude = position.longitude == null ? '-' : position.longitude.toFixed(6) + const altitude = position.altitude == null ? '-' : `${position.altitude} m` + const time = new Date(position.created_at).toLocaleString() + return ` +
+ ${escapeHTML(title)} +
+
纬度
${latitude}
+
经度
${longitude}
+
海拔
${altitude}
+
时间
${escapeHTML(time)}
+
+
+ ` +} + +function escapeHTML(value: string): string { + return value.replace(/[&<>'"]/g, (char) => { + const entities: Record = { + '&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"', + } + return entities[char] + }) +} + function renderTrajectory() { if (!map || !layer) { return } - layer.clearLayers() - const points = [...props.positions] + const trajectoryLayer = layer + trajectoryLayer.clearLayers() + const positions = [...props.positions] .filter((position) => position.latitude != null && position.longitude != null) .reverse() - .map((position) => [position.latitude as number, position.longitude as number] as L.LatLngTuple) + const points = positions.map((position) => [position.latitude as number, position.longitude as number] as L.LatLngTuple) if (points.length === 0) { map.setView([0, 0], 2) @@ -56,10 +99,16 @@ function renderTrajectory() { } if (points.length > 1) { - L.polyline(points, { color: '#7d8f9a', weight: 4, opacity: 0.78 }).addTo(layer) + L.polyline(points, { color: '#7d8f9a', weight: 4, opacity: 0.78 }).addTo(trajectoryLayer) } - L.circleMarker(points[0], { radius: 6, color: '#7f9183', fillColor: '#9aaa95', fillOpacity: 0.88 }).bindPopup('起点').addTo(layer) - L.circleMarker(points[points.length - 1], { radius: 6, color: '#b4877f', fillColor: '#c59b93', fillOpacity: 0.88 }).bindPopup('终点').addTo(layer) + + positions.forEach((position, index) => { + const isStart = index === 0 + const isEnd = index === positions.length - 1 + L.circleMarker(points[index], pointStyle(isStart, isEnd)) + .bindPopup(buildPositionPopupHTML(position, isStart, isEnd), { maxWidth: 280, className: 'trajectory-point-popup' }) + .addTo(trajectoryLayer) + }) map.fitBounds(L.latLngBounds(points), { padding: [24, 24], maxZoom: 14 }) }