From 7589cd256fb347f7ff6c86da5dd90ae2dfa4e071 Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Mon, 26 Jan 2026 17:42:16 +0800
Subject: [PATCH] feat:数据驾驶舱

---
 applications/drone-command/src/components/map-container/device-map-container.vue |  990 +++++++++++++++++++++++++++++-----------------------------
 1 files changed, 497 insertions(+), 493 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 1dcdcc8..a8f8566 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
@@ -1,7 +1,7 @@
-<template>
+<template>
 	<div class="map-shell">
 		<CommonCesiumMap ref="mapRef" class="command-cesium map-container" :dom-id="props.containerId" :active="true"
-			:flat-mode="false" :terrain="false" :layer-mode="4" :contour="false" :boundary="false"
+			:flat-mode="false" :terrain="true" :layer-mode="4" :contour="false" :boundary="false"
 			:show-admin-boundary="true" :zoom-to-boundary="true" :enable-stage-emit="true" :cluster-height="100000"
 			@ready="handleMapReady" @stage-change="handleStageChange" />
 		<div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }">
@@ -20,58 +20,23 @@
 			</div>
 		</div>
 
-		<div v-if="popupVisible" class="device-popup" :style="popupStyle" @click.stop>
-			<div class="device-popup__header">
-				<span class="device-popup__title">{{ popupTitle }}</span>
+		<DevicePopup
+			:visible="popupVisible && !isDronePopup"
+			:position="popupPosition"
+			:device="selectedDevice"
+			@close="closePopup"
+		/>
 
-				<img class="device-popup__close" :src="popupClose" alt="" @click.stop="closePopup">
-			</div>
-			<div class="device-popup__type">
-				<span class="value">{{ popupTypeText }}</span>
-			</div>
-
-			<div class="device-popup__subtitle">
-				<span class="device-popup__dot" :class="popupActionClass"></span>
-				<span class="device-popup__action">{{ popupActionText }}</span>
-			</div>
-
-			<div class="device-popup__rows">
-				<div class="device-popup__row">
-					<div class="device-popup__col">
-						<span class="label">状态</span>
-						<span class="value" :class="popupStatusClass">{{ popupStatusText }}</span>
-					</div>
-
-					<span class="divider">|</span>
-
-					<div class="device-popup__col">
-						<span class="label">电量</span>
-						<span class="value">{{ popupBatteryText }}</span>
-					</div>
-				</div>
-				<div class="device-popup__row">
-					<div class="device-popup__col">
-						<span class="label">方位角</span>
-						<span class="value">{{ popupAzimuthText }}</span>
-					</div>
-
-					<span class="divider">|</span>
-
-					<div class="device-popup__col">
-						<span class="label">俯仰角</span>
-						<span class="value">{{ popupElevationText }}</span>
-					</div>
-				</div>
-				<div class="device-popup__row">
-					<span class="label">侦测目标</span>
-					<span class="value">{{ popupDetectCountText }}</span>
-				</div>
-				<div class="device-popup__row">
-					<span class="label">有效范围</span>
-					<span class="value">{{ popupRangeText }}</span>
-				</div>
-			</div>
-		</div>
+		<DronePopup
+			:visible="popupVisible && isDronePopup"
+			:position="popupPosition"
+			:drone="selectedDevice"
+			:favorite="Boolean(selectedDevice?.isFavorite)"
+			@close="closePopup"
+			@toggle-favorite="toggleDroneFavorite"
+			@signal="handleDroneSignal"
+			@counter="handleDroneCounter"
+		/>
 	</div>
 </template>
 
@@ -85,10 +50,19 @@
 import { cockpitAggregationApi } from '@/api/dataCockpit'
 import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
 import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
+import droneIcon from '@/assets/images/dataCockpit/map/drone.png'
 import aggregationIcon from '@/assets/images/dataCockpit/map/aggregation.png'
 import commandPostIcon from '@/assets/images/dataCockpit/map/command-post.png'
-import popupClose from '@/assets/images/dataCockpit/map/popup-close.png'
 import jaGeojsonRaw from '@/assets/geojson/ja.geojson?raw'
+import DevicePopup from './components/DevicePopup.vue'
+import DronePopup from './components/DronePopup.vue'
+import {
+	RING_STYLES,
+	addDeviceRings,
+	createDroneTrackMaterial,
+	createRadialGradientMaterial,
+	getTexturedVertexFormat,
+} from './device-map-materials'
 
 const props = defineProps({
 	onlineDevices: {
@@ -108,24 +82,37 @@
 		default: true,
 	},
 })
+const emit = defineEmits(['droneSignal', 'droneCounter'])
 
 const DEFAULT_ZONE_PAGE_SIZE = 999
 
 const mapRef = ref(null)
 let viewer = null
-const deviceEntityIds = new Set()
-const deviceEntityMap = new Map()
-let defenseZoneSource = null
-let partitionSource = null
+let cockpitPrimitiveLayer = null
+const devicePickMap = new Map()
+const dronePickMap = new Map()
+let deviceBillboardCollection = null
+let deviceRingOutlinePrimitives = []
+let deviceRingFillPrimitives = []
+let defenseZonePrimitive = null
+let defenseZoneOutlinePrimitive = null
+let partitionPrimitive = null
+let partitionOutlinePrimitive = null
 let aggregationSource = null
-let commandPostSource = null
+let commandPostBillboardCollection = null
+let droneTrackBillboardCollection = null
+let droneTrackPolylineCollection = null
+let droneTrackTickHandler = null
+let droneTrackStartTime = null
+let droneTrackRuntime = []
 const detailVisible = ref(true)
 const clusterVisible = ref(false)
 const countyCenterMap = new Map()
 const showLayerPanel = ref(false)
 const layerWrapRef = ref(null)
 const selectedDevice = ref(null)
-let selectedDeviceEntity = null
+const selectedTargetType = ref('device')
+let selectedDeviceBillboard = null
 let deviceClickHandler = null
 let popupRenderHandler = null
 const layerTreeProps = {
@@ -172,23 +159,101 @@
 	return { longitude, latitude }
 }
 
+const ensureCockpitPrimitiveLayer = () => {
+	if (!viewer) return
+	if (!cockpitPrimitiveLayer) {
+		cockpitPrimitiveLayer = new Cesium.PrimitiveCollection({ destroyPrimitives: false })
+		viewer.scene.primitives.add(cockpitPrimitiveLayer)
+	}
+}
+
+const addCockpitPrimitive = primitive => {
+	if (!primitive) return
+	if (cockpitPrimitiveLayer) {
+		cockpitPrimitiveLayer.add(primitive)
+		return
+	}
+	viewer?.scene?.primitives?.add(primitive)
+}
+
+const removeCockpitPrimitive = (primitive, destroy = true) => {
+	if (!primitive) return
+	if (cockpitPrimitiveLayer) {
+		cockpitPrimitiveLayer.remove(primitive, destroy)
+		return
+	}
+	viewer?.scene?.primitives?.remove(primitive)
+}
+
+const reorderCockpitPrimitives = () => {
+	if (!viewer) return
+	ensureCockpitPrimitiveLayer()
+	cockpitPrimitiveLayer.removeAll(false)
+	if (defenseZonePrimitive) cockpitPrimitiveLayer.add(defenseZonePrimitive)
+	if (defenseZoneOutlinePrimitive) cockpitPrimitiveLayer.add(defenseZoneOutlinePrimitive)
+	if (partitionPrimitive) cockpitPrimitiveLayer.add(partitionPrimitive)
+	if (partitionOutlinePrimitive) cockpitPrimitiveLayer.add(partitionOutlinePrimitive)
+	if (deviceRingFillPrimitives.length) {
+		deviceRingFillPrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
+	}
+	if (deviceRingOutlinePrimitives.length) {
+		deviceRingOutlinePrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
+	}
+	if (droneTrackPolylineCollection) cockpitPrimitiveLayer.add(droneTrackPolylineCollection)
+	if (droneTrackBillboardCollection) cockpitPrimitiveLayer.add(droneTrackBillboardCollection)
+	if (deviceBillboardCollection) cockpitPrimitiveLayer.add(deviceBillboardCollection)
+	if (commandPostBillboardCollection) cockpitPrimitiveLayer.add(commandPostBillboardCollection)
+}
+
 const clearDeviceEntities = () => {
 	if (!viewer) return
-	deviceEntityIds.forEach(id => {
-		viewer.entities.removeById(id)
-	})
-	deviceEntityIds.clear()
-	deviceEntityMap.clear()
+	if (deviceBillboardCollection) {
+		deviceBillboardCollection.removeAll()
+		removeCockpitPrimitive(deviceBillboardCollection)
+		deviceBillboardCollection = null
+	}
+	if (deviceRingFillPrimitives.length) {
+		deviceRingFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+		deviceRingFillPrimitives = []
+	}
+	if (deviceRingOutlinePrimitives.length) {
+		deviceRingOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+		deviceRingOutlinePrimitives = []
+	}
+	devicePickMap.clear()
+}
+
+const ensureDeviceCollections = () => {
+	if (!viewer) return
+	ensureCockpitPrimitiveLayer()
+	if (!deviceBillboardCollection) {
+		deviceBillboardCollection = new Cesium.BillboardCollection()
+		addCockpitPrimitive(deviceBillboardCollection)
+	}
 }
 
 const clearDefenseZoneEntities = () => {
-	if (!defenseZoneSource) return
-	defenseZoneSource.entities.removeAll()
+	if (!viewer) return
+	if (defenseZonePrimitive) {
+		removeCockpitPrimitive(defenseZonePrimitive)
+		defenseZonePrimitive = null
+	}
+	if (defenseZoneOutlinePrimitive) {
+		removeCockpitPrimitive(defenseZoneOutlinePrimitive)
+		defenseZoneOutlinePrimitive = null
+	}
 }
 
 const clearPartitionEntities = () => {
-	if (!partitionSource) return
-	partitionSource.entities.removeAll()
+	if (!viewer) return
+	if (partitionPrimitive) {
+		removeCockpitPrimitive(partitionPrimitive)
+		partitionPrimitive = null
+	}
+	if (partitionOutlinePrimitive) {
+		removeCockpitPrimitive(partitionOutlinePrimitive)
+		partitionOutlinePrimitive = null
+	}
 }
 
 const clearAggregationEntities = () => {
@@ -197,20 +262,42 @@
 }
 
 const clearCommandPostEntities = () => {
-	if (!commandPostSource) return
-	commandPostSource.entities.removeAll()
+	if (!viewer) return
+	if (commandPostBillboardCollection) {
+		commandPostBillboardCollection.removeAll()
+		removeCockpitPrimitive(commandPostBillboardCollection)
+		commandPostBillboardCollection = null
+	}
+}
+
+const ensureCommandPostCollection = () => {
+	if (!viewer) return
+	ensureCockpitPrimitiveLayer()
+	if (!commandPostBillboardCollection) {
+		commandPostBillboardCollection = new Cesium.BillboardCollection()
+		addCockpitPrimitive(commandPostBillboardCollection)
+	}
 }
 
 const setDetailVisibility = visible => {
 	detailVisible.value = visible
-	if (defenseZoneSource) defenseZoneSource.show = visible
-	if (partitionSource) partitionSource.show = visible
-	if (commandPostSource) commandPostSource.show = visible
+	if (defenseZonePrimitive) defenseZonePrimitive.show = visible
+	if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = visible
+	if (partitionPrimitive) partitionPrimitive.show = visible
+	if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = visible
+	if (commandPostBillboardCollection) commandPostBillboardCollection.show = visible
+	if (deviceBillboardCollection) deviceBillboardCollection.show = visible
+	if (deviceRingFillPrimitives.length) {
+		deviceRingFillPrimitives.forEach(primitive => {
+			primitive.show = visible
+		})
+	}
+	if (deviceRingOutlinePrimitives.length) {
+		deviceRingOutlinePrimitives.forEach(primitive => {
+			primitive.show = visible
+		})
+	}
 	if (!visible) closePopup()
-	deviceEntityIds.forEach(id => {
-		const entity = viewer?.entities?.getById(id)
-		if (entity) entity.show = visible
-	})
 }
 
 const setClusterVisibility = visible => {
@@ -229,140 +316,244 @@
 	})
 }
 
-
-const RING_STYLES = [
-	{ inner: 0, outer: 2000, gradient: ['#FF361C', '#360B00'] }
-]
-
-const MATERIAL_TYPE = 'RadialGradientMaterial'
-let materialRegistered = false
-
-const registerRadialGradientMaterial = () => {
-	if (materialRegistered || !Cesium?.Material) return
-	materialRegistered = true
-	Cesium.Material._materialCache.addMaterial(MATERIAL_TYPE, {
-		fabric: {
-			type: MATERIAL_TYPE,
-			uniforms: {
-				color1: new Cesium.Color(1.0, 1.0, 1.0, 1.0),
-				color2: new Cesium.Color(0.0, 0.0, 0.0, 1.0),
-			},
-			source: `
-				czm_material czm_getMaterial(czm_materialInput materialInput) {
-					czm_material material = czm_getDefaultMaterial(materialInput);
-					vec2 st = materialInput.st - vec2(0.5);
-					float t = clamp(length(st) * 2.0, 0.0, 1.0);
-					vec4 color = mix(color1, color2, t);
-					material.diffuse = color.rgb;
-					material.alpha = color.a;
-					return material;
-				}
-			`,
-		},
-		translucent: () => true,
-	})
+const buildSimulatedTrackPoints = base => {
+	ensureCountyCenterMap()
+	const anchor = base || { longitude: 116.397, latitude: 39.908 }
+	const offsets = [
+		[0, 0, 120],
+		[0.02, 0.012, 150],
+		[0.04, 0.006, 180],
+		[0.06, -0.008, 200],
+		[0.03, -0.02, 160],
+		[0.0, -0.015, 140],
+		[-0.02, -0.005, 120],
+		[-0.01, 0.01, 130],
+	]
+	return offsets.map(item => ({
+		longitude: anchor.longitude + item[0],
+		latitude: anchor.latitude + item[1],
+		height: item[2],
+	}))
 }
 
-const buildCirclePositions = (center, radiusMeters, steps = 64) => {
-	const positions = []
-	const latRad = Cesium.Math.toRadians(center.latitude)
-	const metersToLat = 1 / 111320
-	const metersToLon = 1 / (111320 * Math.cos(latRad))
-	for (let i = 0; i <= steps; i += 1) {
-		const angle = Cesium.Math.toRadians((i / steps) * 360)
-		const dx = radiusMeters * Math.cos(angle)
-		const dy = radiusMeters * Math.sin(angle)
-		const lon = center.longitude + dx * metersToLon
-		const lat = center.latitude + dy * metersToLat
-		positions.push(Cesium.Cartesian3.fromDegrees(lon, lat))
+
+const clearDroneTrackEntities = () => {
+	if (!viewer) return
+	stopDroneTrackAnimation()
+	if (droneTrackBillboardCollection) {
+		if (!droneTrackBillboardCollection.isDestroyed?.()) {
+			removeCockpitPrimitive(droneTrackBillboardCollection)
+		}
+		droneTrackBillboardCollection = null
 	}
-	return positions
+	if (droneTrackPolylineCollection) {
+		if (!droneTrackPolylineCollection.isDestroyed?.()) {
+			removeCockpitPrimitive(droneTrackPolylineCollection)
+		}
+		droneTrackPolylineCollection = null
+	}
+	droneTrackRuntime = []
+	dronePickMap.clear()
+	if (selectedTargetType.value === 'drone') {
+		closePopup()
+	}
 }
 
-const addDeviceRings = (center, baseId, visible) => {
-	RING_STYLES.forEach((ring, index) => {
-		const outerPositions = buildCirclePositions(center, ring.outer)
-		const holes = ring.inner
-			? [new Cesium.PolygonHierarchy(buildCirclePositions(center, ring.inner))]
-			: []
-		const entityId = `${baseId}-ring-${index}`
-		deviceEntityIds.add(entityId)
-		registerRadialGradientMaterial()
-		const [startColor, endColor] = ring.gradient
-		const color1 = Cesium.Color.fromCssColorString(startColor).withAlpha(0.34)
-		const color2 = Cesium.Color.fromCssColorString(endColor).withAlpha(0.34)
-		const material = new RadialGradientMaterialProperty(color1, color2)
-		viewer.entities.add({
-			id: entityId,
-			show: visible,
-			polygon: {
-				hierarchy: new Cesium.PolygonHierarchy(outerPositions, holes),
-				material,
+
+const ensureDroneTrackCollections = () => {
+	if (!viewer) return
+	ensureCockpitPrimitiveLayer()
+	if (droneTrackBillboardCollection?.isDestroyed?.()) {
+		droneTrackBillboardCollection = null
+	}
+	if (droneTrackPolylineCollection?.isDestroyed?.()) {
+		droneTrackPolylineCollection = null
+	}
+	if (!droneTrackBillboardCollection) {
+		droneTrackBillboardCollection = new Cesium.BillboardCollection()
+		addCockpitPrimitive(droneTrackBillboardCollection)
+	}
+	if (!droneTrackPolylineCollection) {
+		droneTrackPolylineCollection = new Cesium.PolylineCollection()
+		addCockpitPrimitive(droneTrackPolylineCollection)
+	}
+}
+
+const startDroneTrackAnimation = () => {
+	if (!viewer) return
+	stopDroneTrackAnimation()
+	droneTrackStartTime = Cesium.JulianDate.now()
+	droneTrackTickHandler = clock => {
+		if (!viewer || viewer.isDestroyed?.() || !viewer.scene) {
+			stopDroneTrackAnimation()
+			return
+		}
+		if (!droneTrackRuntime.length) return
+		const current = clock.currentTime
+		const elapsed = Cesium.JulianDate.secondsDifference(current, droneTrackStartTime)
+		droneTrackRuntime.forEach(track => {
+			if (!track?.billboard || track.billboard.isDestroyed?.()) return
+			if (!track.billboard._scene) return
+			if (!track.positions || track.positions.length < 2) return
+			const duration = track.duration
+			if (duration <= 0) return
+			const t = ((elapsed % duration) + duration) % duration
+			const seg = Math.min(track.positions.length - 2, Math.floor(t / track.segmentDuration))
+			const ratio = (t - seg * track.segmentDuration) / track.segmentDuration
+			const pos = Cesium.Cartesian3.lerp(
+				track.positions[seg],
+				track.positions[seg + 1],
+				ratio,
+				new Cesium.Cartesian3()
+			)
+			track.billboard.position = pos
+		})
+	}
+	viewer.clock.onTick.addEventListener(droneTrackTickHandler)
+	viewer.clock.shouldAnimate = true
+}
+
+const stopDroneTrackAnimation = () => {
+	if (!viewer || !droneTrackTickHandler) return
+	viewer.clock.onTick.removeEventListener(droneTrackTickHandler)
+	droneTrackTickHandler = null
+}
+
+const renderSimulatedDroneTrack = () => {
+	if (!viewer) return
+	clearDroneTrackEntities()
+	ensureCountyCenterMap()
+	ensureDroneTrackCollections()
+	droneTrackRuntime = []
+	const centers = Array.from(countyCenterMap.entries())
+	const baseCenters = centers.length
+		? centers.slice(0, 4)
+		: [['默认区域', { longitude: 116.397, latitude: 39.908 }]]
+	const segmentDuration = 6
+	const baseTrackColor = Cesium.Color.fromCssColorString('red')
+	baseCenters.forEach(([centerName, center], trackIndex) => {
+		const points = buildSimulatedTrackPoints(center)
+		if (points.length < 2) return
+		const positions = points.map(point =>
+			Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
+		)
+		let trackMaterial = createDroneTrackMaterial({
+			color: baseTrackColor,
+			speed: 4.5,
+			headWidth: 0.2,
+			glowPower: 1.8,
+			backgroundAlpha: 0.34,
+		})
+		if (!trackMaterial) {
+			trackMaterial = Cesium.Material.fromType('Color', { color: baseTrackColor })
+		}
+		droneTrackPolylineCollection.add({
+			positions,
+			width: 3,
+			material: trackMaterial,
+		})
+		const droneId = `drone-track-${trackIndex}`
+		const billboard = droneTrackBillboardCollection.add({
+			position: positions[0],
+			image: droneIcon,
+			width: 36,
+			height: 36,
+			verticalOrigin: Cesium.VerticalOrigin.CENTER,
+			disableDepthTestDistance: Number.POSITIVE_INFINITY,
+		})
+		billboard.id = droneId
+		const speedMs = Math.round(Cesium.Cartesian3.distance(positions[0], positions[1]) / segmentDuration)
+		dronePickMap.set(droneId, {
+			data: {
+				droneName: `无人机-${trackIndex + 1}`,
+				droneSerialNo: `UAV-${trackIndex + 1}`,
+				flightHeightM: points[0].height,
+				flightSpeedMs: speedMs,
+				longitude: points[0].longitude,
+				latitude: points[0].latitude,
+				flightStatus: 1,
+				deviceName: centerName,
+				dataSource: centerName,
+				counterDeviceName: centerName,
 			},
+			billboard,
+		})
+		droneTrackRuntime.push({
+			positions,
+			billboard,
+			segmentDuration,
+			duration: (points.length - 1) * segmentDuration,
 		})
 	})
-}
-
-class RadialGradientMaterialProperty {
-	constructor(color1, color2) {
-		this._definitionChanged = new Cesium.Event()
-		this.color1 = color1
-		this.color2 = color2
-	}
-
-	get isConstant () {
-		return true
-	}
-
-	get definitionChanged () {
-		return this._definitionChanged
-	}
-
-	getType () {
-		return MATERIAL_TYPE
-	}
-
-	getValue (time, result) {
-		const target = result || {}
-		target.color1 = this.color1
-		target.color2 = this.color2
-		return target
-	}
-
-	equals (other) {
-		return (
-			other instanceof RadialGradientMaterialProperty &&
-			Cesium.Color.equals(this.color1, other.color1) &&
-			Cesium.Color.equals(this.color2, other.color2)
-		)
-	}
+	viewer.clock.multiplier = 1
+	startDroneTrackAnimation()
+	reorderCockpitPrimitives()
 }
 
 const renderDeviceEntities = devices => {
 	if (!viewer) return
-	clearDeviceEntities()
+	ensureCockpitPrimitiveLayer()
+	ensureDeviceCollections()
+	deviceBillboardCollection.removeAll()
+	if (deviceRingFillPrimitives.length) {
+		deviceRingFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+		deviceRingFillPrimitives = []
+	}
+	if (deviceRingOutlinePrimitives.length) {
+		deviceRingOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+		deviceRingOutlinePrimitives = []
+	}
+	devicePickMap.clear()
+	deviceBillboardCollection.show = detailVisible.value
+	const ringFillInstancesByStyle = RING_STYLES.map(() => [])
+	const ringOutlineInstancesByStyle = RING_STYLES.map(() => [])
 	devices.forEach((item, index) => {
 		const position = getDevicePosition(item)
 		if (!position) return
-		const entityId = `online-device-${item.id ?? index}`
-		deviceEntityIds.add(entityId)
-		addDeviceRings(position, entityId, detailVisible.value)
-		const entity = viewer.entities.add({
-			id: entityId,
-			show: detailVisible.value,
+		const entityId = `online-device-${item.id ?? index}-${index}`
+		addDeviceRings(position, ringFillInstancesByStyle, ringOutlineInstancesByStyle)
+		const billboard = deviceBillboardCollection.add({
 			position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
-			billboard: {
-				image: equipmentIcon,
-				width: 40,
-				height: 56,
-				verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
-			},
+			image: equipmentIcon,
+			width: 40,
+			height: 56,
+			verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
 		})
-		deviceEntityMap.set(entityId, { data: item, entity })
+		billboard.id = entityId
+		devicePickMap.set(entityId, { data: item, billboard })
 	})
-	if (selectedDeviceEntity && !deviceEntityMap.has(selectedDeviceEntity.id)) {
+	if (selectedTargetType.value === 'device' && selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) {
 		closePopup()
 	}
+	ringFillInstancesByStyle.forEach((instances, index) => {
+		if (!instances.length) return
+		const ring = RING_STYLES[index]
+		const material = createRadialGradientMaterial(
+			Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.64),
+			Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(0.64)
+		)
+		const primitive = new Cesium.GroundPrimitive({
+			geometryInstances: instances,
+			appearance: new Cesium.MaterialAppearance({
+				material,
+				translucent: true,
+			}),
+		})
+		primitive.show = detailVisible.value
+		addCockpitPrimitive(primitive)
+		deviceRingFillPrimitives.push(primitive)
+	})
+	ringOutlineInstancesByStyle.forEach(instances => {
+		if (!instances.length) return
+		const primitive = new Cesium.GroundPolylinePrimitive({
+			geometryInstances: instances,
+			appearance: new Cesium.PolylineColorAppearance(),
+		})
+		primitive.show = detailVisible.value
+		addCockpitPrimitive(primitive)
+		deviceRingOutlinePrimitives.push(primitive)
+	})
+	reorderCockpitPrimitives()
 }
 
 const getDefenseZonePositions = geom => {
@@ -377,70 +568,84 @@
 	return list.map(item => Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude))
 }
 
-const renderZonePolygons = ({ zones, source, idPrefix, lineColor, fillGradient }) => {
-	if (!viewer || !source) return
-	registerRadialGradientMaterial()
-	const borderColor = Cesium.Color.fromCssColorString(lineColor)
-	const fillColorStart = Cesium.Color.fromCssColorString(fillGradient[0]).withAlpha(0.34)
-	const fillColorEnd = Cesium.Color.fromCssColorString(fillGradient[1]).withAlpha(0.34)
-	const fillMaterial = new RadialGradientMaterialProperty(fillColorStart, fillColorEnd)
-	zones.forEach((zone, index) => {
+const buildZonePrimitives = (zones, lineColor, fillColor1, fillColor2) => {
+	if (!viewer) return { primitive: null, outlinePrimitive: null }
+	const polygonInstances = []
+	const lineInstances = []
+	const texturedVertexFormat = getTexturedVertexFormat()
+	; (zones || []).forEach((zone, index) => {
 		if (!zone?.geom) return
 		const positions = getDefenseZonePositions(zone.geom)
 		if (!positions.length) return
-		const entityId = `${idPrefix}-${zone.id ?? index}`
-		const linePositions = positions.length > 1 ? [...positions, positions[0]] : positions
-		source.entities.add({
-			id: entityId,
-			polygon: {
-				hierarchy: new Cesium.PolygonHierarchy(positions),
-				material: fillMaterial,
-				outline: true,
-				outlineColor: borderColor,
-				heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
-			},
-			polyline: {
-				positions: linePositions,
-				clampToGround: true,
-				width: 2,
-				material: borderColor,
-			},
+		const polygon = new Cesium.PolygonGeometry({
+			polygonHierarchy: new Cesium.PolygonHierarchy(positions),
+			vertexFormat: texturedVertexFormat,
 		})
+		polygonInstances.push(
+			new Cesium.GeometryInstance({
+				geometry: polygon,
+			})
+		)
+		const linePositions = positions.length > 1 ? [...positions, positions[0]] : positions
+		lineInstances.push(
+			new Cesium.GeometryInstance({
+				geometry: new Cesium.GroundPolylineGeometry({
+					positions: linePositions,
+					width: 2,
+				}),
+				attributes: {
+					color: Cesium.ColorGeometryInstanceAttribute.fromColor(
+						Cesium.Color.fromCssColorString(lineColor)
+					),
+				},
+			})
+		)
 	})
+	let primitive = null
+	if (polygonInstances.length) {
+		const baseColor1 = Cesium.Color.fromCssColorString(fillColor1)
+		const baseColor2 = Cesium.Color.fromCssColorString(fillColor2)
+		const material = createRadialGradientMaterial(baseColor1.withAlpha(0.64), baseColor2.withAlpha(0.64))
+		primitive = new Cesium.GroundPrimitive({
+			geometryInstances: polygonInstances,
+			appearance: new Cesium.MaterialAppearance({
+				material,
+				translucent: true,
+			}),
+		})
+		addCockpitPrimitive(primitive)
+	}
+	let outlinePrimitive = null
+	if (lineInstances.length) {
+		outlinePrimitive = new Cesium.GroundPolylinePrimitive({
+			geometryInstances: lineInstances,
+			appearance: new Cesium.PolylineColorAppearance(),
+		})
+		addCockpitPrimitive(outlinePrimitive)
+	}
+	return { primitive, outlinePrimitive }
 }
 
 const renderDefenseZones = zones => {
 	if (!viewer) return
-	if (!defenseZoneSource) {
-		defenseZoneSource = new Cesium.CustomDataSource('defenseZoneSource')
-		viewer.dataSources.add(defenseZoneSource)
-	}
 	clearDefenseZoneEntities()
-	defenseZoneSource.show = detailVisible.value
-	renderZonePolygons({
-		zones,
-		source: defenseZoneSource,
-		idPrefix: 'defense-zone',
-		lineColor: '#19D266',
-		fillGradient: ['#2AEDBF', '#012B11'],
-	})
+	const result = buildZonePrimitives(zones, '#19D266', '#2AEDBF', '#012B11')
+	defenseZonePrimitive = result.primitive
+	defenseZoneOutlinePrimitive = result.outlinePrimitive
+	if (defenseZonePrimitive) defenseZonePrimitive.show = detailVisible.value
+	if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = detailVisible.value
+	reorderCockpitPrimitives()
 }
 
 const renderPartitions = zones => {
 	if (!viewer) return
-	if (!partitionSource) {
-		partitionSource = new Cesium.CustomDataSource('partitionSource')
-		viewer.dataSources.add(partitionSource)
-	}
 	clearPartitionEntities()
-	partitionSource.show = detailVisible.value
-	renderZonePolygons({
-		zones,
-		source: partitionSource,
-		idPrefix: 'partition-zone',
-		lineColor: '#FFCD2A',
-		fillGradient: ['#FFC609', '#583300'],
-	})
+	const result = buildZonePrimitives(zones, '#FFCD2A', '#FFC609', '#583300')
+	partitionPrimitive = result.primitive
+	partitionOutlinePrimitive = result.outlinePrimitive
+	if (partitionPrimitive) partitionPrimitive.show = detailVisible.value
+	if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = detailVisible.value
+	reorderCockpitPrimitives()
 }
 
 const loadDefenseZones = async () => {
@@ -496,11 +701,10 @@
 				style: Cesium.LabelStyle.FILL_AND_OUTLINE,
 				verticalOrigin: Cesium.VerticalOrigin.CENTER,
 				horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
-				font: '12pt Source Han Sans CN',
+				font: '11pt Source Han Sans CN',
 				eyeOffset: new Cesium.Cartesian3(0, 0, -20), // 让label "浮" 在广告牌前面
-				pixelOffset: new Cesium.Cartesian2(2, -11),
 
-				pixelOffset: new Cesium.Cartesian2(0, -48),
+				pixelOffset: new Cesium.Cartesian2(0, -35),
 				disableDepthTestDistance: Number.POSITIVE_INFINITY,
 				heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
 			},
@@ -508,21 +712,23 @@
 	})
 }
 
-const getPickedDevice = picks => {
+const getPickedTarget = picks => {
 	for (const pick of picks) {
-		const entityId = typeof pick?.id === 'string' ? pick.id : pick?.id?.id
-		if (entityId && deviceEntityMap.has(entityId)) {
-			return deviceEntityMap.get(entityId)
+		const pickId = pick?.id
+		const resolvedId = typeof pickId === 'string' ? pickId : pickId?.id
+		if (resolvedId && devicePickMap.has(resolvedId)) {
+			return { type: 'device', ...devicePickMap.get(resolvedId) }
+		}
+		if (resolvedId && dronePickMap.has(resolvedId)) {
+			return { type: 'drone', ...dronePickMap.get(resolvedId) }
 		}
 	}
 	return null
 }
 
 const updatePopupPosition = () => {
-	if (!viewer || !selectedDeviceEntity) return
-	const cartesian =
-		selectedDeviceEntity.position?.getValue?.(Cesium.JulianDate.now()) ||
-		selectedDeviceEntity.position?._value
+	if (!viewer || !selectedDeviceBillboard) return
+	const cartesian = selectedDeviceBillboard.position
 	if (!cartesian) return
 	const screenPosition = viewer.scene.cartesianToCanvasCoordinates(cartesian)
 	if (!screenPosition) return
@@ -545,13 +751,14 @@
 const handleDeviceClick = movement => {
 	if (!viewer) return
 	const picks = viewer.scene.drillPick(movement.position) || []
-	const pickedDevice = getPickedDevice(picks)
-	if (!pickedDevice) {
+	const pickedTarget = getPickedTarget(picks)
+	if (!pickedTarget) {
 		closePopup()
 		return
 	}
-	selectedDevice.value = pickedDevice.data
-	selectedDeviceEntity = pickedDevice.entity
+	selectedTargetType.value = pickedTarget.type
+	selectedDevice.value = pickedTarget.data
+	selectedDeviceBillboard = pickedTarget.billboard
 	startPopupRender()
 }
 
@@ -569,104 +776,45 @@
 
 function closePopup () {
 	selectedDevice.value = null
-	selectedDeviceEntity = null
+	selectedDeviceBillboard = null
+	selectedTargetType.value = 'device'
 	stopPopupRender()
 }
 
 const popupPosition = ref({ x: 0, y: 0 })
 const popupVisible = computed(() => Boolean(selectedDevice.value))
-const popupStyle = computed(() => ({
-	left: `${popupPosition.value.x}px`,
-	top: `${popupPosition.value.y}px`,
-}))
-
-const popupTitle = computed(() => selectedDevice.value?.deviceName || selectedDevice.value?.deviceModel || '-')
-const popupStatusText = computed(() => {
-	const status = selectedDevice.value?.status
-	if (status === 0) return '在线'
-	if (status === 1) return '离线'
-	if (status === 2) return '故障'
-	if (status === 3) return '报废'
-	return '-'
-})
-const popupStatusClass = computed(() => (selectedDevice.value?.status === 0 ? 'online' : ''))
-const popupActionText = computed(() => {
-	const workMode = selectedDevice.value?.workMode
-	if (workMode === 1 || workMode === '1') return '侦测中'
-	if (workMode === 2 || workMode === '2') return '信号干扰中'
-	if (workMode === 3 || workMode === '3') return '诱导驱离中'
-	if (workMode === 4 || workMode === '4') return '待机'
-	return workMode || '-'
-})
-const popupActionClass = computed(() => {
-	const workMode = selectedDevice.value?.workMode
-	if (workMode === 1 || workMode === '1') return 'is-detecting'
-	if (workMode === 2 || workMode === '2') return 'is-jamming'
-	if (workMode === 3 || workMode === '3') return 'is-driving'
-	if (workMode === 4 || workMode === '4') return 'is-standby'
-	return 'is-standby'
-})
-const popupTypeText = computed(() => {
-	const type = selectedDevice.value?.deviceType
-	if (type === 1 || type === '1') return '便捷侦测箱'
-	if (type === 2 || type === '2') return '反制枪'
-	if (type === 3 || type === '3') return '察打一体'
-	return type || '-'
-})
-const popupBatteryText = computed(() => {
-	const val = selectedDevice.value?.batteryPct
-	if (val == null) return '-'
-	return `${val}%`
-})
-const popupAzimuthText = computed(() => {
-	const val = selectedDevice.value?.azimuth
-	if (val == null) return '-'
-	return `${val}°`
-})
-const popupElevationText = computed(() => {
-	const val = selectedDevice.value?.elevation
-	if (val == null) return '-'
-	return `${val}°`
-})
-const popupDetectCountText = computed(() => {
-	const val = selectedDevice.value?.detectTargetCnt
-	if (val == null) return '-'
-	return `${val}台`
-})
-const popupRangeText = computed(() => {
-	const val = selectedDevice.value?.effectiveRangeKm
-	if (val == null) return '-'
-	return `${val}KM`
-})
+const isDronePopup = computed(() => selectedTargetType.value === 'drone')
+const toggleDroneFavorite = () => {
+	if (!selectedDevice.value) return
+	selectedDevice.value.isFavorite = !selectedDevice.value.isFavorite
+}
+const handleDroneSignal = () => emit('droneSignal', selectedDevice.value)
+const handleDroneCounter = () => emit('droneCounter', selectedDevice.value)
 
 const renderCommandPosts = list => {
 	if (!viewer) return
-	if (!commandPostSource) {
-		commandPostSource = new Cesium.CustomDataSource('commandPostSource')
-		viewer.dataSources.add(commandPostSource)
-	}
-	clearCommandPostEntities()
-	commandPostSource.show = detailVisible.value
-		; (list || []).forEach((item, index) => {
-			const position = getDevicePosition(item)
-			if (!position) return
-			commandPostSource.entities.add({
-				id: `command-post-${item.id ?? index}`,
-				position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
-				billboard: {
-					image: commandPostIcon,
-					width: 40,
-					height: 56,
-					verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
-					heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
-				},
-			})
+	ensureCommandPostCollection()
+	commandPostBillboardCollection.removeAll()
+	commandPostBillboardCollection.show = detailVisible.value
+	; (list || []).forEach((item, index) => {
+		const position = getDevicePosition(item)
+		if (!position) return
+		commandPostBillboardCollection.add({
+			position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
+			image: commandPostIcon,
+			width: 40,
+			height: 56,
+			verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
 		})
+	})
+	reorderCockpitPrimitives()
 }
 
 const loadAggregation = async () => {
 	try {
-		const res = await cockpitAggregationApi()
+		const res = await cockpitAggregationApi({
+			effectiveRangeKmIsNotNull: 1
+		})
 		renderAggregation(res?.data?.data ?? [])
 	} catch (error) {
 		renderAggregation([])
@@ -711,6 +859,7 @@
 
 const handleMapReady = ({ viewer: mapViewer }) => {
 	viewer = mapViewer
+	ensureCockpitPrimitiveLayer()
 	const height = viewer?.camera?.positionCartographic?.height
 	const stage = height != null && height >= 100000 ? 'cluster' : 'detail'
 	updateStageDisplay(stage)
@@ -719,6 +868,7 @@
 	loadPartitions()
 	loadAggregation()
 	loadCommandPosts()
+	renderSimulatedDroneTrack()
 	initDeviceClickHandler()
 }
 
@@ -746,8 +896,13 @@
 	clearPartitionEntities()
 	clearAggregationEntities()
 	clearCommandPostEntities()
+	clearDroneTrackEntities()
 	closePopup()
 	destroyDeviceClickHandler()
+	if (cockpitPrimitiveLayer && viewer?.scene?.primitives) {
+		viewer.scene.primitives.remove(cockpitPrimitiveLayer)
+	}
+	cockpitPrimitiveLayer = null
 	viewer = null
 })
 </script>
@@ -765,158 +920,6 @@
 	left: 0;
 	width: 100%;
 	height: 100%;
-}
-
-.device-popup {
-	position: absolute;
-	transform: translate(-50%, calc(-100% - 72px));
-	width: 202px;
-	padding: 0 16px 20px;
-	border-radius: 16px;
-	background: rgba(5, 5, 15, 0.7);
-	backdrop-filter: blur(16px);
-	color: #C3C3DD;
-	font-family: Source Han Sans CN, Source Han Sans CN;
-	font-size: 12px;
-	pointer-events: auto;
-	z-index: 11;
-	box-sizing: border-box;
-
-	&::after {
-		content: "";
-		position: absolute;
-		left: 50%;
-		bottom: -8px;
-		transform: translateX(-50%);
-		width: 0;
-		height: 0;
-		border-left: 8px solid transparent;
-		border-right: 8px solid transparent;
-		border-top: 8px solid rgba(5, 5, 15, 0.7);
-	}
-
-	.device-popup__header {
-		height: 40px;
-		display: flex;
-		align-items: center;
-		justify-content: space-between;
-		position: relative;
-
-		font-weight: bold;
-		font-size: 14px;
-		color: #FFFFFF;
-
-		&::after {
-			content: "";
-			position: absolute;
-			left: 0;
-			bottom: 0;
-			width: 50px;
-			height: 2px;
-			background: #284FE3;
-			box-shadow: 0px 3px 2px 0px rgba(15, 89, 255, 0.1), 0px 7px 5px 0px rgba(15, 89, 255, 0.15), 0px 13px 10px 0px rgba(15, 89, 255, 0.18), 0px 22px 18px 0px rgba(15, 89, 255, 0.21), 0px 42px 33px 0px rgba(15, 89, 255, 0.26), 0px 100px 80px 0px rgba(15, 89, 255, 0.36);
-			border-radius: 5px 5px 0px 0px;
-		}
-
-		&::before {
-			content: "";
-			position: absolute;
-			left: 55px;
-			right: 0;
-			bottom: 0;
-			height: 2px;
-			background: #323245;
-			border-radius: 0px 0px 5px 5px;
-		}
-
-		.device-popup__close {
-			cursor: pointer;
-		}
-	}
-
-	.device-popup__type {
-		display: flex;
-		align-items: center;
-		gap: 6px;
-		font-size: 12px;
-		margin-top: 8px;
-		margin-bottom: 10px;
-		color: #d2d7df;
-
-		.label {
-			color: #9aa4b2;
-		}
-	}
-
-	.device-popup__subtitle {
-		display: inline-flex;
-		align-items: center;
-		gap: 6px;
-		padding: 2px 10px;
-		border-radius: 999px;
-		background: rgba(255, 255, 255, 0.06);
-		font-size: 12px;
-		margin-bottom: 10px;
-
-		.device-popup__dot {
-			width: 6px;
-			height: 6px;
-			border-radius: 50%;
-			background: #6b7685;
-
-			&.is-detecting {
-				background: #35f08c;
-				box-shadow: 0 0 6px rgba(53, 240, 140, 0.8);
-			}
-
-			&.is-jamming {
-				background: #ff4444;
-				box-shadow: 0 0 6px rgba(255, 68, 68, 0.7);
-			}
-
-			&.is-driving {
-				background: #ffb020;
-				box-shadow: 0 0 6px rgba(255, 176, 32, 0.7);
-			}
-
-			&.is-standby {
-				background: #9aa4b2;
-			}
-		}
-	}
-
-	.device-popup__rows {
-		display: flex;
-		flex-direction: column;
-		gap: 6px;
-		font-size: 12px;
-		color: #d2d7df;
-
-		.device-popup__row {
-			display: flex;
-			align-items: center;
-			margin-bottom: 10px;
-
-			.device-popup__col {
-				flex: 1;
-				display: flex;
-				align-items: center;
-
-				&:last-child {
-					justify-content: flex-end;
-				}
-			}
-		}
-
-		.value {
-			margin-left: 10px;
-		}
-
-		.value.online {
-			color: #35f08c;
-		}
-	}
-
 }
 
 .layer-control-root {
@@ -1065,3 +1068,4 @@
 	border-color: #a1a3d4;
 }
 </style>
+

--
Gitblit v1.9.3