吉安感知网项目-前端
chenyao
2026-01-30 fb39a7bb69cc55d2e1ce61d47e0677aa40540f95
applications/drone-command/src/views/areaManage/partition/FormDiaLog copy.vue
@@ -12,54 +12,6 @@
               :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>
               </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>
               </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>
            </div>
         </div>
         <div class="right-container">
@@ -180,28 +132,6 @@
                  </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="360">
                     <el-table-column prop="areaType" label="类型" width="120">
                        <template v-slot="{ row }">
                           {{ getDictLabel(row.areaType, dictObj.areaType) }}
                        </template>
                     </el-table-column>
                     <el-table-column prop="drawType" label="绘制类型" width="120">
                        <template v-slot="{ row }">
                           {{ drawTypeLabelMap[row.drawType] || '-' }}
                        </template>
                     </el-table-column>
                     <el-table-column label="操作" class-name="operation-btns">
                        <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">
                     <div class="label">
@@ -243,17 +173,15 @@
<script setup>
import { Close } from '@element-plus/icons-vue'
import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { computed, inject, nextTick, onMounted, 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 { DrawManager } from './shapeTools/DrawManager'
import { EditManager } from './shapeTools/EditManager'
import { DrawPolygon } from '@/utils/cesium/DrawPolygon'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
import { buildEllipsePositions } from './shapeTools/shapeUtils'
import * as turf from '@turf/turf'
import * as Cesium from 'cesium'
import { fwPoliceStationListApi } from '@/views/areaManage/precinctInfo/precinctInfoApi'
@@ -291,39 +219,9 @@
const deviceOptions = ref([]) // 关联设备
const dictObj = inject('dictObj')
const mapRef = ref(null)
const currentShapeType = ref('polygon')
const shapeList = ref([])
const activeShapeId = ref(null)
const drawTypeLabelMap = {
   polygon: '多边形',
   rectangle: '矩形',
   ellipse: '椭圆',
   buffer: '缓冲圆',
}
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
let activeTool
let drawPolygonExample
let pointList = []
let viewEntity
let shapeDisplaySource
const activeToolMode = ref('')
const rules = {
   areaName: fieldRules(true, 50),
@@ -408,24 +306,6 @@
      formData.value.flyDateEnd = flyDateRange.value[1] || ''
   }
)
watch(
   () => visible.value,
   val => {
      if (!val) {
         viewEntity && viewer?.entities?.remove(viewEntity)
         viewEntity = null
         clearActiveTool()
         hideTypePanel()
         clearShapeDisplay()
      }
   }
)
watch(
   () => formData.value.areaType,
   val => {
      updateActiveShapeAreaType(val)
   }
)
// 加载详情
async function loadDetail () {
@@ -454,191 +334,14 @@
   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 clearShapeDisplay () {
   if (!shapeDisplaySource) return
   shapeDisplaySource.entities.removeAll()
}
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 buildShapePositions (points = []) {
   const normalized = points.map(normalizeShapePoint).filter(Boolean)
   return normalized.map(point =>
      Cesium.Cartesian3.fromDegrees(point.lng, point.lat, point.height)
   )
}
function getAreaTypeStyle (areaType) {
   return areaTypeStyleMap?.[String(areaType)] || {
      fill: Cesium.Color.fromBytes(45, 140, 240, 99),
      outline: Cesium.Color.fromBytes(45, 140, 240, 255),
   }
}
function renderShapeList () {
   if (!viewer || !visible.value || readonly.value) {
      clearShapeDisplay()
      return
   }
   const dataSource = ensureShapeDisplaySource()
   if (!dataSource) return
   dataSource.entities.removeAll()
   shapeList.value.forEach(shape => {
      if (!shape?.points?.length) return
      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) {
            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]
               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,
                  },
               })
            })
         }
         return
      }
      const positions = buildShapePositions(shape.points)
      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,
         },
      })
   })
}
// 绘制完成回调
const drawFinished = async data => {
   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
   }
   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 => {
   pointList = _.cloneDeep(data).map(item => {
      const val = cartesian3Convert(item, viewer)
      return { ...val, lng: val.longitude, lat: val.latitude }
   })
   applyPolygonMetrics(pointList)
   if (!formData.value.areaType) {
      const detectAreaType = getDetectAreaTypeKey()
      if (detectAreaType) {
         formData.value.areaType = detectAreaType
      }
   }
   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 shapePayload = {
      id: activeShapeId.value,
      areaType: formData.value.areaType,
      drawType: currentShapeType.value,
      points: _.cloneDeep(pointList),
      meta: bufferRadii && centerLngLat
         ? {
            bufferRadii,
            center: {
               lng: centerLngLat.longitude,
               lat: centerLngLat.latitude,
               height: centerLngLat.height ?? 0,
            },
         }
         : null,
   }
   if (targetIndex === -1) {
      shapeList.value.push(shapePayload)
   } else {
      shapeList.value.splice(targetIndex, 1, shapePayload)
   }
   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]],
      [...pointList.map(item => [item.longitude, item.latitude]), [pointList[0].longitude, pointList[0].latitude]],
   ])
   const center = turf.centerOfMass(polygon)
   const areaSqm = turf.area(polygon) // 平方米
@@ -647,152 +350,41 @@
   formData.value.latitude = center.geometry.coordinates[1]
}
function getDetectAreaTypeKey () {
   const areaTypeOptions = Array.isArray(dictObj?.value?.areaType)
      ? dictObj.value.areaType
      : dictObj?.areaType
   if (!Array.isArray(areaTypeOptions)) return ''
   const target = areaTypeOptions.find(item => item?.dictKey === '1')
   return target?.dictKey ?? ''
// 新增模式绘制
function addPolygon () {
   drawPolygonExample = new DrawPolygon(viewer)
   drawPolygonExample.initHandler(viewer)
   drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
}
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)
   }
   clearActiveTool()
}
function handleAreaTypeChange (value) {
   updateActiveShapeAreaType(value)
}
function updateActiveShapeAreaType (value) {
   if (currentShapeType.value !== 'buffer') {
      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(formData.value.areaType) })
   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(formData.value.areaType) })
   activeTool?.subscribe('getPolygonPositions', drawFinished)
}
function handleDraw (type) {
   if (readonly.value) return
   if (!viewer) return
   clearActiveTool()
   viewEntity && viewer?.entities?.remove(viewEntity)
   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()
         }
// 编辑面
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)
      }
      activeShapeId.value = activeShapeId.value || (shapeList.value[0]?.id ?? null)
      startEdit(type, editPoints)
      return
   }
   const defaultAreaType = getDetectAreaTypeKey()
   if (defaultAreaType) {
      formData.value.areaType = defaultAreaType
   }
   activeShapeId.value = null
   startDraw(type)
}
function handleClearShape () {
   if (readonly.value) return
   clearActiveTool()
   viewEntity && viewer?.entities?.remove(viewEntity)
   viewEntity = null
   pointList = []
   shapeList.value = []
   activeShapeId.value = null
   formData.value.areaSize = null
   formData.value.longitude = null
   formData.value.latitude = null
   hideTypePanel()
   clearShapeDisplay()
}
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
   formData.value.areaType = row.areaType
   pointList = _.cloneDeep(row.points)
   clearActiveTool()
   const editPayload = row.drawType === 'buffer' ? { points: row.points, meta: row.meta } : row.points
   startEdit(row.drawType, editPayload)
   applyPolygonMetrics(pointList)
   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 = []
      formData.value.areaSize = null
      formData.value.longitude = null
      formData.value.latitude = null
      hideTypePanel()
   }
}
@@ -801,7 +393,7 @@
   if (!formData.value?.geom) return
   pointList = geomAnalysis(formData.value.geom)
   const result = pointList.map(item => [item.longitude, item.latitude]).flat()
   viewEntity = viewer.entities?.add({
   const mian = viewer.entities?.add({
      customType: 'control_group',
      position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
      polyline: {
@@ -811,7 +403,7 @@
         material: Cesium.Color.RED,
      },
   })
   viewer.flyTo(viewEntity, { duration: 0 })
   viewer.flyTo(mian, { duration: 0 })
}
// 获取派出所列表
@@ -857,32 +449,14 @@
   dialogMode.value = mode || 'add'
   formData.value = dialogMode.value === 'add' ? initForm() : row
   selectedDeviceRows.value = []
   shapeList.value = []
   activeShapeId.value = null
   await nextTick()
   initMap()
   clearShapeDisplay()
   viewEntity && viewer?.entities?.remove(viewEntity)
   viewEntity = null
   clearActiveTool()
   await getDeviceList()
   if (dialogMode.value === 'add') {
      // default no draw mode
      addPolygon()
   } 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()
      editPolygon()
   } else {
      await loadDetail()
      viewPolygon()
@@ -896,170 +470,9 @@
   getDeviceList()
})
onBeforeUnmount(() => {
   hideTypePanel()
})
defineExpose({
   open,
})
</script>
<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: 10px;
   padding: 10px 8px;
   background: rgba(19, 28, 36, 0.75);
   backdrop-filter: blur(4px);
   border-radius: 8px;
   box-shadow: 0 6px 18px rgba(0, 0, 0, 0.2);
}
.tool-btn {
   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;
   color: #ffffff;
   font-size: 12px;
   line-height: 1;
   cursor: pointer;
   transition: all 0.2s ease;
}
.tool-btn:hover {
   background: rgba(255, 255, 255, 0.16);
   border-color: rgba(255, 255, 255, 0.35);
   transform: translateY(-1px);
}
.tool-btn:active {
   transform: translateY(0);
}
.tool-btn.danger {
   border-color: rgba(255, 112, 112, 0.6);
   color: #ffb3b3;
}
.tool-icon {
   position: relative;
   width: 20px;
   height: 20px;
   border: 2px solid #ffffff;
   border-radius: 3px;
}
.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-top: 8px;
   padding: 8px;
   border-radius: 8px;
   background: rgba(27, 34, 56, 0.6);
}
.shape-table-title {
   margin-bottom: 8px;
   color: #e5e7f0;
   font-size: 13px;
}
</style>
<style scoped lang="scss"></style>