From ea6e49ccb4d3bdb510ccf87844490823a96ebf08 Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Fri, 12 Sep 2025 20:11:51 +0800
Subject: [PATCH] feat:空域录入完善,增删改查

---
 src/api/airspace/airspace.js                  |   49 +++++-
 src/views/airspace/airspaceEntering.vue       |  110 ++++++++-------
 src/utils/cesium/publicCesium.js              |    2 
 src/utils/drawPolygon/drawPolygon.js          |   69 ++++++++-
 src/views/airspace/components/AirspaceMap.vue |  145 ++++++++++++++++++--
 5 files changed, 288 insertions(+), 87 deletions(-)

diff --git a/src/api/airspace/airspace.js b/src/api/airspace/airspace.js
index d01085a..8d9c3f2 100644
--- a/src/api/airspace/airspace.js
+++ b/src/api/airspace/airspace.js
@@ -1,4 +1,4 @@
-import request from '@/axios';
+import request from '@/axios'
 // 算法列表
 export const getAirSpaceTypeList = (params) => {
   return request({
@@ -6,27 +6,60 @@
     method: 'get',
     params: params,
   })
-};
+}
 
 export const airSpaceTypeEdit = data => {
   return request({
     url: '/drone-device-core/airspace/update',
     method: 'put',
     data: data,
-  });
-};
+  })
+}
 
 export const airSpaceTypeAdd = data => {
   return request({
     url: '/drone-device-core/airspace/add',
     method: 'post',
     data: data,
-  });
-};
+  })
+}
 
 export const airSpaceTypeDelete = data => {
   return request({
     url: '/drone-device-core/airspace/delete/' + data,
     method: 'delete',
-  });
-};
\ No newline at end of file
+  })
+}
+
+// ----------空域录入相关接口调用----------
+export const airspaceEnteringAdd = data => {
+  return request({
+    url: '/drone-device-core/airrange/add',
+    method: 'post',
+    data
+  })
+}
+
+export const airspaceEnteringDel = id => {
+  return request({
+    url: `/drone-device-core/airrange/delete/${id}`,
+    method: 'delete',
+  })
+}
+
+export const airspaceEnteringUpdate = data => {
+  return request({
+    url: '/drone-device-core/airrange/update',
+    method: 'put',
+    data
+  })
+}
+
+export const airspaceEnteringPage = params => {
+  return request({
+    url: '/drone-device-core/airrange/page',
+    method: 'get',
+    params
+  })
+}
+// ----------空域录入相关接口调用----------
\ No newline at end of file
diff --git a/src/utils/cesium/publicCesium.js b/src/utils/cesium/publicCesium.js
index 2bdcc29..ef65590 100644
--- a/src/utils/cesium/publicCesium.js
+++ b/src/utils/cesium/publicCesium.js
@@ -149,6 +149,8 @@
 					requestWaterMask: true, // 启用水体遮罩效果
 				}).then(terrainProvider => {
 					this.viewer.terrainProvider = terrainProvider
+
+					options?.terrainInit?.()
 				})
 			} catch (error) {
 				console.error('地形加载失败:', error)
diff --git a/src/utils/drawPolygon/drawPolygon.js b/src/utils/drawPolygon/drawPolygon.js
index 2f98842..8bf2482 100644
--- a/src/utils/drawPolygon/drawPolygon.js
+++ b/src/utils/drawPolygon/drawPolygon.js
@@ -1,4 +1,5 @@
 import * as Cesium from 'cesium'
+import { flyVisual } from '@/utils/cesium/mapUtil'
 import _, { cloneDeep, throttle } from 'lodash'
 import * as turf from '@turf/turf'
 import '@/utils/drawPolygon/drawPolygon.css'
@@ -33,6 +34,7 @@
 	constructor() {
 		// Cesium 视图对象
 		this.viewer = null
+		this.polygonHeight = 120
 		// 当前绘制的多边形
 		this.curPolygon = null
 		// 绘制模式标识
@@ -47,6 +49,7 @@
 		this.polygonEntity = null
 		// 存储端点的 DataSource
 		this.editPolygonDataSource = null
+		this.editPolyhedronDataSource = null
 		this.editPolygonPointDataSource = null
 		// 鼠标事件处理器
 		this.handler = null
@@ -97,7 +100,9 @@
 		DEFAULT_POLYGON: Cesium.Color.fromBytes(45, 140, 240, 99), // 默认面颜色
 		DEFAULT_LINE: Cesium.Color.fromBytes(45, 140, 240, 255),  // 默认边界线颜色
 		ERROR_POLYGON: Cesium.Color.fromCssColorString('rgba(255, 0, 0, .3)'), // 错误(自交)面颜色
-		ERROR_LINE: Cesium.Color.fromCssColorString('rgba(255, 0, 0, 1)')      // 错误(自交)线颜色
+		ERROR_LINE: Cesium.Color.fromCssColorString('rgba(255, 0, 0, 1)'),      // 错误(自交)线颜色
+		// 新增多变体(立体)颜色:橙色,透明度 0.3
+		DEFAULT_POLYHEDRON: Cesium.Color.fromCssColorString('rgba(255, 165, 0, 0.3)')
 	};
 
 	// ============ 发布订阅机制 ============
@@ -111,7 +116,13 @@
 	notify (key, data) {
 		this.listeners
 			.filter(subscriber => subscriber.key === key)
-			.forEach(subscriber => subscriber.listener(data))
+			.forEach(subscriber => {
+				subscriber.listener(data)
+			})
+	}
+
+	setPolygonHeight (height) {
+		this.polygonHeight = height
 	}
 
 	// ============ 绘制相关 ============
@@ -128,6 +139,11 @@
 			this.viewer?.dataSources.add(this.editPolygonDataSource)
 		}
 
+		if (!this.editPolyhedronDataSource) {
+			this.editPolyhedronDataSource = new Cesium.CustomDataSource('editPolyhedronDataSource')
+			this.viewer?.dataSources.add(this.editPolyhedronDataSource)
+		}
+
 		if (!this.editPolygonPointDataSource) {
 			this.editPolygonPointDataSource = new Cesium.CustomDataSource('editPolygonPointDataSource')
 			this.viewer?.dataSources.add(this.editPolygonPointDataSource)
@@ -135,6 +151,7 @@
 
 		// 清空之前的点
 		this.editPolygonDataSource?.entities.removeAll()
+		this.editPolyhedronDataSource?.entities.removeAll()
 		this.editPolygonPointDataSource?.entities.removeAll()
 	}
 
@@ -159,6 +176,29 @@
 					false
 				)
 			},
+		})
+
+		this.editPolyhedronDataSource.entities.add({
+			polygon: {
+				hierarchy: new Cesium.CallbackProperty(() => this.curPolygon, false),
+				material: DrawPolygon.COLORS.DEFAULT_POLYHEDRON,
+				outline: false,
+				outlineWidth: 2,
+				height: 0,
+				extrudedHeight: new Cesium.CallbackProperty(() => {
+					let heights = this.curPolygon.positions.map(item => {
+						// 获取点的位置
+						const cartographic = Cesium.Cartographic.fromCartesian(item)
+						// 获取地形高度
+						const terrainHeight = this.viewer.scene.globe.getHeight(cartographic) || 0
+
+
+						return terrainHeight
+					})
+
+					return Math.max(...heights) + Number(this.polygonHeight)
+				}, false),
+			}
 		})
 	}
 
@@ -392,6 +432,11 @@
 			this.editPolygonDataSource = null
 		}
 
+		if (this.editPolyhedronDataSource) {
+			this.editPolyhedronDataSource.entities.removeAll()
+			this.editPolyhedronDataSource = null
+		}
+
 		if (this.editPolygonPointDataSource) {
 			this.editPolygonPointDataSource.entities.removeAll()
 			this.editPolygonPointDataSource = null
@@ -538,20 +583,25 @@
 		this.initHandler(viewer)
 		this.startDrawing()
 
-		positions.forEach(item => {
+		let disposePosition = positions.map(item => Cesium.Cartesian3.fromDegrees(Number(item.lng), Number(item.lat), Number(item.height)))
+
+		disposePosition.forEach(item => {
 			this.addPosition(
-				Cesium.Cartesian3.fromDegrees(Number(item.lng), Number(item.lat), Number(item.height)),
+				item,
 				false
 			)
 		})
 
+		this.notify('getPolygonPositions', disposePosition)
+
 		// 视角飞入区域
-		const newBox = boxTransformScale(positions.map(item => [item.lng, item.lat]), 5)
-		viewer.camera.flyTo({
-			destination: Cesium.Rectangle.fromDegrees(...newBox),
-			offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90), 0),
-			duration: 0.5,
+		flyVisual({
+			positionsData: positions.map(item => [item.lng, item.lat]),
+			viewer,
+			pitch: -60,
+			multiple: 6
 		})
+
 
 		this.drawingMode = false
 		this.editingMode = true
@@ -603,6 +653,7 @@
 	 * 销毁实例,释放资源
 	 */
 	destroy () {
+		this.prompt.destroy()
 		if (!this.viewer) return
 
 		this.removeMenuPopup()
diff --git a/src/views/airspace/airspaceEntering.vue b/src/views/airspace/airspaceEntering.vue
index 927c2f9..117677a 100644
--- a/src/views/airspace/airspaceEntering.vue
+++ b/src/views/airspace/airspaceEntering.vue
@@ -1,9 +1,9 @@
 <template>
   <basic-container>
     <avue-crud :option="option" :table-loading="loading" :data="data" v-model:page="page" :permission="permissionList"
-      v-model="form" ref="crud" @row-update="rowUpdate" @row-save="rowSave" @row-del="rowDel" :before-open="beforeOpen"
-      @search-change="searchChange" @search-reset="searchReset" @selection-change="selectionChange"
-      @current-change="currentChange" @size-change="sizeChange" @refresh-change="refreshChange" @on-load="onLoad">
+      v-model="form" ref="crud" @row-update="rowUpdate" @row-save="rowSave" :before-open="beforeOpen"
+      @search-change="searchChange" @search-reset="searchReset" @current-change="currentChange"
+      @size-change="sizeChange" @refresh-change="refreshChange" @on-load="onLoad">
       <template #menu-left>
         <el-button type="primary" icon="el-icon-plus" @click="addAirspace">新增空域</el-button>
       </template>
@@ -14,10 +14,16 @@
       <template #category="{ row }">
         <el-tag>{{ row.categoryName }}</el-tag>
       </template>
+
+      <template #menu="{ row }">
+        <el-button type="text" icon="el-icon-edit" @click="handleEdit(row)">编辑</el-button>
+        <el-button type="text" icon="el-icon-delete" @click="rowDel(row)">删除</el-button>
+      </template>
     </avue-crud>
   </basic-container>
 
-  <AirspaceMap v-model:show="airspaceMapShow" @submitClick="searchReset" />
+  <AirspaceMap :title="dialogTitle" :type="dialogType" :curEditRow="curEditRow" v-model:show="airspaceMapShow"
+    @submitClick="searchReset" />
 </template>
 
 <script setup>
@@ -28,6 +34,12 @@
   editSpotTypeApi,
   deleteSpotTypeApi,
 } from '@/api/patchManagement/index'
+
+import {
+  airspaceEnteringPage,
+  airspaceEnteringDel
+} from '@/api/airspace/airspace'
+
 import { ref, computed, watch } from 'vue'
 import { enable, disable } from '@/api/resource/oss'
 import { useStore } from 'vuex'
@@ -46,22 +58,21 @@
   pageSize: 10,
   currentPage: 1,
   total: 0,
-  lotValue: '',
-  userName: '',
 })
-const selectionList = ref([])
 
 const option = ref({
   addBtn: false,
+  editBtn: false,
+  delBtn: false,
   tip: false,
   searchShow: true,
-  searchMenuSpan: 8,
+  searchMenuSpan: 16,
   searchMenuPosition: 'right',
   border: true,
   index: true,
   indexLabel: '序号',
   indexWidth: 60,
-  selection: true,
+  selection: false,
   grid: false,
   menuWidth: 180,
   labelWidth: 100,
@@ -73,10 +84,11 @@
   gridBtn: false,
   searchShowBtn: false,
   columnBtn: false,
+  align: 'center',
   column: [
     {
       label: '名称',
-      prop: 'patches_type',
+      prop: 'name',
       search: true,
       searchSpan: 4,
       searchLabelWidth: 54,
@@ -84,12 +96,15 @@
     },
     {
       label: '类型',
-      prop: 'patches_type',
+      prop: 'flight_type',
       type: 'select',
-      dicUrl: `/blade-system/dict/dictionary?code=flow`,
+      dicUrl: `/drone-device-core/airspace/page?current=1&size=10000&typeName=`,
       props: {
-        label: 'dictValue',
-        value: 'dictKey',
+        label: 'type_name',
+        value: 'id',
+      },
+      dicFormatter (res) {
+        return res.data.records
       },
       search: true,
       searchSpan: 4,
@@ -98,34 +113,31 @@
     },
     {
       label: '高度',
-      prop: 'patches_type',
+      prop: 'height',
       rules: [{ required: true, message: '请输入高度', trigger: 'blur' }],
     },
     {
-      label: '面积',
-      prop: 'patches_type',
+      label: '面积(㎡)',
+      prop: 'area',
       rules: [{ required: true, message: '请输入面积', trigger: 'blur' }],
     },
     {
       label: '管控时段',
-      prop: 'daterange',
+      prop: 'time_period',
       type: 'daterange',
       format: 'YYYY-MM-DD',
       valueFormat: 'YYYY-MM-DD',
       startPlaceholder: '开始日期',
       endPlaceholder: '结束日期',
-      search: true,
-      searchSpan: 8,
-      searchRange: true,
     },
     {
       label: '创建人',
-      prop: 'patches_type',
+      prop: 'nick_name',
       rules: [{ required: true, message: '请输入创建人', trigger: 'blur' }],
     },
     {
       label: '创建时间',
-      prop: 'patches_type',
+      prop: 'create_time',
       rules: [{ required: true, message: '请输入创建时间', trigger: 'blur' }],
     },
   ],
@@ -170,15 +182,23 @@
   editBtn: validData(permission.value.oss_edit),
 }))
 
-const ids = computed(() => {
-  return selectionList.value.map(ele => ele.id).join(',')
-})
-
 const airspaceMapShow = ref(false)
+const curEditRow = ref({})
+const dialogTitle = ref('新增空域')
+const dialogType = ref('add')
 
 // ---------------- methods ----------------
 const addAirspace = () => {
   form.value = {}
+  dialogTitle.value = '新增空域'
+  dialogType.value = 'add'
+  airspaceMapShow.value = true
+}
+
+const handleEdit = (row) => {
+  curEditRow.value = row
+  dialogTitle.value = '编辑空域'
+  dialogType.value = 'edit'
   airspaceMapShow.value = true
 }
 
@@ -219,16 +239,14 @@
     cancelButtonText: '取消',
     type: 'warning',
   })
-    .then(() => deleteSpotTypeApi([row.id]))
+    .then(() => airspaceEnteringDel(row.id))
     .then(() => {
-      onLoad(page.value)
       ElMessage.success('操作成功!')
+      onLoad(page.value)
     })
 }
 
 const searchReset = () => {
-  page.value.userName = ''
-  page.value.lotValue = ''
   page.value.currentPage = 1
   page.value.pageSize = 10
   onLoad(page.value)
@@ -236,19 +254,8 @@
 
 const searchChange = (params, done) => {
   page.value.currentPage = 1
-  page.value.lotValue = params.patches_type
-  page.value.userName = params.user_name
-  onLoad(page.value)
+  onLoad(page.value, params)
   done()
-}
-
-const selectionChange = list => {
-  // selectionList.value = list
-}
-
-const selectionClear = () => {
-  selectionList.value = []
-  // crudRef.value.toggleSelection()
 }
 
 const handleEnable = row => {
@@ -300,19 +307,20 @@
 }
 
 const onLoad = (pageParam, params = {}) => {
-  const searchparams = {
+  loading.value = true
+  airspaceEnteringPage({
     current: pageParam.currentPage,
     size: pageParam.pageSize,
-    lotValue: pageParam.lotValue,
-    userName: pageParam.userName,
-  }
-  loading.value = true
-  listOfSpotTypesApi(searchparams).then(res => {
+    name: params.name || '',
+    flightType: params.flight_type || '',
+  }).then(res => {
     const resData = res.data.data
     page.value.total = resData.total
-    data.value = resData.records
+    data.value = resData.records.map(item => ({
+      ...item,
+      time_period: item.time_period.split('~'),
+    }))
     loading.value = false
-    selectionClear()
   })
 }
 
diff --git a/src/views/airspace/components/AirspaceMap.vue b/src/views/airspace/components/AirspaceMap.vue
index 89707c6..828c467 100644
--- a/src/views/airspace/components/AirspaceMap.vue
+++ b/src/views/airspace/components/AirspaceMap.vue
@@ -6,7 +6,8 @@
 				<avue-form ref="formEle" :option="option" v-model="form"></avue-form>
 			</div>
 
-			<div class="map-container">
+			<div class="map-container" v-loading="mapLoading" element-loading-text="地形初始化中,请稍后..."
+				element-loading-background="rgba(0, 0, 0, 0.7)">
 				<div class="tool-tip warning" v-show="isShowWaringTip">
 					<span class="icon">
 						<el-icon>
@@ -31,15 +32,33 @@
 
 import * as Cesium from 'cesium'
 import * as turf from '@turf/turf'
+
+import { getPointPositionsHeight } from '@/utils/cesium/mapUtil'
 import { DrawPolygon } from '@/utils/drawPolygon/drawPolygon'
 import { PublicCesium } from '@/utils/cesium/publicCesium'
 import { ElMessageBox, ElMessage } from 'element-plus'
+
+import {
+	airspaceEnteringAdd,
+	airspaceEnteringUpdate
+} from '@/api/airspace/airspace'
+
 
 const props = defineProps({
 	title: {
 		type: String,
 		default: '新增空域',
 	},
+
+	type: {
+		type: String,
+		default: 'add',
+	},
+
+	curEditRow: {
+		type: Object,
+		default: () => { },
+	}
 })
 
 const emit = defineEmits(['submitClick'])
@@ -47,7 +66,9 @@
 const dialogShow = defineModel('show')
 
 const formEle = ref(null)
-const form = ref({})
+const form = ref({
+	height: 120,
+})
 const option = ref({
 	submitBtn: false,
 	emptyBtn: false,
@@ -70,23 +91,38 @@
 			label: '高度',
 			prop: 'height',
 			type: 'input',
+			value: 120,
+			min: 5,
+			max: 500,
+			type: 'number',
+			change ({ column, value }) {
+				drawPolygonExample?.setPolygonHeight?.(value)
+			},
 			rules: [
 				{
 					required: true,
 					message: '请输入高度',
 					trigger: 'blur'
+				},
+				{
+					pattern: /^(?:[5-9]|[1-9]\d|[1-4]\d{2}|500)$/, // 5-500
+					message: '高度需在5-500之间',
+					trigger: ['blur', 'change']
 				}
 			]
 		},
 
 		{
 			label: '类型',
-			prop: 'type',
+			prop: 'flight_type',
 			type: 'select',
-			dicUrl: `/blade-system/dict/dictionary?code=flow`,
+			dicUrl: `/drone-device-core/airspace/page?current=1&size=10000&typeName=`,
 			props: {
-				label: 'dictValue',
-				value: 'dictKey',
+				label: 'type_name',
+				value: 'id',
+			},
+			dicFormatter (res) {
+				return res.data.records
 			},
 			rules: [
 				{
@@ -98,7 +134,7 @@
 		},
 		{
 			label: '管控时段',
-			prop: 'daterange',
+			prop: 'time_period',
 			type: 'daterange',
 			format: 'YYYY-MM-DD',
 			valueFormat: 'YYYY-MM-DD',
@@ -130,11 +166,13 @@
 let publicCesiumInstance = null
 let viewer = null
 // 地图
+const mapLoading = ref(true)
+
 const isShowWaringTip = ref(false)
 
 const curDrawPolygonData = ref([])
 
-const initMap = () => {
+const initMap = async () => {
 	if (!document.getElementById('AirspaceMap')) {
 		return
 	}
@@ -145,24 +183,36 @@
 		layerMode: 4,
 		contour: true,
 		flyToContour: true,
+		terrainInit () {
+			setTimeout(async () => {
+				initDrawPolygon()
+				mapLoading.value = false
+			}, 1500)
+		}
 	})
+
 	viewer = publicCesiumInstance.getViewer()
+
 	drawPolygonExample = new DrawPolygon()
+
 	drawPolygonExample.initHandler(viewer)
 
-	drawPolygonExample?.subscribe('getShowWaringTip', data => {
+	drawPolygonExample.subscribe('getShowWaringTip', data => {
 		isShowWaringTip.value = data
 	})
 
-	drawPolygonExample?.subscribe('getPolygonPositions', data => {
+	drawPolygonExample.subscribe('getPolygonPositions', data => {
 		curDrawPolygonData.value = data.map(item => {
 			let cartographic = Cesium.Cartographic.fromCartesian(item)
+
 			let lng = Cesium.Math.toDegrees(cartographic.longitude) // 经度
 			let lat = Cesium.Math.toDegrees(cartographic.latitude) // 纬度
+			let height = cartographic.height
 
 			return {
 				lng: _.round(lng, 6),
 				lat: _.round(lat, 6),
+				height
 			}
 		})
 	})
@@ -170,11 +220,32 @@
 
 watch(dialogShow, async (show) => {
 	if (show) {
+		if (props.type === 'edit') {
+			form.value = { ...props.curEditRow }
+		}
+
+		initDrawPolygon()
+
+		if (viewer) return
+
 		await nextTick()
 		cesiumContextMenu()
 		initMap()
 	}
 })
+
+const initDrawPolygon = async () => {
+	if (props.type === 'edit' && drawPolygonExample) {
+		drawPolygonExample?.setPolygonHeight?.(props.curEditRow.height)
+
+		let polygon = JSON.parse(props.curEditRow.flight_range)
+
+		if (polygon.length > 0) {
+			const aslData = await getPointPositionsHeight(polygon, viewer)
+			drawPolygonExample?.initPolygon(viewer, aslData.map(item => ({ ...item, height: item.ASL })))
+		}
+	}
+}
 
 const handleSubmit = () => {
 	if (ElMessage.value) {
@@ -201,8 +272,38 @@
 
 				let area = turf.area(polygon)
 
-				emit('submitClick')
-				done()
+				if (props.type === 'edit') {
+					airspaceEnteringUpdate({
+						id: props.curEditRow.id,
+						name: form.value.name,
+						flight_range: JSON.stringify(curDrawPolygonData.value),
+						height: form.value.height,
+						time_period: form.value.time_period.join('~'),
+						flight_type: form.value.flight_type,
+						remark: form.value.remark,
+						area: area
+					}).then(res => {
+						ElMessage.success('更新成功')
+						dialogShow.value = false
+						emit('submitClick')
+						done()
+					})
+				} else {
+					airspaceEnteringAdd({
+						name: form.value.name,
+						flight_range: JSON.stringify(curDrawPolygonData.value),
+						height: form.value.height,
+						time_period: form.value.time_period.join('~'),
+						flight_type: form.value.flight_type,
+						remark: form.value.remark,
+						area: area
+					}).then(res => {
+						ElMessage.success('新增成功')
+						dialogShow.value = false
+						emit('submitClick')
+						done()
+					})
+				}
 			} else {
 				return false
 			}
@@ -211,15 +312,12 @@
 }
 
 const handleClose = () => {
+	drawPolygonExample?.delPolygon()
 	isShowWaringTip.value = false
 	curDrawPolygonData.value = []
-	cesiumContextMenu(false)
-	drawPolygonExample?.destroy()
-	publicCesiumInstance?.viewerDestroy()
-	publicCesiumInstance = null
-	viewer = null
-	drawPolygonExample = null
-	form.value = {}
+	form.value = {
+		height: 120
+	}
 	formEle.value.resetForm()
 	dialogShow.value = false
 }
@@ -239,6 +337,15 @@
 		cesium.removeEventListener('contextmenu', preventDefault)
 	}
 }
+
+onBeforeUnmount(() => {
+	drawPolygonExample?.destroy()
+	drawPolygonExample = null
+	cesiumContextMenu(false)
+	publicCesiumInstance?.viewerDestroy()
+	publicCesiumInstance = null
+	viewer = null
+})
 </script>
 
 <style scoped lang="scss">

--
Gitblit v1.9.3