吉安感知网项目-前端
罗广辉
2026-08-13 1d552a3bad95caae4af15322df28e6240b797693
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
@@ -97,7 +97,7 @@
               <div class="detail-title">绘制区域列表</div>
               <div class="command-table-container">
                  <div class="command-table-content">
                     <el-table class="command-table" :data="shapeList" row-key="id">
                     <el-table class="command-table" :data="shapeList" row-key="id"  height="240">
                        <el-table-column prop="areaType" label="区域类型">
                           <template v-slot="{ row }">
                              {{ resolveShapeAreaTypeLabel(row) }}
@@ -116,7 +116,7 @@
               <div class="command-table-container">
                  <div class="command-table-content">
                     <el-table class="command-table" ref="deviceTableRef" :data="deviceOptions" row-key="id"
                        @row-click="handleDeviceRowClick">
                        @row-click="handleDeviceRowClick"  height="240">
                        <el-table-column prop="deviceName" label="设备名称" />
                        <el-table-column prop="deviceType" label="类型">
                           <template v-slot="{ row }">
@@ -129,7 +129,7 @@
            </div>
            <el-form class="dialog-form" v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly"
               label-width="96px">
               label-width="116px">
               <el-form-item label="区域名称" prop="areaName">
                  <el-input class="command-input" v-model="formData.areaName" maxlength="50" placeholder="请输入"
                     clearable />
@@ -169,7 +169,7 @@
               <div class="shape-table-container">
                  <div class="shape-table-title">绘制区域列表</div>
                  <el-table class="command-table" :data="shapeList" row-key="id" height="auto">
                  <el-table class="command-table" :data="shapeList" row-key="id" height="240">
                     <el-table-column prop="areaType" label="区域类型">
                        <template v-slot="{ row }">
                           {{ resolveShapeAreaTypeLabel(row) }}
@@ -180,7 +180,7 @@
                           {{ drawTypeLabelMap[row.drawType] || '-' }}
                        </template>
                     </el-table-column>
                     <el-table-column label="操作" class-name="operation-btns" width="120">
                     <el-table-column label="操作" class-name="operation-btns" fixed="right" width="120">
                        <template v-slot="{ row }">
                           <el-link @click="handleShapeEdit(row)">编辑</el-link>
                           <el-link @click="handleShapeDelete(row)">删除</el-link>
@@ -201,7 +201,7 @@
                  <el-form-item prop="deviceIds" label-width="0">
                     <el-table class="command-table" ref="deviceTableRef"
                        :data="deviceOptions.filter(item => item.deviceName.includes(searchName))" row-key="id"
                        @selection-change="handleDeviceSelectionChange" @row-click="handleDeviceRowClick">
                        @selection-change="handleDeviceSelectionChange" @row-click="handleDeviceRowClick"  height="240">
                        <el-table-column type="selection" width="55" :reserve-selection="true" />
                        <el-table-column prop="deviceName" label="设备名称" />
                        <el-table-column prop="deviceType" label="类型">
@@ -260,6 +260,7 @@
import ellipseIcon from '@/assets/images/areaMap/ellipse.png'
import bufferIcon from '@/assets/images/areaMap/buffer.png'
import trashIcon from '@/assets/images/areaMap/trash.png'
import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
const initForm = () => ({
   areaName: '', // 区域名称
@@ -314,6 +315,7 @@
let viewEntity
let shapeDisplaySource
const deviceRangePrimitiveMap = new Map()
const deviceIconEntityMap = new Map()
const activeToolMode = ref('')
const suppressDeviceFly = ref(false)
const isDialogInitializing = ref(false)
@@ -321,6 +323,7 @@
let deviceRangeRenderToken = 0
let deviceListTimer = null
let deviceListRequestToken = 0
let deviceMapRequestToken = 0
const rules = {
   areaName: fieldRules(true, 50),
@@ -437,6 +440,7 @@
         hideTypePanel()
         clearShapeDisplay()
         clearDeviceRangePrimitives()
         clearDeviceIconEntities()
      }
   }
)
@@ -504,6 +508,14 @@
   deviceRangePrimitiveMap.clear()
}
function clearDeviceIconEntities () {
   if (!viewer || !deviceIconEntityMap.size) return
   for (const entity of deviceIconEntityMap.values()) {
      viewer.entities.remove(entity)
   }
   deviceIconEntityMap.clear()
}
function clearDeviceListState () {
   deviceOptions.value = []
   selectedDeviceRows.value = []
@@ -521,6 +533,60 @@
function isSameWktPoint (left, right) {
   if (!left || !right) return false
   return Math.abs(left.lat - right.lat) < 0.000001 && Math.abs(left.lng - right.lng) < 0.000001
}
function normalizePolygonPoints (points) {
   if (!Array.isArray(points)) return []
   const ring = points.filter(point => point && Number.isFinite(point.lng) && Number.isFinite(point.lat))
   if (ring.length < 2) return ring
   const first = ring[0]
   const last = ring[ring.length - 1]
   if (isSameWktPoint(first, last)) {
      return ring.slice(0, -1)
   }
   return ring
}
function isSelfIntersectingPolygon (points) {
   const ring = normalizePolygonPoints(points)
   if (ring.length < 4) return false
   const epsilon = 1e-12
   const orientation = (a, b, c) => {
      const value = (b.lat - a.lat) * (c.lng - b.lng) - (b.lng - a.lng) * (c.lat - b.lat)
      if (Math.abs(value) < epsilon) return 0
      return value > 0 ? 1 : 2
   }
   const onSegment = (a, b, c) =>
      b.lng <= Math.max(a.lng, c.lng) + epsilon &&
      b.lng + epsilon >= Math.min(a.lng, c.lng) &&
      b.lat <= Math.max(a.lat, c.lat) + epsilon &&
      b.lat + epsilon >= Math.min(a.lat, c.lat)
   const segmentsIntersect = (p1, q1, p2, q2) => {
      const o1 = orientation(p1, q1, p2)
      const o2 = orientation(p1, q1, q2)
      const o3 = orientation(p2, q2, p1)
      const o4 = orientation(p2, q2, q1)
      if (o1 !== o2 && o3 !== o4) return true
      if (o1 === 0 && onSegment(p1, p2, q1)) return true
      if (o2 === 0 && onSegment(p1, q2, q1)) return true
      if (o3 === 0 && onSegment(p2, p1, q2)) return true
      if (o4 === 0 && onSegment(p2, q1, q2)) return true
      return false
   }
   const count = ring.length
   for (let i = 0; i < count; i += 1) {
      const p1 = ring[i]
      const q1 = ring[(i + 1) % count]
      for (let j = i + 1; j < count; j += 1) {
         const p2 = ring[j]
         const q2 = ring[(j + 1) % count]
         const isAdjacent = j === i || j === i + 1 || (i === 0 && j === count - 1)
         if (isAdjacent) continue
         if (segmentsIntersect(p1, q1, p2, q2)) return true
      }
   }
   return false
}
function buildPolygonWktFromPoints (points) {
@@ -596,18 +662,6 @@
   return polygons
}
async function renderDeviceRangeSingle (device) {
   if (!viewer) return
   clearDeviceRangePrimitives()
   const position = getDeviceLngLat(device)
   if (!position) return
   const rangeMeters = normalizeDeviceRange(device.effectiveRangeKm)
   const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters)
   if (!primitive) return
   deviceRangePrimitiveMap.set(String(device?.id ?? Date.now()), primitive)
   viewer.scene.primitives.add(primitive)
}
async function renderDeviceRanges (rows) {
   if (!viewer) return
   deviceRangeRenderToken += 1
@@ -620,6 +674,12 @@
         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)
@@ -628,14 +688,17 @@
   const primitives = await Promise.all(
      pendingRows.map(async row => {
         const position = getDeviceLngLat(row)
         if (!position) return { row, primitive: null }
         if (!position) return { row, primitive: null, position: null }
         const rangeMeters = normalizeDeviceRange(row.effectiveRangeKm)
         const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters)
         return { row, primitive }
         return { row, primitive, position }
      })
   )
   if (renderToken !== deviceRangeRenderToken) return
   primitives.forEach(({ row, primitive }) => {
   primitives.forEach(({ row, primitive, position }) => {
      if (position) {
         addDeviceIconEntity(row, position)
      }
      if (!primitive) return
      const id = String(row?.id)
      if (!nextIds.has(id)) return
@@ -665,6 +728,23 @@
function clearShapeDisplay () {
   if (!shapeDisplaySource) return
   shapeDisplaySource.entities.removeAll()
}
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)
}
function normalizeShapePoint (point) {
@@ -887,6 +967,16 @@
      const val = cartesian3Convert(item, viewer)
      return { ...val, lng: val.longitude, lat: val.latitude }
   })
   if (currentShapeType.value === 'polygon' && isSelfIntersectingPolygon(pointList)) {
      ElMessage.warning('多边形存在自相交,请调整后再保存')
      if (activeToolMode.value === 'draw') {
         pointList = []
         activeShapeId.value = null
         clearActiveTool()
         startDraw('polygon')
      }
      return
   }
   const controlPoints = resolveControlPoints(meta, currentShapeType.value)
   if (!activeAreaType.value) {
      const detectAreaType = getDetectAreaTypeKey()
@@ -1279,9 +1369,29 @@
   suppressDeviceFly.value = true
   syncDeviceSelection()
   suppressDeviceFly.value = prevSuppressDeviceFly
   if (!readonly.value) {
      renderDeviceRanges(selectedDeviceRows.value)
   if (readonly.value) {
      await renderDeviceRanges(deviceOptions.value)
   }
}
// 设备上图(新增/编辑用)
async function getDeviceListForMapDisplay () {
   if (!visible.value || !viewer) return
   deviceMapRequestToken += 1
   const requestToken = deviceMapRequestToken
   const res = await fwDeviceListByPolygonsApi({
      areaId: dialogMode.value === 'add' ? '' : formData.value.id,
      isAreaSelect: 1,
      isTrack: 1,
   })
   if (requestToken !== deviceMapRequestToken) return
   const list = res?.data?.data ?? []
   if (!list.length) {
      clearDeviceRangePrimitives()
      clearDeviceIconEntities()
      return
   }
   await renderDeviceRanges(list)
}
// 关联设备变更
@@ -1289,7 +1399,6 @@
   selectedDeviceRows.value = rows
   const ids = rows.map(item => item.id)
   formData.value.deviceIds = ids.join(',')
   renderDeviceRanges(rows)
   if (suppressDeviceFly.value) {
      lastSelectedDeviceIds = [...ids]
      return
@@ -1304,12 +1413,13 @@
function handleDeviceRowClick (row) {
   if (readonly.value) {
      renderDeviceRangeSingle(row)
      void flyToDevice(row)
      return
   }
   renderDeviceRanges(selectedDeviceRows.value)
   void flyToDevice(row)
   const isSelected = selectedDeviceRows.value.some(item => String(item?.id) === String(row?.id))
   if (isSelected) {
      void flyToDevice(row)
   }
}
// 同步选择状态
@@ -1324,10 +1434,6 @@
   })
   selectedDeviceRows.value = rows
   formData.value.deviceIds = rows.map(item => item.id).join(',')
   if (readonly.value) {
      renderDeviceRanges(selectedDeviceRows.value)
      return
   }
   if (!deviceTableRef.value) return
   deviceTableRef.value.clearSelection()
   rows.forEach(row => {
@@ -1357,6 +1463,7 @@
   initMap()
   clearShapeDisplay()
   clearDeviceRangePrimitives()
   clearDeviceIconEntities()
   viewEntity && viewer?.entities?.remove(viewEntity)
   viewEntity = null
   clearActiveTool()
@@ -1393,11 +1500,11 @@
   lastSelectedDeviceIds = selectedDeviceRows.value.map(row => row.id)
   await nextTick()
   suppressDeviceFly.value = false
   if (!readonly.value) {
      renderDeviceRanges(selectedDeviceRows.value)
   }
   isDialogInitializing.value = false
   queueDeviceListRefresh()
   if (dialogMode.value === 'add' || dialogMode.value === 'edit') {
      void getDeviceListForMapDisplay()
   }
   void getPoliceStationList()
}