From 5667f2b4fc7555ba678d5aef13b096cd38da64db Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Mon, 12 Jan 2026 15:45:34 +0800
Subject: [PATCH] feat:数据驾驶舱相关处理

---
 applications/drone-command/src/views/dataCockpit/components/MapContainer.vue |  400 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 395 insertions(+), 5 deletions(-)

diff --git a/applications/drone-command/src/views/dataCockpit/components/MapContainer.vue b/applications/drone-command/src/views/dataCockpit/components/MapContainer.vue
index aa94178..4576555 100644
--- a/applications/drone-command/src/views/dataCockpit/components/MapContainer.vue
+++ b/applications/drone-command/src/views/dataCockpit/components/MapContainer.vue
@@ -1,28 +1,274 @@
 <template>
-	<div class="ztzf-cesium map-container" id="cesium">
+	<div class="ztzf-cesium map-container" id="cesium"></div>
+	<div 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="panel-content">
+					<el-tree :data="layerTree" show-checkbox default-expand-all node-key="key" :props="layerTreeProps"
+						:default-checked-keys="defaultCheckedKeys" />
+				</div>
+			</div>
+		</div>
 	</div>
 </template>
 
 <script setup>
+import * as Cesium from 'cesium'
 import { PublicCesium } from '@/utils/cesium/publicCesium'
+import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
+import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
+
+const props = defineProps({
+	onlineDevices: {
+		type: Array,
+		default: () => []
+	},
+	leftCollapsed: {
+		type: Boolean,
+		default: false
+	}
+})
+
 let viewInstance = null
 let viewer = null
+const deviceEntityIds = new Set()
+const showLayerPanel = ref(false)
+const layerWrapRef = ref(null)
+const layerTreeProps = {
+	label: 'label',
+	children: 'children'
+}
+const defaultCheckedKeys = ['global', 'city-base']
+const layerTree = ref([
+	{
+		key: 'base',
+		label: '地理信息图层',
+		children: [
+			{ key: 'global', label: '全球地形' },
+			{ key: 'admin', label: '行政区划' }
+		]
+	},
+	{
+		key: 'city',
+		label: '城市CIM图层',
+		children: [
+			{ key: 'city-base', label: '皖山白模' },
+			{ key: 'city-grid', label: '皖山白模光栅网格' },
+			{ key: 'city-tilt', label: '皖山倾斜摄影' },
+			{ key: 'city-tilt-grid', label: '皖山倾斜摄影网格' }
+		]
+	},
+	{
+		key: 'sky',
+		label: '空域要素图层',
+		children: [
+			{ key: 'airspace', label: '空域边界' },
+			{ key: 'route', label: '飞行航路' }
+		]
+	}
+])
+
+const getDevicePosition = (item) => {
+	const longitudeRaw = item.longitude ?? item.lng ?? item.lon
+	const latitudeRaw = item.latitude ?? item.lat
+	if (longitudeRaw == null || latitudeRaw == null) return null
+	const longitude = Number(longitudeRaw)
+	const latitude = Number(latitudeRaw)
+	if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return null
+	return { longitude, latitude }
+}
+
+const clearDeviceEntities = () => {
+	if (!viewer) return
+	deviceEntityIds.forEach((id) => {
+		viewer.entities.removeById(id)
+	})
+	deviceEntityIds.clear()
+}
+
+const RING_STYLES = [
+	{ inner: 0, outer: 5000, gradient: ['#FF361C', '#360B00'] },
+	{ inner: 5000, outer: 8000, gradient: ['#FFC609', '#583300'] },
+	{ inner: 8000, outer: 10000, gradient: ['#2AEDBF', '#012B11'] }
+]
+
+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
+	})
+}
+
+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 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
+			}
+		})
+	})
+}
+
+const renderDeviceEntities = (devices) => {
+	console.log(devices, 'devices')
+
+	if (!viewer) return
+
+	clearDeviceEntities()
+	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,
+			position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
+			billboard: {
+				image: equipmentIcon,
+				width: 40.34,
+				height: 40.34
+			}
+		})
+	})
+}
+
+watch(
+	() => props.onlineDevices,
+	(devices) => {
+		renderDeviceEntities(devices || [])
+	},
+	{ deep: true }
+)
+
+watch(
+	() => props.leftCollapsed,
+	(isCollapsed) => {
+		if (isCollapsed) showLayerPanel.value = false
+	}
+)
+
+const toggleLayerPanel = () => {
+	showLayerPanel.value = !showLayerPanel.value
+}
+
+const handleClickOutside = (event) => {
+	if (!showLayerPanel.value) return
+	const target = event.target
+	if (layerWrapRef.value?.contains(target)) return
+	showLayerPanel.value = false
+}
+
 onMounted(() => {
+	document.addEventListener('click', handleClickOutside)
 	viewInstance = new PublicCesium({
 		dom: 'cesium',
 		flatMode: false,
 		terrain: false,
 		layerMode: 4,
-		contour: false,
+		contour: false
 	})
 
 	viewer = viewInstance.getViewer()
-}) 
+	renderDeviceEntities(props.onlineDevices)
+})
 
 onBeforeUnmount(() => {
+	document.removeEventListener('click', handleClickOutside)
+	clearDeviceEntities()
 	if (viewInstance) {
-		viewInstance?.destroy()
+		viewInstance?.viewerDestroy()
 		viewInstance = null
 	}
 
@@ -38,4 +284,148 @@
 	width: 100%;
 	height: 100%;
 }
-</style>
\ No newline at end of file
+
+.layer-control-root {
+	position: absolute;
+	left: 411px;
+	bottom: 22px;
+	z-index: 9;
+	transition: transform 0.3s ease-in-out;
+	pointer-events: none;
+
+	&.collapsed {
+		transform: translateX(-401px);
+	}
+}
+
+.layer-control-wrap {
+	position: relative;
+	display: flex;
+	align-items: flex-end;
+	pointer-events: auto;
+}
+
+.layer-control {
+	width: 34px;
+	height: 34px;
+	cursor: pointer;
+
+	img {
+		width: 100%;
+		height: 100%;
+		display: block;
+	}
+}
+
+.layer-panel {
+	display: flex;
+	flex-direction: column;
+	position: absolute;
+	left: 46px;
+	bottom: 0;
+	width: 160px;
+	max-height: 442px;
+	background: #191932;
+	border-radius: 8px 8px 8px 8px;
+
+	.panel-title {
+		padding: 0 16px;
+		line-height: 42px;
+
+		font-family: Open Sans, Open Sans;
+		font-weight: 400;
+		font-size: 12px;
+		color: #FFFFFF;
+		text-align: left;
+		font-style: normal;
+		text-transform: none;
+
+		border-bottom: 1px solid rgba(70,70,100,0.5);
+
+		box-sizing: border-box;
+	}
+
+	.panel-content {
+		padding: 0 16px;
+		height: 0;
+		flex: 1;
+		overflow: auto;
+	}
+
+	::v-deep(.el-tree) {
+		background: transparent;
+		color: #C3C3DD;
+		font-size: 12px;
+
+		.el-tree-node {
+			line-height: 30px !important;
+
+			.el-tree-node__content {
+				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;
+			}
+
+			.el-tree-node__children {
+				.el-tree-node__content {
+					border: none;
+				}
+			}
+		}
+
+	}
+}
+
+
+
+.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) {
+	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