From 6e88705bd5b443a259b24c17c8a299765d059d96 Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Thu, 23 Jul 2026 16:50:41 +0800
Subject: [PATCH] Merge branch 'master' of http://139.196.74.78:10010/r/jagzwxm/ja_web
---
applications/drone-command/src/components/map-container/device-map-container.vue | 1845 ++++++++++++++++++++++++++++++++++++++--------------------
1 files changed, 1,207 insertions(+), 638 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 1dcdcc8..a84f2e0 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,101 +1,109 @@
-<template>
+<template>
<div class="map-shell">
<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" :enable-stage-emit="true" :cluster-height="100000"
- @ready="handleMapReady" @stage-change="handleStageChange" />
- <div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }">
+ :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.rightCollapsed }">
<div class="layer-control-wrap" ref="layerWrapRef">
- <div class="layer-control" @click="toggleLayerPanel">
+ <div class="layer-control" @click.stop="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" />
+ <transition name="layer-panel-slide">
+ <div v-if="showLayerPanel" class="layer-panel">
+ <div class="panel-title">图层管理</div>
+ <div class="panel-content">
+ <el-tree ref="layerTreeRef" class="command-tree map-layer-tree" :data="layerTree"
+ icon="el-icon-arrow-down-bold"
+ show-checkbox default-expand-all node-key="key" :props="layerTreeProps"
+ :default-checked-keys="treeCheckedKeys" @check="handleLayerCheck" />
+ </div>
</div>
+ </transition>
+ </div>
+ </div>
+ <div v-if="props.showLayerControl" class="base-layer-switch-root" :class="{ collapsed: props.rightCollapsed }">
+ <div class="base-layer-switch-wrap" @mouseenter="showBaseLayerPanel = true"
+ @mouseleave="showBaseLayerPanel = false">
+ <div class="base-layer-trigger">
+ <img class="base-layer-trigger-thumb" :src="currentBaseLayer.icon" :alt="currentBaseLayer.label">
+ <div class="base-layer-trigger-label">{{ currentBaseLayer.shortLabel }}</div>
</div>
+ <transition name="layer-panel-slide">
+ <div v-if="showBaseLayerPanel" class="base-layer-panel">
+ <div class="base-map-options">
+ <div class="base-map-card" :class="{ active: baseLayerKey === 'base-satellite' }"
+ @click="handleBaseLayerSelect('base-satellite')">
+ <img class="base-map-thumb" :src="yxIcon" alt="">
+ <div class="base-map-label">卫星地图</div>
+ </div>
+ <div class="base-map-card" :class="{ active: baseLayerKey === 'base-standard' }"
+ @click="handleBaseLayerSelect('base-standard')">
+ <img class="base-map-thumb" :src="dzIcon" alt="">
+ <div class="base-map-label">标准地图</div>
+ </div>
+ </div>
+ </div>
+ </transition>
</div>
</div>
- <div v-if="popupVisible" class="device-popup" :style="popupStyle" @click.stop>
- <div class="device-popup__header">
- <span class="device-popup__title">{{ popupTitle }}</span>
+ <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>
<script setup>
+
import * as Cesium from 'cesium'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
-import { geomAnalysis } from '@ztzf/utils'
-import { fwDefenseZonePageApi } from '@/views/areaManage/defenseZone/defenseZoneApi'
-import { fwAreaDividePageApi } from '@/views/areaManage/partition/partitionApi'
-import { fwDefenseSceneListApi } from '@/views/areaManage/sceneConfig/sceneConfigApi'
-import { cockpitAggregationApi } from '@/api/dataCockpit'
+import { buildEllipsePositions } from '@/utils/cesium/shapeTools'
+import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
+
+import { newAreaDivideList, newDefenseSceneManageList } from '@/api/dataCockpit/index'
+
+import { newCockpitAggregationApi } from '@/api/dataCockpit'
import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
+import offlineEquipmentIcon from '@/assets/images/dataCockpit/map/offline-equipment.png'
+import droneIcon from '@/assets/images/dataCockpit/map/drone.png'
import aggregationIcon from '@/assets/images/dataCockpit/map/aggregation.png'
import commandPostIcon from '@/assets/images/dataCockpit/map/command-post.png'
-import 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 dayjs from 'dayjs'
+import {
+ createDroneTrackMaterial,
+ createRadialGradientMaterial,
+ getTexturedVertexFormat,
+} from './device-map-materials'
+import { getPointPositionsHeight } from '@/utils/cesium/mapUtil'
+import { createDeviceRangePrimitiveWithHeight } from '@/utils/cesium/deviceRange'
+import dzIcon from '@/assets/images/dataCockpit/map/dz-map-layer.png'
+import yxIcon from '@/assets/images/dataCockpit/map/yx-map-layer.png'
+
+const CLUSTER_HEIGHT = 100000
+const DETAIL_HEIGHT = 10000
+const POLYGON_HEIGHT_M = 0.2
+const DRONE_TRACK_DURATION_S = 30 * 60
+const FIXED_DRONE_TRACK_IDS = ['6000000000609', '6000000000608']
const props = defineProps({
- onlineDevices: {
+ allDevices: {
type: Array,
default: () => [],
},
- leftCollapsed: {
+ alarmDrones: {
+ type: Array,
+ default: () => [],
+ },
+ rightCollapsed: {
type: Boolean,
default: false,
},
@@ -108,41 +116,79 @@
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()
-const deviceEntityMap = new Map()
-let defenseZoneSource = null
-let partitionSource = null
+let publicCesium = null
+let cockpitPrimitiveLayer = null
+const devicePickMap = new Map()
+const dronePickMap = new Map()
+let deviceBillboardCollection = null
+let deviceRingOutlinePrimitives = []
+let deviceRingFillPrimitives = []
+let partitionFillPrimitives = []
+let partitionOutlinePrimitives = []
let aggregationSource = null
-let commandPostSource = null
+let commandPostBillboardCollection = null
+let droneTrackBillboardCollection = null
+let droneTrackPolylineCollection = null
+let droneTrackTickHandler = null
+let droneTrackRafId = null
+let droneTrackStartTime = null
+let droneTrackAnimStartAt = 0
+let droneTrackLastTickAt = 0
+let droneTrackRuntime = []
+let droneTrackSource = null
+let mapReadyHandled = false
+let favoritePulseRafId = null
+let favoritePulseStartAt = 0
+let favoritePulseElapsed = 0
+const favoritePulseEntities = []
+const pulseBaseColor = Cesium.Color.fromCssColorString('#FF3B30')
+const PULSE_MIN_RADIUS_M = 20
+const PULSE_MAX_RADIUS_M = 80
+const PULSE_DURATION_S = 1.4
const detailVisible = ref(true)
const clusterVisible = ref(false)
const countyCenterMap = new Map()
const showLayerPanel = ref(false)
+const showBaseLayerPanel = ref(false)
const layerWrapRef = ref(null)
+const layerTreeRef = ref(null)
const selectedDevice = ref(null)
-let selectedDeviceEntity = 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 baseLayerMeta = {
+ 'base-standard': { icon: dzIcon, label: '标准地图', shortLabel: '标准' },
+ 'base-satellite': { icon: yxIcon, label: '卫星地图', shortLabel: '影像' },
+}
+const currentBaseLayer = computed(() => baseLayerMeta[baseLayerKey.value] || baseLayerMeta['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: [
@@ -160,7 +206,62 @@
{ 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 pulse = {
+ offset: Math.random() * PULSE_DURATION_S,
+ entity: null,
+ }
+ pulse.entity = viewer.entities.add({
+ position: new Cesium.CallbackProperty(() => getBillboardPosition(billboard), false),
+ point: {
+ pixelSize: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
+ color: pulseBaseColor.withAlpha(0.4),
+ },
+ })
+ favoritePulseEntities.push(pulse)
+}
+
+const getBillboardPosition = billboard => {
+ if (!viewer || !billboard) return null
+ const position = billboard.position
+ if (position?.getValue) {
+ return position.getValue(viewer.clock.currentTime)
+ }
+ return position
+}
const getDevicePosition = item => {
const longitudeRaw = item.longitude ?? item.lng ?? item.lon
@@ -172,23 +273,101 @@
return { longitude, latitude }
}
+const getDeviceRange = item => {
+ if (!item) return null
+ const rawRange = item.effectiveRangeKm ?? item.range ?? item.coverRadiusM
+ const range = Number(rawRange)
+ if (!Number.isFinite(range) || range <= 0) return null
+ return range
+}
+
+
+const ensureCockpitPrimitiveLayer = () => {
+ if (!viewer) return
+ if (!cockpitPrimitiveLayer) {
+ cockpitPrimitiveLayer = new Cesium.PrimitiveCollection({ destroyPrimitives: false })
+ viewer.scene.primitives.add(cockpitPrimitiveLayer)
+ }
+}
+
+const addCockpitPrimitive = primitive => {
+ if (!primitive) return
+ if (cockpitPrimitiveLayer) {
+ cockpitPrimitiveLayer.add(primitive)
+ return
+ }
+ viewer?.scene?.primitives?.add(primitive)
+}
+
+const removeCockpitPrimitive = (primitive, destroy = true) => {
+ if (!primitive) return
+ if (cockpitPrimitiveLayer) {
+ cockpitPrimitiveLayer.remove(primitive, destroy)
+ return
+ }
+ viewer?.scene?.primitives?.remove(primitive)
+}
+
+const reorderCockpitPrimitives = () => {
+ if (!viewer) return
+ ensureCockpitPrimitiveLayer()
+ cockpitPrimitiveLayer.removeAll(false)
+ if (partitionFillPrimitives.length) {
+ partitionFillPrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
+ }
+ if (partitionOutlinePrimitives.length) {
+ partitionOutlinePrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
+ }
+ if (deviceRingFillPrimitives.length) {
+ deviceRingFillPrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
+ }
+ if (deviceRingOutlinePrimitives.length) {
+ deviceRingOutlinePrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
+ }
+ if (commandPostBillboardCollection) cockpitPrimitiveLayer.add(commandPostBillboardCollection)
+ if (deviceBillboardCollection) cockpitPrimitiveLayer.add(deviceBillboardCollection)
+ if (droneTrackPolylineCollection) cockpitPrimitiveLayer.add(droneTrackPolylineCollection)
+ if (droneTrackBillboardCollection) cockpitPrimitiveLayer.add(droneTrackBillboardCollection)
+}
+
const clearDeviceEntities = () => {
if (!viewer) return
- deviceEntityIds.forEach(id => {
- viewer.entities.removeById(id)
- })
- deviceEntityIds.clear()
- deviceEntityMap.clear()
+ if (deviceBillboardCollection) {
+ deviceBillboardCollection.removeAll()
+ removeCockpitPrimitive(deviceBillboardCollection)
+ deviceBillboardCollection = null
+ }
+ if (deviceRingFillPrimitives.length) {
+ deviceRingFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+ deviceRingFillPrimitives = []
+ }
+ if (deviceRingOutlinePrimitives.length) {
+ deviceRingOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+ deviceRingOutlinePrimitives = []
+ }
+ devicePickMap.clear()
}
-const clearDefenseZoneEntities = () => {
- if (!defenseZoneSource) return
- defenseZoneSource.entities.removeAll()
+const ensureDeviceCollections = () => {
+ if (!viewer) return
+ ensureCockpitPrimitiveLayer()
+ if (!deviceBillboardCollection) {
+ deviceBillboardCollection = new Cesium.BillboardCollection()
+ addCockpitPrimitive(deviceBillboardCollection)
+ }
}
+
const clearPartitionEntities = () => {
- if (!partitionSource) return
- partitionSource.entities.removeAll()
+ if (!viewer) return
+ if (partitionFillPrimitives.length) {
+ partitionFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+ partitionFillPrimitives = []
+ }
+ if (partitionOutlinePrimitives.length) {
+ partitionOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+ partitionOutlinePrimitives = []
+ }
}
const clearAggregationEntities = () => {
@@ -197,25 +376,84 @@
}
const clearCommandPostEntities = () => {
- if (!commandPostSource) return
- commandPostSource.entities.removeAll()
+ 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 (defenseZoneSource) defenseZoneSource.show = visible
- if (partitionSource) partitionSource.show = visible
- if (commandPostSource) commandPostSource.show = visible
+ if (partitionFillPrimitives.length) {
+ partitionFillPrimitives.forEach(primitive => {
+ primitive.show = visible
+ })
+ }
+ if (partitionOutlinePrimitives.length) {
+ partitionOutlinePrimitives.forEach(primitive => {
+ primitive.show = visible
+ })
+ }
+ if (commandPostBillboardCollection) commandPostBillboardCollection.show = visible
+ if (deviceBillboardCollection) deviceBillboardCollection.show = visible
+ if (deviceRingFillPrimitives.length) {
+ deviceRingFillPrimitives.forEach(primitive => {
+ primitive.show = visible
+ })
+ }
+ if (deviceRingOutlinePrimitives.length) {
+ deviceRingOutlinePrimitives.forEach(primitive => {
+ primitive.show = visible
+ })
+ }
if (!visible) closePopup()
- deviceEntityIds.forEach(id => {
- const entity = viewer?.entities?.getById(id)
- if (entity) entity.show = visible
- })
}
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 (droneTrackSource) droneTrackSource.show = visible
+ if (favoritePulseEntities.length) {
+ favoritePulseEntities.forEach(pulse => {
+ if (pulse?.entity) pulse.entity.show = visible
+ })
+ }
+ if (!visible && selectedTargetType.value === 'drone') {
+ closePopup()
+ }
+}
+const getStageByHeight = height => {
+ if (height == null) return 'detail'
+ if (height >= CLUSTER_HEIGHT) return 'cluster'
+ if (height <= DETAIL_HEIGHT) return 'detail'
+ return 'mid'
}
const ensureCountyCenterMap = () => {
@@ -229,237 +467,645 @@
})
}
-
-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),
+const buildSimulatedTrackPoints = center => {
+ const basePoints = [
+ [
+ {
+ longitude: 114.929475,
+ latitude: 27.136575,
+ height: 200
},
- source: `
- czm_material czm_getMaterial(czm_materialInput materialInput) {
- czm_material material = czm_getDefaultMaterial(materialInput);
- vec2 st = materialInput.st - vec2(0.5);
- float t = clamp(length(st) * 2.0, 0.0, 1.0);
- vec4 color = mix(color1, color2, t);
- material.diffuse = color.rgb;
- material.alpha = color.a;
- return material;
- }
- `,
- },
- translucent: () => true,
- })
-}
-
-const buildCirclePositions = (center, radiusMeters, steps = 64) => {
- const positions = []
- const latRad = Cesium.Math.toRadians(center.latitude)
- const metersToLat = 1 / 111320
- const metersToLon = 1 / (111320 * Math.cos(latRad))
- for (let i = 0; i <= steps; i += 1) {
- const angle = Cesium.Math.toRadians((i / steps) * 360)
- const dx = radiusMeters * Math.cos(angle)
- const dy = radiusMeters * Math.sin(angle)
- const lon = center.longitude + dx * metersToLon
- const lat = center.latitude + dy * metersToLat
- positions.push(Cesium.Cartesian3.fromDegrees(lon, lat))
- }
- return positions
-}
-
-const addDeviceRings = (center, baseId, visible) => {
- 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,
- show: visible,
- polygon: {
- hierarchy: new Cesium.PolygonHierarchy(outerPositions, holes),
- material,
+ {
+ longitude: 114.928466,
+ latitude: 27.134556,
+ height: 200
},
- })
- })
+ {
+ longitude: 114.928466,
+ latitude: 27.134556,
+ height: 320
+ },
+ {
+ longitude: 114.928474,
+ latitude: 27.134397,
+ height: 320
+ },
+ {
+ longitude: 114.927932,
+ latitude: 27.133385,
+ height: 320
+ },
+ {
+ longitude: 114.927365,
+ latitude: 27.132097,
+ height: 320
+ },
+ {
+ longitude: 114.929818,
+ latitude: 27.131645,
+ height: 320
+ },
+ {
+ longitude: 114.924644,
+ latitude: 27.136622,
+ height: 120
+ },
+ ],
+
+ [
+ {
+ longitude: 114.936386,
+ latitude: 26.825890,
+ height: 120
+ },
+ {
+ longitude: 114.933976,
+ latitude: 26.827145,
+ height: 120
+ },
+ {
+ longitude: 114.931281,
+ latitude: 26.827801,
+ height: 120
+ },
+ {
+ longitude: 114.931281,
+ latitude: 26.827801,
+ height: 400
+ },
+ {
+ longitude: 114.931159,
+ latitude: 26.826940,
+ height: 400
+ },
+ {
+ longitude: 114.933707,
+ latitude: 26.826704,
+ height: 400
+ },
+ {
+ longitude: 114.935197,
+ latitude: 26.826933,
+ height: 400
+ },
+ {
+ longitude: 114.936371,
+ latitude: 26.828504,
+ height: 400
+ },
+ ],
+ ]
+
+ return basePoints[center.trackIndex] || basePoints[1]
}
-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 => {
+const clearDroneTrackEntities = () => {
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, detailVisible.value)
- const entity = viewer.entities.add({
- id: entityId,
- show: detailVisible.value,
- position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
- billboard: {
- image: equipmentIcon,
- width: 40,
- height: 56,
- verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
- },
+ stopDroneTrackAnimation()
+ stopFavoritePulseAnimation()
+ if (droneTrackBillboardCollection) {
+ if (!droneTrackBillboardCollection.isDestroyed?.()) {
+ removeCockpitPrimitive(droneTrackBillboardCollection)
+ }
+ droneTrackBillboardCollection = null
+ }
+ if (droneTrackPolylineCollection) {
+ if (!droneTrackPolylineCollection.isDestroyed?.()) {
+ removeCockpitPrimitive(droneTrackPolylineCollection)
+ }
+ droneTrackPolylineCollection = null
+ }
+ if (droneTrackSource) {
+ viewer.dataSources.remove(droneTrackSource)
+ droneTrackSource = null
+ }
+ droneTrackRuntime = []
+ if (favoritePulseEntities.length) {
+ favoritePulseEntities.forEach(pulse => {
+ if (pulse?.entity) viewer?.entities?.remove(pulse.entity)
})
- deviceEntityMap.set(entityId, { data: item, entity })
- })
- if (selectedDeviceEntity && !deviceEntityMap.has(selectedDeviceEntity.id)) {
+ favoritePulseEntities.length = 0
+ }
+ dronePickMap.clear()
+ if (selectedTargetType.value === 'drone') {
closePopup()
}
}
-const getDefenseZonePositions = geom => {
- const points = geomAnalysis(geom)
- if (points.length < 3) return []
- const first = points[0]
- const last = points[points.length - 1]
- const list =
- first && last && first.longitude === last.longitude && first.latitude === last.latitude
- ? points.slice(0, -1)
- : points
- return list.map(item => Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude))
+
+const 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 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 ensureDroneTrackSource = () => {
+ if (!viewer) return
+ if (!droneTrackSource) {
+ droneTrackSource = new Cesium.CustomDataSource('droneTrackSource')
+ viewer.dataSources.add(droneTrackSource)
+ }
+}
+
+const setupDroneTrackClock = () => {
+ if (!viewer) return
+ const now = new Date()
+ const noon = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 12, 0, 0, 0)
+ const startTime = Cesium.JulianDate.fromDate(noon)
+ const stopTime = Cesium.JulianDate.addSeconds(startTime, DRONE_TRACK_DURATION_S, new Cesium.JulianDate())
+ viewer.clock.startTime = startTime
+ viewer.clock.stopTime = stopTime
+ viewer.clock.currentTime = startTime
+ viewer.clock.multiplier = 1
+ viewer.clock.clockStep = Cesium.ClockStep.SYSTEM_CLOCK_MULTIPLIER
+ viewer.clock.clockRange = Cesium.ClockRange.CLAMPED
+}
+
+const updateDroneTrackPositions = elapsed => {
+ if (!viewer || !viewer.scene || !droneTrackRuntime.length) return
+ droneTrackRuntime.forEach(track => {
+ if (!track?.billboard || track.billboard.isDestroyed?.()) return
+ if (!track?.polyline) return
+ if (!track.positions || track.positions.length < 2) return
+ const duration = track.duration
+ if (duration <= 0) return
+ const t = Math.min(Math.max(elapsed, 0), duration)
+ const seg = Math.min(track.positions.length - 2, Math.floor(t / track.segmentDuration))
+ const ratio = (t - seg * track.segmentDuration) / track.segmentDuration
+ const isEntityPosition = typeof track.billboard?.position?.getValue === 'function'
+ const pos = isEntityPosition
+ ? getBillboardPosition(track.billboard)
+ : Cesium.Cartesian3.lerp(
+ track.positions[seg],
+ track.positions[seg + 1],
+ ratio,
+ new Cesium.Cartesian3()
+ )
+ if (!pos) return
+ if (!isEntityPosition) {
+ track.billboard.position = pos
+ }
+ const pathCount = Math.min(track.positions.length - 1, Math.floor(t / track.segmentDuration))
+ const pathPositions = track.positions.slice(0, pathCount + 1)
+ track.polyline.positions = [...pathPositions, pos]
+ })
+}
+
+const startDroneTrackAnimation = () => {
+ if (!viewer) return
+ stopDroneTrackAnimation()
+ droneTrackStartTime = viewer.clock.startTime
+ viewer.clock.shouldAnimate = true
+ droneTrackAnimStartAt = performance.now()
+ droneTrackLastTickAt = droneTrackAnimStartAt
+ const renderTick = now => {
+ if (!viewer || viewer.isDestroyed?.()) {
+ droneTrackRafId = null
+ return
+ }
+ const deltaSeconds = Math.max(0, (now - droneTrackLastTickAt) / 1000)
+ droneTrackLastTickAt = now
+ if (viewer.clock?.currentTime && viewer.clock?.stopTime) {
+ const nextTime = Cesium.JulianDate.addSeconds(
+ viewer.clock.currentTime,
+ deltaSeconds * (viewer.clock.multiplier || 1),
+ new Cesium.JulianDate()
+ )
+ if (Cesium.JulianDate.greaterThan(nextTime, viewer.clock.stopTime)) {
+ viewer.clock.currentTime = viewer.clock.stopTime
+ } else {
+ viewer.clock.currentTime = nextTime
+ }
+ }
+ const elapsed = Cesium.JulianDate.secondsDifference(viewer.clock.currentTime, droneTrackStartTime)
+ updateDroneTrackPositions(elapsed)
+ viewer.scene?.requestRender?.()
+ droneTrackRafId = requestAnimationFrame(renderTick)
+ }
+ droneTrackRafId = requestAnimationFrame(renderTick)
+}
+
+const stopDroneTrackAnimation = () => {
+ droneTrackTickHandler = null
+ if (droneTrackRafId) {
+ cancelAnimationFrame(droneTrackRafId)
+ droneTrackRafId = null
+ }
+}
+
+const startFavoritePulseAnimation = () => {
+ if (!viewer || favoritePulseRafId) return
+ favoritePulseStartAt = performance.now()
+ const tick = now => {
+ if (!viewer || viewer.isDestroyed?.() || !favoritePulseEntities.length) {
+ favoritePulseRafId = null
+ return
+ }
+ favoritePulseElapsed = (now - favoritePulseStartAt) / 1000
+ viewer.scene.requestRender()
+ favoritePulseRafId = requestAnimationFrame(tick)
+ }
+ favoritePulseRafId = requestAnimationFrame(tick)
+}
+
+const stopFavoritePulseAnimation = () => {
+ if (favoritePulseRafId) {
+ cancelAnimationFrame(favoritePulseRafId)
+ favoritePulseRafId = null
+ }
+}
+
+const renderSimulatedDroneTrack = (list) => {
+ if (!viewer) return
+ clearDroneTrackEntities()
+ if (!list?.length) return
+ ensureDroneTrackCollections()
+ ensureDroneTrackSource()
+ setupDroneTrackClock()
+ droneTrackBillboardCollection.show = detailVisible.value
+ droneTrackPolylineCollection.show = detailVisible.value
+ droneTrackRuntime = []
+ const baseTrackColor = Cesium.Color.fromCssColorString('red')
+ ; (FIXED_DRONE_TRACK_IDS || []).forEach((trackId, trackIndex) => {
+ const item = (list || []).find(entry => String(entry?.id) === String(trackId))
+ if (!item) return
+ const position = getDevicePosition(item)
+ if (!position) return
+ const points = buildSimulatedTrackPoints({ ...position, height: item.flightHeightM, trackIndex })
+ if (points.length < 2) return
+ const positions = points.map(point =>
+ Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
+ )
+ const segmentDuration = DRONE_TRACK_DURATION_S / Math.max(positions.length - 1, 1)
+ let trackMaterial = createDroneTrackMaterial({
+ color: baseTrackColor,
+ speed: 4.5,
+ headWidth: 0.2,
+ glowPower: 1.8,
+ backgroundAlpha: 0.34,
+ })
+ if (!trackMaterial) {
+ trackMaterial = Cesium.Material.fromType('Color', { color: baseTrackColor })
+ }
+ const polyline = droneTrackPolylineCollection.add({
+ positions: [positions[0]],
+ width: 3,
+ material: trackMaterial,
+ })
+ const droneId = `drone-alarm-${trackId}`
+ const startTime = viewer.clock.startTime
+ const stopTime = viewer.clock.stopTime
+ const positionProperty = new Cesium.SampledPositionProperty()
+ positions.forEach((pos, index) => {
+ const sampleTime = Cesium.JulianDate.addSeconds(
+ startTime,
+ index * segmentDuration,
+ new Cesium.JulianDate()
+ )
+ positionProperty.addSample(sampleTime, pos)
+ })
+ positionProperty.setInterpolationOptions({
+ interpolationDegree: 1,
+ interpolationAlgorithm: Cesium.LinearApproximation,
+ })
+ const entity = droneTrackSource.entities.add({
+ id: droneId,
+ availability: new Cesium.TimeIntervalCollection([
+ new Cesium.TimeInterval({
+ start: startTime,
+ stop: stopTime,
+ }),
+ ]),
+ position: positionProperty,
+ orientation: new Cesium.VelocityOrientationProperty(positionProperty),
+ billboard: {
+ image: droneIcon,
+ width: 36,
+ height: 36,
+ verticalOrigin: Cesium.VerticalOrigin.CENTER,
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
+ },
+ })
+ const speedMs = Math.round(Cesium.Cartesian3.distance(positions[0], positions[1]) / segmentDuration)
+ dronePickMap.set(droneId, {
+ data: {
+ ...item,
+ flightHeightM: item.flightHeightM ?? points[0].height,
+ flightSpeedMs: item.flightSpeedMs ?? speedMs,
+ longitude: item.longitude ?? points[0].longitude,
+ latitude: item.latitude ?? points[0].latitude,
+ },
+ billboard: entity,
+ })
+ if (isFavorited(item)) {
+ createFavoritePulseEntity(entity)
+ }
+ droneTrackRuntime.push({
+ positions,
+ polyline,
+ billboard: entity,
+ segmentDuration,
+ duration: DRONE_TRACK_DURATION_S,
+ })
})
- })
+ startDroneTrackAnimation()
+ startFavoritePulseAnimation()
+ reorderCockpitPrimitives()
}
-const renderDefenseZones = zones => {
+const renderDeviceEntities = async devices => {
if (!viewer) return
- if (!defenseZoneSource) {
- defenseZoneSource = new Cesium.CustomDataSource('defenseZoneSource')
- viewer.dataSources.add(defenseZoneSource)
+ ensureCockpitPrimitiveLayer()
+ ensureDeviceCollections()
+ deviceBillboardCollection.removeAll()
+ if (deviceRingFillPrimitives.length) {
+ deviceRingFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+ deviceRingFillPrimitives = []
}
- clearDefenseZoneEntities()
- defenseZoneSource.show = detailVisible.value
- renderZonePolygons({
- zones,
- source: defenseZoneSource,
- idPrefix: 'defense-zone',
- lineColor: '#19D266',
- fillGradient: ['#2AEDBF', '#012B11'],
- })
-}
-
-const renderPartitions = zones => {
- if (!viewer) return
- if (!partitionSource) {
- partitionSource = new Cesium.CustomDataSource('partitionSource')
- viewer.dataSources.add(partitionSource)
+ if (deviceRingOutlinePrimitives.length) {
+ deviceRingOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
+ deviceRingOutlinePrimitives = []
}
- clearPartitionEntities()
- partitionSource.show = detailVisible.value
- renderZonePolygons({
- zones,
- source: partitionSource,
- idPrefix: 'partition-zone',
- lineColor: '#FFCD2A',
- fillGradient: ['#FFC609', '#583300'],
+ devicePickMap.clear()
+ deviceBillboardCollection.show = detailVisible.value
+ const deviceEntries = []
+ const devicePositions = []
+ devices.forEach((item, index) => {
+ const isOnline = String(item.status) === '0'
+
+ const position = getDevicePosition(item)
+ if (!position) return
+ const entityId = `online-device-${item.id ?? index}-${index}`
+ const rangeMeters = getDeviceRange(item)
+ devicePositions.push({ lng: position.longitude, lat: position.latitude })
+ deviceEntries.push({ position, rangeMeters, isOnline, entityId, data: item })
})
+ if (deviceEntries.length) {
+ const heights = await getPointPositionsHeight(devicePositions, viewer)
+ deviceEntries.forEach((entry, index) => {
+ const height = Number(heights?.[index]?.ASL)
+ const centerHeight = Number.isFinite(height) ? height : 0
+ const billboard = deviceBillboardCollection.add({
+ position: Cesium.Cartesian3.fromDegrees(
+ entry.position.longitude,
+ entry.position.latitude,
+ centerHeight
+ ),
+ image: entry.isOnline ? equipmentIcon : offlineEquipmentIcon,
+ width: 40,
+ height: 56,
+ verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
+ })
+ billboard.id = entry.entityId
+ devicePickMap.set(entry.entityId, { data: entry.data, billboard })
+ if (!entry.isOnline) return
+ if (!Number.isFinite(entry.rangeMeters) || entry.rangeMeters <= 0) return
+ const primitive = createDeviceRangePrimitiveWithHeight(entry.position, entry.rangeMeters, centerHeight)
+ if (!primitive) return
+ primitive.show = detailVisible.value
+ addCockpitPrimitive(primitive)
+ deviceRingFillPrimitives.push(primitive)
+ })
+ }
+ if (selectedTargetType.value === 'device' && selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) {
+ closePopup()
+ }
+ reorderCockpitPrimitives()
}
-const loadDefenseZones = async () => {
- if (!viewer) return
+const elevatePositions = async (positions, height = POLYGON_HEIGHT_M) => {
+ if (!Array.isArray(positions) || !positions.length) return []
+ if (!viewer) return positions
+ const points = positions
+ .map(pos => {
+ const carto = Cesium.Cartographic.fromCartesian(pos)
+ if (!carto) return null
+ return {
+ lng: Cesium.Math.toDegrees(carto.longitude),
+ lat: Cesium.Math.toDegrees(carto.latitude),
+ }
+ })
+ .filter(Boolean)
+ if (!points.length) return positions
+ const heights = await getPointPositionsHeight(points, viewer)
+ return heights.map(item => Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude, (item.ASL ?? 0) + height))
+}
+
+const getAreaTypeStyle = areaType => {
+ return AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
+}
+
+const buildPartitionPrimitives = async shapes => {
+ if (!viewer) return { primitives: [], outlinePrimitives: [] }
+ const vertexFormat = getTexturedVertexFormat()
+ const groups = new Map()
+ const tasks = []
+ ; (shapes || []).forEach(shape => {
+ if (!shape?.positions || shape.positions.length < 3) return
+ const outlineColor = shape.outlineColor
+ const fillColor = shape.fillColor
+ const key = `${outlineColor.toCssColorString()}|${fillColor.toCssColorString()}`
+ if (!groups.has(key)) {
+ groups.set(key, { outlineColor, fillColor, polygonInstances: [], lineInstances: [] })
+ }
+ const group = groups.get(key)
+ const positions = shape.positions
+ tasks.push(
+ elevatePositions(positions).then(elevatedPositions => {
+ if (!elevatedPositions.length) return
+ const polygon = new Cesium.PolygonGeometry({
+ polygonHierarchy: new Cesium.PolygonHierarchy(elevatedPositions),
+ perPositionHeight: true,
+ vertexFormat,
+ })
+ group.polygonInstances.push(
+ new Cesium.GeometryInstance({
+ geometry: polygon,
+ })
+ )
+ const linePositions =
+ elevatedPositions.length > 1
+ ? [...elevatedPositions, elevatedPositions[0]]
+ : elevatedPositions
+ group.lineInstances.push(
+ new Cesium.GeometryInstance({
+ geometry: new Cesium.PolylineGeometry({
+ positions: linePositions,
+ width: 2,
+ }),
+ attributes: {
+ color: Cesium.ColorGeometryInstanceAttribute.fromColor(outlineColor),
+ },
+ })
+ )
+ })
+ )
+ })
+
+ if (tasks.length) await Promise.all(tasks)
+
+ const primitives = []
+ const outlinePrimitives = []
+ groups.forEach(group => {
+ if (group.polygonInstances.length) {
+ const material = createRadialGradientMaterial(group.outlineColor, group.fillColor, {
+ gamma: 1.7,
+ innerCutoff: 0,
+ })
+ const primitive = new Cesium.Primitive({
+ geometryInstances: group.polygonInstances,
+ appearance: new Cesium.MaterialAppearance({
+ material,
+ translucent: true,
+ }),
+ })
+ addCockpitPrimitive(primitive)
+ primitives.push(primitive)
+ }
+ if (group.lineInstances.length) {
+ const outlinePrimitive = new Cesium.Primitive({
+ geometryInstances: group.lineInstances,
+ appearance: new Cesium.PolylineColorAppearance(),
+ })
+ addCockpitPrimitive(outlinePrimitive)
+ outlinePrimitives.push(outlinePrimitive)
+ }
+ })
+
+ return { primitives, outlinePrimitives }
+}
+
+const parseGeomJson = geomJson => {
+ if (!geomJson) return null
+ if (typeof geomJson === 'object') return geomJson
+ if (typeof geomJson !== 'string') return null
+ const trimmed = geomJson.trim()
+ if (!trimmed) return null
try {
- const res = await fwDefenseZonePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE })
- renderDefenseZones(res?.data?.data?.records ?? [])
- } catch (error) {
- renderDefenseZones([])
+ return JSON.parse(trimmed)
+ } catch (error) { }
+ return null
+}
+
+const normalizeShapePoint = point => {
+ if (!point) return null
+ const lng = point?.lng ?? point?.longitude
+ const lat = point?.lat ?? point?.latitude
+ const height = Number.isFinite(Number(point?.height)) ? Number(point.height) : 0
+ if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
+ return { lng: Number(lng), lat: Number(lat), height }
+}
+
+const buildShapePositions = (points = []) => {
+ const normalized = points.map(normalizeShapePoint).filter(Boolean)
+ return normalized.map(point => Cesium.Cartesian3.fromDegrees(point.lng, point.lat, point.height))
+}
+
+const getShapeDisplayPoints = shape => {
+ if (Array.isArray(shape?.displayPoints) && shape.displayPoints.length) {
+ return shape.displayPoints
}
+ return shape?.points || []
+}
+
+const resolvePartitionShapes = areas => {
+ const shapes = []
+ ; (areas || []).forEach(area => {
+ const extList = Array.isArray(area?.fwAreaDivideExtList) ? area.fwAreaDivideExtList : []
+ extList.forEach((item, index) => {
+ const isShapePayload = item?.drawType || item?.points
+ const parsed = isShapePayload ? item : parseGeomJson(item?.geomJson)
+ if (!parsed) return
+ const shape = {
+ id: parsed?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
+ drawType: parsed?.drawType ?? 'polygon',
+ areaType: parsed?.areaType ?? item?.areaTypeKey ?? item?.areaType ?? '',
+ points: Array.isArray(parsed?.points) ? parsed.points : [],
+ displayPoints: Array.isArray(parsed?.displayPoints) ? parsed.displayPoints : null,
+ meta: parsed?.meta ?? null,
+ }
+ if (shape.drawType === 'buffer' && shape.meta?.bufferRadii?.length && shape.meta?.center) {
+ const center = shape.meta.center
+ const centerCartesian = Cesium.Cartesian3.fromDegrees(
+ center.lng,
+ center.lat,
+ center.height || 0
+ )
+ const radii = shape.meta.bufferRadii
+ .map(radius => Number(radius))
+ .filter(radius => Number.isFinite(radius) && radius > 0)
+ radii.forEach((radius, levelIndex) => {
+ const positions = buildEllipsePositions(centerCartesian, radius, radius)
+ const style = BUFFER_LEVEL_STYLES[levelIndex] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
+ if (positions.length >= 3) {
+ shapes.push({
+ positions,
+ fillColor: style.fill,
+ outlineColor: style.outline,
+ })
+ }
+ })
+ return
+ }
+ const positions = buildShapePositions(getShapeDisplayPoints(shape))
+ if (positions.length >= 3) {
+ const style = getAreaTypeStyle(shape.areaType)
+ shapes.push({
+ positions,
+ fillColor: style.fill,
+ outlineColor: style.outline,
+ })
+ }
+ })
+ })
+ return shapes
+}
+
+const renderPartitions = async zones => {
+ if (!viewer) return
+ clearPartitionEntities()
+ const shapes = resolvePartitionShapes(zones)
+ const result = await buildPartitionPrimitives(shapes)
+ partitionFillPrimitives = result.primitives
+ partitionOutlinePrimitives = result.outlinePrimitives
+ if (partitionFillPrimitives.length) {
+ partitionFillPrimitives.forEach(primitive => {
+ primitive.show = detailVisible.value
+ })
+ }
+ if (partitionOutlinePrimitives.length) {
+ partitionOutlinePrimitives.forEach(primitive => {
+ primitive.show = detailVisible.value
+ })
+ }
+ reorderCockpitPrimitives()
}
const loadPartitions = async () => {
if (!viewer) return
try {
- const res = await fwAreaDividePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE })
- renderPartitions(res?.data?.data?.records ?? [])
+ const res = await newAreaDivideList({
+ isSetSceneManage: 1,
+ flyTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+ })
+ await renderPartitions(res?.data?.data ?? [])
} catch (error) {
- renderPartitions([])
+ await renderPartitions([])
}
}
@@ -471,7 +1117,7 @@
viewer.dataSources.add(aggregationSource)
}
clearAggregationEntities()
- aggregationSource.show = clusterVisible.value
+ updateAggregationVisibility()
const countMap = new Map()
; (list || []).forEach(item => {
if (!item?.type) return
@@ -488,7 +1134,7 @@
width: 66.73,
height: 43,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
- heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ // disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
label: {
text: `${count}`,
@@ -496,33 +1142,33 @@
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
verticalOrigin: Cesium.VerticalOrigin.CENTER,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
- font: '12pt Source Han Sans CN',
+ font: '11pt Source Han Sans CN',
eyeOffset: new Cesium.Cartesian3(0, 0, -20), // 让label "浮" 在广告牌前面
- pixelOffset: new Cesium.Cartesian2(2, -11),
- pixelOffset: new Cesium.Cartesian2(0, -48),
+ 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 entityId = typeof pick?.id === 'string' ? pick.id : pick?.id?.id
- if (entityId && deviceEntityMap.has(entityId)) {
- return deviceEntityMap.get(entityId)
+ 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 || !selectedDeviceEntity) return
- const cartesian =
- selectedDeviceEntity.position?.getValue?.(Cesium.JulianDate.now()) ||
- selectedDeviceEntity.position?._value
+ if (!viewer || !selectedDeviceBillboard) return
+ const cartesian = getBillboardPosition(selectedDeviceBillboard)
if (!cartesian) return
const screenPosition = viewer.scene.cartesianToCanvasCoordinates(cartesian)
if (!screenPosition) return
@@ -545,13 +1191,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
- selectedDeviceEntity = pickedDevice.entity
+ selectedTargetType.value = pickedTarget.type
+ selectedDevice.value = pickedTarget.data
+ selectedDeviceBillboard = pickedTarget.billboard
startPopupRender()
}
@@ -569,104 +1216,61 @@
function closePopup () {
selectedDevice.value = null
- selectedDeviceEntity = 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 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 popupTitle = computed(() => selectedDevice.value?.deviceName || selectedDevice.value?.deviceModel || '-')
-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 renderCommandPosts = list => {
+const renderCommandPosts = async list => {
if (!viewer) return
- if (!commandPostSource) {
- commandPostSource = new Cesium.CustomDataSource('commandPostSource')
- viewer.dataSources.add(commandPostSource)
- }
- clearCommandPostEntities()
- commandPostSource.show = detailVisible.value
+ ensureCommandPostCollection()
+ commandPostBillboardCollection.removeAll()
+ commandPostBillboardCollection.show = detailVisible.value
+ const entries = []
+ const positions = []
; (list || []).forEach((item, index) => {
const position = getDevicePosition(item)
if (!position) return
- commandPostSource.entities.add({
- id: `command-post-${item.id ?? index}`,
- position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
- billboard: {
- image: commandPostIcon,
- width: 40,
- height: 56,
- verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
- heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
- },
+ entries.push({ position, index })
+ positions.push({ lng: position.longitude, lat: position.latitude })
+ })
+ if (positions.length) {
+ const heights = await getPointPositionsHeight(positions, viewer)
+ entries.forEach((entry, idx) => {
+ const height = Number(heights?.[idx]?.ASL)
+ const centerHeight = Number.isFinite(height) ? height : 0
+ commandPostBillboardCollection.add({
+ position: Cesium.Cartesian3.fromDegrees(
+ entry.position.longitude,
+ entry.position.latitude,
+ centerHeight
+ ),
+ image: commandPostIcon,
+ width: 40,
+ height: 56,
+ verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
})
})
+ }
+ reorderCockpitPrimitives()
}
const loadAggregation = async () => {
try {
- const res = await cockpitAggregationApi()
+ const res = await newCockpitAggregationApi({
+ effectiveRangeKmIsNotNull: 1
+ })
renderAggregation(res?.data?.data ?? [])
} catch (error) {
renderAggregation([])
@@ -675,16 +1279,18 @@
const loadCommandPosts = async () => {
try {
- const res = await fwDefenseSceneListApi()
- renderCommandPosts(res?.data?.data ?? [])
+ const res = await newDefenseSceneManageList({
+ time: dayjs().format('YYYY-MM-DD HH:mm:ss')
+ })
+ await renderCommandPosts(res?.data?.data ?? [])
} catch (error) {
- renderCommandPosts([])
+ await renderCommandPosts([])
}
}
watch(
- () => props.onlineDevices,
+ () => props.allDevices,
devices => {
renderDeviceEntities(devices || [])
},
@@ -692,9 +1298,24 @@
)
watch(
- () => props.leftCollapsed,
+ () => 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.rightCollapsed,
isCollapsed => {
- if (isCollapsed) showLayerPanel.value = false
+ if (!isCollapsed) return
+ showLayerPanel.value = false
+ showBaseLayerPanel.value = false
}
)
@@ -703,22 +1324,51 @@
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 = async ({ viewer: mapViewer, publicCesium: mapPublic }) => {
+ if (mapReadyHandled) return
+ mapReadyHandled = true
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()
+ await loadCommandPosts()
+ renderDeviceEntities(props.allDevices)
loadPartitions()
loadAggregation()
- loadCommandPosts()
+ renderSimulatedDroneTrack(props.alarmDrones)
initDeviceClickHandler()
}
@@ -726,7 +1376,7 @@
updateStageDisplay(stage)
}
-const handleClickOutside = event => {
+const handleLayerClickOutside = event => {
if (!showLayerPanel.value) return
const target = event.target
if (layerWrapRef.value?.contains(target)) return
@@ -734,21 +1384,40 @@
}
onMounted(() => {
- document.addEventListener('click', handleClickOutside)
+ document.addEventListener('click', handleLayerClickOutside)
const map = mapRef.value?.getMap()
if (map?.viewer) handleMapReady(map)
})
onBeforeUnmount(() => {
- document.removeEventListener('click', handleClickOutside)
+ document.removeEventListener('click', handleLayerClickOutside)
clearDeviceEntities()
- clearDefenseZoneEntities()
clearPartitionEntities()
clearAggregationEntities()
clearCommandPostEntities()
+ clearDroneTrackEntities()
closePopup()
destroyDeviceClickHandler()
+ if (cockpitPrimitiveLayer && viewer?.scene?.primitives) {
+ viewer.scene.primitives.remove(cockpitPrimitiveLayer)
+ }
+ cockpitPrimitiveLayer = null
viewer = null
+ publicCesium = null
+})
+
+const getMap = () => {
+ return mapRef.value?.getMap()
+}
+
+// 暴露给父组件调用
+const flyTo = (options) => {
+ mapRef.value?.flyToPoint(options)
+}
+
+defineExpose({
+ getMap,
+ flyTo
})
</script>
@@ -767,168 +1436,16 @@
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__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 {
position: absolute;
- left: 337px;
- bottom: 22px;
- z-index: 9;
+ right: 337px;
+ top: 95px;
+ z-index: 11;
transition: transform 0.3s ease-in-out;
pointer-events: none;
&.collapsed {
- transform: translateX(-317px);
+ transform: translateX(317px);
}
}
@@ -955,12 +1472,13 @@
display: flex;
flex-direction: column;
position: absolute;
- left: 66px;
- bottom: 0;
+ right: 66px;
+ top: 0;
width: 160px;
max-height: 442px;
background: #191932;
border-radius: 8px 8px 8px 8px;
+ z-index: 99;
.panel-title {
padding: 0 16px;
@@ -983,85 +1501,136 @@
overflow: auto;
}
- ::v-deep(.el-tree) {
- background: transparent;
- color: #c3c3dd;
- font-size: 12px;
+}
- .el-tree-node {
- line-height: 30px !important;
+.base-layer-switch-root {
+ position: absolute;
+ right: 337px;
+ bottom: 78px;
+ z-index: 11;
+ transition: transform 0.3s ease-in-out;
+ pointer-events: none;
- .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;
- }
- }
- }
+ &.collapsed {
+ transform: translateX(317px);
}
}
-.layer-panel :deep(.el-tree-node__label) {
- color: #c3c3dd;
+.base-layer-switch-wrap {
+ position: relative;
+ pointer-events: auto;
}
-.layer-panel :deep(.el-tree-node__expand-icon) {
- order: 3;
- margin-left: auto;
+.base-layer-trigger {
+ width: 55px;
+ padding: 8px 0;
+ border-radius: 8px;
+ background: #191932;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+ cursor: pointer;
}
-.layer-panel :deep(.el-tree-node__expand-icon.is-leaf) {
- visibility: hidden;
+.base-layer-trigger-thumb {
+ width: 36px;
+ height: 36px;
+ border-radius: 6px;
+ display: block;
+ // border: 2px solid #fff;
+ background-size: cover;
+ background-position: center;
+ box-sizing: border-box;
}
-.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;
+.base-layer-trigger-label {
+ margin-top: 8px;
+ font-size: 12px;
+ line-height: 1;
color: #ffffff;
+ font-weight: 700;
}
-.layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content .el-tree-node__label) {
- color: #ffffff;
+.base-layer-panel {
+ position: absolute;
+ right: calc(100% + 10px);
+ bottom: 0;
+ margin-bottom: 0;
+ width: 160px;
+ background: #191932;
+ border-radius: 8px;
+ z-index: 99;
+
+ &::after {
+ content: '';
+ position: absolute;
+ right: -10px;
+ top: 0;
+ width: 10px;
+ height: 100%;
+ background: transparent;
+ }
}
-.layer-panel :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
- background-color: #023aff;
- border-color: #023aff;
+.base-map-options {
+ padding: 8px;
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 8px;
}
-.layer-panel :deep(.el-checkbox__inner) {
- background-color: transparent;
- border-color: #a1a3d4;
+.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: 36px;
+ height: 36px;
+ border-radius: 6px;
+ // border: 2px solid #fff;
+ background-size: cover;
+ background-position: center;
+ box-sizing: border-box;
+ }
+
+ .base-map-label {
+ margin-top: 8px;
+ font-size: 12px;
+ line-height: 1;
+ color: #ffffff;
+ font-weight: 700;
+ }
+
+
+ // &.active {
+ // .base-map-thumb {
+ // border: 2px solid #2ea8ff;
+ // }
+
+ // .base-map-label {
+ // color: #2ea8ff;
+ // }
+ // }
+}
+
+.layer-panel-slide-enter-active,
+.layer-panel-slide-leave-active {
+ transition: transform 0.24s ease, opacity 0.24s ease;
+ transform-origin: right center;
+}
+
+.layer-panel-slide-enter-from,
+.layer-panel-slide-leave-to {
+ transform: translateX(24px);
+ opacity: 0;
}
</style>
--
Gitblit v1.9.3