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 | 1129 ++++++++++++++++++++++++++--------------------------------
 1 files changed, 512 insertions(+), 617 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 6e584a0..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
@@ -2,84 +2,45 @@
 	<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="100000"
-			@ready="handleMapReady" @stage-change="handleStageChange" />
+			: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-title">图层管理</div>
-
 					<div class="panel-content">
-						<el-tree :data="layerTree" show-checkbox default-expand-all node-key="key"
-							:props="layerTreeProps" :default-checked-keys="defaultCheckedKeys" />
+						<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>
 
-		<div v-if="popupVisible" class="device-popup" :style="popupStyle" @click.stop>
-			<div class="device-popup__header">
-				<el-tooltip
-					class="device-popup__title-tooltip"
-					:content="popupTitle"
-					placement="top"
-					effect="dark"
-					:show-after="200"
-				>
-					<span class="device-popup__title">{{ popupTitleDisplay }}</span>
-				</el-tooltip>
+		<DevicePopup :visible="popupVisible && !isDronePopup" :position="popupPosition" :device="selectedDevice"
+			@close="closePopup" />
 
-				<img class="device-popup__close" :src="popupClose" alt="" @click.stop="closePopup">
-			</div>
-			<div class="device-popup__type">
-				<span class="value">{{ popupTypeText }}</span>
-			</div>
-
-			<div class="device-popup__subtitle">
-				<span class="device-popup__dot" :class="popupActionClass"></span>
-				<span class="device-popup__action">{{ popupActionText }}</span>
-			</div>
-
-			<div class="device-popup__rows">
-				<div class="device-popup__row">
-					<div class="device-popup__col">
-						<span class="label">状态</span>
-						<span class="value" :class="popupStatusClass">{{ popupStatusText }}</span>
-					</div>
-
-					<span class="divider">|</span>
-
-					<div class="device-popup__col">
-						<span class="label">电量</span>
-						<span class="value">{{ popupBatteryText }}</span>
-					</div>
-				</div>
-				<div class="device-popup__row">
-					<div class="device-popup__col">
-						<span class="label">方位角</span>
-						<span class="value">{{ popupAzimuthText }}</span>
-					</div>
-
-					<span class="divider">|</span>
-
-					<div class="device-popup__col">
-						<span class="label">俯仰角</span>
-						<span class="value">{{ popupElevationText }}</span>
-					</div>
-				</div>
-				<div class="device-popup__row">
-					<span class="label">侦测目标</span>
-					<span class="value">{{ popupDetectCountText }}</span>
-				</div>
-				<div class="device-popup__row">
-					<span class="label">有效范围</span>
-					<span class="value">{{ popupRangeText }}</span>
-				</div>
-			</div>
-		</div>
+		<DronePopup :visible="popupVisible && isDronePopup" :position="popupPosition" :drone="selectedDevice"
+			:favorite="Boolean(selectedDevice?.isFavorite)" @close="closePopup" @toggle-favorite="handleDroneFavorite"
+			@signal="handleDroneSignal" @counter="handleDroneCounter" />
 	</div>
 </template>
 
@@ -96,11 +57,28 @@
 import droneIcon from '@/assets/images/dataCockpit/map/drone.png'
 import aggregationIcon from '@/assets/images/dataCockpit/map/aggregation.png'
 import commandPostIcon from '@/assets/images/dataCockpit/map/command-post.png'
-import popupClose from '@/assets/images/dataCockpit/map/popup-close.png'
 import jaGeojsonRaw from '@/assets/geojson/ja.geojson?raw'
+import DevicePopup from './components/DevicePopup.vue'
+import DronePopup from './components/DronePopup.vue'
+import {
+	RING_STYLES,
+	addDeviceRings,
+	createDroneTrackMaterial,
+	createRadialGradientMaterial,
+	getTexturedVertexFormat,
+} from './device-map-materials'
+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: () => [],
 	},
@@ -117,13 +95,16 @@
 		default: true,
 	},
 })
+const emit = defineEmits(['droneSignal', 'droneCounter', 'droneFavorite'])
 
 const DEFAULT_ZONE_PAGE_SIZE = 999
 
 const mapRef = ref(null)
 let viewer = null
+let publicCesium = null
 let cockpitPrimitiveLayer = null
 const devicePickMap = new Map()
+const dronePickMap = new Map()
 let deviceBillboardCollection = null
 let deviceRingOutlinePrimitives = []
 let deviceRingFillPrimitives = []
@@ -138,12 +119,22 @@
 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
@@ -151,17 +142,24 @@
 	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: [
@@ -179,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
@@ -189,6 +240,14 @@
 	const latitude = Number(latitudeRaw)
 	if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return null
 	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 = () => {
@@ -311,6 +370,12 @@
 	}
 }
 
+const getPulseRadius = pulse => {
+	const phase = ((favoritePulseElapsed + pulse.offset) % PULSE_DURATION_S) / PULSE_DURATION_S
+	const wave = phase < 0.5 ? phase * 2 : (1 - phase) * 2
+	return PULSE_MIN_RADIUS_M + (PULSE_MAX_RADIUS_M - PULSE_MIN_RADIUS_M) * wave
+}
+
 const setDetailVisibility = visible => {
 	detailVisible.value = visible
 	if (defenseZonePrimitive) defenseZonePrimitive.show = visible
@@ -334,7 +399,31 @@
 
 const setClusterVisibility = visible => {
 	clusterVisible.value = visible
-	if (aggregationSource) aggregationSource.show = 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 = () => {
@@ -348,47 +437,128 @@
 	})
 }
 
-const buildSimulatedTrackPoints = base => {
-	ensureCountyCenterMap()
-	const anchor = base || { longitude: 116.397, latitude: 39.908 }
-	const offsets = [
-		[0, 0, 120],
-		[0.02, 0.012, 150],
-		[0.04, 0.006, 180],
-		[0.06, -0.008, 200],
-		[0.03, -0.02, 160],
-		[0.0, -0.015, 140],
-		[-0.02, -0.005, 120],
-		[-0.01, 0.01, 130],
+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 offsets.map(item => ({
-		longitude: anchor.longitude + item[0],
-		latitude: anchor.latitude + item[1],
-		height: item[2],
-	}))
+
+	return basePoints[center.trackIndex]
 }
 
 
 const clearDroneTrackEntities = () => {
 	if (!viewer) return
+	stopDroneTrackAnimation()
+	stopFavoritePulseAnimation()
 	if (droneTrackBillboardCollection) {
-		droneTrackBillboardCollection.removeAll()
-		removeCockpitPrimitive(droneTrackBillboardCollection)
+		if (!droneTrackBillboardCollection.isDestroyed?.()) {
+			removeCockpitPrimitive(droneTrackBillboardCollection)
+		}
 		droneTrackBillboardCollection = null
 	}
 	if (droneTrackPolylineCollection) {
-		droneTrackPolylineCollection.removeAll()
-		removeCockpitPrimitive(droneTrackPolylineCollection)
+		if (!droneTrackPolylineCollection.isDestroyed?.()) {
+			removeCockpitPrimitive(droneTrackPolylineCollection)
+		}
 		droneTrackPolylineCollection = null
 	}
 	droneTrackRuntime = []
-	stopDroneTrackAnimation()
+	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)
@@ -404,10 +574,17 @@
 	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
@@ -432,168 +609,96 @@
 	droneTrackTickHandler = null
 }
 
-const renderSimulatedDroneTrack = () => {
+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
-	ensureCountyCenterMap()
+	clearDroneTrackEntities()
+	if (!list?.length) return
 	ensureDroneTrackCollections()
-	droneTrackBillboardCollection.removeAll()
-	droneTrackPolylineCollection.removeAll()
+	droneTrackBillboardCollection.show = detailVisible.value
+	droneTrackPolylineCollection.show = detailVisible.value
 	droneTrackRuntime = []
-	const centers = Array.from(countyCenterMap.values())
-	const baseCenters = centers.length ? centers.slice(0, 4) : [{ longitude: 116.397, latitude: 39.908 }]
 	const segmentDuration = 6
-	baseCenters.forEach((center, trackIndex) => {
-		const points = buildSimulatedTrackPoints(center)
-		if (points.length < 2) return
-		const positions = points.map(point =>
-			Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
-		)
-		droneTrackPolylineCollection.add({
-			positions,
-			width: 2,
-			material: Cesium.Material.fromType('Color', {
-				color: Cesium.Color.fromCssColorString('#2AEDBF'),
-			}),
-		})
-		const billboard = droneTrackBillboardCollection.add({
-			position: positions[0],
-			image: droneIcon,
-			width: 36,
-			height: 36,
-			verticalOrigin: Cesium.VerticalOrigin.CENTER,
-		})
-		droneTrackRuntime.push({
-			positions,
+	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,
-			segmentDuration,
-			duration: (points.length - 1) * segmentDuration,
 		})
-	})
+		if (isFavorited(item)) {
+			createFavoritePulseEntity(billboard)
+		}
+			droneTrackRuntime.push({
+				positions,
+				billboard,
+				segmentDuration,
+				duration: (points.length - 1) * segmentDuration,
+			})
+		})
 	viewer.clock.multiplier = 1
 	startDroneTrackAnimation()
+	startFavoritePulseAnimation()
 	reorderCockpitPrimitives()
-}
-
-const RING_STYLES = [
-	{ inner: 0, outer: 2000, gradient: ['#FF361C', '#360B00'] }
-]
-
-const MATERIAL_TYPE = 'RadialGradientMaterial'
-const TEXTURED_VERTEX_FORMAT =
-	Cesium.MaterialAppearance?.MaterialSupport?.TEXTURED?.vertexFormat || Cesium.VertexFormat.POSITION_AND_ST
-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, ringFillInstancesByStyle, ringOutlineInstancesByStyle) => {
-	if (!ringFillInstancesByStyle) return
-	const centerPosition = Cesium.Cartesian3.fromDegrees(center.longitude, center.latitude, 0)
-	RING_STYLES.forEach((ring, index) => {
-		if (!ringFillInstancesByStyle[index]) return
-		ringFillInstancesByStyle[index].push(
-			new Cesium.GeometryInstance({
-				geometry: new Cesium.EllipseGeometry({
-					center: centerPosition,
-					semiMajorAxis: ring.outer,
-					semiMinorAxis: ring.outer,
-					vertexFormat: TEXTURED_VERTEX_FORMAT,
-				}),
-			})
-		)
-	})
-	if (!ringOutlineInstancesByStyle) return
-	RING_STYLES.forEach((ring, index) => {
-		if (!ringOutlineInstancesByStyle[index]) return
-		const positions = buildCirclePositions(center, ring.outer)
-		ringOutlineInstancesByStyle[index].push(
-			new Cesium.GeometryInstance({
-				geometry: new Cesium.GroundPolylineGeometry({
-					positions,
-					width: 2,
-				}),
-				attributes: {
-					color: Cesium.ColorGeometryInstanceAttribute.fromColor(
-						Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.8)
-					),
-				},
-			})
-		)
-	})
-}
-
-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 => {
@@ -617,7 +722,8 @@
 		const position = getDevicePosition(item)
 		if (!position) return
 		const entityId = `online-device-${item.id ?? index}-${index}`
-		addDeviceRings(position, ringFillInstancesByStyle, ringOutlineInstancesByStyle)
+		const rangeMeters = getDeviceRange(item)
+		addDeviceRings(position, ringFillInstancesByStyle, ringOutlineInstancesByStyle, rangeMeters)
 		const billboard = deviceBillboardCollection.add({
 			position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
 			image: equipmentIcon,
@@ -628,17 +734,27 @@
 		billboard.id = entityId
 		devicePickMap.set(entityId, { data: item, billboard })
 	})
-	if (selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) {
+	if (selectedTargetType.value === 'device' && selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) {
 		closePopup()
 	}
-	registerRadialGradientMaterial()
 	ringFillInstancesByStyle.forEach((instances, index) => {
 		if (!instances.length) return
 		const ring = RING_STYLES[index]
-		const material = Cesium.Material.fromType(MATERIAL_TYPE, {
-			color1: Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.64),
-			color2: Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(0.64),
-		})
+		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({
@@ -679,43 +795,40 @@
 	if (!viewer) return { primitive: null, outlinePrimitive: null }
 	const polygonInstances = []
 	const lineInstances = []
-	registerRadialGradientMaterial()
-	; (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: TEXTURED_VERTEX_FORMAT,
+	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)
+						),
+					},
+				})
+			)
 		})
-		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 = Cesium.Material.fromType(MATERIAL_TYPE, {
-			color1: baseColor1.withAlpha(0.64),
-			color2: baseColor2.withAlpha(0.64),
-		})
+		const material = createRadialGradientMaterial(baseColor1.withAlpha(0.64), baseColor2.withAlpha(0.64))
 		primitive = new Cesium.GroundPrimitive({
 			geometryInstances: polygonInstances,
 			appearance: new Cesium.MaterialAppearance({
@@ -739,7 +852,7 @@
 const renderDefenseZones = zones => {
 	if (!viewer) return
 	clearDefenseZoneEntities()
-	const result = buildZonePrimitives(zones, '#19D266', '#2AEDBF', '#012B11')
+	const result = buildZonePrimitives(zones, '#72F3FF', '#72F3FF', '#0A7C88')
 	defenseZonePrimitive = result.primitive
 	defenseZoneOutlinePrimitive = result.outlinePrimitive
 	if (defenseZonePrimitive) defenseZonePrimitive.show = detailVisible.value
@@ -750,7 +863,7 @@
 const renderPartitions = zones => {
 	if (!viewer) return
 	clearPartitionEntities()
-	const result = buildZonePrimitives(zones, '#FFCD2A', '#FFC609', '#583300')
+	const result = buildZonePrimitives(zones, '#FFD772', '#FFD772', '#A86B00')
 	partitionPrimitive = result.primitive
 	partitionOutlinePrimitive = result.outlinePrimitive
 	if (partitionPrimitive) partitionPrimitive.show = detailVisible.value
@@ -786,7 +899,7 @@
 		viewer.dataSources.add(aggregationSource)
 	}
 	clearAggregationEntities()
-	aggregationSource.show = clusterVisible.value
+	updateAggregationVisibility()
 	const countMap = new Map()
 		; (list || []).forEach(item => {
 			if (!item?.type) return
@@ -803,7 +916,7 @@
 				width: 66.73,
 				height: 43,
 				verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
-				heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+				// disableDepthTestDistance: Number.POSITIVE_INFINITY,
 			},
 			label: {
 				text: `${count}`,
@@ -816,18 +929,20 @@
 
 				pixelOffset: new Cesium.Cartesian2(0, -35),
 				disableDepthTestDistance: Number.POSITIVE_INFINITY,
-				heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
 			},
 		})
 	})
 }
 
-const getPickedDevice = picks => {
+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 devicePickMap.get(resolvedId)
+			return { type: 'device', ...devicePickMap.get(resolvedId) }
+		}
+		if (resolvedId && dronePickMap.has(resolvedId)) {
+			return { type: 'drone', ...dronePickMap.get(resolvedId) }
 		}
 	}
 	return null
@@ -858,13 +973,14 @@
 const handleDeviceClick = movement => {
 	if (!viewer) return
 	const picks = viewer.scene.drillPick(movement.position) || []
-	const pickedDevice = getPickedDevice(picks)
-	if (!pickedDevice) {
+	const pickedTarget = getPickedTarget(picks)
+	if (!pickedTarget) {
 		closePopup()
 		return
 	}
-	selectedDevice.value = pickedDevice.data
-	selectedDeviceBillboard = pickedDevice.billboard
+	selectedTargetType.value = pickedTarget.type
+	selectedDevice.value = pickedTarget.data
+	selectedDeviceBillboard = pickedTarget.billboard
 	startPopupRender()
 }
 
@@ -883,103 +999,44 @@
 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 popupStyle = computed(() => ({
-	left: `${popupPosition.value.x}px`,
-	top: `${popupPosition.value.y}px`,
-}))
-
-const POPUP_TITLE_MAX = 12
-const popupTitle = computed(() => selectedDevice.value?.deviceName || selectedDevice.value?.deviceModel || '-')
-const popupTitleOverflow = computed(() => popupTitle.value.length > POPUP_TITLE_MAX)
-const popupTitleDisplay = computed(() => {
-	if (!popupTitleOverflow.value) return popupTitle.value
-	return `${popupTitle.value.slice(0, POPUP_TITLE_MAX)}……`
-})
-const popupStatusText = computed(() => {
-	const status = selectedDevice.value?.status
-	if (status === 0) return '在线'
-	if (status === 1) return '离线'
-	if (status === 2) return '故障'
-	if (status === 3) return '报废'
-	return '-'
-})
-const popupStatusClass = computed(() => (selectedDevice.value?.status === 0 ? 'online' : ''))
-const popupActionText = computed(() => {
-	const workMode = selectedDevice.value?.workMode
-	if (workMode === 1 || workMode === '1') return '侦测中'
-	if (workMode === 2 || workMode === '2') return '信号干扰中'
-	if (workMode === 3 || workMode === '3') return '诱导驱离中'
-	if (workMode === 4 || workMode === '4') return '待机'
-	return workMode || '-'
-})
-const popupActionClass = computed(() => {
-	const workMode = selectedDevice.value?.workMode
-	if (workMode === 1 || workMode === '1') return 'is-detecting'
-	if (workMode === 2 || workMode === '2') return 'is-jamming'
-	if (workMode === 3 || workMode === '3') return 'is-driving'
-	if (workMode === 4 || workMode === '4') return 'is-standby'
-	return 'is-standby'
-})
-const popupTypeText = computed(() => {
-	const type = selectedDevice.value?.deviceType
-	if (type === 1 || type === '1') return '便捷侦测箱'
-	if (type === 2 || type === '2') return '反制枪'
-	if (type === 3 || type === '3') return '察打一体'
-	return type || '-'
-})
-const popupBatteryText = computed(() => {
-	const val = selectedDevice.value?.batteryPct
-	if (val == null) return '-'
-	return `${val}%`
-})
-const popupAzimuthText = computed(() => {
-	const val = selectedDevice.value?.azimuth
-	if (val == null) return '-'
-	return `${val}°`
-})
-const popupElevationText = computed(() => {
-	const val = selectedDevice.value?.elevation
-	if (val == null) return '-'
-	return `${val}°`
-})
-const popupDetectCountText = computed(() => {
-	const val = selectedDevice.value?.detectTargetCnt
-	if (val == null) return '-'
-	return `${val}台`
-})
-const popupRangeText = computed(() => {
-	const val = selectedDevice.value?.effectiveRangeKm
-	if (val == null) return '-'
-	return `${val}KM`
-})
+const isDronePopup = computed(() => selectedTargetType.value === 'drone')
+const 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,
+		; (list || []).forEach((item, index) => {
+			const position = getDevicePosition(item)
+			if (!position) return
+			commandPostBillboardCollection.add({
+				position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
+				image: commandPostIcon,
+				width: 40,
+				height: 56,
+				verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+			})
 		})
-	})
 	reorderCockpitPrimitives()
 }
 
 const loadAggregation = async () => {
 	try {
-		const res = await cockpitAggregationApi()
+		const res = await cockpitAggregationApi({
+			effectiveRangeKmIsNotNull: 1
+		})
 		renderAggregation(res?.data?.data ?? [])
 	} catch (error) {
 		renderAggregation([])
@@ -1005,6 +1062,19 @@
 )
 
 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 }
+)
+
+watch(
 	() => props.leftCollapsed,
 	isCollapsed => {
 		if (isCollapsed) showLayerPanel.value = false
@@ -1016,24 +1086,50 @@
 	showLayerPanel.value = !showLayerPanel.value
 }
 
+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 }) => {
+const handleMapReady = ({ viewer: mapViewer, publicCesium: mapPublic }) => {
 	viewer = mapViewer
+	publicCesium = mapPublic
 	ensureCockpitPrimitiveLayer()
+	applyLayerVisibility(treeCheckedKeys.value)
 	const height = viewer?.camera?.positionCartographic?.height
-	const stage = height != null && height >= 100000 ? 'cluster' : 'detail'
+	const stage = getStageByHeight(height)
 	updateStageDisplay(stage)
 	renderDeviceEntities(props.onlineDevices)
 	loadDefenseZones()
 	loadPartitions()
 	loadAggregation()
 	loadCommandPosts()
-	renderSimulatedDroneTrack()
+	renderSimulatedDroneTrack(props.alarmDrones)
 	initDeviceClickHandler()
 }
 
@@ -1069,6 +1165,7 @@
 	}
 	cockpitPrimitiveLayer = null
 	viewer = null
+	publicCesium = null
 })
 </script>
 
@@ -1085,172 +1182,6 @@
 	left: 0;
 	width: 100%;
 	height: 100%;
-}
-
-.device-popup {
-	position: absolute;
-	transform: translate(-50%, calc(-100% - 72px));
-	width: 202px;
-	padding: 0 16px 20px;
-	border-radius: 16px;
-	background: rgba(5, 5, 15, 0.7);
-	backdrop-filter: blur(16px);
-	color: #C3C3DD;
-	font-family: Source Han Sans CN, Source Han Sans CN;
-	font-size: 12px;
-	pointer-events: auto;
-	z-index: 11;
-	box-sizing: border-box;
-
-	&::after {
-		content: "";
-		position: absolute;
-		left: 50%;
-		bottom: -8px;
-		transform: translateX(-50%);
-		width: 0;
-		height: 0;
-		border-left: 8px solid transparent;
-		border-right: 8px solid transparent;
-		border-top: 8px solid rgba(5, 5, 15, 0.7);
-	}
-
-	.device-popup__header {
-		height: 40px;
-		display: flex;
-		align-items: center;
-		justify-content: space-between;
-		position: relative;
-
-		font-weight: bold;
-		font-size: 14px;
-		color: #FFFFFF;
-
-		&::after {
-			content: "";
-			position: absolute;
-			left: 0;
-			bottom: 0;
-			width: 50px;
-			height: 2px;
-			background: #284FE3;
-			box-shadow: 0px 3px 2px 0px rgba(15, 89, 255, 0.1), 0px 7px 5px 0px rgba(15, 89, 255, 0.15), 0px 13px 10px 0px rgba(15, 89, 255, 0.18), 0px 22px 18px 0px rgba(15, 89, 255, 0.21), 0px 42px 33px 0px rgba(15, 89, 255, 0.26), 0px 100px 80px 0px rgba(15, 89, 255, 0.36);
-			border-radius: 5px 5px 0px 0px;
-		}
-
-		&::before {
-			content: "";
-			position: absolute;
-			left: 55px;
-			right: 0;
-			bottom: 0;
-			height: 2px;
-			background: #323245;
-			border-radius: 0px 0px 5px 5px;
-		}
-
-		.device-popup__title-tooltip {
-			flex: 1;
-			min-width: 0;
-			display: block;
-		}
-
-		.device-popup__title {
-			display: inline-block;
-			max-width: 100%;
-			white-space: nowrap;
-			overflow: hidden;
-			text-overflow: ellipsis;
-		}
-
-		.device-popup__close {
-			cursor: pointer;
-		}
-	}
-
-	.device-popup__type {
-		display: flex;
-		align-items: center;
-		gap: 6px;
-		font-size: 12px;
-		margin-top: 8px;
-		margin-bottom: 10px;
-		color: #d2d7df;
-
-		.label {
-			color: #9aa4b2;
-		}
-	}
-
-	.device-popup__subtitle {
-		display: inline-flex;
-		align-items: center;
-		gap: 6px;
-		padding: 2px 10px;
-		border-radius: 999px;
-		background: rgba(255, 255, 255, 0.06);
-		font-size: 12px;
-		margin-bottom: 10px;
-
-		.device-popup__dot {
-			width: 6px;
-			height: 6px;
-			border-radius: 50%;
-			background: #6b7685;
-
-			&.is-detecting {
-				background: #35f08c;
-				box-shadow: 0 0 6px rgba(53, 240, 140, 0.8);
-			}
-
-			&.is-jamming {
-				background: #ff4444;
-				box-shadow: 0 0 6px rgba(255, 68, 68, 0.7);
-			}
-
-			&.is-driving {
-				background: #ffb020;
-				box-shadow: 0 0 6px rgba(255, 176, 32, 0.7);
-			}
-
-			&.is-standby {
-				background: #9aa4b2;
-			}
-		}
-	}
-
-	.device-popup__rows {
-		display: flex;
-		flex-direction: column;
-		gap: 6px;
-		font-size: 12px;
-		color: #d2d7df;
-
-		.device-popup__row {
-			display: flex;
-			align-items: center;
-			margin-bottom: 10px;
-
-			.device-popup__col {
-				flex: 1;
-				display: flex;
-				align-items: center;
-
-				&:last-child {
-					justify-content: flex-end;
-				}
-			}
-		}
-
-		.value {
-			margin-left: 10px;
-		}
-
-		.value.online {
-			color: #35f08c;
-		}
-	}
-
 }
 
 .layer-control-root {
@@ -1317,86 +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