From 89380e6260a75d1d3b94de687ebcc2f50d50659d Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Tue, 03 Feb 2026 15:44:33 +0800
Subject: [PATCH] feat:环境变量配置调整

---
 applications/drone-command/src/components/map-container/device-map-container.vue | 1657 +++++++++++++++++++++++++++++++++++++++++++++++-----------
 1 files changed, 1,325 insertions(+), 332 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 b54ab6d..7819129 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,52 +1,90 @@
-<template>
-	<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"
-		:show-admin-boundary="true"
-		:zoom-to-boundary="true"
-		@ready="handleMapReady"
-	/>
-	<div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }">
-		<div class="layer-control-wrap" ref="layerWrapRef">
-			<div class="layer-control" @click="toggleLayerPanel">
-				<img :src="layerControlIcon" alt="图层控制" />
-			</div>
-			<div v-if="showLayerPanel" class="layer-panel">
-				<div class="panel-title">图层管理</div>
+<template>
+	<div class="map-shell">
+		<CommonCesiumMap ref="mapRef" class="command-cesium map-container" :dom-id="props.containerId" :active="true"
+			: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="CLUSTER_HEIGHT" :detail-height="DETAIL_HEIGHT" @ready="handleMapReady"
+			@stage-change="handleStageChange" />
+		<div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }">
+			<div class="layer-control-wrap" ref="layerWrapRef">
+				<div class="layer-control" @click="toggleLayerPanel">
+					<img :src="layerControlIcon" alt="图层控制" />
+				</div>
+				<div v-if="showLayerPanel" class="layer-panel">
+					<div class="panel-title">底图切换</div>
+					<div class="base-map-options">
+						<div class="base-map-card" :class="{ active: baseLayerKey === 'base-standard' }"
+							@click="handleBaseLayerSelect('base-standard')">
+							<img class="base-map-thumb standard" :src="dzIcon" alt="">
+							<div class="base-map-label">标准地图</div>
+						</div>
+						<div class="base-map-card" :class="{ active: baseLayerKey === 'base-satellite' }"
+							@click="handleBaseLayerSelect('base-satellite')">
+							<img class="base-map-thumb satellite" :src="yxIcon" alt="">
+							<div class="base-map-label">卫星地图</div>
+						</div>
+					</div>
 
-				<div class="panel-content">
-					<el-tree
-						:data="layerTree"
-						show-checkbox
-						default-expand-all
-						node-key="key"
-						:props="layerTreeProps"
-						:default-checked-keys="defaultCheckedKeys"
-					/>
+					<div class="panel-title">图层管理</div>
+					<div class="panel-content">
+						<el-tree ref="layerTreeRef" class="command-tree map-layer-tree" :data="layerTree" show-checkbox
+							default-expand-all node-key="key" :props="layerTreeProps"
+							:default-checked-keys="treeCheckedKeys" @check="handleLayerCheck" />
+					</div>
 				</div>
 			</div>
 		</div>
+
+		<DevicePopup :visible="popupVisible && !isDronePopup" :position="popupPosition" :device="selectedDevice"
+			@close="closePopup" />
+
+		<DronePopup :visible="popupVisible && isDronePopup" :position="popupPosition" :drone="selectedDevice"
+			:favorite="Boolean(selectedDevice?.isFavorite)" @close="closePopup" @toggle-favorite="handleDroneFavorite"
+			@signal="handleDroneSignal" @counter="handleDroneCounter" />
 	</div>
 </template>
 
 <script setup>
 import * as Cesium from 'cesium'
 import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
-import { geomAnalysis } from '@ztzf/utils'
-import { fwDefenseZonePageApi } from '@/views/areaManage/defenseZone/defenseZoneApi'
-import { fwAreaDividePageApi } from '@/views/areaManage/partition/partitionApi'
+import { buildEllipsePositions } from '@/utils/cesium/shapeTools'
+import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
+import { fwAreaDivideListApi } from '@/views/areaManage/partition/partitionApi'
+
+import { fwDefenseSceneManageListApi } from '@/views/areaManage/sceneManage/sceneManageApi'
+
+import { newCockpitAggregationApi } from '@/api/dataCockpit'
 import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
 import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
+import offlineEquipmentIcon from '@/assets/images/dataCockpit/map/offline-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 jaGeojsonRaw from '@/assets/geojson/ja.geojson?raw'
+import DevicePopup from './components/DevicePopup.vue'
+import DronePopup from './components/DronePopup.vue'
+import dayjs from 'dayjs'
+import {
+	createDroneTrackMaterial,
+	createRadialGradientMaterial,
+	getTexturedVertexFormat,
+} from './device-map-materials'
+import { getPointPositionsHeight } from '@/utils/cesium/mapUtil'
+import { createDeviceRangePrimitiveWithHeight } from '@/utils/cesium/deviceRange'
+import dzIcon from '@/assets/images/dataCockpit/map/dz-map-layer.png'
+import yxIcon from '@/assets/images/dataCockpit/map/yx-map-layer.png'
+
+const CLUSTER_HEIGHT = 100000
+const DETAIL_HEIGHT = 10000
+const POLYGON_HEIGHT_M = 0.2
+const DRONE_TRACK_DURATION_S = 30 * 60
 
 const props = defineProps({
-	onlineDevices: {
+	allDevices: {
+		type: Array,
+		default: () => [],
+	},
+	alarmDrones: {
 		type: Array,
 		default: () => [],
 	},
@@ -63,31 +101,73 @@
 		default: true,
 	},
 })
+const emit = defineEmits(['droneSignal', 'droneCounter', 'droneFavorite'])
 
-const DEFAULT_ZONE_PAGE_SIZE = 999
 
 const mapRef = ref(null)
 let viewer = null
-const deviceEntityIds = new Set()
-let defenseZoneSource = null
-let partitionSource = null
+let publicCesium = null
+let cockpitPrimitiveLayer = null
+const devicePickMap = new Map()
+const dronePickMap = new Map()
+let deviceBillboardCollection = null
+let deviceRingOutlinePrimitives = []
+let deviceRingFillPrimitives = []
+let partitionFillPrimitives = []
+let partitionOutlinePrimitives = []
+let aggregationSource = null
+let commandPostBillboardCollection = null
+let droneTrackBillboardCollection = null
+let droneTrackPolylineCollection = null
+let droneTrackTickHandler = null
+let droneTrackRafId = null
+let droneTrackStartTime = null
+let droneTrackAnimStartAt = 0
+let droneTrackLastTickAt = 0
+let droneTrackRuntime = []
+let droneTrackSource = null
+let mapReadyHandled = false
+let favoritePulseRafId = null
+let favoritePulseStartAt = 0
+let favoritePulseElapsed = 0
+const favoritePulseEntities = []
+const pulseBaseColor = Cesium.Color.fromCssColorString('#FF3B30')
+const PULSE_MIN_RADIUS_M = 2
+const PULSE_MAX_RADIUS_M = 30
+const PULSE_DURATION_S = 1.4
+const detailVisible = ref(true)
+const clusterVisible = ref(false)
+const countyCenterMap = new Map()
 const showLayerPanel = ref(false)
 const layerWrapRef = ref(null)
+const layerTreeRef = ref(null)
+const selectedDevice = ref(null)
+const selectedTargetType = ref('device')
+let selectedDeviceBillboard = null
+let deviceClickHandler = null
+let popupRenderHandler = null
 const layerTreeProps = {
 	label: 'label',
 	children: 'children',
 }
-const defaultCheckedKeys = ['global', 'city-base']
+const baseLayerKeys = ['base-standard', 'base-satellite']
+const defaultCheckedKeys = ['ja-terrain', 'admin', 'city-base']
+const baseLayerKey = ref('base-satellite')
+const treeCheckedKeys = ref([...defaultCheckedKeys])
 const layerTree = ref([
 	{
 		key: 'base',
 		label: '地理信息图层',
 		children: [
-			{ key: 'global', label: '全球地形' },
+			{ key: 'ja-terrain', label: '吉安地形' },
 			{ key: 'admin', label: '行政区划' },
 		],
 	},
-	{
+
+])
+
+/**
+ * {
 		key: 'city',
 		label: '城市CIM图层',
 		children: [
@@ -105,7 +185,138 @@
 			{ key: 'route', label: '飞行航路' },
 		],
 	},
-])
+ */
+
+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
+}
+
+class PulseSphereMaterialProperty {
+	constructor(color, speed = 1.2, bands = 3.0, softness = 0.18) {
+		this._definitionChanged = new Cesium.Event()
+		this.color = color
+		this.speed = speed
+		this.bands = bands
+		this.softness = softness
+	}
+	get isConstant() {
+		return true
+	}
+	get definitionChanged() {
+		return this._definitionChanged
+	}
+	getType() {
+		return 'PulseSphereMaterial'
+	}
+	getValue(_time, result = {}) {
+		result.color = this.color
+		result.time = (performance.now() / 1000) * this.speed
+		result.bands = this.bands
+		result.softness = this.softness
+		return result
+	}
+	equals(other) {
+		return (
+			other instanceof PulseSphereMaterialProperty &&
+			Cesium.Color.equals(this.color, other.color) &&
+			this.speed === other.speed &&
+			this.bands === other.bands &&
+			this.softness === other.softness
+		)
+	}
+}
+
+let pulseSphereMaterial = null
+const getPulseSphereMaterial = () => {
+	if (pulseSphereMaterial) return pulseSphereMaterial
+	Cesium.Material._materialCache.addMaterial('PulseSphereMaterial', {
+		fabric: {
+			type: 'PulseSphereMaterial',
+			uniforms: {
+				color: pulseBaseColor.withAlpha(0.45),
+				time: 0,
+				bands: 3.0,
+				softness: 0.18,
+			},
+			source: `
+				czm_material czm_getMaterial(czm_materialInput materialInput)
+				{
+					czm_material material = czm_getDefaultMaterial(materialInput);
+					vec3 normalEC = normalize(materialInput.normalEC);
+					vec3 viewEC = normalize(-materialInput.positionToEyeEC);
+					float ndv = max(dot(normalEC, viewEC), 0.0);
+					float fresnel = pow(1.0 - ndv, 1.6);
+					float wave = sin((ndv * bands + time) * 6.2831853);
+					float ring = smoothstep(-softness, softness, wave);
+					float alpha = (ring * 0.6 + fresnel * 0.4) * color.a;
+					material.diffuse = color.rgb;
+					material.alpha = alpha;
+					return material;
+				}
+			`,
+		},
+		translucent: () => true,
+	})
+	pulseSphereMaterial = new PulseSphereMaterialProperty(pulseBaseColor.withAlpha(0.45), 1.1, 3.2, 0.22)
+	return pulseSphereMaterial
+}
+
+const createFavoritePulseEntity = (billboard) => {
+	if (!viewer || !billboard) return
+	const material = getPulseSphereMaterial()
+	const pulse = {
+		material,
+		offset: Math.random() * PULSE_DURATION_S,
+		entity: null,
+	}
+	const getPulseRadius3D = () => {
+		const radius = getPulseRadius(pulse)
+		return new Cesium.Cartesian3(radius, radius, radius)
+	}
+	pulse.entity = viewer.entities.add({
+		position: new Cesium.CallbackProperty(() => getBillboardPosition(billboard), false),
+		ellipsoid: {
+			radii: new Cesium.CallbackProperty(() => getPulseRadius3D(), false),
+			material,
+		},
+	})
+	favoritePulseEntities.push(pulse)
+}
+
+const getBillboardPosition = billboard => {
+	if (!viewer || !billboard) return null
+	const position = billboard.position
+	if (position?.getValue) {
+		return position.getValue(viewer.clock.currentTime)
+	}
+	return position
+}
 
 const getDevicePosition = item => {
 	const longitudeRaw = item.longitude ?? item.lng ?? item.lon
@@ -117,254 +328,1011 @@
 	return { longitude, latitude }
 }
 
+const getDeviceRange = item => {
+	if (!item) return null
+	const rawRange = item.effectiveRangeKm ?? item.range ?? item.coverRadiusM
+	const range = Number(rawRange)
+	if (!Number.isFinite(range) || range <= 0) return null
+	return range
+}
+
+
+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 (partitionFillPrimitives.length) {
+		partitionFillPrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
+	}
+	if (partitionOutlinePrimitives.length) {
+		partitionOutlinePrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
+	}
+	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()
+	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 clearDefenseZoneEntities = () => {
-	if (!defenseZoneSource) return
-	defenseZoneSource.entities.removeAll()
+const ensureDeviceCollections = () => {
+	if (!viewer) return
+	ensureCockpitPrimitiveLayer()
+	if (!deviceBillboardCollection) {
+		deviceBillboardCollection = new Cesium.BillboardCollection()
+		addCockpitPrimitive(deviceBillboardCollection)
+	}
 }
+
 
 const clearPartitionEntities = () => {
-	if (!partitionSource) return
-	partitionSource.entities.removeAll()
-}
-
-
-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 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))
-	}
-	return positions
-}
-
-const addDeviceRings = (center, baseId) => {
-	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,
-			polygon: {
-				hierarchy: new Cesium.PolygonHierarchy(outerPositions, holes),
-				material,
-			},
-		})
-	})
-}
-
-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)
-		)
-	}
-}
-
-const renderDeviceEntities = devices => {
 	if (!viewer) return
-	clearDeviceEntities()
+	if (partitionFillPrimitives.length) {
+		partitionFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+		partitionFillPrimitives = []
+	}
+	if (partitionOutlinePrimitives.length) {
+		partitionOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+		partitionOutlinePrimitives = []
+	}
+}
+
+const clearAggregationEntities = () => {
+	if (!aggregationSource) return
+	aggregationSource.entities.removeAll()
+}
+
+const clearCommandPostEntities = () => {
+	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 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 (partitionFillPrimitives.length) {
+		partitionFillPrimitives.forEach(primitive => {
+			primitive.show = visible
+		})
+	}
+	if (partitionOutlinePrimitives.length) {
+		partitionOutlinePrimitives.forEach(primitive => {
+			primitive.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()
+}
+
+const setClusterVisibility = visible => {
+	clusterVisible.value = visible
+	updateAggregationVisibility()
+}
+
+const updateAggregationVisibility = () => {
+	if (!aggregationSource) return
+	aggregationSource.show = clusterVisible.value && adminBoundaryVisible.value
+}
+
+const setDroneVisibility = visible => {
+	if (droneTrackBillboardCollection) droneTrackBillboardCollection.show = visible
+	if (droneTrackPolylineCollection) droneTrackPolylineCollection.show = visible
+	if (droneTrackSource) droneTrackSource.show = visible
+	if (favoritePulseEntities.length) {
+		favoritePulseEntities.forEach(pulse => {
+			if (pulse?.entity) pulse.entity.show = visible
+		})
+	}
+	if (!visible && selectedTargetType.value === 'drone') {
+		closePopup()
+	}
+}
+const getStageByHeight = height => {
+	if (height == null) return 'detail'
+	if (height >= CLUSTER_HEIGHT) return 'cluster'
+	if (height <= DETAIL_HEIGHT) return 'detail'
+	return 'mid'
+}
+
+const ensureCountyCenterMap = () => {
+	if (countyCenterMap.size) return
+	const geojson = JSON.parse(jaGeojsonRaw)
+	geojson.features?.forEach(feature => {
+		const name = feature?.properties?.name
+		const center = feature?.properties?.centroid || feature?.properties?.center
+		if (!name || !Array.isArray(center) || center.length < 2) return
+		countyCenterMap.set(name, { longitude: center[0], latitude: center[1] })
+	})
+}
+
+const buildSimulatedTrackPoints = center => {
+	const basePoints = [
+		[
+			{
+				longitude: 114.963191,
+				latitude: 27.136716,
+				height: 120
+			},
+			{
+				longitude: 114.957308,
+				latitude: 27.138452,
+				height: 120
+			},
+			{
+				longitude: 114.952,
+				latitude: 27.136317,
+				height: 120
+			},
+			{
+				longitude: 114.949293,
+				latitude: 27.133864,
+				height: 120
+			},
+			{
+				longitude: 114.944666,
+				latitude: 27.130526,
+				height: 120
+			},
+			{
+				longitude: 114.945909,
+				latitude: 27.127845,
+				height: 120
+			},
+			{
+				longitude: 114.962974,
+				latitude: 27.136242,
+				height: 120
+			},
+		],
+
+		[
+			{
+				longitude: 114.931063,
+				latitude: 27.095052,
+				height: 120
+			},
+			{
+				longitude: 114.928653,
+				latitude: 27.096307,
+				height: 120
+			},
+			{
+				longitude: 114.925958,
+				latitude: 27.096963,
+				height: 120
+			},
+			{
+				longitude: 114.925836, 
+				latitude: 27.096102,
+				height: 120
+			},
+			{
+				longitude: 114.928384, 
+				latitude: 27.095866,
+				height: 120
+			},
+			{
+				longitude: 114.929874, 
+				latitude: 27.096095,
+				height: 120
+			},
+			{
+				longitude: 114.931048, 
+				latitude: 27.097666,
+				height: 120
+			},
+		]
+	]
+
+	return basePoints[center.trackIndex]
+}
+
+
+const clearDroneTrackEntities = () => {
+	if (!viewer) return
+	stopDroneTrackAnimation()
+	stopFavoritePulseAnimation()
+	if (droneTrackBillboardCollection) {
+		if (!droneTrackBillboardCollection.isDestroyed?.()) {
+			removeCockpitPrimitive(droneTrackBillboardCollection)
+		}
+		droneTrackBillboardCollection = null
+	}
+	if (droneTrackPolylineCollection) {
+		if (!droneTrackPolylineCollection.isDestroyed?.()) {
+			removeCockpitPrimitive(droneTrackPolylineCollection)
+		}
+		droneTrackPolylineCollection = null
+	}
+	if (droneTrackSource) {
+		viewer.dataSources.remove(droneTrackSource)
+		droneTrackSource = 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()
+	}
+}
+
+
+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 ensureDroneTrackSource = () => {
+	if (!viewer) return
+	if (!droneTrackSource) {
+		droneTrackSource = new Cesium.CustomDataSource('droneTrackSource')
+		viewer.dataSources.add(droneTrackSource)
+	}
+}
+
+const setupDroneTrackClock = () => {
+	if (!viewer) return
+	const startTime = Cesium.JulianDate.now()
+	const stopTime = Cesium.JulianDate.addSeconds(startTime, DRONE_TRACK_DURATION_S, new Cesium.JulianDate())
+	viewer.clock.startTime = startTime
+	viewer.clock.stopTime = stopTime
+	viewer.clock.currentTime = startTime
+	viewer.clock.multiplier = 1
+	viewer.clock.clockStep = Cesium.ClockStep.SYSTEM_CLOCK_MULTIPLIER
+	viewer.clock.clockRange = Cesium.ClockRange.CLAMPED
+}
+
+const updateDroneTrackPositions = elapsed => {
+	if (!viewer || !viewer.scene || !droneTrackRuntime.length) return
+	droneTrackRuntime.forEach(track => {
+		if (!track?.billboard || track.billboard.isDestroyed?.()) return
+		if (!track?.polyline) return
+		if (!track.positions || track.positions.length < 2) return
+		const duration = track.duration
+		if (duration <= 0) return
+		const t = Math.min(Math.max(elapsed, 0), duration)
+		const seg = Math.min(track.positions.length - 2, Math.floor(t / track.segmentDuration))
+		const ratio = (t - seg * track.segmentDuration) / track.segmentDuration
+		const isEntityPosition = typeof track.billboard?.position?.getValue === 'function'
+		const pos = isEntityPosition
+			? getBillboardPosition(track.billboard)
+			: Cesium.Cartesian3.lerp(
+					track.positions[seg],
+					track.positions[seg + 1],
+					ratio,
+					new Cesium.Cartesian3()
+			  )
+		if (!pos) return
+		if (!isEntityPosition) {
+			track.billboard.position = pos
+		}
+		const pathCount = Math.min(track.positions.length - 1, Math.floor(t / track.segmentDuration))
+		const pathPositions = track.positions.slice(0, pathCount + 1)
+		track.polyline.positions = [...pathPositions, pos]
+	})
+}
+
+const startDroneTrackAnimation = () => {
+	if (!viewer) return
+	stopDroneTrackAnimation()
+	droneTrackStartTime = viewer.clock.startTime
+	viewer.clock.shouldAnimate = true
+	droneTrackAnimStartAt = performance.now()
+	droneTrackLastTickAt = droneTrackAnimStartAt
+	const renderTick = now => {
+		if (!viewer || viewer.isDestroyed?.()) {
+			droneTrackRafId = null
+			return
+		}
+		const deltaSeconds = Math.max(0, (now - droneTrackLastTickAt) / 1000)
+		droneTrackLastTickAt = now
+		if (viewer.clock?.currentTime && viewer.clock?.stopTime) {
+			const nextTime = Cesium.JulianDate.addSeconds(
+				viewer.clock.currentTime,
+				deltaSeconds * (viewer.clock.multiplier || 1),
+				new Cesium.JulianDate()
+			)
+			if (Cesium.JulianDate.greaterThan(nextTime, viewer.clock.stopTime)) {
+				viewer.clock.currentTime = viewer.clock.stopTime
+			} else {
+				viewer.clock.currentTime = nextTime
+			}
+		}
+		const elapsed = Cesium.JulianDate.secondsDifference(viewer.clock.currentTime, droneTrackStartTime)
+		updateDroneTrackPositions(elapsed)
+		viewer.scene?.requestRender?.()
+		droneTrackRafId = requestAnimationFrame(renderTick)
+	}
+	droneTrackRafId = requestAnimationFrame(renderTick)
+}
+
+const stopDroneTrackAnimation = () => {
+	droneTrackTickHandler = null
+	if (droneTrackRafId) {
+		cancelAnimationFrame(droneTrackRafId)
+		droneTrackRafId = 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()
+	if (!list?.length) return
+	ensureDroneTrackCollections()
+	ensureDroneTrackSource()
+	setupDroneTrackClock()
+	droneTrackBillboardCollection.show = detailVisible.value
+	droneTrackPolylineCollection.show = detailVisible.value
+	droneTrackRuntime = []
+	const baseTrackColor = Cesium.Color.fromCssColorString('red')
+		; (list || []).forEach((item, trackIndex) => {
+			const position = getDevicePosition(item)
+			if (!position) return
+			const points = buildSimulatedTrackPoints({ ...position, height: item.flightHeightM, trackIndex })
+			if (points.length < 2) return
+			const positions = points.map(point =>
+				Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
+			)
+			const segmentDuration = DRONE_TRACK_DURATION_S / Math.max(positions.length - 1, 1)
+			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 })
+			}
+			const polyline = droneTrackPolylineCollection.add({
+				positions: [positions[0]],
+				width: 3,
+				material: trackMaterial,
+			})
+			const droneId = `drone-alarm-${item?.alarmRecordId ?? item?.id ?? trackIndex}`
+			const startTime = viewer.clock.startTime
+			const stopTime = viewer.clock.stopTime
+			const positionProperty = new Cesium.SampledPositionProperty()
+			positions.forEach((pos, index) => {
+				const sampleTime = Cesium.JulianDate.addSeconds(
+					startTime,
+					index * segmentDuration,
+					new Cesium.JulianDate()
+				)
+				positionProperty.addSample(sampleTime, pos)
+			})
+			positionProperty.setInterpolationOptions({
+				interpolationDegree: 1,
+				interpolationAlgorithm: Cesium.LinearApproximation,
+			})
+			const entity = droneTrackSource.entities.add({
+				id: droneId,
+				availability: new Cesium.TimeIntervalCollection([
+					new Cesium.TimeInterval({
+						start: startTime,
+						stop: stopTime,
+					}),
+				]),
+				position: positionProperty,
+				orientation: new Cesium.VelocityOrientationProperty(positionProperty),
+				billboard: {
+					image: droneIcon,
+					width: 36,
+					height: 36,
+					verticalOrigin: Cesium.VerticalOrigin.CENTER,
+					disableDepthTestDistance: Number.POSITIVE_INFINITY,
+				},
+			})
+			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: entity,
+			})
+			if (isFavorited(item)) {
+				createFavoritePulseEntity(entity)
+			}
+			droneTrackRuntime.push({
+				positions,
+				polyline,
+				billboard: entity,
+				segmentDuration,
+				duration: DRONE_TRACK_DURATION_S,
+			})
+		})
+	startDroneTrackAnimation()
+	startFavoritePulseAnimation()
+	reorderCockpitPrimitives()
+}
+
+const renderDeviceEntities = async devices => {
+	if (!viewer) return
+	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 deviceEntries = []
+	const devicePositions = []
 	devices.forEach((item, index) => {
+		const isOnline = String(item.status) === '0'
+
 		const position = getDevicePosition(item)
 		if (!position) return
-		const entityId = `online-device-${item.id ?? index}`
-		deviceEntityIds.add(entityId)
-		addDeviceRings(position, entityId)
-		viewer.entities.add({
-			id: entityId,
+		const entityId = `online-device-${item.id ?? index}-${index}`
+		const rangeMeters = getDeviceRange(item)
+		devicePositions.push({ lng: position.longitude, lat: position.latitude })
+		const billboard = deviceBillboardCollection.add({
 			position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
-			billboard: {
-				image: equipmentIcon,
-				width: 40.34,
-				height: 40.34,
-			},
+			image: isOnline ? equipmentIcon : offlineEquipmentIcon,
+			width: 40,
+			height: 56,
+			verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
 		})
+		billboard.id = entityId
+		devicePickMap.set(entityId, { data: item, billboard })
+		deviceEntries.push({ position, rangeMeters, billboard, isOnline })
 	})
-}
-
-const getDefenseZonePositions = geom => {
-	const points = geomAnalysis(geom)
-	if (points.length < 3) return []
-	const first = points[0]
-	const last = points[points.length - 1]
-	const list =
-		first && last && first.longitude === last.longitude && first.latitude === last.latitude
-			? points.slice(0, -1)
-			: points
-	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) => {
-		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,
-			},
+	if (selectedTargetType.value === 'device' && selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) {
+		closePopup()
+	}
+	if (deviceEntries.length) {
+		const heights = await getPointPositionsHeight(devicePositions, viewer)
+		deviceEntries.forEach((entry, index) => {
+			const height = Number(heights?.[index]?.ASL)
+			const centerHeight = Number.isFinite(height) ? height : 0
+			if (entry.billboard) {
+				entry.billboard.position = Cesium.Cartesian3.fromDegrees(
+					entry.position.longitude,
+					entry.position.latitude,
+					centerHeight
+				)
+			}
+			if (!entry.isOnline) return
+			if (!Number.isFinite(entry.rangeMeters) || entry.rangeMeters <= 0) return
+			const primitive = createDeviceRangePrimitiveWithHeight(entry.position, entry.rangeMeters, centerHeight)
+			if (!primitive) return
+			primitive.show = detailVisible.value
+			addCockpitPrimitive(primitive)
+			deviceRingFillPrimitives.push(primitive)
 		})
-	})
-}
-
-const renderDefenseZones = zones => {
-	if (!viewer) return
-	if (!defenseZoneSource) {
-		defenseZoneSource = new Cesium.CustomDataSource('defenseZoneSource')
-		viewer.dataSources.add(defenseZoneSource)
 	}
-	clearDefenseZoneEntities()
-	renderZonePolygons({
-		zones,
-		source: defenseZoneSource,
-		idPrefix: 'defense-zone',
-		lineColor: '#19D266',
-		fillGradient: ['#2AEDBF', '#012B11'],
-	})
+	reorderCockpitPrimitives()
 }
 
-const renderPartitions = zones => {
-	if (!viewer) return
-	if (!partitionSource) {
-		partitionSource = new Cesium.CustomDataSource('partitionSource')
-		viewer.dataSources.add(partitionSource)
-	}
-	clearPartitionEntities()
-	renderZonePolygons({
-		zones,
-		source: partitionSource,
-		idPrefix: 'partition-zone',
-		lineColor: '#FFCD2A',
-		fillGradient: ['#FFC609', '#583300'],
-	})
+const elevatePositions = async (positions, height = POLYGON_HEIGHT_M) => {
+	if (!Array.isArray(positions) || !positions.length) return []
+	if (!viewer) return positions
+	const points = positions
+		.map(pos => {
+			const carto = Cesium.Cartographic.fromCartesian(pos)
+			if (!carto) return null
+			return {
+				lng: Cesium.Math.toDegrees(carto.longitude),
+				lat: Cesium.Math.toDegrees(carto.latitude),
+			}
+		})
+		.filter(Boolean)
+	if (!points.length) return positions
+	const heights = await getPointPositionsHeight(points, viewer)
+	return heights.map(item => Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude, (item.ASL ?? 0) + height))
 }
 
-const loadDefenseZones = async () => {
-	if (!viewer) return
+const getAreaTypeStyle = areaType => {
+	return AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
+}
+
+const buildPartitionPrimitives = async shapes => {
+	if (!viewer) return { primitives: [], outlinePrimitives: [] }
+	const vertexFormat = getTexturedVertexFormat()
+	const groups = new Map()
+	const tasks = []
+		; (shapes || []).forEach(shape => {
+			if (!shape?.positions || shape.positions.length < 3) return
+			const outlineColor = shape.outlineColor
+			const fillColor = shape.fillColor
+			const key = `${outlineColor.toCssColorString()}|${fillColor.toCssColorString()}`
+			if (!groups.has(key)) {
+				groups.set(key, { outlineColor, fillColor, polygonInstances: [], lineInstances: [] })
+			}
+			const group = groups.get(key)
+			const positions = shape.positions
+			tasks.push(
+				elevatePositions(positions).then(elevatedPositions => {
+					if (!elevatedPositions.length) return
+					const polygon = new Cesium.PolygonGeometry({
+						polygonHierarchy: new Cesium.PolygonHierarchy(elevatedPositions),
+						perPositionHeight: true,
+						vertexFormat,
+					})
+					group.polygonInstances.push(
+						new Cesium.GeometryInstance({
+							geometry: polygon,
+						})
+					)
+					const linePositions =
+						elevatedPositions.length > 1
+							? [...elevatedPositions, elevatedPositions[0]]
+							: elevatedPositions
+					group.lineInstances.push(
+						new Cesium.GeometryInstance({
+							geometry: new Cesium.PolylineGeometry({
+								positions: linePositions,
+								width: 2,
+							}),
+							attributes: {
+								color: Cesium.ColorGeometryInstanceAttribute.fromColor(outlineColor),
+							},
+						})
+					)
+				})
+			)
+		})
+
+	if (tasks.length) await Promise.all(tasks)
+
+	const primitives = []
+	const outlinePrimitives = []
+	groups.forEach(group => {
+		if (group.polygonInstances.length) {
+			const material = createRadialGradientMaterial(group.outlineColor, group.fillColor, {
+				gamma: 1.7,
+				innerCutoff: 0,
+			})
+			const primitive = new Cesium.Primitive({
+				geometryInstances: group.polygonInstances,
+				appearance: new Cesium.MaterialAppearance({
+					material,
+					translucent: true,
+				}),
+			})
+			addCockpitPrimitive(primitive)
+			primitives.push(primitive)
+		}
+		if (group.lineInstances.length) {
+			const outlinePrimitive = new Cesium.Primitive({
+				geometryInstances: group.lineInstances,
+				appearance: new Cesium.PolylineColorAppearance(),
+			})
+			addCockpitPrimitive(outlinePrimitive)
+			outlinePrimitives.push(outlinePrimitive)
+		}
+	})
+
+	return { primitives, outlinePrimitives }
+}
+
+const parseGeomJson = geomJson => {
+	if (!geomJson) return null
+	if (typeof geomJson === 'object') return geomJson
+	if (typeof geomJson !== 'string') return null
+	const trimmed = geomJson.trim()
+	if (!trimmed) return null
 	try {
-		const res = await fwDefenseZonePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE })
-		renderDefenseZones(res?.data?.data?.records ?? [])
-	} catch (error) {
-		renderDefenseZones([])
+		return JSON.parse(trimmed)
+	} catch (error) {}
+	return null
+}
+
+const normalizeShapePoint = point => {
+	if (!point) return null
+	const lng = point?.lng ?? point?.longitude
+	const lat = point?.lat ?? point?.latitude
+	const height = Number.isFinite(Number(point?.height)) ? Number(point.height) : 0
+	if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
+	return { lng: Number(lng), lat: Number(lat), height }
+}
+
+const buildShapePositions = (points = []) => {
+	const normalized = points.map(normalizeShapePoint).filter(Boolean)
+	return normalized.map(point => Cesium.Cartesian3.fromDegrees(point.lng, point.lat, point.height))
+}
+
+const getShapeDisplayPoints = shape => {
+	if (Array.isArray(shape?.displayPoints) && shape.displayPoints.length) {
+		return shape.displayPoints
 	}
+	return shape?.points || []
+}
+
+const resolvePartitionShapes = areas => {
+	const shapes = []
+		; (areas || []).forEach(area => {
+			const extList = Array.isArray(area?.fwAreaDivideExtList) ? area.fwAreaDivideExtList : []
+			extList.forEach((item, index) => {
+				const isShapePayload = item?.drawType || item?.points
+				const parsed = isShapePayload ? item : parseGeomJson(item?.geomJson)
+				if (!parsed) return
+				const shape = {
+					id: parsed?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
+					drawType: parsed?.drawType ?? 'polygon',
+					areaType: parsed?.areaType ?? item?.areaTypeKey ?? item?.areaType ?? '',
+					points: Array.isArray(parsed?.points) ? parsed.points : [],
+					displayPoints: Array.isArray(parsed?.displayPoints) ? parsed.displayPoints : null,
+					meta: parsed?.meta ?? null,
+				}
+				if (shape.drawType === 'buffer' && shape.meta?.bufferRadii?.length && shape.meta?.center) {
+					const center = shape.meta.center
+					const centerCartesian = Cesium.Cartesian3.fromDegrees(
+						center.lng,
+						center.lat,
+						center.height || 0
+					)
+					const radii = shape.meta.bufferRadii
+						.map(radius => Number(radius))
+						.filter(radius => Number.isFinite(radius) && radius > 0)
+					radii.forEach((radius, levelIndex) => {
+						const positions = buildEllipsePositions(centerCartesian, radius, radius)
+					const style = BUFFER_LEVEL_STYLES[levelIndex] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
+						if (positions.length >= 3) {
+							shapes.push({
+								positions,
+								fillColor: style.fill,
+								outlineColor: style.outline,
+							})
+						}
+					})
+					return
+				}
+				const positions = buildShapePositions(getShapeDisplayPoints(shape))
+				if (positions.length >= 3) {
+					const style = getAreaTypeStyle(shape.areaType)
+					shapes.push({
+						positions,
+						fillColor: style.fill,
+						outlineColor: style.outline,
+					})
+				}
+			})
+		})
+	return shapes
+}
+
+const renderPartitions = async zones => {
+	if (!viewer) return
+	clearPartitionEntities()
+	const shapes = resolvePartitionShapes(zones)
+	const result = await buildPartitionPrimitives(shapes)
+	partitionFillPrimitives = result.primitives
+	partitionOutlinePrimitives = result.outlinePrimitives
+	if (partitionFillPrimitives.length) {
+		partitionFillPrimitives.forEach(primitive => {
+			primitive.show = detailVisible.value
+		})
+	}
+	if (partitionOutlinePrimitives.length) {
+		partitionOutlinePrimitives.forEach(primitive => {
+			primitive.show = detailVisible.value
+		})
+	}
+	reorderCockpitPrimitives()
 }
 
 const loadPartitions = async () => {
 	if (!viewer) return
 	try {
-		const res = await fwAreaDividePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE })
-		renderPartitions(res?.data?.data?.records ?? [])
+		const res = await fwAreaDivideListApi({
+			isSetSceneManage: 1,
+			flyTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+		})
+		await renderPartitions(res?.data?.data ?? [])
 	} catch (error) {
-		renderPartitions([])
+		await renderPartitions([])
+	}
+}
+
+const renderAggregation = list => {
+	if (!viewer) return
+	ensureCountyCenterMap()
+	if (!aggregationSource) {
+		aggregationSource = new Cesium.CustomDataSource('aggregationSource')
+		viewer.dataSources.add(aggregationSource)
+	}
+	clearAggregationEntities()
+	updateAggregationVisibility()
+	const countMap = new Map()
+		; (list || []).forEach(item => {
+			if (!item?.type) return
+			countMap.set(item.type, Number(item.count ?? 0))
+		})
+	Array.from(countyCenterMap.entries()).forEach(([name, center], index) => {
+		const position = Cesium.Cartesian3.fromDegrees(center.longitude, center.latitude, 0)
+		const count = countMap.get(name) ?? 0
+		aggregationSource.entities.add({
+			id: `aggregation-${name}-${index}`,
+			position,
+			billboard: {
+				image: aggregationIcon,
+				width: 66.73,
+				height: 43,
+				verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+				// disableDepthTestDistance: Number.POSITIVE_INFINITY,
+			},
+			label: {
+				text: `${count}`,
+				fillColor: Cesium.Color.WHITE,
+				style: Cesium.LabelStyle.FILL_AND_OUTLINE,
+				verticalOrigin: Cesium.VerticalOrigin.CENTER,
+				horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
+				font: '11pt Source Han Sans CN',
+				eyeOffset: new Cesium.Cartesian3(0, 0, -20), // 让label "浮" 在广告牌前面
+
+				pixelOffset: new Cesium.Cartesian2(0, -35),
+				disableDepthTestDistance: Number.POSITIVE_INFINITY,
+			},
+		})
+	})
+}
+
+const getPickedTarget = picks => {
+	for (const pick of picks) {
+		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 || !selectedDeviceBillboard) return
+	const cartesian = getBillboardPosition(selectedDeviceBillboard)
+	if (!cartesian) return
+	const screenPosition = viewer.scene.cartesianToCanvasCoordinates(cartesian)
+	if (!screenPosition) return
+	popupPosition.value = { x: screenPosition.x, y: screenPosition.y }
+}
+
+const startPopupRender = () => {
+	if (!viewer || popupRenderHandler) return
+	popupRenderHandler = () => updatePopupPosition()
+	viewer.scene.postRender.addEventListener(popupRenderHandler)
+	updatePopupPosition()
+}
+
+const stopPopupRender = () => {
+	if (!viewer || !popupRenderHandler) return
+	viewer.scene.postRender.removeEventListener(popupRenderHandler)
+	popupRenderHandler = null
+}
+
+const handleDeviceClick = movement => {
+	if (!viewer) return
+	const picks = viewer.scene.drillPick(movement.position) || []
+	const pickedTarget = getPickedTarget(picks)
+	if (!pickedTarget) {
+		closePopup()
+		return
+	}
+	selectedTargetType.value = pickedTarget.type
+	selectedDevice.value = pickedTarget.data
+	selectedDeviceBillboard = pickedTarget.billboard
+	startPopupRender()
+}
+
+const initDeviceClickHandler = () => {
+	if (deviceClickHandler || !viewer) return
+	deviceClickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
+	deviceClickHandler.setInputAction(handleDeviceClick, Cesium.ScreenSpaceEventType.LEFT_CLICK)
+}
+
+const destroyDeviceClickHandler = () => {
+	deviceClickHandler?.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK)
+	deviceClickHandler?.destroy()
+	deviceClickHandler = null
+}
+
+function closePopup () {
+	selectedDevice.value = null
+	selectedDeviceBillboard = null
+	selectedTargetType.value = 'device'
+	stopPopupRender()
+}
+
+const popupPosition = ref({ x: 0, y: 0 })
+const popupVisible = computed(() => Boolean(selectedDevice.value))
+const isDronePopup = computed(() => selectedTargetType.value === 'drone')
+const handleDroneFavorite = () => {
+	if (!selectedDevice.value) return
+	emit('droneFavorite', selectedDevice.value)
+}
+const handleDroneSignal = () => emit('droneSignal', selectedDevice.value)
+const handleDroneCounter = () => emit('droneCounter', selectedDevice.value)
+
+const renderCommandPosts = list => {
+	if (!viewer) return
+	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 newCockpitAggregationApi({
+			effectiveRangeKmIsNotNull: 1
+		})
+		renderAggregation(res?.data?.data ?? [])
+	} catch (error) {
+		renderAggregation([])
+	}
+}
+
+const loadCommandPosts = async () => {
+	try {
+		const res = await fwDefenseSceneManageListApi({
+			time: dayjs().format('YYYY-MM-DD HH:mm:ss')
+		})
+		renderCommandPosts(res?.data?.data ?? [])
+	} catch (error) {
+		renderCommandPosts([])
 	}
 }
 
 
 watch(
-	() => props.onlineDevices,
+	() => props.allDevices,
 	devices => {
 		renderDeviceEntities(devices || [])
+	},
+	{ deep: true }
+)
+
+watch(
+	() => props.alarmDrones,
+	list => {
+		renderSimulatedDroneTrack(list || [])
+		if (selectedTargetType.value === 'drone' && selectedDevice.value) {
+			const selectedId = selectedDevice.value.alarmRecordId ?? selectedDevice.value.id
+			const match = (list || []).find(item => (item?.alarmRecordId ?? item?.id) === selectedId)
+			if (match) selectedDevice.value = { ...selectedDevice.value, ...match }
+		}
 	},
 	{ deep: true }
 )
@@ -381,11 +1349,56 @@
 	showLayerPanel.value = !showLayerPanel.value
 }
 
-const handleMapReady = ({ viewer: mapViewer }) => {
+const applyLayerVisibility = checkedKeys => {
+	publicCesium?.switchLayers?.(baseLayerKey.value === 'base-standard' ? 0 : 4)
+	const showTerrain = checkedKeys.includes('ja-terrain')
+	publicCesium?.setTerrainVisible?.(showTerrain)
+	const showAdmin = checkedKeys.includes('admin')
+	adminBoundaryVisible.value = showAdmin
+	mapRef.value?.setAdminBoundaryVisible?.(showAdmin)
+	updateAggregationVisibility()
+}
+
+const handleLayerCheck = (_data, state) => {
+	const checkedKeys = state?.checkedKeys ?? layerTreeRef.value?.getCheckedKeys?.() ?? []
+	treeCheckedKeys.value = checkedKeys
+	applyLayerVisibility(checkedKeys)
+}
+
+const handleBaseLayerSelect = key => {
+	if (!baseLayerKeys.includes(key)) return
+	if (baseLayerKey.value === key) return
+	baseLayerKey.value = key
+	applyLayerVisibility(treeCheckedKeys.value)
+}
+
+const updateStageDisplay = stage => {
+	const showCluster = stage === 'cluster'
+	setClusterVisibility(showCluster)
+	setDetailVisibility(!showCluster)
+	setDroneVisibility(!showCluster)
+}
+
+const handleMapReady = ({ viewer: mapViewer, publicCesium: mapPublic }) => {
+	if (mapReadyHandled) return
+	mapReadyHandled = true
 	viewer = mapViewer
-	renderDeviceEntities(props.onlineDevices)
-	loadDefenseZones()
+	publicCesium = mapPublic
+	ensureCockpitPrimitiveLayer()
+	applyLayerVisibility(treeCheckedKeys.value)
+	const height = viewer?.camera?.positionCartographic?.height
+	const stage = getStageByHeight(height)
+	updateStageDisplay(stage)
+	renderDeviceEntities(props.allDevices)
 	loadPartitions()
+	loadAggregation()
+	loadCommandPosts()
+	renderSimulatedDroneTrack(props.alarmDrones)
+	initDeviceClickHandler()
+}
+
+const handleStageChange = stage => {
+	updateStageDisplay(stage)
 }
 
 const handleClickOutside = event => {
@@ -404,13 +1417,28 @@
 onBeforeUnmount(() => {
 	document.removeEventListener('click', handleClickOutside)
 	clearDeviceEntities()
-	clearDefenseZoneEntities()
 	clearPartitionEntities()
+	clearAggregationEntities()
+	clearCommandPostEntities()
+	clearDroneTrackEntities()
+	closePopup()
+	destroyDeviceClickHandler()
+	if (cockpitPrimitiveLayer && viewer?.scene?.primitives) {
+		viewer.scene.primitives.remove(cockpitPrimitiveLayer)
+	}
+	cockpitPrimitiveLayer = null
 	viewer = null
+	publicCesium = null
 })
 </script>
 
 <style lang="scss" scoped>
+.map-shell {
+	position: relative;
+	width: 100%;
+	height: 100%;
+}
+
 .map-container {
 	position: absolute;
 	top: 0;
@@ -483,85 +1511,50 @@
 		overflow: auto;
 	}
 
-	::v-deep(.el-tree) {
-		background: transparent;
-		color: #c3c3dd;
+	.base-map-title {
+		margin-bottom: 8px;
 		font-size: 12px;
+		color: #ffffff;
+		font-weight: 600;
+	}
 
-		.el-tree-node {
-			line-height: 30px !important;
+	.base-map-options {
+		padding: 16px;
+		display: grid;
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+		gap: 8px;
+	}
 
-			.el-tree-node__content {
-				padding-left: 0 !important;
-				display: flex;
-				align-items: center;
-				height: 40px !important;
-				line-height: 40px !important;
-				border-bottom: 1px solid rgba(70, 70, 100, 0.5);
-				box-sizing: border-box;
+	.base-map-card {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+		gap: 6px;
+		border-radius: 6px;
+		color: #d8e6ff;
+		font-size: 12px;
+		cursor: pointer;
+		transition: all 0.2s ease;
+
+		.base-map-thumb {
+			width: 46px;
+			height: 46px;
+			border-radius: 4px;
+			background-size: cover;
+			background-position: center;
+			box-sizing: border-box;
+		}
+
+		&.active {
+			.base-map-thumb {
+				border: 2px solid #2ea8ff;
 			}
 
-			.el-tree-node__children {
-				.el-tree-node__content {
-					border: none;
-				}
-			}
-
-			&:focus {
-				.el-tree-node__content {
-					background: transparent !important;
-				}
+			.base-map-label {
+				color: #2ea8ff;
 			}
 		}
 	}
-}
-
-.layer-panel :deep(.el-tree-node__label) {
-	color: #c3c3dd;
-}
-
-.layer-panel :deep(.el-tree-node__expand-icon) {
-	order: 3;
-	margin-left: auto;
-}
-
-.layer-panel :deep(.el-tree-node__expand-icon.is-leaf) {
-	visibility: hidden;
-}
-
-.layer-panel :deep(.el-checkbox) {
-	order: 1;
-}
-
-.layer-panel :deep(.el-tree-node__label) {
-	order: 2;
-}
-
-.layer-panel :deep(.el-tree-node__expand-icon:not(.is-leaf) ~ .el-checkbox) {
-	display: none;
-}
-
-.layer-panel :deep(.el-tree-node__content:hover),
-.layer-panel :deep(.el-tree-node__content:focus) {
-	background: transparent !important;
-}
-
-.layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
-	background: transparent !important;
-	color: #ffffff;
-}
-
-.layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content .el-tree-node__label) {
-	color: #ffffff;
-}
-
-.layer-panel :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
-	background-color: #023aff;
-	border-color: #023aff;
-}
-
-.layer-panel :deep(.el-checkbox__inner) {
-	background-color: transparent;
-	border-color: #a1a3d4;
 }
 </style>

--
Gitblit v1.9.3