From 794dfadf25327ed83f87fb63bacf6d86c80563d3 Mon Sep 17 00:00:00 2001
From: 张含笑 <zhx18749296735@163.com>
Date: Tue, 27 Jan 2026 14:51:50 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master'

---
 applications/drone-command/src/components/map-container/device-map-container.vue |  123 +++++++++++++++++++++++++++++++++++++---
 1 files changed, 113 insertions(+), 10 deletions(-)

diff --git a/applications/drone-command/src/components/map-container/device-map-container.vue b/applications/drone-command/src/components/map-container/device-map-container.vue
index a4311a4..f7fb7e9 100644
--- a/applications/drone-command/src/components/map-container/device-map-container.vue
+++ b/applications/drone-command/src/components/map-container/device-map-container.vue
@@ -119,6 +119,14 @@
 let droneTrackTickHandler = null
 let droneTrackStartTime = null
 let droneTrackRuntime = []
+let favoritePulseRafId = null
+let favoritePulseStartAt = 0
+let favoritePulseElapsed = 0
+const favoritePulseEntities = []
+const pulseBaseColor = Cesium.Color.fromCssColorString('#FF3B30')
+const PULSE_MIN_RADIUS_M = 60
+const PULSE_MAX_RADIUS_M = 180
+const PULSE_DURATION_S = 1.6
 const detailVisible = ref(true)
 const clusterVisible = ref(false)
 const countyCenterMap = new Map()
@@ -172,6 +180,57 @@
  */
 
 const adminBoundaryVisible = ref(treeCheckedKeys.value.includes('admin'))
+
+const isFavorited = item => {
+	const value = item?.favorited ?? item?.isFavorite
+	return value === 1 || value === '1' || value === true
+}
+
+let pulseCanvas = null
+const getPulseCanvas = () => {
+	if (pulseCanvas) return pulseCanvas
+	const size = 256
+	const canvas = document.createElement('canvas')
+	canvas.width = size
+	canvas.height = size
+	const ctx = canvas.getContext('2d')
+	const center = size / 2
+	const radius = center - 2
+	const gradient = ctx.createRadialGradient(center, center, radius * 0.1, center, center, radius)
+	gradient.addColorStop(0, 'rgba(255,59,48,0.55)')
+	gradient.addColorStop(0.35, 'rgba(255,59,48,0.35)')
+	gradient.addColorStop(0.7, 'rgba(255,59,48,0.15)')
+	gradient.addColorStop(1, 'rgba(255,59,48,0)')
+	ctx.fillStyle = gradient
+	ctx.beginPath()
+	ctx.arc(center, center, radius, 0, Math.PI * 2)
+	ctx.fill()
+	pulseCanvas = canvas
+	return canvas
+}
+
+const createFavoritePulseEntity = (billboard) => {
+	if (!viewer || !billboard) return
+	const material = new Cesium.ImageMaterialProperty({
+		image: getPulseCanvas(),
+		transparent: true,
+	})
+	const pulse = {
+		material,
+		offset: Math.random() * PULSE_DURATION_S,
+		entity: null,
+	}
+	pulse.entity = viewer.entities.add({
+		position: new Cesium.CallbackProperty(() => billboard.position, false),
+		ellipse: {
+			semiMajorAxis: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
+			semiMinorAxis: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
+			material,
+			heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+		},
+	})
+	favoritePulseEntities.push(pulse)
+}
 
 const getDevicePosition = item => {
 	const longitudeRaw = item.longitude ?? item.lng ?? item.lon
@@ -311,6 +370,12 @@
 	}
 }
 
+const getPulseRadius = pulse => {
+	const phase = ((favoritePulseElapsed + pulse.offset) % PULSE_DURATION_S) / PULSE_DURATION_S
+	const wave = phase < 0.5 ? phase * 2 : (1 - phase) * 2
+	return PULSE_MIN_RADIUS_M + (PULSE_MAX_RADIUS_M - PULSE_MIN_RADIUS_M) * wave
+}
+
 const setDetailVisibility = visible => {
 	detailVisible.value = visible
 	if (defenseZonePrimitive) defenseZonePrimitive.show = visible
@@ -345,6 +410,11 @@
 const setDroneVisibility = visible => {
 	if (droneTrackBillboardCollection) droneTrackBillboardCollection.show = visible
 	if (droneTrackPolylineCollection) droneTrackPolylineCollection.show = visible
+	if (favoritePulseEntities.length) {
+		favoritePulseEntities.forEach(pulse => {
+			if (pulse?.entity) pulse.entity.show = visible
+		})
+	}
 	if (!visible && selectedTargetType.value === 'drone') {
 		closePopup()
 	}
@@ -453,6 +523,7 @@
 const clearDroneTrackEntities = () => {
 	if (!viewer) return
 	stopDroneTrackAnimation()
+	stopFavoritePulseAnimation()
 	if (droneTrackBillboardCollection) {
 		if (!droneTrackBillboardCollection.isDestroyed?.()) {
 			removeCockpitPrimitive(droneTrackBillboardCollection)
@@ -466,6 +537,12 @@
 		droneTrackPolylineCollection = null
 	}
 	droneTrackRuntime = []
+	if (favoritePulseEntities.length) {
+		favoritePulseEntities.forEach(pulse => {
+			if (pulse?.entity) viewer?.entities?.remove(pulse.entity)
+		})
+		favoritePulseEntities.length = 0
+	}
 	dronePickMap.clear()
 	if (selectedTargetType.value === 'drone') {
 		closePopup()
@@ -532,6 +609,28 @@
 	droneTrackTickHandler = null
 }
 
+const startFavoritePulseAnimation = () => {
+	if (!viewer || favoritePulseRafId) return
+	favoritePulseStartAt = performance.now()
+	const tick = now => {
+		if (!viewer || viewer.isDestroyed?.() || !favoritePulseEntities.length) {
+			favoritePulseRafId = null
+			return
+		}
+		favoritePulseElapsed = (now - favoritePulseStartAt) / 1000
+		viewer.scene.requestRender()
+		favoritePulseRafId = requestAnimationFrame(tick)
+	}
+	favoritePulseRafId = requestAnimationFrame(tick)
+}
+
+const stopFavoritePulseAnimation = () => {
+	if (favoritePulseRafId) {
+		cancelAnimationFrame(favoritePulseRafId)
+		favoritePulseRafId = null
+	}
+}
+
 const renderSimulatedDroneTrack = (list) => {
 	if (!viewer) return
 	clearDroneTrackEntities()
@@ -576,16 +675,19 @@
 			})
 			billboard.id = droneId
 			const speedMs = Math.round(Cesium.Cartesian3.distance(positions[0], positions[1]) / segmentDuration)
-			dronePickMap.set(droneId, {
-				data: {
-					...item,
-					flightHeightM: item.flightHeightM ?? points[0].height,
-					flightSpeedMs: item.flightSpeedMs ?? speedMs,
-					longitude: item.longitude ?? points[0].longitude,
-					latitude: item.latitude ?? points[0].latitude,
-				},
-				billboard,
-			})
+		dronePickMap.set(droneId, {
+			data: {
+				...item,
+				flightHeightM: item.flightHeightM ?? points[0].height,
+				flightSpeedMs: item.flightSpeedMs ?? speedMs,
+				longitude: item.longitude ?? points[0].longitude,
+				latitude: item.latitude ?? points[0].latitude,
+			},
+			billboard,
+		})
+		if (isFavorited(item)) {
+			createFavoritePulseEntity(billboard)
+		}
 			droneTrackRuntime.push({
 				positions,
 				billboard,
@@ -595,6 +697,7 @@
 		})
 	viewer.clock.multiplier = 1
 	startDroneTrackAnimation()
+	startFavoritePulseAnimation()
 	reorderCockpitPrimitives()
 }
 

--
Gitblit v1.9.3