From 7869a868a0a4e40a612298617defd3bde7bf5112 Mon Sep 17 00:00:00 2001
From: 罗广辉 <guanghui.luo@foxmail.com>
Date: Tue, 20 Jan 2026 14:47:40 +0800
Subject: [PATCH] feat: 加载地图
---
applications/task-work-order/src/components/map-container/device-map-container.vue | 763 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 710 insertions(+), 53 deletions(-)
diff --git a/applications/task-work-order/src/components/map-container/device-map-container.vue b/applications/task-work-order/src/components/map-container/device-map-container.vue
index 923fec0..46199a1 100644
--- a/applications/task-work-order/src/components/map-container/device-map-container.vue
+++ b/applications/task-work-order/src/components/map-container/device-map-container.vue
@@ -1,22 +1,82 @@
<template>
- <div class="work-cesium map-container" :id="props.containerId"></div>
- <div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }">
- <div class="layer-control-wrap" ref="layerWrapRef">
- <div class="layer-control" @click="toggleLayerPanel">
- <img :src="layerControlIcon" alt="图层控制" />
- </div>
- <div v-if="showLayerPanel" class="layer-panel">
- <div class="panel-title">图层管理</div>
+ <div class="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 }">
+ <div class="layer-control-wrap" ref="layerWrapRef">
+ <div class="layer-control" @click="toggleLayerPanel">
+ <img :src="layerControlIcon" alt="图层控制" />
+ </div>
+ <div v-if="showLayerPanel" class="layer-panel">
+ <div class="panel-title">图层管理</div>
- <div class="panel-content">
- <el-tree
- :data="layerTree"
- show-checkbox
- default-expand-all
- node-key="key"
- :props="layerTreeProps"
- :default-checked-keys="defaultCheckedKeys"
- />
+ <div class="panel-content">
+ <el-tree :data="layerTree" show-checkbox default-expand-all node-key="key"
+ :props="layerTreeProps" :default-checked-keys="defaultCheckedKeys" />
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <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>
+
+ <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>
@@ -25,9 +85,18 @@
<script setup>
import * as Cesium from 'cesium'
-import { PublicCesium } from '@/utils/cesium/publicCesium'
+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 layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.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'
const props = defineProps({
onlineDevices: {
@@ -48,11 +117,25 @@
},
})
-let viewInstance = null
+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 aggregationSource = null
+let commandPostSource = null
+const detailVisible = ref(true)
+const clusterVisible = ref(false)
+const countyCenterMap = new Map()
const showLayerPanel = ref(false)
const layerWrapRef = ref(null)
+const selectedDevice = ref(null)
+let selectedDeviceEntity = null
+let deviceClickHandler = null
+let popupRenderHandler = null
const layerTreeProps = {
label: 'label',
children: 'children',
@@ -103,12 +186,60 @@
viewer.entities.removeById(id)
})
deviceEntityIds.clear()
+ deviceEntityMap.clear()
}
+const clearDefenseZoneEntities = () => {
+ if (!defenseZoneSource) return
+ defenseZoneSource.entities.removeAll()
+}
+
+const clearPartitionEntities = () => {
+ if (!partitionSource) return
+ partitionSource.entities.removeAll()
+}
+
+const clearAggregationEntities = () => {
+ if (!aggregationSource) return
+ aggregationSource.entities.removeAll()
+}
+
+const clearCommandPostEntities = () => {
+ if (!commandPostSource) return
+ commandPostSource.entities.removeAll()
+}
+
+const setDetailVisibility = visible => {
+ detailVisible.value = visible
+ if (defenseZoneSource) defenseZoneSource.show = visible
+ if (partitionSource) partitionSource.show = visible
+ if (commandPostSource) commandPostSource.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
+}
+
+const ensureCountyCenterMap = () => {
+ if (countyCenterMap.size) return
+ const geojson = JSON.parse(jaGeojsonRaw)
+ geojson.features?.forEach(feature => {
+ const name = feature?.properties?.name
+ const center = feature?.properties?.centroid || feature?.properties?.center
+ if (!name || !Array.isArray(center) || center.length < 2) return
+ countyCenterMap.set(name, { longitude: center[0], latitude: center[1] })
+ })
+}
+
+
const RING_STYLES = [
- { inner: 0, outer: 5000, gradient: ['#FF361C', '#360B00'] },
- { inner: 5000, outer: 8000, gradient: ['#FFC609', '#583300'] },
- { inner: 8000, outer: 10000, gradient: ['#2AEDBF', '#012B11'] },
+ { inner: 0, outer: 2000, gradient: ['#FF361C', '#360B00'] }
]
const MATERIAL_TYPE = 'RadialGradientMaterial'
@@ -156,7 +287,7 @@
return positions
}
-const addDeviceRings = (center, baseId) => {
+const addDeviceRings = (center, baseId, visible) => {
RING_STYLES.forEach((ring, index) => {
const outerPositions = buildCirclePositions(center, ring.outer)
const holes = ring.inner
@@ -171,6 +302,7 @@
const material = new RadialGradientMaterialProperty(color1, color2)
viewer.entities.add({
id: entityId,
+ show: visible,
polygon: {
hierarchy: new Cesium.PolygonHierarchy(outerPositions, holes),
material,
@@ -186,26 +318,26 @@
this.color2 = color2
}
- get isConstant() {
+ get isConstant () {
return true
}
- get definitionChanged() {
+ get definitionChanged () {
return this._definitionChanged
}
- getType() {
+ getType () {
return MATERIAL_TYPE
}
- getValue(time, result) {
+ getValue (time, result) {
const target = result || {}
target.color1 = this.color1
target.color2 = this.color2
return target
}
- equals(other) {
+ equals (other) {
return (
other instanceof RadialGradientMaterialProperty &&
Cesium.Color.equals(this.color1, other.color1) &&
@@ -222,18 +354,347 @@
if (!position) return
const entityId = `online-device-${item.id ?? index}`
deviceEntityIds.add(entityId)
- addDeviceRings(position, entityId)
- viewer.entities.add({
+ 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.34,
- height: 40.34,
+ width: 40,
+ height: 56,
+ verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+ },
+ })
+ deviceEntityMap.set(entityId, { data: item, entity })
+ })
+ if (selectedDeviceEntity && !deviceEntityMap.has(selectedDeviceEntity.id)) {
+ 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 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 renderDefenseZones = zones => {
+ if (!viewer) return
+ if (!defenseZoneSource) {
+ defenseZoneSource = new Cesium.CustomDataSource('defenseZoneSource')
+ viewer.dataSources.add(defenseZoneSource)
+ }
+ 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)
+ }
+ clearPartitionEntities()
+ partitionSource.show = detailVisible.value
+ renderZonePolygons({
+ zones,
+ source: partitionSource,
+ idPrefix: 'partition-zone',
+ lineColor: '#FFCD2A',
+ fillGradient: ['#FFC609', '#583300'],
+ })
+}
+
+const loadDefenseZones = async () => {
+ if (!viewer) return
+ try {
+ const res = await fwDefenseZonePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE })
+ renderDefenseZones(res?.data?.data?.records ?? [])
+ } catch (error) {
+ renderDefenseZones([])
+ }
+}
+
+const loadPartitions = async () => {
+ if (!viewer) return
+ try {
+ const res = await fwAreaDividePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE })
+ renderPartitions(res?.data?.data?.records ?? [])
+ } catch (error) {
+ renderPartitions([])
+ }
+}
+
+const renderAggregation = list => {
+ if (!viewer) return
+ ensureCountyCenterMap()
+ if (!aggregationSource) {
+ aggregationSource = new Cesium.CustomDataSource('aggregationSource')
+ viewer.dataSources.add(aggregationSource)
+ }
+ clearAggregationEntities()
+ aggregationSource.show = clusterVisible.value
+ const countMap = new Map()
+ ; (list || []).forEach(item => {
+ if (!item?.type) return
+ countMap.set(item.type, Number(item.count ?? 0))
+ })
+ Array.from(countyCenterMap.entries()).forEach(([name, center], index) => {
+ const position = Cesium.Cartesian3.fromDegrees(center.longitude, center.latitude, 0)
+ const count = countMap.get(name) ?? 0
+ aggregationSource.entities.add({
+ id: `aggregation-${name}-${index}`,
+ position,
+ billboard: {
+ image: aggregationIcon,
+ width: 66.73,
+ height: 43,
+ verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ },
+ label: {
+ text: `${count}`,
+ fillColor: Cesium.Color.WHITE,
+ style: Cesium.LabelStyle.FILL_AND_OUTLINE,
+ verticalOrigin: Cesium.VerticalOrigin.CENTER,
+ horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
+ font: '11pt Source Han Sans CN',
+ eyeOffset: new Cesium.Cartesian3(0, 0, -20), // 让label "浮" 在广告牌前面
+
+ pixelOffset: new Cesium.Cartesian2(0, -35),
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ },
+ })
+ })
+}
+
+const getPickedDevice = 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)
+ }
+ }
+ return null
+}
+
+const updatePopupPosition = () => {
+ if (!viewer || !selectedDeviceEntity) return
+ const cartesian =
+ selectedDeviceEntity.position?.getValue?.(Cesium.JulianDate.now()) ||
+ selectedDeviceEntity.position?._value
+ if (!cartesian) return
+ const screenPosition = viewer.scene.cartesianToCanvasCoordinates(cartesian)
+ if (!screenPosition) return
+ popupPosition.value = { x: screenPosition.x, y: screenPosition.y }
+}
+
+const startPopupRender = () => {
+ if (!viewer || popupRenderHandler) return
+ popupRenderHandler = () => updatePopupPosition()
+ viewer.scene.postRender.addEventListener(popupRenderHandler)
+ updatePopupPosition()
+}
+
+const stopPopupRender = () => {
+ if (!viewer || !popupRenderHandler) return
+ viewer.scene.postRender.removeEventListener(popupRenderHandler)
+ popupRenderHandler = null
+}
+
+const handleDeviceClick = movement => {
+ if (!viewer) return
+ const picks = viewer.scene.drillPick(movement.position) || []
+ const pickedDevice = getPickedDevice(picks)
+ if (!pickedDevice) {
+ closePopup()
+ return
+ }
+ selectedDevice.value = pickedDevice.data
+ selectedDeviceEntity = pickedDevice.entity
+ startPopupRender()
+}
+
+const initDeviceClickHandler = () => {
+ if (deviceClickHandler || !viewer) return
+ deviceClickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
+ deviceClickHandler.setInputAction(handleDeviceClick, Cesium.ScreenSpaceEventType.LEFT_CLICK)
+}
+
+const destroyDeviceClickHandler = () => {
+ deviceClickHandler?.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK)
+ deviceClickHandler?.destroy()
+ deviceClickHandler = null
+}
+
+function closePopup () {
+ selectedDevice.value = null
+ selectedDeviceEntity = null
+ 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 renderCommandPosts = list => {
+ if (!viewer) return
+ if (!commandPostSource) {
+ commandPostSource = new Cesium.CustomDataSource('commandPostSource')
+ viewer.dataSources.add(commandPostSource)
+ }
+ clearCommandPostEntities()
+ commandPostSource.show = detailVisible.value
+ ; (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,
+ },
+ })
+ })
+}
+
+const loadAggregation = async () => {
+ try {
+ const res = await cockpitAggregationApi()
+ renderAggregation(res?.data?.data ?? [])
+ } catch (error) {
+ renderAggregation([])
+ }
+}
+
+const loadCommandPosts = async () => {
+ try {
+ const res = await fwDefenseSceneListApi()
+ renderCommandPosts(res?.data?.data ?? [])
+ } catch (error) {
+ renderCommandPosts([])
+ }
+}
+
watch(
() => props.onlineDevices,
@@ -255,6 +716,29 @@
showLayerPanel.value = !showLayerPanel.value
}
+const updateStageDisplay = stage => {
+ const showCluster = stage === 'cluster'
+ setClusterVisibility(showCluster)
+ setDetailVisibility(!showCluster)
+}
+
+const handleMapReady = ({ viewer: mapViewer }) => {
+ viewer = mapViewer
+ const height = viewer?.camera?.positionCartographic?.height
+ const stage = height != null && height >= 100000 ? 'cluster' : 'detail'
+ updateStageDisplay(stage)
+ renderDeviceEntities(props.onlineDevices)
+ loadDefenseZones()
+ loadPartitions()
+ loadAggregation()
+ loadCommandPosts()
+ initDeviceClickHandler()
+}
+
+const handleStageChange = stage => {
+ updateStageDisplay(stage)
+}
+
const handleClickOutside = event => {
if (!showLayerPanel.value) return
const target = event.target
@@ -264,31 +748,30 @@
onMounted(() => {
document.addEventListener('click', handleClickOutside)
- viewInstance = new PublicCesium({
- dom: props.containerId,
- flatMode: false,
- terrain: false,
- layerMode: 4,
- contour: false,
- })
-
- viewer = viewInstance.getViewer()
- renderDeviceEntities(props.onlineDevices)
+ const map = mapRef.value?.getMap()
+ if (map?.viewer) handleMapReady(map)
})
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside)
clearDeviceEntities()
- if (viewInstance) {
- viewInstance?.viewerDestroy()
- viewInstance = null
- }
-
+ clearDefenseZoneEntities()
+ clearPartitionEntities()
+ clearAggregationEntities()
+ clearCommandPostEntities()
+ closePopup()
+ destroyDeviceClickHandler()
viewer = null
})
</script>
<style lang="scss" scoped>
+.map-shell {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
.map-container {
position: absolute;
top: 0;
@@ -297,16 +780,182 @@
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 {
position: absolute;
- left: 411px;
+ left: 337px;
bottom: 22px;
z-index: 9;
transition: transform 0.3s ease-in-out;
pointer-events: none;
&.collapsed {
- transform: translateX(-401px);
+ transform: translateX(-317px);
}
}
@@ -318,8 +967,8 @@
}
.layer-control {
- width: 34px;
- height: 34px;
+ width: 46px;
+ height: 46px;
cursor: pointer;
img {
@@ -333,7 +982,7 @@
display: flex;
flex-direction: column;
position: absolute;
- left: 46px;
+ left: 66px;
bottom: 0;
width: 160px;
max-height: 442px;
@@ -370,6 +1019,7 @@
line-height: 30px !important;
.el-tree-node__content {
+ padding-left: 0 !important;
display: flex;
align-items: center;
height: 40px !important;
@@ -381,6 +1031,12 @@
.el-tree-node__children {
.el-tree-node__content {
border: none;
+ }
+ }
+
+ &:focus {
+ .el-tree-node__content {
+ background: transparent !important;
}
}
}
@@ -412,7 +1068,8 @@
display: none;
}
-.layer-panel :deep(.el-tree-node__content:hover) {
+.layer-panel :deep(.el-tree-node__content:hover),
+.layer-panel :deep(.el-tree-node__content:focus) {
background: transparent !important;
}
--
Gitblit v1.9.3