吉安感知网项目-前端
罗广辉
2026-01-30 559e8f771c283c19efec60c400a538824185bebf
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
@@ -1,72 +1,62 @@
<template>
   <el-dialog class="command-page-map-view-dialog" v-model="visible" :show-close="false"
      :close-on-click-modal="false">
   <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"
            />
            <div
               v-if="showTypePanel && !readonly"
               class="shape-type-panel"
            >
            <CommonCesiumMap ref="mapRef" class="leftMap command-cesium" :active="visible" :flat-mode="false"
               :terrain="true" :layer-mode="4" :boundary="false" />
            <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>
                  <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="formData.areaType"
                     placeholder="请选择"
                     @change="handleAreaTypeChange"
                  >
                     <el-option
                        v-for="item in dictObj.areaType"
                        :key="item.dictKey"
                        :label="item.dictValue"
                        :value="item.dictKey"
                     />
                  <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">
               <button class="tool-btn" type="button" @click="handleDraw('polygon')">
                  <span class="tool-icon polygon"></span>
                  <span class="tool-text">多边形</span>
               </button>
               <button class="tool-btn" type="button" @click="handleDraw('rectangle')">
                  <span class="tool-icon rect"></span>
                  <span class="tool-text">矩形</span>
               </button>
               <button class="tool-btn" type="button" @click="handleDraw('ellipse')">
                  <span class="tool-icon ellipse"></span>
                  <span class="tool-text">椭圆</span>
               </button>
               <button class="tool-btn" type="button" @click="handleDraw('buffer')">
                  <span class="tool-icon buffer"></span>
                  <span class="tool-text">缓冲圆</span>
               </button>
               <button class="tool-btn danger" type="button" @click="handleClearShape">
                  <span class="tool-icon trash"></span>
                  <span class="tool-text">删除</span>
               </button>
               <el-tooltip content="多边形" placement="left">
                  <button class="tool-btn" 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" 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" 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" 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">
@@ -75,18 +65,6 @@
                  <el-col :span="24">
                     <div class="label">区域名称</div>
                     <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>
@@ -112,11 +90,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" @row-click="handleDeviceRowClick">
                     <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 }">
@@ -131,25 +127,12 @@
            <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-select class="command-select" popper-class="command-select-popper"
@@ -166,10 +149,9 @@
                  </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="结束时间"
                     :disabled-date="disabledFlyDate" :disabled-time="disabledFlyTime"
                  <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">
@@ -182,10 +164,10 @@
               <div class="shape-table-container">
                  <div class="shape-table-title">绘制区域列表</div>
                  <el-table class="command-table" :data="shapeList" row-key="id" height="360">
                     <el-table-column prop="areaType" label="类型">
                  <el-table class="command-table" :data="shapeList" row-key="id" height="auto">
                     <el-table-column prop="areaType" label="区域类型">
                        <template v-slot="{ row }">
                           {{ getDictLabel(row.areaType, dictObj.areaType) }}
                           {{ resolveShapeAreaTypeLabel(row) }}
                        </template>
                     </el-table-column>
                     <el-table-column prop="drawType" label="绘制类型">
@@ -208,15 +190,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"
                        @row-click="handleDeviceRowClick">
                        @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="类型">
@@ -230,7 +210,9 @@
            </el-form>
            <div class="footer">
               <el-button v-if="dialogMode != 'view'" 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">
                  确定
@@ -251,25 +233,30 @@
import { useRoute } from 'vue-router'
import { fieldRules, geomAnalysis, getDictLabel } from '@ztzf/utils'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { DrawManager } from './shapeTools/DrawManager'
import { EditManager } from './shapeTools/EditManager'
import { DrawManager, EditManager, buildEllipsePositions } from '@/utils/cesium/shapeTools'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
import { buildEllipsePositions } from './shapeTools/shapeUtils'
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 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'
const initForm = () => ({
   areaName: '', // 区域名称
   areaSize: null, // 区域面积
   areaType: '', // 区域类型
   controlLevel: '', // 管控级别
   deviceIds: '', // 关联设备ID
   flyDateEnd: '', // 可飞行结束
   flyDateStart: '', // 可飞行开始
   latitude: null, // 区域中心纬度
   longitude: null, // 区域中心经度
   policeStationId: '', // 关联派出所id
   responseMechanism: '', // 响应机制
   triggerCondition: '', // 触发条件
@@ -293,6 +280,7 @@
const dictObj = inject('dictObj')
const mapRef = ref(null)
const currentShapeType = ref('polygon')
const activeAreaType = ref('')
const shapeList = ref([])
const activeShapeId = ref(null)
const drawTypeLabelMap = {
@@ -301,22 +289,9 @@
   ellipse: '椭圆',
   buffer: '缓冲圆',
}
const bufferAreaTypeValue = '1,2,3'
const showTypePanel = ref(false)
const suppressTypePanelOnce = ref(false)
const areaTypeStyleMap = {
   '1': {
      fill: Cesium.Color.fromBytes(25, 178, 230, 128),
      outline: Cesium.Color.fromBytes(0, 251, 255, 255),
   },
   '2': {
      fill: Cesium.Color.fromBytes(255, 235, 59, 128),
      outline: Cesium.Color.fromBytes(255, 235, 59, 255),
   },
   '3': {
      fill: Cesium.Color.fromBytes(247, 20, 20, 128),
      outline: Cesium.Color.fromBytes(255, 0, 0, 255),
   },
}
let viewer
let drawManager
let editManager
@@ -325,13 +300,10 @@
let viewEntity
let shapeDisplaySource
const deviceRangePrimitiveMap = new Map()
let deviceRangeMaterialRegistered = false
const activeToolMode = ref('')
const rules = {
   areaName: fieldRules(true, 50),
   longitude: fieldRules(true),
   areaType: fieldRules(true),
   responseMechanism: fieldRules(true, 200),
   triggerCondition: fieldRules(true, 200),
   controlLevel: fieldRules(true),
@@ -384,19 +356,28 @@
      return
   }
   console.log(shapeList.value, 1111111)
   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²,请重新绘制')
      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,
@@ -432,7 +413,7 @@
   }
)
watch(
   () => formData.value.areaType,
   () => activeAreaType.value,
   val => {
      updateActiveShapeAreaType(val)
   }
@@ -442,14 +423,19 @@
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)
}
// 格式化可飞行时间段
@@ -474,42 +460,6 @@
   return shapeDisplaySource
}
function registerDeviceRangeMaterial () {
   if (deviceRangeMaterialRegistered || !Cesium?.Material) return
   deviceRangeMaterialRegistered = true
   Cesium.Material._materialCache.addMaterial('DeviceRangeEllipsoid', {
      fabric: {
         type: 'DeviceRangeEllipsoid',
         uniforms: {
            color: new Cesium.Color(1.0, 0.0, 0.0, 0.7),
            speed: 3.0,
         },
         source: `
            uniform vec4 color;
            uniform float speed;
            czm_material czm_getMaterial(czm_materialInput materialInput){
               czm_material material = czm_getDefaultMaterial(materialInput);
               vec2 st = materialInput.st;
               float time = fract(czm_frameNumber * speed / 1000.0);
               float alpha = abs(smoothstep(0.5,1.,fract( -st.t - time)));
               alpha += .1;
               material.alpha = alpha;
               material.diffuse = color.rgb;
               return material;
            }
         `,
      },
      translucent: () => true,
   })
}
function createDeviceRangeMaterial () {
   registerDeviceRangeMaterial()
   return Cesium.Material.fromType('DeviceRangeEllipsoid', {
      color: Cesium.Color.fromBytes(0, 255, 255, 155),
   })
}
function clearDeviceRangePrimitives () {
   if (!viewer || !deviceRangePrimitiveMap.size) return
   for (const primitive of deviceRangePrimitiveMap.values()) {
@@ -518,61 +468,19 @@
   deviceRangePrimitiveMap.clear()
}
function normalizeDeviceRange (value) {
   const range = Number(value)
   if (!Number.isFinite(range) || range <= 0) return 2000
   return range
}
function getDeviceLngLat (device) {
   const lngRaw = device?.longitude ?? device?.lng ?? device?.lon
   const latRaw = device?.latitude ?? device?.lat
   const lng = Number(lngRaw)
   const lat = Number(latRaw)
   if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null
   return { lng, lat }
}
function createDeviceRangePrimitive (device) {
   if (!viewer) return null
   if (!device) return null
   const position = getDeviceLngLat(device)
   if (!position) return null
   const rangeMeters = normalizeDeviceRange(device.effectiveRangeKm)
   const center = Cesium.Cartesian3.fromDegrees(position.lng, position.lat, 0)
   const radii = new Cesium.Cartesian3(rangeMeters, rangeMeters, rangeMeters)
   const geometry = new Cesium.EllipsoidGeometry({
      radii,
      vertexFormat: Cesium.MaterialAppearance.MaterialSupport.TEXTURED?.vertexFormat
         || Cesium.VertexFormat.POSITION_AND_NORMAL,
   })
   const instance = new Cesium.GeometryInstance({
      geometry,
      modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(center),
   })
   return new Cesium.Primitive({
      geometryInstances: instance,
      appearance: new Cesium.MaterialAppearance({
         material: createDeviceRangeMaterial(),
         closed: true,
         translucent: true,
         flat: true,
         faceForward: true,
      }),
      asynchronous: false,
   })
}
function renderDeviceRangeSingle (device) {
async function renderDeviceRangeSingle (device) {
   if (!viewer) return
   clearDeviceRangePrimitives()
   const primitive = createDeviceRangePrimitive(device)
   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)
}
function renderDeviceRanges (rows) {
async function renderDeviceRanges (rows) {
   if (!viewer) return
   const selectedRows = Array.isArray(rows) ? rows : []
   const nextIds = new Set(selectedRows.map(row => String(row?.id)))
@@ -582,13 +490,43 @@
         deviceRangePrimitiveMap.delete(id)
      }
   }
   selectedRows.forEach(row => {
   const pendingRows = selectedRows.filter(row => {
      const id = String(row?.id)
      if (!id || deviceRangePrimitiveMap.has(id)) return
      const primitive = createDeviceRangePrimitive(row)
      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 }
         const rangeMeters = normalizeDeviceRange(row.effectiveRangeKm)
         const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters)
         return { row, primitive }
      })
   )
   primitives.forEach(({ row, primitive }) => {
      if (!primitive) return
      const id = String(row?.id)
      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,
   })
}
@@ -661,14 +599,11 @@
}
function getAreaTypeStyle (areaType) {
   return areaTypeStyleMap?.[String(areaType)] || {
      fill: Cesium.Color.fromBytes(45, 140, 240, 99),
      outline: Cesium.Color.fromBytes(45, 140, 240, 255),
   }
   return AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
}
function renderShapeList () {
   if (!viewer || !visible.value || readonly.value) {
   if (!viewer || !visible.value) {
      clearShapeDisplay()
      return
   }
@@ -683,22 +618,8 @@
         const [radius1, radius2, radius3] = shape.meta.bufferRadii
         const radii = [radius1, radius2, radius3].filter(item => Number.isFinite(item) && item > 0)
         if (radii.length) {
            const styles = [
               {
                  fill: Cesium.Color.fromBytes(247, 20, 20, 128),
                  outline: Cesium.Color.fromBytes(255, 0, 0, 255),
               },
               {
                  fill: Cesium.Color.fromBytes(255, 235, 59, 128),
                  outline: Cesium.Color.fromBytes(255, 235, 59, 255),
               },
               {
                  fill: Cesium.Color.fromBytes(25, 178, 230, 128),
                  outline: Cesium.Color.fromBytes(0, 251, 255, 255),
               },
            ]
            radii.forEach((radius, index) => {
               const style = styles[index] || styles[styles.length - 1]
               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 },
@@ -755,9 +676,6 @@
   const positions = Array.isArray(data?.positions) ? data.positions : data
   if (!Array.isArray(positions) || positions.length < 3) {
      pointList = []
      formData.value.areaSize = null
      formData.value.longitude = null
      formData.value.latitude = null
      hideTypePanel()
      return
   }
@@ -769,13 +687,15 @@
      const val = cartesian3Convert(item, viewer)
      return { ...val, lng: val.longitude, lat: val.latitude }
   })
   applyPolygonMetrics(pointList)
   const controlPoints = resolveControlPoints(meta, currentShapeType.value)
   if (!formData.value.areaType) {
   if (!activeAreaType.value) {
      const detectAreaType = getDetectAreaTypeKey()
      if (detectAreaType) {
         formData.value.areaType = detectAreaType
         activeAreaType.value = detectAreaType
      }
   }
   if (currentShapeType.value === 'buffer') {
      activeAreaType.value = bufferAreaTypeValue.split(',')
   }
   if (!suppressTypePanelOnce.value) {
      showTypePanelAtCurrent()
@@ -817,7 +737,7 @@
   }
   const shapePayload = {
      id: activeShapeId.value,
      areaType: formData.value.areaType,
      areaType: currentShapeType.value === 'buffer' ? bufferAreaTypeValue : activeAreaType.value,
      drawType: currentShapeType.value,
      points: ['ellipse', 'buffer'].includes(currentShapeType.value) && controlPoints.length
         ? controlPoints
@@ -833,29 +753,86 @@
   renderShapeList()
}
function applyPolygonMetrics(points) {
   if (!points?.length) return
   const polygon = turf.polygon([
      [...points.map(item => [item.longitude, item.latitude]), [points[0].longitude, points[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 getDetectAreaTypeKey () {
   const areaTypeOptions = Array.isArray(dictObj?.value?.areaType)
      ? dictObj.value.areaType
      : dictObj?.areaType
   console.log(areaTypeOptions, 1)
   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 () {
@@ -888,9 +865,8 @@
}
function updateActiveShapeAreaType (value) {
   if (currentShapeType.value !== 'buffer') {
      activeTool?.setStyle?.(getAreaTypeStyle(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
@@ -916,7 +892,7 @@
   currentShapeType.value = type
   activeToolMode.value = 'draw'
   drawManager ||= new DrawManager(viewer)
   activeTool = drawManager.start(type, { style: getAreaTypeStyle(formData.value.areaType) })
   activeTool = drawManager.start(type, { style: getAreaTypeStyle(activeAreaType.value) })
   activeTool?.subscribe('getPolygonPositions', drawFinished)
}
@@ -924,7 +900,7 @@
   currentShapeType.value = type
   activeToolMode.value = 'edit'
   editManager ||= new EditManager(viewer)
   activeTool = editManager.start(type, points, { style: getAreaTypeStyle(formData.value.areaType) })
   activeTool = editManager.start(type, points, { style: getAreaTypeStyle(activeAreaType.value) })
   activeTool?.subscribe('getPolygonPositions', drawFinished)
}
@@ -936,22 +912,12 @@
   viewEntity = null
   hideTypePanel()
   renderShapeList()
   if (dialogMode.value === 'edit' && pointList.length >= 3) {
      const editPoints = _.cloneDeep(pointList)
      if (editPoints.length > 1) {
         const first = editPoints[0]
         const last = editPoints[editPoints.length - 1]
         if (first.longitude === last.longitude && first.latitude === last.latitude) {
            editPoints.pop()
         }
      }
      activeShapeId.value = activeShapeId.value || (shapeList.value[0]?.id ?? null)
      startEdit(type, editPoints)
      return
   }
   pointList = []
   const defaultAreaType = getDetectAreaTypeKey()
   if (defaultAreaType) {
      formData.value.areaType = defaultAreaType
   if (type === 'buffer') {
      activeAreaType.value = bufferAreaTypeValue
   } else if (defaultAreaType) {
      activeAreaType.value = defaultAreaType
   }
   activeShapeId.value = null
   startDraw(type)
@@ -965,9 +931,7 @@
   pointList = []
   shapeList.value = []
   activeShapeId.value = null
   formData.value.areaSize = null
   formData.value.longitude = null
   formData.value.latitude = null
   activeAreaType.value = ''
   hideTypePanel()
   clearShapeDisplay()
}
@@ -977,7 +941,7 @@
   if (activeToolMode.value === 'edit' && activeShapeId.value === row.id) return
   activeShapeId.value = row.id
   currentShapeType.value = row.drawType
   formData.value.areaType = row.areaType
   activeAreaType.value = row.drawType === 'buffer' ? bufferAreaTypeValue.split(',') : row.areaType
   const displayPoints = getShapeDisplayPoints(row)
   pointList = _.cloneDeep(displayPoints)
   clearActiveTool()
@@ -985,7 +949,6 @@
      ? { points: displayPoints, meta: row.meta }
      : row.points
   startEdit(row.drawType, editPayload)
   applyPolygonMetrics(pointList)
   showTypePanelAtCurrent()
   renderShapeList()
}
@@ -998,15 +961,17 @@
      activeShapeId.value = null
      clearActiveTool()
      pointList = []
      formData.value.areaSize = null
      formData.value.longitude = null
      formData.value.latitude = null
      hideTypePanel()
   }
}
// 查看面
function viewPolygon () {
   if (!viewer) return
   if (shapeList.value.length) {
      renderShapeList()
      return
   }
   if (!formData.value?.geom) return
   pointList = geomAnalysis(formData.value.geom)
   const result = pointList.map(item => [item.longitude, item.latitude]).flat()
@@ -1045,29 +1010,41 @@
   const ids = rows.map(item => item.id)
   formData.value.deviceIds = ids.join(',')
   renderDeviceRanges(rows)
   const target = rows[rows.length - 1]
   if (target) {
      void flyToDevice(target)
   }
}
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()
   const rows = []
   const arr = formData.value.deviceIds.split(',')
   deviceOptions.value.forEach(row => {
      if (arr.includes(row.id)) {
         deviceTableRef.value.toggleRowSelection(row, true)
         rows.push(row)
      }
   })
   selectedDeviceRows.value = rows
   if (readonly.value) {
      renderDeviceRanges(selectedDeviceRows.value)
      return
   }
   if (!deviceTableRef.value) return
   deviceTableRef.value.clearSelection()
   rows.forEach(row => {
      deviceTableRef.value.toggleRowSelection(row, true)
   })
}
// 打开弹框
@@ -1077,6 +1054,8 @@
   selectedDeviceRows.value = []
   shapeList.value = []
   activeShapeId.value = null
   activeAreaType.value = ''
   pointList = []
   await nextTick()
   initMap()
   clearShapeDisplay()
@@ -1089,18 +1068,6 @@
      // default no draw mode
   } else if (dialogMode.value === 'edit') {
      await loadDetail()
      pointList = geomAnalysis(formData.value.geom) || []
      if (pointList.length) {
         activeShapeId.value = `shape_${Date.now()}_${Math.random().toString(16).slice(2, 6)}`
         shapeList.value = [
            {
               id: activeShapeId.value,
               areaType: formData.value.areaType,
               drawType: currentShapeType.value,
               points: _.cloneDeep(pointList),
            },
         ]
      }
      viewPolygon()
   } else {
      await loadDetail()
@@ -1188,20 +1155,20 @@
}
.tool-btn {
   padding: 0;
   display: flex;
   flex-direction: column;
   align-items: center;
   gap: 6px;
   width: 64px;
   padding: 8px 6px;
   background: rgba(255, 255, 255, 0.08);
   border: 1px solid rgba(255, 255, 255, 0.18);
   border-radius: 6px;
   border-radius: 8px;
   color: #ffffff;
   font-size: 12px;
   line-height: 1;
   cursor: pointer;
   transition: all 0.2s ease;
   overflow: hidden;
}
.tool-btn:hover {
@@ -1221,10 +1188,11 @@
.tool-icon {
   position: relative;
   width: 20px;
   height: 20px;
   border: 2px solid #ffffff;
   border-radius: 3px;
}
.tool-icon-image {
   width: 36px;
   height: 36px;
}
.tool-icon.polygon {
@@ -1272,23 +1240,21 @@
}
.shape-table-container {
   margin-top: 8px;
   padding: 8px;
   border-radius: 8px;
   background: rgba(27, 34, 56, 0.6);
}
   margin-bottom: 20px;
.shape-table-title {
   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;
   .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>