| | |
| | | * @Author : yuan |
| | | * @Date : 2026-01-09 11:14:04 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-26 09:38:16 |
| | | * @LastEditTime : 2026-01-26 19:02:07 |
| | | * @FilePath : \applications\drone-command\src\api\dataCockpit\index.js |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | |
| | | export const cockpitAggregationApi = (params) => { |
| | | return request({ |
| | | url: '/drone-fw/device/fwDevice/statisticalDeviceOut', |
| | | method: 'get' |
| | | method: 'get', |
| | | params |
| | | }) |
| | | } |
| | | |
| | |
| | | <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"> |
| | |
| | | createRadialGradientMaterial, |
| | | getTexturedVertexFormat, |
| | | } from './device-map-materials' |
| | | |
| | | const CLUSTER_HEIGHT = 100000 |
| | | const DETAIL_HEIGHT = 10000 |
| | | |
| | | const props = defineProps({ |
| | | onlineDevices: { |
| | |
| | | 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 = () => { |
| | |
| | | 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) |
| | |
| | | }) |
| | | } |
| | | |
| | | 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 renderSimulatedDroneTrack = () => { |
| | | if (!viewer) return |
| | | clearDroneTrackEntities() |
| | | ensureCountyCenterMap() |
| | | ensureDroneTrackCollections() |
| | | droneTrackBillboardCollection.show = detailVisible.value |
| | | droneTrackPolylineCollection.show = detailVisible.value |
| | | droneTrackRuntime = [] |
| | | const centers = Array.from(countyCenterMap.entries()) |
| | | 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 |
| | | const baseTrackColor = Cesium.Color.fromCssColorString('red') |
| | | baseCenters.forEach(([centerName, center], trackIndex) => { |
| | |
| | | }) |
| | | const droneId = `drone-track-${trackIndex}` |
| | | const billboard = droneTrackBillboardCollection.add({ |
| | | position: positions[0], |
| | | position: positions[positions.length - 1], |
| | | image: droneIcon, |
| | | width: 36, |
| | | height: 36, |
| | |
| | | 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, |
| | |
| | | ringFillInstancesByStyle.forEach((instances, index) => { |
| | | if (!instances.length) return |
| | | const ring = RING_STYLES[index] |
| | | 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(0.64), |
| | | Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(0.64) |
| | | 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, |
| | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | 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() |
| | |
| | | import * as Cesium from 'cesium' |
| | | |
| | | export const RING_STYLES = [{ inner: 0, outer: 2000, gradient: ['#FF361C', '#360B00'] }] |
| | | export const RING_STYLES = [ |
| | | { |
| | | inner: 0, |
| | | outer: 2000, |
| | | gradient: ['#FF6A57', '#A11806'], |
| | | alpha: 0.42, |
| | | outlineAlpha: 0.85, |
| | | innerRatio: 0.12, |
| | | }, |
| | | ] |
| | | |
| | | export const MATERIAL_TYPE = 'RadialGradientMaterial' |
| | | export const DRONE_TRACK_MATERIAL_TYPE = 'DroneTrackFlowMaterial' |
| | |
| | | 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), |
| | | gamma: 1.7, |
| | | innerCutoff: 0.0, |
| | | }, |
| | | source: ` |
| | | uniform vec4 color1; |
| | | uniform vec4 color2; |
| | | uniform float gamma; |
| | | uniform float innerCutoff; |
| | | |
| | | 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); |
| | | float ramp = smoothstep(innerCutoff, 1.0, t); |
| | | float shaped = pow(t, gamma); |
| | | float alpha = color.a * ramp * shaped; |
| | | material.diffuse = color.rgb; |
| | | material.alpha = color.a; |
| | | material.alpha = alpha; |
| | | return material; |
| | | } |
| | | `, |
| | |
| | | }) |
| | | } |
| | | |
| | | export const createRadialGradientMaterial = (color1, color2) => { |
| | | export const createRadialGradientMaterial = (color1, color2, options = {}) => { |
| | | registerRadialGradientMaterial() |
| | | return Cesium.Material.fromType(MATERIAL_TYPE, { color1, color2 }) |
| | | return Cesium.Material.fromType(MATERIAL_TYPE, { color1, color2, ...(options || {}) }) |
| | | } |
| | | |
| | | export const createDroneTrackMaterial = options => { |
| | |
| | | return positions |
| | | } |
| | | |
| | | export const addDeviceRings = (center, ringFillInstancesByStyle, ringOutlineInstancesByStyle) => { |
| | | export const addDeviceRings = (center, ringFillInstancesByStyle, ringOutlineInstancesByStyle, rangeMeters) => { |
| | | if (!ringFillInstancesByStyle) return |
| | | const centerPosition = Cesium.Cartesian3.fromDegrees(center.longitude, center.latitude, 0) |
| | | const vertexFormat = getTexturedVertexFormat() |
| | | RING_STYLES.forEach((ring, index) => { |
| | | if (!ringFillInstancesByStyle[index]) return |
| | | const outer = Number.isFinite(rangeMeters) && rangeMeters > 0 ? rangeMeters : ring.outer |
| | | ringFillInstancesByStyle[index].push( |
| | | new Cesium.GeometryInstance({ |
| | | geometry: new Cesium.EllipseGeometry({ |
| | | center: centerPosition, |
| | | semiMajorAxis: ring.outer, |
| | | semiMinorAxis: ring.outer, |
| | | semiMajorAxis: outer, |
| | | semiMinorAxis: outer, |
| | | vertexFormat, |
| | | }), |
| | | }) |
| | |
| | | if (!ringOutlineInstancesByStyle) return |
| | | RING_STYLES.forEach((ring, index) => { |
| | | if (!ringOutlineInstancesByStyle[index]) return |
| | | const positions = buildCirclePositions(center, ring.outer) |
| | | const outer = Number.isFinite(rangeMeters) && rangeMeters > 0 ? rangeMeters : ring.outer |
| | | const positions = buildCirclePositions(center, outer) |
| | | const outlineAlpha = typeof ring.outlineAlpha === 'number' ? ring.outlineAlpha : 0.8 |
| | | ringOutlineInstancesByStyle[index].push( |
| | | new Cesium.GeometryInstance({ |
| | | geometry: new Cesium.GroundPolylineGeometry({ |
| | |
| | | }), |
| | | attributes: { |
| | | color: Cesium.ColorGeometryInstanceAttribute.fromColor( |
| | | Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.8) |
| | | Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(outlineAlpha) |
| | | ), |
| | | }, |
| | | }) |
| | |
| | | } |
| | | |
| | | .legend-color.defense { |
| | | background: radial-gradient(50% 50% at 50% 50%, #2aedbf 0%, #012b11 100%); |
| | | border: 1px solid #19d266; |
| | | background: radial-gradient(50% 50% at 50% 50%, rgba(114, 243, 255, 0.45) 0%, rgba(10, 124, 136, 0.55) 100%); |
| | | border: 1px solid rgba(114, 243, 255, 0.85); |
| | | } |
| | | |
| | | .legend-color.partition { |
| | | background: radial-gradient(50% 50% at 50% 50%, #ffc609 0%, #583300 100%); |
| | | border: 1px solid #ffcd2a; |
| | | background: radial-gradient(50% 50% at 50% 50%, rgba(255, 215, 114, 0.5) 0%, rgba(168, 107, 0, 0.6) 100%); |
| | | border: 1px solid rgba(255, 215, 114, 0.9); |
| | | } |
| | | |
| | | .legend-icon { |
| | |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">有效范围</span> |
| | | {{ data.effectiveRangeKm }}m |
| | | {{ Number(data.effectiveRangeKm).toFixed(0) }}m |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | v-model="formData.effectiveRangeKm" |
| | | :min="0" |
| | | :precision="0" |
| | | :step="1" |
| | | :controls="false" |
| | | placeholder="请输入" |
| | | /> |
| | |
| | | |
| | | function formatRange(value) { |
| | | if (value == null || value === '') return '-' |
| | | return `${value}` |
| | | const normalized = normalizeRange(value) |
| | | return normalized == null ? '-' : `${normalized}` |
| | | } |
| | | |
| | | function normalizeRange(value) { |
| | | const numeric = Number(value) |
| | | if (!Number.isFinite(numeric)) return null |
| | | return Math.trunc(numeric) |
| | | } |
| | | |
| | | // 关闭弹框 |
| | |
| | | submitting.value = true |
| | | try { |
| | | const {longitude, latitude} = parseDeployLocation(formData.value.deployLocation) |
| | | const normalizedRange = normalizeRange(formData.value.effectiveRangeKm) |
| | | |
| | | const payload = { |
| | | id: formData.value.id || formData.value.deviceId || undefined, |
| | | effectiveRangeKmIsNotNull: formData.value.effectiveRangeKm ? 1 : 2, |
| | | effectiveRangeKm: formData.value.effectiveRangeKm, |
| | | effectiveRangeKmIsNotNull: normalizedRange != null ? 1 : 2, |
| | | effectiveRangeKm: normalizedRange, |
| | | longitude, |
| | | latitude |
| | | } |
| | | formData.value.effectiveRangeKm = normalizedRange |
| | | delete payload.deviceId |
| | | await detectionRangeSubmitApi(payload) |
| | | |
| | |
| | | current: 1, |
| | | size: 200, |
| | | effectiveRangeKmIsNotNull: 2, |
| | | isTrack: 1 |
| | | }) |
| | | const records = res?.data?.data?.records ?? [] |
| | | deviceOptions.value = records.map(item => ({ |
| | |
| | | watch( |
| | | () => formData.value.effectiveRangeKm, |
| | | value => { |
| | | const normalized = normalizeRange(value) |
| | | if (value != null && normalized == null) { |
| | | formData.value.effectiveRangeKm = null |
| | | return |
| | | } |
| | | if (value != null && normalized !== value) { |
| | | formData.value.effectiveRangeKm = normalized |
| | | return |
| | | } |
| | | const parsed = parseDeployLocation(formData.value.deployLocation) |
| | | if (!parsed) return |
| | | updateRangeCircle(parsed.longitude, parsed.latitude, value) |
| | | updateRangeCircle(parsed.longitude, parsed.latitude, normalized) |
| | | } |
| | | ) |
| | | </script> |
| | |
| | | |
| | | function formatRange(row) { |
| | | if (!row) return '-' |
| | | const value = row.effectiveRangeKm ?? row.range |
| | | return value === null || value === undefined || value === '' ? '-' : value |
| | | |
| | | let value = row.effectiveRangeKm ?? row.range |
| | | |
| | | return value === null || value === undefined || value === '' ? '-' : Number(value).toFixed(0) |
| | | } |
| | | |
| | | function formatDeviceCode(row) { |