吉安感知网项目-前端
罗广辉
2026-08-13 1d552a3bad95caae4af15322df28e6240b797693
applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue
@@ -10,6 +10,7 @@
               :terrain="true"
               :layer-mode="4"
               :boundary="false"
               :zoom-to-boundary="dialogMode === 'add'"
            />
         </div>
         <div class="right-container">
@@ -23,7 +24,7 @@
               <div class="detail-title">场景详情</div>
               <el-row>
                  <el-col :span="24">
                     <div class="label">场景名称</div>
                     <div class="label">配置名称</div>
                     <div class="val">{{ formData.sceneName }}</div>
                  </el-col>
                  <el-col :span="24">
@@ -49,6 +50,24 @@
                     </el-table>
                  </div>
               </div>
               <div class="detail-title">设备列表</div>
               <div class="command-table-container">
                  <div class="command-table-content">
                     <el-table
                        class="command-table"
                        :data="deviceList"
                        :loading="deviceLoading"
                        row-key="id"
                     >
                        <el-table-column prop="deviceName" show-overflow-tooltip label="设备名称" />
                        <el-table-column prop="deviceType" show-overflow-tooltip label="设备类型">
                           <template v-slot="{ row }">
                              {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                           </template>
                        </el-table-column>
                     </el-table>
                  </div>
               </div>
            </div>
            <el-form
@@ -60,7 +79,7 @@
               :disabled="readonly"
               label-width="96px"
            >
               <el-form-item label="场景名称" prop="sceneName">
               <el-form-item label="配置名称" prop="sceneName">
                  <el-input
                     class="command-input"
                     v-model="formData.sceneName"
@@ -127,6 +146,25 @@
                     </el-table>
                  </el-form-item>
               </div>
               <div class="detail-title">设备列表</div>
               <div class="command-table-container">
                  <div class="command-table-content">
                     <el-table
                        class="command-table"
                        :data="deviceList"
                        :loading="deviceLoading"
                        row-key="id"
                     >
                        <el-table-column prop="deviceName" show-overflow-tooltip label="设备名称" />
                        <el-table-column prop="deviceType" show-overflow-tooltip label="设备类型">
                           <template v-slot="{ row }">
                              {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                           </template>
                        </el-table-column>
                     </el-table>
                  </div>
               </div>
            </el-form>
            <div class="footer">
@@ -159,13 +197,24 @@
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import * as Cesium from 'cesium'
import { fwAreaDivideDetailApi, fwAreaDivideListApi } from '../partition/partitionApi'
import { buildEllipsePositions } from '@/utils/cesium/shapeTools'
import { fwDeviceListApi } from '@/views/basicManage/deviceStock/fwDevice'
import {
   addBufferRadiusLabelEntity,
   buildEllipsePositions,
} from '@/utils/cesium/shapeTools'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
import {
   createDeviceRangePrimitive,
   getDeviceLngLat,
   normalizeDeviceRange,
} from '@/utils/cesium/deviceRange'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
const initForm = () => ({
   sceneName: '', // 场景名称
   sceneName: '', // 配置名称
   sceneType: '', // 场景类型
   deviceMode: '', // 设备模式
   areaDivideIds: '', // 关联区域ID
@@ -176,7 +225,7 @@
const formRef = ref(null) // 表单实例
const formData = ref(initForm()) // 表单数据
const visible = defineModel() // 弹框显隐
const dialogMode = ref('add') // 弹框模式
const dialogMode = ref() // 弹框模式
const submitting = ref(false) // 提交中
const readonly = computed(() => dialogMode.value === 'view')
const titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' })
@@ -184,11 +233,17 @@
const searchName = ref('')
const areaList = ref([]) // 关联区域列表
const selectedAreaRows = ref([]) //选中列表
let lastSelectedAreaIds = []
const deviceList = ref([]) // 设备列表
const deviceLoading = ref(false)
const dictObj = inject('dictObj')
const mapRef = ref(null)
const route = useRoute()
let viewer
let areaDisplaySource
const deviceRangePrimitiveMap = new Map()
const deviceIconEntityMap = new Map()
let deviceRangeRenderToken = 0
const rules = {
   sceneName: fieldRules(true, 50),
@@ -239,13 +294,37 @@
   areaList.value = res?.data?.data ?? []
}
async function loadDeviceListByAreaIds(areaIdsKey) {
   if (!areaIdsKey) {
      deviceList.value = []
      clearDeviceRangePrimitives()
      clearDeviceIconEntities()
      return
   }
   deviceLoading.value = true
   try {
      const res = await fwDeviceListApi({ areaIds: areaIdsKey })
      deviceList.value = res?.data?.data ?? []
      await nextTick()
      renderDeviceRanges(deviceList.value)
   } finally {
      deviceLoading.value = false
   }
}
// 关联区域变更
async function handleAreaSelectionChange(rows) {
   const prevIds = new Set(lastSelectedAreaIds)
   const selectedRows = await ensureAreaExtList(rows)
   selectedAreaRows.value = selectedRows
   const ids = selectedRows.map(item => item.id)
   lastSelectedAreaIds = [...ids]
   formData.value.areaDivideIds = ids.join(',')
   formData.value.areaCount = ids.length
   if (ids.some(id => !prevIds.has(id))) {
      await nextTick()
      fitViewToSelectedAreas()
   }
}
// 渲染面
@@ -263,6 +342,22 @@
   areaDisplaySource.entities.removeAll()
}
function clearDeviceRangePrimitives() {
   if (!viewer || !deviceRangePrimitiveMap.size) return
   for (const primitive of deviceRangePrimitiveMap.values()) {
      viewer.scene.primitives.remove(primitive)
   }
   deviceRangePrimitiveMap.clear()
}
function clearDeviceIconEntities() {
   if (!viewer || !deviceIconEntityMap.size) return
   for (const entity of deviceIconEntityMap.values()) {
      viewer.entities.remove(entity)
   }
   deviceIconEntityMap.clear()
}
function normalizeShapePoint(point) {
   if (!point) return null
   const lng = point?.lng ?? point?.longitude
@@ -270,6 +365,28 @@
   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 }
}
function resolveLngLatPoint(point) {
   if (!point) return null
   const lng = point?.lng ?? point?.longitude
   const lat = point?.lat ?? point?.latitude
   if (Number.isFinite(Number(lng)) && Number.isFinite(Number(lat))) {
      return {
         lng: Number(lng),
         lat: Number(lat),
         height: Number.isFinite(Number(point?.height)) ? Number(point.height) : 0,
      }
   }
   if (!viewer || !Number.isFinite(point?.x) || !Number.isFinite(point?.y) || !Number.isFinite(point?.z)) {
      return null
   }
   const val = cartesian3Convert(point, viewer)
   return {
      lng: val.longitude,
      lat: val.latitude,
      height: Number.isFinite(val.height) ? val.height : 0,
   }
}
function buildShapePositions(points = []) {
@@ -286,6 +403,20 @@
function getAreaTypeStyle(areaType) {
   return AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
}
function resolveControlPoints(meta, drawType) {
   if (!meta) return []
   if (Array.isArray(meta.controlPoints) && meta.controlPoints.length) {
      return meta.controlPoints.map(resolveLngLatPoint).filter(Boolean)
   }
   if (drawType === 'buffer') {
      return [resolveLngLatPoint(meta.center)].filter(Boolean)
   }
   if (drawType === 'ellipse') {
      return [meta.center, meta.majorPoint, meta.minorPoint].map(resolveLngLatPoint).filter(Boolean)
   }
   return []
}
function parseGeomJson(geomJson) {
@@ -374,6 +505,10 @@
                     material: style.outline,
                  },
               })
               addBufferRadiusLabelEntity(dataSource, centerCartesian, radius, {
                  name: 'scene-area-display',
                  customData: { drawType: 'buffer', label: 'radius' },
               })
            })
            return
         }
@@ -400,11 +535,162 @@
   })
}
function pushRadiusBoundingPoints(positions, center, radius) {
   if (!center || !Number.isFinite(Number(radius)) || radius <= 0) return
   const resolved = resolveLngLatPoint(center) || {}
   const lng = Number(resolved.lng ?? resolved.longitude)
   const lat = Number(resolved.lat ?? resolved.latitude)
   if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
   const baseLat = Cesium.Math.toRadians(lat)
   const earthRadius = Cesium.Ellipsoid.WGS84.maximumRadius
   const deltaLat = radius / earthRadius
   const deltaLng = radius / (earthRadius * Math.max(Math.cos(baseLat), 0.000001))
   const points = [
      { lng: Cesium.Math.toDegrees(Cesium.Math.toRadians(lng) + deltaLng), lat },
      { lng: Cesium.Math.toDegrees(Cesium.Math.toRadians(lng) - deltaLng), lat },
      { lng, lat: Cesium.Math.toDegrees(baseLat + deltaLat) },
      { lng, lat: Cesium.Math.toDegrees(baseLat - deltaLat) },
   ]
   points.forEach(point => {
      positions.push(Cesium.Cartesian3.fromDegrees(point.lng, point.lat, resolved.height || 0))
   })
}
function collectSelectedAreaPositions() {
   const positions = []
   if (!viewer || !selectedAreaRows.value.length) return positions
   selectedAreaRows.value.forEach(area => {
      const shapes = resolveAreaShapes(area)
      shapes.forEach(shape => {
         let points = getShapeDisplayPoints(shape)
         if (!Array.isArray(points) || !points.length) {
            points = resolveControlPoints(shape?.meta, shape?.drawType)
         }
         if (Array.isArray(points) && points.length) {
            points.forEach(point => {
               const resolved = resolveLngLatPoint(point)
               const normalized = normalizeShapePoint(resolved)
               if (!normalized) return
               positions.push(
                  Cesium.Cartesian3.fromDegrees(normalized.lng, normalized.lat, normalized.height)
               )
            })
         }
         if (shape?.drawType === 'buffer' && shape?.meta?.center && Array.isArray(shape?.meta?.bufferRadii)) {
            const maxRadius = Math.max(...shape.meta.bufferRadii.filter(item => Number.isFinite(item)))
            pushRadiusBoundingPoints(positions, shape.meta.center, maxRadius)
         }
         if (shape?.drawType === 'ellipse' && shape?.meta?.center) {
            const major = Number(shape?.meta?.semiMajor)
            const minor = Number(shape?.meta?.semiMinor)
            const radius = Number.isFinite(major) && Number.isFinite(minor) ? Math.max(major, minor) : null
            pushRadiusBoundingPoints(positions, shape.meta.center, radius)
         }
      })
   })
   return positions
}
function fitViewToSelectedAreas() {
   if (!viewer || !selectedAreaRows.value.length) return
   const positions = collectSelectedAreaPositions()
   if (positions.length < 2) return
   const boundingSphere = Cesium.BoundingSphere.fromPoints(positions)
   const range = Math.max(boundingSphere.radius * 2.4, 200)
   viewer.camera.flyToBoundingSphere(boundingSphere, {
      duration: 0,
      offset: new Cesium.HeadingPitchRange(viewer.camera.heading, Cesium.Math.toRadians(-90), range),
   })
}
function addDeviceIconEntity(device, position) {
   if (!viewer || !position) return
   const id = String(device?.id ?? '')
   if (!id || deviceIconEntityMap.has(id)) return
   const entity = viewer.entities.add({
      position: Cesium.Cartesian3.fromDegrees(position.lng, position.lat, position.height || 0),
      billboard: {
         image: equipmentIcon,
         width: 28,
         height: 38,
         verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
         heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
      },
   })
   deviceIconEntityMap.set(id, entity)
}
async function renderDeviceRanges(rows) {
   if (!visible.value) return
   if (!viewer) initMap()
   if (!viewer) return
   deviceRangeRenderToken += 1
   const renderToken = deviceRangeRenderToken
   const selectedRows = Array.isArray(rows) ? rows : []
   const nextIds = new Set(selectedRows.map(row => String(row?.id)))
   for (const [id, primitive] of deviceRangePrimitiveMap.entries()) {
      if (!nextIds.has(id)) {
         viewer.scene.primitives.remove(primitive)
         deviceRangePrimitiveMap.delete(id)
      }
   }
   for (const [id, entity] of deviceIconEntityMap.entries()) {
      if (!nextIds.has(id)) {
         viewer.entities.remove(entity)
         deviceIconEntityMap.delete(id)
      }
   }
   const pendingRows = selectedRows.filter(row => {
      const id = String(row?.id)
      return id && !deviceRangePrimitiveMap.has(id)
   })
   if (!pendingRows.length) return
   const primitives = await Promise.all(
      pendingRows.map(async row => {
         const position = getDeviceLngLat(row)
         if (!position) return { row, primitive: null, position: null }
         const rangeMeters = normalizeDeviceRange(row.effectiveRangeKm)
         const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters)
         return { row, primitive, position }
      })
   )
   if (renderToken !== deviceRangeRenderToken) return
   primitives.forEach(({ row, primitive, position }) => {
      if (position) {
         addDeviceIconEntity(row, position)
      }
      if (!primitive) return
      const id = String(row?.id)
      if (!nextIds.has(id)) return
      deviceRangePrimitiveMap.set(id, primitive)
      viewer.scene.primitives.add(primitive)
   })
}
watch(() => selectedAreaRows.value, renderSelectedAreas)
const deviceAreaIdsKey = computed(() =>
   selectedAreaRows.value.map(item => item?.id).filter(Boolean).join(',')
)
watch(
   () => deviceAreaIdsKey.value,
   val => {
      loadDeviceListByAreaIds(val)
   }
)
watch(
   () => deviceList.value,
   val => {
      renderDeviceRanges(val)
   }
)
watch(
   () => visible.value,
   val => {
      if (!val) clearAreaDisplay()
      if (!val) {
         clearAreaDisplay()
         clearDeviceRangePrimitives()
         clearDeviceIconEntities()
      }
   }
)
@@ -451,6 +737,7 @@
   selectedAreaRows.value = ensuredRows
   if (!ensuredRows.length) return
   formData.value.areaCount = ensuredRows.length
   lastSelectedAreaIds = ensuredRows.map(row => row.id)
}
// 初始化地图实例
@@ -468,11 +755,15 @@
   await nextTick()
   initMap()
   await getAreaList()
   if (dialogMode.value !== 'add') {
   if (dialogMode.value === 'add') {
      mapRef.value?.zoomToAdminBoundary?.()
   } else {
      await loadDetail()
   }
   await nextTick()
   await syncAreaSelection()
   await nextTick()
   fitViewToSelectedAreas()
}
defineExpose({
   open,