吉安感知网项目-前端
罗广辉
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 />
@@ -160,7 +160,8 @@
               </el-form-item>
               <el-form-item label="关联派出所" prop="policeStationId">
                  <el-select class="command-select" popper-class="command-select-popper"
                     v-model="formData.policeStationId" placeholder="请选择" clearable>
                     v-model="formData.policeStationId" placeholder="请选择" clearable filterable remote
                     :remote-method="handlePoliceStationSearch" :loading="policeStationLoading">
                     <el-option v-for="item in policeStationOptions" :key="item.id" :label="item.stationName"
                        :value="item.id" />
                  </el-select>
@@ -168,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) }}
@@ -179,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>
@@ -200,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="类型">
@@ -252,13 +253,14 @@
} from '@/utils/cesium/deviceRange'
import * as Cesium from 'cesium'
import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
import { fwPoliceStationQueryByPolygonsApi } from '@/views/areaManage/precinctInfo/precinctInfoApi'
import { fwPoliceStationListApi } from '@/views/areaManage/precinctInfo/precinctInfoApi'
import { fwDeviceListByPolygonsApi } from '@/views/basicManage/deviceStock/fwDevice'
import polygonIcon from '@/assets/images/areaMap/polygon.png'
import rectIcon from '@/assets/images/areaMap/rect.png'
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: '', // 区域名称
@@ -285,6 +287,10 @@
const searchName = ref('')
const selectedDeviceRows = ref([])
const policeStationOptions = ref([]) // 关联派出所
const policeStationAllOptions = ref([])
const policeStationLoading = ref(false)
const policeStationSearchTimer = ref(null)
const policeStationSearchKeyword = ref('')
const deviceOptions = ref([]) // 关联设备
const dictObj = inject('dictObj')
const mapRef = ref(null)
@@ -309,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)
@@ -316,8 +323,7 @@
let deviceRangeRenderToken = 0
let deviceListTimer = null
let deviceListRequestToken = 0
let policeStationListTimer = null
let policeStationRequestToken = 0
let deviceMapRequestToken = 0
const rules = {
   areaName: fieldRules(true, 50),
@@ -424,9 +430,9 @@
            clearTimeout(deviceListTimer)
            deviceListTimer = null
         }
         if (policeStationListTimer) {
            clearTimeout(policeStationListTimer)
            policeStationListTimer = null
         if (policeStationSearchTimer.value) {
            clearTimeout(policeStationSearchTimer.value)
            policeStationSearchTimer.value = null
         }
         viewEntity && viewer?.entities?.remove(viewEntity)
         viewEntity = null
@@ -434,6 +440,7 @@
         hideTypePanel()
         clearShapeDisplay()
         clearDeviceRangePrimitives()
         clearDeviceIconEntities()
      }
   }
)
@@ -448,7 +455,6 @@
   () => {
      if (isDialogInitializing.value) return
      queueDeviceListRefresh()
      queuePoliceStationRefresh()
   },
   { deep: true }
)
@@ -502,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 = []
@@ -509,11 +523,6 @@
   if (deviceTableRef.value) {
      deviceTableRef.value.clearSelection()
   }
}
function clearPoliceStationState () {
   policeStationOptions.value = []
   formData.value.policeStationId = ''
}
function formatWktNumber (value) {
@@ -524,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) {
@@ -599,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
@@ -623,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)
@@ -631,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
@@ -668,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) {
@@ -890,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()
@@ -1203,36 +1290,45 @@
}
// 获取派出所列表
function queuePoliceStationRefresh () {
async function getPoliceStationList (keyword = '') {
   if (!visible.value) return
   if (policeStationListTimer) {
      clearTimeout(policeStationListTimer)
   policeStationLoading.value = true
   try {
      const res = await fwPoliceStationListApi({
         stationName: keyword || undefined,
      })
      const list = res?.data?.data ?? []
      policeStationOptions.value = list
      if (!keyword) {
         policeStationAllOptions.value = list
      }
      await nextTick()
      if (!keyword) {
         const matched = policeStationOptions.value.find(
            item => String(item.id) === String(formData.value.policeStationId)
         )
         if (!matched) {
            formData.value.policeStationId = ''
         }
      }
   } finally {
      policeStationLoading.value = false
   }
   policeStationListTimer = setTimeout(() => {
      policeStationListTimer = null
      void getPoliceStationListByPolygons()
   }, 200)
}
async function getPoliceStationListByPolygons () {
   if (!visible.value) return
   const polygons = resolveShapePolygonsWkt()
   if (!polygons.length) {
      clearPoliceStationState()
function handlePoliceStationSearch (query) {
   policeStationSearchKeyword.value = (query || '').trim()
   if (!policeStationSearchKeyword.value) {
      policeStationOptions.value = [...policeStationAllOptions.value]
      return
   }
   policeStationRequestToken += 1
   const requestToken = policeStationRequestToken
   const res = await fwPoliceStationQueryByPolygonsApi({ polygons })
   if (requestToken !== policeStationRequestToken) return
   policeStationOptions.value = res?.data?.data ?? []
   await nextTick()
   const matched = policeStationOptions.value.find(
      item => String(item.id) === String(formData.value.policeStationId)
   )
   if (!matched) {
      formData.value.policeStationId = ''
   if (policeStationSearchTimer.value) {
      clearTimeout(policeStationSearchTimer.value)
   }
   policeStationSearchTimer.value = setTimeout(() => {
      policeStationSearchTimer.value = null
      void getPoliceStationList(policeStationSearchKeyword.value)
   }, 300)
}
// 获取设备列表
@@ -1273,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)
}
// 关联设备变更
@@ -1283,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
@@ -1298,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)
   }
}
// 同步选择状态
@@ -1318,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 => {
@@ -1337,6 +1449,12 @@
   selectedDeviceRows.value = []
   deviceOptions.value = []
   policeStationOptions.value = []
   policeStationAllOptions.value = []
   policeStationSearchKeyword.value = ''
   if (policeStationSearchTimer.value) {
      clearTimeout(policeStationSearchTimer.value)
      policeStationSearchTimer.value = null
   }
   shapeList.value = []
   activeShapeId.value = null
   activeAreaType.value = ''
@@ -1345,6 +1463,7 @@
   initMap()
   clearShapeDisplay()
   clearDeviceRangePrimitives()
   clearDeviceIconEntities()
   viewEntity && viewer?.entities?.remove(viewEntity)
   viewEntity = null
   clearActiveTool()
@@ -1381,12 +1500,12 @@
   lastSelectedDeviceIds = selectedDeviceRows.value.map(row => row.id)
   await nextTick()
   suppressDeviceFly.value = false
   if (!readonly.value) {
      renderDeviceRanges(selectedDeviceRows.value)
   }
   isDialogInitializing.value = false
   queueDeviceListRefresh()
   queuePoliceStationRefresh()
   if (dialogMode.value === 'add' || dialogMode.value === 'edit') {
      void getDeviceListForMapDisplay()
   }
   void getPoliceStationList()
}
onBeforeUnmount(() => {
@@ -1394,9 +1513,9 @@
      clearTimeout(deviceListTimer)
      deviceListTimer = null
   }
   if (policeStationListTimer) {
      clearTimeout(policeStationListTimer)
      policeStationListTimer = null
   if (policeStationSearchTimer.value) {
      clearTimeout(policeStationSearchTimer.value)
      policeStationSearchTimer.value = null
   }
   hideTypePanel()
})