吉安感知网项目-前端
shuishen
2026-02-03 89380e6260a75d1d3b94de687ebcc2f50d50659d
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
@@ -1,24 +1,66 @@
<template>
   <el-dialog class="command-page-map-view-dialog" v-model="visible" :show-close="false"
      :close-on-click-modal="false">
<template>
   <el-dialog class="command-page-map-view-dialog" v-model="visible" :show-close="false" :close-on-click-modal="false">
      <div class="dialog-container">
         <div class="left-container">
            <CommonCesiumMap
               ref="mapRef"
               class="leftMap command-cesium"
               :active="visible"
               :flat-mode="false"
               :terrain="true"
               :layer-mode="4"
               :boundary="false"
            />
            <CommonCesiumMap ref="mapRef" class="leftMap command-cesium" :active="visible" :flat-mode="false"
               :terrain="true" :layer-mode="4" :boundary="false" :zoom-to-boundary="dialogMode === 'add'" />
            <div v-if="showTypePanel && !readonly" class="shape-type-panel">
               <div class="panel-header">
                  <span>{{ drawTypeLabelMap[currentShapeType] || '区域' }}</span>
                  <el-icon class="panel-close" @click.stop="handleTypePanelClose">
                     <Close />
                  </el-icon>
               </div>
               <div class="panel-body">
                  <span class="panel-label">区域类型</span>
                  <el-select class="command-select" popper-class="command-select-popper" v-model="activeAreaType"
                     placeholder="请选择" :disabled="currentShapeType === 'buffer'" collapse-tags
                     :multiple="currentShapeType === 'buffer'" @change="handleAreaTypeChange">
                     <el-option v-for="item in dictObj.areaType" :key="item.dictKey" :label="item.dictValue"
                        :value="item.dictKey" />
                  </el-select>
               </div>
            </div>
            <div class="map-toolbar">
               <el-tooltip content="多边形" placement="left">
                  <button class="tool-btn" :class="{ active: currentShapeType === 'polygon' }" type="button"
                     @click="handleDraw('polygon')">
                     <img class="tool-icon-image" :src="polygonIcon" alt="多边形" />
                  </button>
               </el-tooltip>
               <el-tooltip content="矩形" placement="left">
                  <button class="tool-btn" :class="{ active: currentShapeType === 'rectangle' }" type="button"
                     @click="handleDraw('rectangle')">
                     <img class="tool-icon-image" :src="rectIcon" alt="矩形" />
                  </button>
               </el-tooltip>
               <el-tooltip content="椭圆" placement="left">
                  <button class="tool-btn" :class="{ active: currentShapeType === 'ellipse' }" type="button"
                     @click="handleDraw('ellipse')">
                     <img class="tool-icon-image" :src="ellipseIcon" alt="椭圆" />
                  </button>
               </el-tooltip>
               <el-tooltip content="缓冲圆" placement="left">
                  <button class="tool-btn" :class="{ active: currentShapeType === 'buffer' }" type="button"
                     @click="handleDraw('buffer')">
                     <img class="tool-icon-image" :src="bufferIcon" alt="缓冲圆" />
                  </button>
               </el-tooltip>
               <el-tooltip content="删除" placement="left">
                  <button class="tool-btn danger" type="button" @click="handleClearShape">
                     <img class="tool-icon-image" :src="trashIcon" alt="删除" />
                  </button>
               </el-tooltip>
            </div>
         </div>
         <div class="right-container">
            <div class="header">
               <span>{{ titleEnum[dialogMode] }}</span>
               <el-icon class="close-btn" @click.stop="visible = false"><Close /></el-icon>
               <el-icon class="close-btn" @click.stop="visible = false">
                  <Close />
               </el-icon>
            </div>
            <div class="content" v-if="readonly">
@@ -29,20 +71,10 @@
                     <div class="val">{{ formData.areaName }}</div>
                  </el-col>
                  <el-col :span="24">
                     <div class="label">区域位置</div>
                     <div class="val">{{ formatLocation(formData) }}</div>
                  </el-col>
                  <el-col :span="24">
                     <div class="label">区域面积</div>
                     <div class="val">{{ formData.areaSize || '-' }}k㎡</div>
                  </el-col>
                  <el-col :span="24">
                     <div class="label">区域类型</div>
                     <div class="val">{{ getDictLabel(formData.areaType, dictObj.areaType) }}</div>
                  </el-col>
                  <el-col :span="24">
                     <div class="label">触发条件</div>
                     <div class="val">{{ formData.triggerCondition }}</div>
                     <div class="val">
                        {{ getDictLabel(formData.triggerCondition, dictObj.triggeringCondition) }}
                     </div>
                  </el-col>
                  <el-col :span="24">
                     <div class="label">响应机制</div>
@@ -62,11 +94,29 @@
                  </el-col>
               </el-row>
               <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-column prop="areaType" label="区域类型">
                           <template v-slot="{ row }">
                              {{ resolveShapeAreaTypeLabel(row) }}
                           </template>
                        </el-table-column>
                        <el-table-column prop="drawType" label="绘制类型">
                           <template v-slot="{ row }">
                              {{ drawTypeLabelMap[row.drawType] || '-' }}
                           </template>
                        </el-table-column>
                     </el-table>
                  </div>
               </div>
               <div class="detail-title">关联设备</div>
               <div class="command-table-container">
                  <div class="command-table-content">
                     <el-table class="command-table" ref="deviceTableRef" :data="deviceOptions"
                        row-key="id">
                     <el-table class="command-table" ref="deviceTableRef" :data="deviceOptions" row-key="id"
                        @row-click="handleDeviceRowClick">
                        <el-table-column prop="deviceName" label="设备名称" />
                        <el-table-column prop="deviceType" label="类型">
                           <template v-slot="{ row }">
@@ -81,29 +131,19 @@
            <el-form class="dialog-form" v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly"
               label-width="96px">
               <el-form-item label="区域名称" prop="areaName">
                  <el-input class="command-input" v-model="formData.areaName" maxlength="50"
                     placeholder="请输入" clearable />
               </el-form-item>
               <el-form-item label="区域位置" prop="longitude">
                  <div class="val">{{ formatLocation(formData) }}</div>
               </el-form-item>
               <el-form-item label="区域面积" prop="areaSize">
                  <div class="val">{{ formData.areaSize || '-' }}k㎡</div>
               </el-form-item>
               <el-form-item label="区域类型" prop="areaType">
                  <el-select class="command-select" popper-class="command-select-popper"
                     v-model="formData.areaType" placeholder="请选择" clearable>
                     <el-option v-for="item in dictObj.areaType" :key="item.dictKey" :label="item.dictValue"
                        :value="item.dictKey" />
                  </el-select>
                  <el-input class="command-input" v-model="formData.areaName" maxlength="50" placeholder="请输入"
                     clearable />
               </el-form-item>
               <el-form-item label="响应机制" prop="responseMechanism">
                  <el-input class="command-input" v-model="formData.responseMechanism"
                     maxlength="200" placeholder="请输入" clearable />
                  <el-input class="command-input" v-model="formData.responseMechanism" maxlength="200"
                     placeholder="请输入" clearable />
               </el-form-item>
               <el-form-item label="触发条件" prop="triggerCondition">
                  <el-input class="command-input" v-model="formData.triggerCondition"
                     maxlength="200" placeholder="请输入" clearable />
                  <el-select class="command-select" popper-class="command-select-popper"
                     v-model="formData.triggerCondition" placeholder="请选择" clearable>
                     <el-option v-for="item in dictObj.triggeringCondition" :key="item.dictKey"
                        :label="item.dictValue" :value="item.dictKey" />
                  </el-select>
               </el-form-item>
               <el-form-item label="管控级别" prop="controlLevel">
                  <el-select class="command-select" popper-class="command-select-popper"
@@ -113,18 +153,41 @@
                  </el-select>
               </el-form-item>
               <el-form-item label="可飞行时段" prop="flyDateStart">
                  <el-date-picker class="command-date-picker"
                     popper-class="command-date-picker-popper" v-model="flyDateRange"
                     type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间"
                  <el-date-picker class="command-date-picker" popper-class="command-date-picker-popper"
                     v-model="flyDateRange" type="datetimerange" range-separator="至" start-placeholder="开始时间"
                     end-placeholder="结束时间" :disabled-date="disabledFlyDate" :disabled-time="disabledFlyTime"
                     value-format="YYYY-MM-DD HH:mm:ss" clearable />
               </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>
               </el-form-item>
               <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-column prop="areaType" label="区域类型">
                        <template v-slot="{ row }">
                           {{ resolveShapeAreaTypeLabel(row) }}
                        </template>
                     </el-table-column>
                     <el-table-column prop="drawType" label="绘制类型">
                        <template v-slot="{ row }">
                           {{ drawTypeLabelMap[row.drawType] || '-' }}
                        </template>
                     </el-table-column>
                     <el-table-column label="操作" class-name="operation-btns" width="120">
                        <template v-slot="{ row }">
                           <el-link @click="handleShapeEdit(row)">编辑</el-link>
                           <el-link @click="handleShapeDelete(row)">删除</el-link>
                        </template>
                     </el-table-column>
                  </el-table>
               </div>
               <div class="search-table-container">
                  <div class="search-box">
@@ -132,14 +195,13 @@
                        关联设备
                     </div>
                     <el-input class="command-input" v-model="searchName" placeholder="请输入"
                        clearable></el-input>
                     <el-input class="command-input" v-model="searchName" placeholder="请输入" clearable></el-input>
                  </div>
                  <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">
                        @selection-change="handleDeviceSelectionChange" @row-click="handleDeviceRowClick">
                        <el-table-column type="selection" width="55" :reserve-selection="true" />
                        <el-table-column prop="deviceName" label="设备名称" />
                        <el-table-column prop="deviceType" label="类型">
@@ -153,7 +215,9 @@
            </el-form>
            <div class="footer">
               <el-button color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button>
               <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' :
                  '取消'
               }}</el-button>
               <el-button color="#284FE3" v-if="!readonly" type="primary" :loading="submitting"
                  :disabled="submitting" @click="handleSubmit">
                  确定
@@ -167,28 +231,43 @@
<script setup>
import { Close } from '@element-plus/icons-vue'
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
import { computed, inject, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { fwAreaDivideDetailApi, fwAreaDivideSubmitApi } from './partitionApi'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
import { fieldRules, geomAnalysis, getDictLabel } from '@ztzf/utils'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { DrawPolygon } from '@/utils/cesium/DrawPolygon'
import {
   DrawManager,
   EditManager,
   addBufferRadiusLabelEntity,
   buildEllipsePositions,
} from '@/utils/cesium/shapeTools'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
import * as turf from '@turf/turf'
import {
   createDeviceRangePrimitive,
   getDeviceLngLat,
   normalizeDeviceRange,
   resolveDeviceTerrainHeight,
} from '@/utils/cesium/deviceRange'
import * as Cesium from 'cesium'
import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
import { fwPoliceStationListApi } from '@/views/areaManage/precinctInfo/precinctInfoApi'
import { fwDeviceListApi } from '@/views/basicManage/deviceStock/fwDevice'
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: '', // 区域名称
   areaSize: null, // 区域面积
   areaType: '', // 区域类型
   controlLevel: '', // 管控级别
   deviceIds: '', // 关联设备ID
   flyDateEnd: '', // 可飞行结束
   flyDateStart: '', // 可飞行开始
   latitude: null, // 区域中心纬度
   longitude: null, // 区域中心经度
   policeStationId: '', // 关联派出所id
   responseMechanism: '', // 响应机制
   triggerCondition: '', // 触发条件
@@ -198,32 +277,93 @@
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 route = useRoute()
const titleEnum = ref({ edit: '编辑区域', view: '查看区域', add: '新增区域' })
const flyDateRange = ref([]) // 飞行时间
const deviceTableRef = ref(null)
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)
const currentShapeType = ref('polygon')
const activeAreaType = ref('')
const shapeList = ref([])
const activeShapeId = ref(null)
const drawTypeLabelMap = {
   polygon: '多边形',
   rectangle: '矩形',
   ellipse: '椭圆',
   buffer: '缓冲圆',
}
const bufferAreaTypeValue = '1,2,3'
const showTypePanel = ref(false)
const suppressTypePanelOnce = ref(false)
let viewer
let drawPolygonExample
let drawManager
let editManager
let activeTool
let pointList = []
let viewEntity
let shapeDisplaySource
const deviceRangePrimitiveMap = new Map()
const deviceIconEntityMap = new Map()
const activeToolMode = ref('')
const suppressDeviceFly = ref(false)
const isDialogInitializing = ref(false)
let lastSelectedDeviceIds = []
let deviceRangeRenderToken = 0
let deviceListTimer = null
let deviceListRequestToken = 0
const rules = {
   areaName: fieldRules(true, 50),
   longitude: fieldRules(true),
   areaType: fieldRules(true),
   responseMechanism: fieldRules(true, 200),
   triggerCondition: fieldRules(true, 200),
   controlLevel: fieldRules(true),
   flyDateStart: fieldRules(true),
   policeStationId: fieldRules(true),
   deviceIds: fieldRules(true),
   deviceIds: fieldRules(false),
}
function isSameDay (left, right) {
   return (
      left.getFullYear() === right.getFullYear() &&
      left.getMonth() === right.getMonth() &&
      left.getDate() === right.getDate()
   )
}
const disabledFlyDate = time => {
   const startOfToday = new Date()
   startOfToday.setHours(0, 0, 0, 0)
   return time.getTime() < startOfToday.getTime()
}
const disabledFlyTime = date => {
   if (!date) return {}
   const now = new Date()
   if (!isSameDay(date, now)) return {}
   const currentHour = now.getHours()
   const currentMinute = now.getMinutes()
   const currentSecond = now.getSeconds()
   return {
      disabledHours: () => Array.from({ length: currentHour }, (_, index) => index),
      disabledMinutes: hour =>
         hour === currentHour ? Array.from({ length: currentMinute }, (_, index) => index) : [],
      disabledSeconds: (hour, minute) =>
         hour === currentHour && minute === currentMinute
            ? Array.from({ length: currentSecond }, (_, index) => index)
            : [],
   }
}
// 关闭弹框
@@ -233,17 +373,39 @@
// 提交新增/编辑
async function handleSubmit () {
   const isValid = await formRef.value?.validate().catch(() => false)
   if (!isValid) return
   if (dialogMode.value === 'add' && Number(formData.value.areaSize) < 0.5) {
      ElMessage.warning('区域面积不能小于0.5km²,请重新绘制')
   if (!shapeList.value.length) {
      ElMessage.warning('区域不可为空,请先绘制区域')
      return
   }
   const isValid = await formRef.value?.validate().catch(() => false)
   if (!isValid) return
   submitting.value = true
   try {
      let str = [...pointList, pointList[0]].map(item => `${item.longitude} ${item.latitude}`).join(',')
      formData.value.geom = `POLYGON((${str}))`
      await fwAreaDivideSubmitApi(formData.value)
      const areaDivide = { ...formData.value }
      delete areaDivide.geom
      const areaDivideExtList = shapeList.value.map(shape => {
         const areaTypeKey = shape?.areaType ?? ''
         const areaTypeValue = getDictLabel(areaTypeKey, resolveAreaTypeOptions())
         return {
            id: shape?.extId,
            areaDivideId: areaDivide?.id ?? '',
            areaCode: areaDivide?.areaCode ?? '',
            areaTypeKey,
            areaTypeValue,
            geomJson: JSON.stringify(shape),
         }
      })
      await fwAreaDivideSubmitApi({
         areaDivide,
         areaDivideExtList,
      })
      const actionText = dialogMode.value === 'add' ? '新增' : '编辑'
      saveOperationLog({
         requestUri: route.path,
         title: `${route.name || '区域划分'}-${actionText}`,
         type: 1,
      })
      ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功')
      visible.value = false
      emit('success')
@@ -259,19 +421,60 @@
      formData.value.flyDateEnd = flyDateRange.value[1] || ''
   }
)
watch(
   () => visible.value,
   val => {
      if (!val) {
         if (deviceListTimer) {
            clearTimeout(deviceListTimer)
            deviceListTimer = null
         }
         if (policeStationSearchTimer.value) {
            clearTimeout(policeStationSearchTimer.value)
            policeStationSearchTimer.value = null
         }
         viewEntity && viewer?.entities?.remove(viewEntity)
         viewEntity = null
         clearActiveTool()
         hideTypePanel()
         clearShapeDisplay()
         clearDeviceRangePrimitives()
         clearDeviceIconEntities()
      }
   }
)
watch(
   () => activeAreaType.value,
   val => {
      updateActiveShapeAreaType(val)
   }
)
watch(
   () => shapeList.value,
   () => {
      if (isDialogInitializing.value) return
      queueDeviceListRefresh()
   },
   { deep: true }
)
// 加载详情
async function loadDetail () {
   if (!formData.value.id) return
   const res = await fwAreaDivideDetailApi({ id: formData.value.id })
   formData.value = res?.data?.data ?? initForm()
   const detail = res?.data?.data ?? {}
   const nextForm = initForm()
   Object.keys(nextForm).forEach(key => {
      if (detail[key] !== undefined) {
         nextForm[key] = detail[key]
      }
   })
   if (detail?.id) {
      nextForm.id = detail.id
   }
   formData.value = nextForm
   flyDateRange.value = [formData.value.flyDateStart, formData.value.flyDateEnd].filter(Boolean)
}
// 格式化区域位置
function formatLocation (row) {
   if (row?.longitude == null || row?.latitude == null) return ''
   return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
   syncShapeListFromDetail(detail)
}
// 格式化可飞行时间段
@@ -287,66 +490,744 @@
   viewer = map?.viewer || null
}
function ensureShapeDisplaySource () {
   if (!viewer) return null
   if (!shapeDisplaySource) {
      shapeDisplaySource = new Cesium.CustomDataSource('partition-shape-display')
      viewer.dataSources.add(shapeDisplaySource)
   }
   return shapeDisplaySource
}
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 clearDeviceListState () {
   deviceOptions.value = []
   selectedDeviceRows.value = []
   formData.value.deviceIds = ''
   clearDeviceRangePrimitives()
   clearDeviceIconEntities()
   if (deviceTableRef.value) {
      deviceTableRef.value.clearSelection()
   }
}
function formatWktNumber (value) {
   if (!Number.isFinite(Number(value))) return null
   return Number(value).toFixed(6)
}
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 buildPolygonWktFromPoints (points) {
   if (!Array.isArray(points) || points.length < 3) return null
   const ring = points
      .map(resolveLngLatPoint)
      .filter(Boolean)
      .map(point => ({ lng: point.lng, lat: point.lat }))
   if (ring.length < 3) return null
   const first = ring[0]
   const last = ring[ring.length - 1]
   if (!isSameWktPoint(first, last)) {
      ring.push({ ...first })
   }
   const coordText = ring
      .map(point => {
         const lng = formatWktNumber(point.lng)
         const lat = formatWktNumber(point.lat)
         if (lat === null || lng === null) return null
         return `${lng} ${lat}`
      })
      .filter(Boolean)
      .join(', ')
   if (!coordText) return null
   return `POLYGON((${coordText}))`
}
function buildEllipseWktFromMeta (center, semiMajor, semiMinor) {
   if (!viewer) return null
   const resolvedCenter = resolveLngLatPoint(center)
   if (!resolvedCenter) return null
   if (!Number.isFinite(Number(semiMajor)) || !Number.isFinite(Number(semiMinor))) return null
   const centerCartesian = Cesium.Cartesian3.fromDegrees(
      resolvedCenter.lng,
      resolvedCenter.lat,
      resolvedCenter.height || 0
   )
   const positions = buildEllipsePositions(centerCartesian, Number(semiMajor), Number(semiMinor))
   if (!positions.length) return null
   const points = positions
      .map(item => {
         const converted = cartesian3Convert(item, viewer)
         return { lng: converted.longitude, lat: converted.latitude }
      })
      .filter(Boolean)
   return buildPolygonWktFromPoints(points)
}
function resolveShapePolygonsWkt () {
   const polygons = []
   shapeList.value.forEach(shape => {
      if (!shape) return
      if (shape.drawType === 'buffer') {
         const radii = Array.isArray(shape?.meta?.bufferRadii)
            ? shape.meta.bufferRadii.filter(item => Number.isFinite(item) && item > 0)
            : []
         const maxRadius = radii.length ? Math.max(...radii) : null
         const wkt = buildEllipseWktFromMeta(shape?.meta?.center, maxRadius, maxRadius)
         if (wkt) polygons.push(wkt)
         return
      }
      if (shape.drawType === 'ellipse') {
         const wkt = buildEllipseWktFromMeta(shape?.meta?.center, shape?.meta?.semiMajor, shape?.meta?.semiMinor)
         if (wkt) {
            polygons.push(wkt)
            return
         }
      }
      const points = getShapeDisplayPoints(shape)
      const wkt = buildPolygonWktFromPoints(points)
      if (wkt) polygons.push(wkt)
   })
   return polygons
}
async function renderDeviceRangeSingle (device) {
   if (!viewer) return
   clearDeviceRangePrimitives()
   clearDeviceIconEntities()
   const position = getDeviceLngLat(device)
   if (!position) return
   addDeviceIconEntity(device, position)
   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
   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)
   })
}
async function flyToDevice (device) {
   if (!viewer) return
   const position = getDeviceLngLat(device)
   if (!position) return
   const height = await resolveDeviceTerrainHeight(viewer, position)
   const rangeMeters = normalizeDeviceRange(device?.effectiveRangeKm)
   const destination = Cesium.Cartesian3.fromDegrees(position.lng, position.lat, height + rangeMeters * 3)
   viewer.camera.flyTo({
      destination,
      orientation: {
         heading: viewer.camera.heading,
         pitch: Cesium.Math.toRadians(-90),
         roll: 0,
      },
      duration: 0.6,
   })
}
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) {
   const lng = point?.lng ?? point?.longitude
   const lat = point?.lat ?? point?.latitude
   if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
   return {
      lng: Number(lng),
      lat: Number(lat),
      height: Number.isFinite(Number(point?.height)) ? Number(point.height) : 0,
   }
}
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 resolveControlPoints (meta, drawType) {
   if (!meta) return []
   if (Array.isArray(meta?.controlPoints)) {
      return meta.controlPoints.map(resolveLngLatPoint).filter(Boolean)
   }
   if (drawType === 'ellipse') {
      return [meta.center, meta.majorPoint, meta.minorPoint].map(resolveLngLatPoint).filter(Boolean)
   }
   if (drawType === 'buffer') {
      return [meta.center, meta.radiusPoint1, meta.radiusPoint2, meta.radiusPoint3]
         .map(resolveLngLatPoint)
         .filter(Boolean)
   }
   return []
}
function buildShapePositions (points = []) {
   const normalized = points.map(normalizeShapePoint).filter(Boolean)
   return normalized.map(point =>
      Cesium.Cartesian3.fromDegrees(point.lng, point.lat, point.height)
   )
}
function getShapeDisplayPoints (shape) {
   if (Array.isArray(shape?.displayPoints) && shape.displayPoints.length) {
      return shape.displayPoints
   }
   return shape?.points || []
}
function getAreaTypeStyle (areaType) {
   return AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
}
function renderShapeList () {
   if (!viewer || !visible.value) {
      clearShapeDisplay()
      return
   }
   const dataSource = ensureShapeDisplaySource()
   if (!dataSource) return
   dataSource.entities.removeAll()
   shapeList.value.forEach(shape => {
      if (activeToolMode.value === 'edit' && shape.id === activeShapeId.value) return
      if (shape.drawType === 'buffer' && shape.meta?.bufferRadii?.length && shape.meta?.center) {
         const center = shape.meta.center
         const centerCartesian = Cesium.Cartesian3.fromDegrees(center.lng, center.lat, center.height || 0)
         const [radius1, radius2, radius3] = shape.meta.bufferRadii
         const radii = [radius1, radius2, radius3].filter(item => Number.isFinite(item) && item > 0)
         if (radii.length) {
            radii.forEach((radius, index) => {
               const style = BUFFER_LEVEL_STYLES[index] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
               dataSource.entities.add({
                  name: 'shape-display',
                  customData: { shapeId: shape.id, drawType: shape.drawType, level: index + 1 },
                  position: centerCartesian,
                  ellipse: {
                     semiMajorAxis: radius,
                     semiMinorAxis: radius,
                     material: style.fill,
                     outline: false,
                     heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                  },
               })
               const positions = buildEllipsePositions(centerCartesian, radius, radius)
               dataSource.entities.add({
                  name: 'shape-display',
                  customData: { shapeId: shape.id, drawType: shape.drawType, level: index + 1, outline: true },
                  polyline: {
                     positions: positions.length ? [...positions, positions[0]] : positions,
                     clampToGround: true,
                     width: 2,
                     material: style.outline,
                  },
               })
               addBufferRadiusLabelEntity(dataSource, centerCartesian, radius, {
                  name: 'shape-display',
                  customData: { drawType: 'buffer', label: 'radius' },
               })
            })
         }
         return
      }
      if (!shape?.points?.length) return
      const positions = buildShapePositions(getShapeDisplayPoints(shape))
      if (positions.length < 3) return
      const style = getAreaTypeStyle(shape.areaType)
      dataSource.entities.add({
         name: 'shape-display',
         customData: { shapeId: shape.id, drawType: shape.drawType },
         polygon: {
            hierarchy: new Cesium.PolygonHierarchy(positions),
            material: style.fill,
            outline: false,
            heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
         },
         polyline: {
            positions: positions.length ? [...positions, positions[0]] : positions,
            clampToGround: true,
            width: 2,
            material: style.outline,
         },
      })
   })
}
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 collectShapeBoundingPositions () {
   const positions = []
   if (!viewer || !shapeList.value.length) return positions
   shapeList.value.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 fitViewToShapeList () {
   if (!viewer || !shapeList.value.length) return
   const positions = collectShapeBoundingPositions()
   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),
   })
}
// 绘制完成回调
const drawFinished = async data => {
   pointList = _.cloneDeep(data).map(item => {
const drawFinished = async (data, options = {}) => {
   const shouldContinueDraw = options?.continueDraw || activeToolMode.value === 'draw'
   const positions = Array.isArray(data?.positions) ? data.positions : data
   if (!Array.isArray(positions) || positions.length < 3) {
      pointList = []
      hideTypePanel()
      return
   }
   const meta = data?.meta
   const bufferRadii = Array.isArray(meta?.bufferRadii) ? meta.bufferRadii : null
   const centerCartesian = meta?.center
   const centerLngLat = centerCartesian ? cartesian3Convert(centerCartesian, viewer) : null
   pointList = _.cloneDeep(positions).map(item => {
      const val = cartesian3Convert(item, viewer)
      return { ...val, lng: val.longitude, lat: val.latitude }
   })
   const polygon = turf.polygon([
      [...pointList.map(item => [item.longitude, item.latitude]), [pointList[0].longitude, pointList[0].latitude]],
   ])
   const center = turf.centerOfMass(polygon)
   const areaSqm = turf.area(polygon) // 平方米
   formData.value.areaSize = _.round(areaSqm / 1_000_000, 2) // 平方千米
   formData.value.longitude = center.geometry.coordinates[0]
   formData.value.latitude = center.geometry.coordinates[1]
}
// 新增模式绘制
function addPolygon () {
   drawPolygonExample = new DrawPolygon(viewer)
   drawPolygonExample.initHandler(viewer)
   drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
}
// 编辑面
function editPolygon () {
   if (!formData.value?.geom) return
   pointList = geomAnalysis(formData.value.geom)
   drawPolygonExample?.destroy()
   drawPolygonExample = new DrawPolygon(viewer)
   let pointList1 = _.cloneDeep(pointList)
   pointList1.pop()
   drawPolygonExample.initPolygon(
      viewer,
      pointList.map(item => ({ lng: item.longitude, lat: item.latitude }))
   )
   drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
   if (pointList1.length) {
      const result = pointList1.map(item => [item.longitude, item.latitude]).flat()
      const tempEntity = viewer.entities?.add({
         position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
         polyline: {
            positions: Cesium.Cartesian3.fromDegreesArray(result),
            clampToGround: true,
            width: 1,
            material: Cesium.Color.TRANSPARENT,
         },
      })
      if (tempEntity) {
         viewer.flyTo(tempEntity, { duration: 0 })
         viewer.entities.remove(tempEntity)
   const controlPoints = resolveControlPoints(meta, currentShapeType.value)
   if (!activeAreaType.value) {
      const detectAreaType = getDetectAreaTypeKey()
      if (detectAreaType) {
         activeAreaType.value = detectAreaType
      }
   }
   if (currentShapeType.value === 'buffer') {
      activeAreaType.value = bufferAreaTypeValue.split(',')
   }
   if (!suppressTypePanelOnce.value) {
      showTypePanelAtCurrent()
   }
   suppressTypePanelOnce.value = false
   if (!activeShapeId.value) {
      activeShapeId.value = `shape_${Date.now()}_${Math.random().toString(16).slice(2, 6)}`
   }
   const targetIndex = shapeList.value.findIndex(item => item.id === activeShapeId.value)
   const displayPoints = ['ellipse', 'buffer'].includes(currentShapeType.value)
      ? _.cloneDeep(pointList)
      : null
   let shapeMeta = null
   if (currentShapeType.value === 'buffer' && bufferRadii?.length && centerLngLat) {
      shapeMeta = {
         bufferRadii,
         center: { lng: centerLngLat.longitude, lat: centerLngLat.latitude, height: centerLngLat.height ?? 0 },
      }
      if (controlPoints.length) {
         shapeMeta.controlPoints = controlPoints
      }
   } else if (currentShapeType.value === 'ellipse') {
      const centerPoint = resolveLngLatPoint(meta?.center)
      const majorPoint = resolveLngLatPoint(meta?.majorPoint)
      const minorPoint = resolveLngLatPoint(meta?.minorPoint)
      if (centerPoint || majorPoint || minorPoint) {
         shapeMeta = {
            center: centerPoint,
            majorPoint,
            minorPoint,
            semiMajor: Number.isFinite(Number(meta?.semiMajor)) ? Number(meta.semiMajor) : null,
            semiMinor: Number.isFinite(Number(meta?.semiMinor)) ? Number(meta.semiMinor) : null,
         }
         if (controlPoints.length) {
            shapeMeta.controlPoints = controlPoints
         }
      }
   }
   const shapePayload = {
      id: activeShapeId.value,
      areaType: currentShapeType.value === 'buffer' ? bufferAreaTypeValue : activeAreaType.value,
      drawType: currentShapeType.value,
      points: ['ellipse', 'buffer'].includes(currentShapeType.value) && controlPoints.length
         ? controlPoints
         : _.cloneDeep(pointList),
      displayPoints,
      meta: shapeMeta,
   }
   if (targetIndex === -1) {
      shapeList.value.push(shapePayload)
   } else {
      shapeList.value.splice(targetIndex, 1, shapePayload)
   }
   renderShapeList()
   if (shouldContinueDraw && !readonly.value) {
      activeShapeId.value = null
      pointList = []
      clearActiveTool()
      startDraw(currentShapeType.value || 'polygon')
   }
}
function getDetectAreaTypeKey () {
   const areaTypeOptions = resolveAreaTypeOptions()
   if (!Array.isArray(areaTypeOptions)) return ''
   const target = areaTypeOptions.find(item => item?.dictKey === '1')
   return target?.dictKey ?? ''
}
function resolveAreaTypeOptions () {
   return Array.isArray(dictObj?.value?.areaType)
      ? dictObj.value.areaType
      : dictObj?.areaType
}
function resolveShapeAreaTypeLabel (shape) {
   return getDictLabel(shape?.areaType, resolveAreaTypeOptions())
}
function parseGeomJson (geomJson) {
   if (!geomJson) return null
   if (typeof geomJson === 'object') return geomJson
   if (typeof geomJson !== 'string') return null
   const trimmed = geomJson.trim()
   if (!trimmed) return null
   try {
      return JSON.parse(trimmed)
   } catch (error) {
      const legacyPoints = geomAnalysis(trimmed)
      if (Array.isArray(legacyPoints) && legacyPoints.length >= 3) {
         return { drawType: 'polygon', points: legacyPoints }
      }
   }
   return null
}
function syncShapeListFromDetail (detail) {
   const extList = Array.isArray(detail?.fwAreaDivideExtList) ? detail.fwAreaDivideExtList : []
   if (!extList.length) {
      shapeList.value = []
      activeAreaType.value = ''
      currentShapeType.value = 'polygon'
      return
   }
   shapeList.value = extList.map((item, index) => {
      const isShapePayload = item?.drawType || item?.points
      if (isShapePayload) {
         const areaType = item?.drawType === 'buffer'
            ? bufferAreaTypeValue
            : (item?.areaType ?? '')
         return {
            id: item?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
            extId: item?.extId ?? item?.id,
            areaType,
            drawType: item?.drawType ?? 'polygon',
            points: Array.isArray(item?.points) ? item.points : [],
            displayPoints: Array.isArray(item?.displayPoints) ? item.displayPoints : null,
            meta: item?.meta ?? null,
         }
      }
      const parsed = parseGeomJson(item?.geomJson) || {}
      const drawType = parsed?.drawType || 'polygon'
      const areaType = drawType === 'buffer'
         ? bufferAreaTypeValue
         : (item?.areaTypeKey ?? parsed?.areaType ?? '')
      const points = Array.isArray(parsed?.points) ? parsed.points : []
      const displayPoints = Array.isArray(parsed?.displayPoints) ? parsed.displayPoints : null
      const meta = parsed?.meta ?? null
      return {
         id: parsed?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
         extId: item?.id,
         areaType,
         drawType,
         points,
         displayPoints,
         meta,
      }
   })
   activeAreaType.value = shapeList.value[0]?.drawType === 'buffer'
      ? bufferAreaTypeValue.split(',')
      : (shapeList.value[0]?.areaType ?? '')
   currentShapeType.value = shapeList.value[0]?.drawType ?? 'polygon'
}
function showTypePanelAtCurrent () {
   if (readonly.value) return
   showTypePanel.value = true
}
function hideTypePanel () {
   showTypePanel.value = false
}
function handleTypePanelClose () {
   hideTypePanel()
   if (readonly.value) return
   if (activeToolMode.value !== 'edit') return
   const positions = activeTool?.getPositions?.()
   if (Array.isArray(positions) && positions.length >= 3) {
      suppressTypePanelOnce.value = true
      drawFinished(positions, { continueDraw: true })
      return
   }
   if (positions?.positions?.length >= 3) {
      suppressTypePanelOnce.value = true
      drawFinished(positions, { continueDraw: true })
      return
   }
   clearActiveTool()
}
function handleAreaTypeChange (value) {
   updateActiveShapeAreaType(value)
}
function updateActiveShapeAreaType (value) {
   if (currentShapeType.value === 'buffer') return
   activeTool?.setStyle?.(getAreaTypeStyle(value))
   if (!activeShapeId.value) return
   const targetIndex = shapeList.value.findIndex(item => item.id === activeShapeId.value)
   if (targetIndex === -1) return
   const target = shapeList.value[targetIndex]
   shapeList.value.splice(targetIndex, 1, {
      ...target,
      areaType: value,
   })
   renderShapeList()
}
// Tool lifecycle
function clearActiveTool () {
   activeTool?.destroy?.()
   activeTool = null
   drawManager?.destroy()
   editManager?.destroy()
   activeToolMode.value = ''
   renderShapeList()
}
function startDraw (type) {
   currentShapeType.value = type
   activeToolMode.value = 'draw'
   drawManager ||= new DrawManager(viewer)
   activeTool = drawManager.start(type, { style: getAreaTypeStyle(activeAreaType.value) })
   activeTool?.subscribe('getPolygonPositions', drawFinished)
}
function startEdit (type, points) {
   currentShapeType.value = type
   activeToolMode.value = 'edit'
   editManager ||= new EditManager(viewer)
   activeTool = editManager.start(type, points, { style: getAreaTypeStyle(activeAreaType.value) })
   activeTool?.subscribe('getPolygonPositions', drawFinished)
}
function handleDraw (type) {
   if (readonly.value) return
   if (!viewer) return
   clearActiveTool()
   viewEntity && viewer?.entities?.remove(viewEntity)
   viewEntity = null
   hideTypePanel()
   renderShapeList()
   pointList = []
   const defaultAreaType = getDetectAreaTypeKey()
   if (type === 'buffer') {
      activeAreaType.value = bufferAreaTypeValue
   } else if (defaultAreaType) {
      activeAreaType.value = defaultAreaType
   }
   activeShapeId.value = null
   startDraw(type)
}
function handleClearShape () {
   if (readonly.value) return
   viewEntity && viewer?.entities?.remove(viewEntity)
   viewEntity = null
   pointList = []
   shapeList.value = []
   activeShapeId.value = null
   hideTypePanel()
   clearShapeDisplay()
   clearActiveTool()
   if (viewer && currentShapeType.value) {
      startDraw(currentShapeType.value)
   }
}
function handleShapeEdit (row) {
   if (!row?.points?.length) return
   if (activeToolMode.value === 'edit' && activeShapeId.value === row.id) return
   activeShapeId.value = row.id
   currentShapeType.value = row.drawType
   activeAreaType.value = row.drawType === 'buffer' ? bufferAreaTypeValue.split(',') : row.areaType
   const displayPoints = getShapeDisplayPoints(row)
   pointList = _.cloneDeep(displayPoints)
   clearActiveTool()
   const editPayload = ['buffer', 'ellipse'].includes(row.drawType)
      ? { points: displayPoints, meta: row.meta }
      : row.points
   startEdit(row.drawType, editPayload)
   showTypePanelAtCurrent()
   renderShapeList()
}
function handleShapeDelete (row) {
   if (!row) return
   shapeList.value = shapeList.value.filter(item => item.id !== row.id)
   renderShapeList()
   if (activeShapeId.value === row.id) {
      activeShapeId.value = null
      clearActiveTool()
      pointList = []
      hideTypePanel()
   }
}
// 查看面
function viewPolygon () {
   if (!viewer) return
   if (shapeList.value.length) {
      renderShapeList()
      fitViewToShapeList()
      return
   }
   if (!formData.value?.geom) return
   pointList = geomAnalysis(formData.value.geom)
   const result = pointList.map(item => [item.longitude, item.latitude]).flat()
   const mian = viewer.entities?.add({
   viewEntity = viewer.entities?.add({
      customType: 'control_group',
      position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
      polyline: {
@@ -356,22 +1237,91 @@
         material: Cesium.Color.RED,
      },
   })
   viewer.flyTo(mian, { duration: 0 })
   viewer.flyTo(viewEntity, { duration: 0 })
}
// 获取派出所列表
async function getPoliceStationList () {
   const res = await fwPoliceStationListApi()
   policeStationOptions.value = res?.data?.data ?? []
async function getPoliceStationList (keyword = '') {
   if (!visible.value) return
   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
   }
}
function handlePoliceStationSearch (query) {
   policeStationSearchKeyword.value = (query || '').trim()
   if (!policeStationSearchKeyword.value) {
      policeStationOptions.value = [...policeStationAllOptions.value]
      return
   }
   if (policeStationSearchTimer.value) {
      clearTimeout(policeStationSearchTimer.value)
   }
   policeStationSearchTimer.value = setTimeout(() => {
      policeStationSearchTimer.value = null
      void getPoliceStationList(policeStationSearchKeyword.value)
   }, 300)
}
// 获取设备列表
async function getDeviceList () {
   if (deviceOptions.value.length) return
   const res = await fwDeviceListApi({ isAreaSelect: 1, areaId: formData.value.id })
function queueDeviceListRefresh () {
   if (!visible.value) return
   if (deviceListTimer) {
      clearTimeout(deviceListTimer)
   }
   deviceListTimer = setTimeout(() => {
      deviceListTimer = null
      void getDeviceListByPolygons()
   }, 200)
}
async function getDeviceListByPolygons () {
   if (!visible.value) return
   const polygons = resolveShapePolygonsWkt()
   if (!polygons.length) {
      clearDeviceListState()
      return
   }
   deviceListRequestToken += 1
   const requestToken = deviceListRequestToken
   const res = await fwDeviceListByPolygonsApi({
      areaId: dialogMode.value === 'add' ? '' : formData.value.id,
      isAreaSelect: 1,
      isTrack: 1,
      polygons,
   })
   if (requestToken !== deviceListRequestToken) return
   deviceOptions.value = res?.data?.data ?? []
   if (dialogMode.value === 'view') {
      deviceOptions.value = deviceOptions.value.filter(item => formData.value.deviceIds.includes(item.id))
   if (dialogMode.value === 'view' && formData.value.deviceIds) {
      const targetIds = formData.value.deviceIds.split(',')
      deviceOptions.value = deviceOptions.value.filter(item => targetIds.includes(String(item.id)))
   }
   await nextTick()
   const prevSuppressDeviceFly = suppressDeviceFly.value
   suppressDeviceFly.value = true
   syncDeviceSelection()
   suppressDeviceFly.value = prevSuppressDeviceFly
   if (!readonly.value) {
      renderDeviceRanges(selectedDeviceRows.value)
   }
}
@@ -380,47 +1330,129 @@
   selectedDeviceRows.value = rows
   const ids = rows.map(item => item.id)
   formData.value.deviceIds = ids.join(',')
   renderDeviceRanges(rows)
   if (suppressDeviceFly.value) {
      lastSelectedDeviceIds = [...ids]
      return
   }
   const prevSet = new Set(lastSelectedDeviceIds)
   const addedRow = rows.find(row => !prevSet.has(row.id))
   lastSelectedDeviceIds = [...ids]
   if (addedRow) {
      void flyToDevice(addedRow)
   }
}
function handleDeviceRowClick (row) {
   if (readonly.value) {
      renderDeviceRangeSingle(row)
      void flyToDevice(row)
      return
   }
   renderDeviceRanges(selectedDeviceRows.value)
   void flyToDevice(row)
}
// 同步选择状态
function syncDeviceSelection () {
   if (!deviceTableRef.value) return
   deviceTableRef.value.clearSelection()
   if (isDialogInitializing.value && !deviceOptions.value.length) return
   const rows = []
   const arr = formData.value.deviceIds.split(',')
   const arr = formData.value.deviceIds ? formData.value.deviceIds.split(',') : []
   deviceOptions.value.forEach(row => {
      if (arr.includes(row.id)) {
         deviceTableRef.value.toggleRowSelection(row, true)
      if (arr.includes(String(row.id))) {
         rows.push(row)
      }
   })
   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 => {
      deviceTableRef.value.toggleRowSelection(row, true)
   })
}
// 打开弹框
async function open ({ mode, row } = {}) {
   isDialogInitializing.value = true
   dialogMode.value = mode || 'add'
   formData.value = dialogMode.value === 'add' ? initForm() : row
   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 = ''
   pointList = []
   await nextTick()
   initMap()
   await getDeviceList()
   clearShapeDisplay()
   clearDeviceRangePrimitives()
   clearDeviceIconEntities()
   viewEntity && viewer?.entities?.remove(viewEntity)
   viewEntity = null
   clearActiveTool()
   if (dialogMode.value === 'add') {
      addPolygon()
      mapRef.value?.zoomToAdminBoundary?.()
      handleDraw('polygon')
   } else if (dialogMode.value === 'edit') {
      await loadDetail()
      editPolygon()
      viewPolygon()
      activeShapeId.value = null
      pointList = []
      const firstShape = shapeList.value[0]
      const initialType = firstShape?.drawType || 'polygon'
      currentShapeType.value = initialType
      if (initialType === 'buffer') {
         activeAreaType.value = bufferAreaTypeValue.split(',')
      } else if (firstShape?.areaType) {
         activeAreaType.value = firstShape.areaType
      } else {
         const defaultAreaType = getDetectAreaTypeKey()
         if (defaultAreaType) {
            activeAreaType.value = defaultAreaType
         }
      }
      clearActiveTool()
      startDraw(initialType)
   } else {
      await loadDetail()
      viewPolygon()
   }
   await nextTick()
   suppressDeviceFly.value = true
   syncDeviceSelection()
   lastSelectedDeviceIds = selectedDeviceRows.value.map(row => row.id)
   await nextTick()
   suppressDeviceFly.value = false
   if (!readonly.value) {
      renderDeviceRanges(selectedDeviceRows.value)
   }
   isDialogInitializing.value = false
   queueDeviceListRefresh()
   void getPoliceStationList()
}
onMounted(() => {
   getPoliceStationList()
   getDeviceList()
onBeforeUnmount(() => {
   if (deviceListTimer) {
      clearTimeout(deviceListTimer)
      deviceListTimer = null
   }
   if (policeStationSearchTimer.value) {
      clearTimeout(policeStationSearchTimer.value)
      policeStationSearchTimer.value = null
   }
   hideTypePanel()
})
defineExpose({
@@ -428,4 +1460,155 @@
})
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.left-container {
   position: relative;
}
.shape-type-panel {
   position: absolute;
   right: 16px;
   bottom: 16px;
   z-index: 6;
   min-width: 260px;
   background: rgba(22, 33, 44, 0.9);
   border: 1px solid rgba(255, 255, 255, 0.15);
   border-radius: 8px;
   box-shadow: 0 10px 24px rgba(0, 0, 0, 0.35);
   backdrop-filter: blur(6px);
   color: #e5e7f0;
}
.panel-header {
   display: flex;
   align-items: center;
   justify-content: space-between;
   padding: 8px 12px;
   border-bottom: 1px solid rgba(255, 255, 255, 0.12);
   font-size: 13px;
   font-weight: 600;
}
.panel-close {
   cursor: pointer;
   color: rgba(255, 255, 255, 0.7);
}
.panel-body {
   display: flex;
   align-items: center;
   gap: 8px;
   padding: 10px 12px 12px;
}
.panel-label {
   font-size: 12px;
   color: rgba(255, 255, 255, 0.75);
   white-space: nowrap;
}
.map-toolbar {
   position: absolute;
   top: 64px;
   right: 16px;
   display: flex;
   flex-direction: column;
   gap: 14px;
   padding: 32px 14px;
   background: rgba(26, 25, 25, 0.6);
   border-radius: 8px 8px 8px 8px;
   backdrop-filter: blur(4px);
}
.tool-btn {
   padding: 0 !important;
   width: 46px;
   height: 46px;
   display: flex;
   flex-direction: column;
   align-items: center;
   border-radius: 8px;
   border: none !important;
   box-shadow: none;
   cursor: pointer;
   overflow: hidden;
   background: transparent !important;
   box-sizing: border-box;
}
.tool-btn.active {
   border: 2px solid #FFFFFF !important;
}
.tool-icon {
   position: relative;
}
.tool-icon-image {
   width: 100%;
   height: 100%;
}
.tool-icon.polygon {
   border-radius: 4px;
   transform: rotate(15deg);
}
.tool-icon.rect {
   border-radius: 2px;
}
.tool-icon.ellipse {
   border-radius: 50%;
}
.tool-icon.buffer {
   border-radius: 50%;
   box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5);
}
.tool-icon.trash {
   border: none;
   box-shadow: none;
}
.tool-icon.trash::before,
.tool-icon.trash::after {
   content: "";
   position: absolute;
   left: 4px;
   right: 4px;
   background: #ffffff;
}
.tool-icon.trash::before {
   top: 4px;
   height: 10px;
   border-radius: 2px;
}
.tool-icon.trash::after {
   top: 1px;
   height: 3px;
   border-radius: 2px;
}
.shape-table-container {
   margin-bottom: 20px;
   .shape-table-title {
      margin-bottom: 20px;
      display: flex;
      align-items: center;
      justify-content: flex-start;
      padding-right: 1.2rem;
      font-family: Source Han Sans CN, Source Han Sans CN;
      font-weight: 500;
      font-size: 1.4rem;
      color: #D4D5D7;
      font-style: normal;
      text-transform: none;
      box-sizing: border-box;
   }
}
</style>