From 98ff0df343bd4e5d2d5e138a02c6db2b7fa9696b Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Tue, 27 Jan 2026 10:23:03 +0800
Subject: [PATCH] feat:角色权限弹窗配置页面,调整
---
applications/drone-command/src/components/map-container/device-map-container.vue | 762 +++++++++++++++-------------------------------------------
1 files changed, 203 insertions(+), 559 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..c2a8168 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,8 +2,9 @@
<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">
@@ -13,73 +14,30 @@
<div class="panel-title">图层管理</div>
<div class="panel-content">
- <el-tree :data="layerTree" show-checkbox default-expand-all node-key="key"
+ <el-tree class="command-tree" :data="layerTree" show-checkbox default-expand-all node-key="key"
:props="layerTreeProps" :default-checked-keys="defaultCheckedKeys" />
</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="toggleDroneFavorite"
+ @signal="handleDroneSignal"
+ @counter="handleDroneCounter"
+ />
</div>
</template>
@@ -96,8 +54,19 @@
import droneIcon from '@/assets/images/dataCockpit/map/drone.png'
import aggregationIcon from '@/assets/images/dataCockpit/map/aggregation.png'
import commandPostIcon from '@/assets/images/dataCockpit/map/command-post.png'
-import popupClose from '@/assets/images/dataCockpit/map/popup-close.png'
import jaGeojsonRaw from '@/assets/geojson/ja.geojson?raw'
+import DevicePopup from './components/DevicePopup.vue'
+import DronePopup from './components/DronePopup.vue'
+import {
+ RING_STYLES,
+ addDeviceRings,
+ createDroneTrackMaterial,
+ createRadialGradientMaterial,
+ getTexturedVertexFormat,
+} from './device-map-materials'
+
+const CLUSTER_HEIGHT = 100000
+const DETAIL_HEIGHT = 10000
const props = defineProps({
onlineDevices: {
@@ -117,6 +86,7 @@
default: true,
},
})
+const emit = defineEmits(['droneSignal', 'droneCounter'])
const DEFAULT_ZONE_PAGE_SIZE = 999
@@ -124,6 +94,7 @@
let viewer = null
let cockpitPrimitiveLayer = null
const devicePickMap = new Map()
+const dronePickMap = new Map()
let deviceBillboardCollection = null
let deviceRingOutlinePrimitives = []
let deviceRingFillPrimitives = []
@@ -144,6 +115,7 @@
const showLayerPanel = ref(false)
const layerWrapRef = ref(null)
const selectedDevice = ref(null)
+const selectedTargetType = ref('device')
let selectedDeviceBillboard = null
let deviceClickHandler = null
let popupRenderHandler = null
@@ -189,6 +161,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 = () => {
@@ -337,6 +317,20 @@
if (aggregationSource) aggregationSource.show = visible
}
+const setDroneVisibility = visible => {
+ if (droneTrackBillboardCollection) droneTrackBillboardCollection.show = visible
+ if (droneTrackPolylineCollection) droneTrackPolylineCollection.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)
@@ -348,47 +342,79 @@
})
}
-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 = () => {
+ return [
+ {
+ 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
+ },
]
- return offsets.map(item => ({
- longitude: anchor.longitude + item[0],
- latitude: anchor.latitude + item[1],
- height: item[2],
- }))
}
const clearDroneTrackEntities = () => {
if (!viewer) return
+ stopDroneTrackAnimation()
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()
+ 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 +430,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
@@ -434,33 +467,60 @@
const renderSimulatedDroneTrack = () => {
if (!viewer) return
- ensureCountyCenterMap()
+ clearDroneTrackEntities()
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 baseCenters = [['默认区域', { longitude:114.958541, latitude: 27.121917 }]]
const segmentDuration = 6
- baseCenters.forEach((center, trackIndex) => {
+ const baseTrackColor = Cesium.Color.fromCssColorString('red')
+ baseCenters.forEach(([centerName, center], trackIndex) => {
const points = buildSimulatedTrackPoints(center)
if (points.length < 2) return
const positions = points.map(point =>
Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
)
+ let trackMaterial = createDroneTrackMaterial({
+ color: baseTrackColor,
+ speed: 4.5,
+ headWidth: 0.2,
+ glowPower: 1.8,
+ backgroundAlpha: 0.34,
+ })
+ if (!trackMaterial) {
+ trackMaterial = Cesium.Material.fromType('Color', { color: baseTrackColor })
+ }
droneTrackPolylineCollection.add({
positions,
- width: 2,
- material: Cesium.Material.fromType('Color', {
- color: Cesium.Color.fromCssColorString('#2AEDBF'),
- }),
+ width: 3,
+ material: trackMaterial,
})
+ const droneId = `drone-track-${trackIndex}`
const billboard = droneTrackBillboardCollection.add({
- position: positions[0],
+ 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: {
+ droneName: `无人机-${trackIndex + 1}`,
+ droneSerialNo: `UAV-${trackIndex + 1}`,
+ flightHeightM: points[0].height,
+ flightSpeedMs: speedMs,
+ longitude: points[0].longitude,
+ latitude: points[0].latitude,
+ flightStatus: 1,
+ deviceName: centerName,
+ dataSource: centerName,
+ counterDeviceName: centerName,
+ },
+ billboard,
})
droneTrackRuntime.push({
positions,
@@ -472,128 +532,6 @@
viewer.clock.multiplier = 1
startDroneTrackAnimation()
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 +555,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 +567,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,14 +628,14 @@
if (!viewer) return { primitive: null, outlinePrimitive: null }
const polygonInstances = []
const lineInstances = []
- registerRadialGradientMaterial()
+ 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: TEXTURED_VERTEX_FORMAT,
+ vertexFormat: texturedVertexFormat,
})
polygonInstances.push(
new Cesium.GeometryInstance({
@@ -712,10 +661,7 @@
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 +685,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 +696,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
@@ -803,7 +749,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 +762,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 +806,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,80 +832,19 @@
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 toggleDroneFavorite = () => {
+ if (!selectedDevice.value) return
+ selectedDevice.value.isFavorite = !selectedDevice.value.isFavorite
+}
+const handleDroneSignal = () => emit('droneSignal', selectedDevice.value)
+const handleDroneCounter = () => emit('droneCounter', selectedDevice.value)
const renderCommandPosts = list => {
if (!viewer) return
@@ -979,7 +867,9 @@
const loadAggregation = async () => {
try {
- const res = await cockpitAggregationApi()
+ const res = await cockpitAggregationApi({
+ effectiveRangeKmIsNotNull: 1
+ })
renderAggregation(res?.data?.data ?? [])
} catch (error) {
renderAggregation([])
@@ -1020,13 +910,14 @@
const showCluster = stage === 'cluster'
setClusterVisibility(showCluster)
setDetailVisibility(!showCluster)
+ setDroneVisibility(!showCluster)
}
const handleMapReady = ({ viewer: mapViewer }) => {
viewer = mapViewer
ensureCockpitPrimitiveLayer()
const height = viewer?.camera?.positionCartographic?.height
- const stage = height != null && height >= 100000 ? 'cluster' : 'detail'
+ const stage = getStageByHeight(height)
updateStageDisplay(stage)
renderDeviceEntities(props.onlineDevices)
loadDefenseZones()
@@ -1085,172 +976,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 {
@@ -1316,87 +1041,6 @@
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 {
- 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;
- }
-
- .el-tree-node__children {
- .el-tree-node__content {
- border: none;
- }
- }
-
- &:focus {
- .el-tree-node__content {
- background: transparent !important;
- }
- }
- }
- }
-}
-
-.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