From affb54c33083cb09cd0ef9da137b446cafdfd0fb Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Tue, 27 Jan 2026 16:47:08 +0800
Subject: [PATCH] feat:用户管理、部门管理调整

---
 applications/drone-command/src/components/map-container/device-map-container.vue | 1280 +++++++++++++++++++++++++++++++++++++++++++++------------
 1 files changed, 1,005 insertions(+), 275 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..f7fb7e9 100644
--- a/applications/drone-command/src/components/map-container/device-map-container.vue
+++ b/applications/drone-command/src/components/map-container/device-map-container.vue
@@ -1,38 +1,46 @@
-<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>
 
@@ -42,11 +50,35 @@
 import { geomAnalysis } from '@ztzf/utils'
 import { fwDefenseZonePageApi } from '@/views/areaManage/defenseZone/defenseZoneApi'
 import { fwAreaDividePageApi } from '@/views/areaManage/partition/partitionApi'
+import { fwDefenseSceneListApi } from '@/views/areaManage/sceneConfig/sceneConfigApi'
+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 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'
+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 props = defineProps({
 	onlineDevices: {
+		type: Array,
+		default: () => [],
+	},
+	alarmDrones: {
 		type: Array,
 		default: () => [],
 	},
@@ -63,31 +95,71 @@
 		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 defenseZonePrimitive = null
+let defenseZoneOutlinePrimitive = null
+let partitionPrimitive = null
+let partitionOutlinePrimitive = null
+let aggregationSource = null
+let commandPostBillboardCollection = null
+let droneTrackBillboardCollection = null
+let droneTrackPolylineCollection = null
+let droneTrackTickHandler = null
+let droneTrackStartTime = null
+let droneTrackRuntime = []
+let favoritePulseRafId = null
+let favoritePulseStartAt = 0
+let favoritePulseElapsed = 0
+const favoritePulseEntities = []
+const pulseBaseColor = Cesium.Color.fromCssColorString('#FF3B30')
+const PULSE_MIN_RADIUS_M = 60
+const PULSE_MAX_RADIUS_M = 180
+const PULSE_DURATION_S = 1.6
+const detailVisible = ref(true)
+const clusterVisible = ref(false)
+const countyCenterMap = new Map()
 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 +177,60 @@
 			{ 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
+}
+
+const createFavoritePulseEntity = (billboard) => {
+	if (!viewer || !billboard) return
+	const material = new Cesium.ImageMaterialProperty({
+		image: getPulseCanvas(),
+		transparent: true,
+	})
+	const pulse = {
+		material,
+		offset: Math.random() * PULSE_DURATION_S,
+		entity: null,
+	}
+	pulse.entity = viewer.entities.add({
+		position: new Cesium.CallbackProperty(() => billboard.position, false),
+		ellipse: {
+			semiMajorAxis: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
+			semiMinorAxis: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
+			material,
+			heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+		},
+	})
+	favoritePulseEntities.push(pulse)
+}
 
 const getDevicePosition = item => {
 	const longitudeRaw = item.longitude ?? item.lng ?? item.lon
@@ -117,151 +242,541 @@
 	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 (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()
+	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()
-}
-
-
-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))
+	if (!viewer) return
+	if (partitionPrimitive) {
+		removeCockpitPrimitive(partitionPrimitive)
+		partitionPrimitive = null
 	}
-	return positions
+	if (partitionOutlinePrimitive) {
+		removeCockpitPrimitive(partitionOutlinePrimitive)
+		partitionOutlinePrimitive = null
+	}
 }
 
-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,
-			},
+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 (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()
+}
+
+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 (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] })
 	})
 }
 
-class RadialGradientMaterialProperty {
-	constructor(color1, color2) {
-		this._definitionChanged = new Cesium.Event()
-		this.color1 = color1
-		this.color2 = color2
-	}
+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
+			},
+		],
 
-	get isConstant() {
-		return true
-	}
+		[
+			{
+				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
+			},
+		]
+	]
 
-	get definitionChanged() {
-		return this._definitionChanged
-	}
+	return basePoints[center.trackIndex]
+}
 
-	getType() {
-		return MATERIAL_TYPE
-	}
 
-	getValue(time, result) {
-		const target = result || {}
-		target.color1 = this.color1
-		target.color2 = this.color2
-		return target
+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
+	}
+	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()
+	}
+}
 
-	equals(other) {
-		return (
-			other instanceof RadialGradientMaterialProperty &&
-			Cesium.Color.equals(this.color1, other.color1) &&
-			Cesium.Color.equals(this.color2, other.color2)
-		)
+
+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 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()
+	droneTrackBillboardCollection.show = detailVisible.value
+	droneTrackPolylineCollection.show = detailVisible.value
+	droneTrackRuntime = []
+	const segmentDuration = 6
+	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)
+			)
+			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-alarm-${item?.alarmRecordId ?? item?.id ?? trackIndex}`
+			const billboard = droneTrackBillboardCollection.add({
+				position: positions[positions.length - 1],
+				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: {
+				...item,
+				flightHeightM: item.flightHeightM ?? points[0].height,
+				flightSpeedMs: item.flightSpeedMs ?? speedMs,
+				longitude: item.longitude ?? points[0].longitude,
+				latitude: item.latitude ?? points[0].latitude,
+			},
+			billboard,
+		})
+		if (isFavorited(item)) {
+			createFavoritePulseEntity(billboard)
+		}
+			droneTrackRuntime.push({
+				positions,
+				billboard,
+				segmentDuration,
+				duration: (points.length - 1) * segmentDuration,
+			})
+		})
+	viewer.clock.multiplier = 1
+	startDroneTrackAnimation()
+	startFavoritePulseAnimation()
+	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)
-		viewer.entities.add({
-			id: entityId,
+		const entityId = `online-device-${item.id ?? index}-${index}`
+		const rangeMeters = getDeviceRange(item)
+		addDeviceRings(position, ringFillInstancesByStyle, ringOutlineInstancesByStyle, rangeMeters)
+		const billboard = deviceBillboardCollection.add({
 			position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
-			billboard: {
-				image: equipmentIcon,
-				width: 40.34,
-				height: 40.34,
-			},
+			image: equipmentIcon,
+			width: 40,
+			height: 56,
+			verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
 		})
+		billboard.id = entityId
+		devicePickMap.set(entityId, { data: item, billboard })
 	})
+	if (selectedTargetType.value === 'device' && selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) {
+		closePopup()
+	}
+	ringFillInstancesByStyle.forEach((instances, index) => {
+		if (!instances.length) return
+		const ring = RING_STYLES[index]
+		const alpha = ring.alpha ?? 0.64
+		const innerRatio =
+			typeof ring.innerRatio === 'number'
+				? ring.innerRatio
+				: ring.inner && ring.outer
+					? Math.min(Math.max(ring.inner / ring.outer, 0), 0.9)
+					: 0
+		const material = createRadialGradientMaterial(
+			Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(alpha),
+			Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(alpha),
+			{
+				gamma: 1.7,
+				innerCutoff: innerRatio,
+			}
+		)
+		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 => {
@@ -276,68 +791,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) => {
-		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 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 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()
-	renderZonePolygons({
-		zones,
-		source: defenseZoneSource,
-		idPrefix: 'defense-zone',
-		lineColor: '#19D266',
-		fillGradient: ['#2AEDBF', '#012B11'],
-	})
+	const result = buildZonePrimitives(zones, '#72F3FF', '#72F3FF', '#0A7C88')
+	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()
-	renderZonePolygons({
-		zones,
-		source: partitionSource,
-		idPrefix: 'partition-zone',
-		lineColor: '#FFCD2A',
-		fillGradient: ['#FFC609', '#583300'],
-	})
+	const result = buildZonePrimitives(zones, '#FFD772', '#FFD772', '#A86B00')
+	partitionPrimitive = result.primitive
+	partitionOutlinePrimitive = result.outlinePrimitive
+	if (partitionPrimitive) partitionPrimitive.show = detailVisible.value
+	if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = detailVisible.value
+	reorderCockpitPrimitives()
 }
 
 const loadDefenseZones = async () => {
@@ -360,11 +891,185 @@
 	}
 }
 
+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 = selectedDeviceBillboard.position
+	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 cockpitAggregationApi({
+			effectiveRangeKmIsNotNull: 1
+		})
+		renderAggregation(res?.data?.data ?? [])
+	} catch (error) {
+		renderAggregation([])
+	}
+}
+
+const loadCommandPosts = async () => {
+	try {
+		const res = await fwDefenseSceneListApi()
+		renderCommandPosts(res?.data?.data ?? [])
+	} catch (error) {
+		renderCommandPosts([])
+	}
+}
+
 
 watch(
 	() => props.onlineDevices,
 	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 +1086,55 @@
 	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 }) => {
 	viewer = mapViewer
+	publicCesium = mapPublic
+	ensureCockpitPrimitiveLayer()
+	applyLayerVisibility(treeCheckedKeys.value)
+	const height = viewer?.camera?.positionCartographic?.height
+	const stage = getStageByHeight(height)
+	updateStageDisplay(stage)
 	renderDeviceEntities(props.onlineDevices)
 	loadDefenseZones()
 	loadPartitions()
+	loadAggregation()
+	loadCommandPosts()
+	renderSimulatedDroneTrack(props.alarmDrones)
+	initDeviceClickHandler()
+}
+
+const handleStageChange = stage => {
+	updateStageDisplay(stage)
 }
 
 const handleClickOutside = event => {
@@ -406,11 +1155,27 @@
 	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 +1248,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