From a2bb326dfbc6024916d360b89d021243f8502e33 Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Fri, 12 Sep 2025 14:51:46 +0800
Subject: [PATCH] feat:空域录入相关调整

---
 src/utils/mapToolTip/prompt.js                |  308 ++++++++++++++
 src/utils/drawPolygon/drawPolygon.css         |   28 +
 src/assets/images/mapEdit/newcon_box.png      |    0 
 src/views/airspace/airspaceEntering.vue       |   26 
 src/utils/drawPolygon/drawPolygon.js          |  613 +++++++++++++++++++++++++++++
 src/styles/element-ui.scss                    |    4 
 src/views/airspace/components/AirspaceMap.vue |  176 +++++++
 src/utils/mapToolTip/prompt.css               |   56 ++
 8 files changed, 1,185 insertions(+), 26 deletions(-)

diff --git a/src/assets/images/mapEdit/newcon_box.png b/src/assets/images/mapEdit/newcon_box.png
new file mode 100644
index 0000000..483bf7a
--- /dev/null
+++ b/src/assets/images/mapEdit/newcon_box.png
Binary files differ
diff --git a/src/styles/element-ui.scss b/src/styles/element-ui.scss
index eb57fd8..b5d66e3 100644
--- a/src/styles/element-ui.scss
+++ b/src/styles/element-ui.scss
@@ -160,6 +160,10 @@
     .el-textarea__inner {
       resize: none !important;
     }
+
+    .el-form-item {
+      margin-bottom: 8px !important;
+    }
   }
 }
 
diff --git a/src/utils/drawPolygon/drawPolygon.css b/src/utils/drawPolygon/drawPolygon.css
new file mode 100644
index 0000000..6714c90
--- /dev/null
+++ b/src/utils/drawPolygon/drawPolygon.css
@@ -0,0 +1,28 @@
+.planar-polygon-edit-tooltip {
+  position: absolute;
+  top: 0px;
+  left: 0px;
+}
+
+.planar-polygon-edit-tooltip .planar-polygon-edit-menu {
+  padding: 4px 0px;
+  transform: translate(24px, 10px);
+  background: url('@/assets/images/mapEdit/newcon_box.png') no-repeat center / 100% 100%;
+  border-radius: 4px;
+}
+
+.planar-polygon-edit-tooltip .planar-polygon-edit-menu div {
+  color: #fff;
+  width: 90px;
+  height: 26px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  font-weight: bold;
+  font-size: 12px;
+}
+
+.planar-polygon-edit-tooltip .planar-polygon-edit-menu div:hover {
+  background-color: rgba(0, 0, 0, 0.3);
+}
diff --git a/src/utils/drawPolygon/drawPolygon.js b/src/utils/drawPolygon/drawPolygon.js
new file mode 100644
index 0000000..2f98842
--- /dev/null
+++ b/src/utils/drawPolygon/drawPolygon.js
@@ -0,0 +1,613 @@
+import * as Cesium from 'cesium'
+import _, { cloneDeep, throttle } from 'lodash'
+import * as turf from '@turf/turf'
+import '@/utils/drawPolygon/drawPolygon.css'
+import '@/utils/mapToolTip/prompt.css'
+import Prompt from '@/utils/mapToolTip/prompt'
+
+
+/**
+ * 盒子缩放通用方法
+ * @param data 数据源 格式 [[lng, lat], [lng, lat], [lng, lat]]
+ * @param multiple 缩放倍数
+ */
+const boxTransformScale = (data, multiple = 3) => {
+	const line = turf.lineString(data)
+	const bbox = turf.bbox(line)
+	const bboxPolygon = turf.bboxPolygon(bbox)
+	const scaledPolygon = turf.transformScale(bboxPolygon, multiple)
+
+	return turf.bbox(scaledPolygon)
+}
+
+/**
+ * 多边形绘制与编辑工具类
+ * 功能:
+ *  - 绘制多边形
+ *  - 拖动编辑端点
+ *  - 删除端点、删除整个多边形
+ *  - 多边形自交检查(避免非法几何)
+ *  - 外部订阅/通知机制
+ */
+export class DrawPolygon {
+	constructor() {
+		// Cesium 视图对象
+		this.viewer = null
+		// 当前绘制的多边形
+		this.curPolygon = null
+		// 绘制模式标识
+		this.drawingMode = false
+		// 编辑模式标识
+		this.editingMode = false
+		// 是否正在拖拽端点
+		this.isDragging = false
+		// 当前拖拽的点实体
+		this.draggedEntity = null
+		// 多边形实体
+		this.polygonEntity = null
+		// 存储端点的 DataSource
+		this.editPolygonDataSource = null
+		this.editPolygonPointDataSource = null
+		// 鼠标事件处理器
+		this.handler = null
+		// 右键菜单 DOM
+		this.menuPopup = null
+		// 被右键选中的点
+		this.delPolygonPoint = null
+		// 是否显示警告提示(自交)
+		this.isShowWaringTip = false
+		// 当前拖拽点是否合法
+		this.currentDragPointIsValid = false
+		// 当前拖拽点的坐标
+		this.currentDragPointPosition = null
+
+		// 事件回调函数绑定 this
+		this.handleLeftDown = this.handleLeftDown.bind(this)
+		this.handleLeftUp = this.handleLeftUp.bind(this)
+		this.handleMouseMove = this.handleMouseMove.bind(this)
+		this.handleLeftClick = this.handleLeftClick.bind(this)
+		this.handleRightClick = this.handleRightClick.bind(this)
+
+		this.delPolygon = this.delPolygon.bind(this)
+		this.delPoint = this.delPoint.bind(this)
+		// 外部订阅者
+		this.listeners = []
+
+		// 鼠标提示窗
+		this.promptStyle = {
+			show: true,
+			offset: {
+				x: 20,
+				y: 0
+			}
+		}
+		this.prompt = undefined
+	}
+
+	// 实体命名常量
+	static ENTITY_NAMES = {
+		POLYGON: '区域-面',
+		POINT: '区域-端点',
+		POLYGON_ID: 'planar-route-polygon',
+		POINT_ID_PREFIX: 'planar-route-point'
+	};
+
+	// 颜色常量
+	static COLORS = {
+		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)')      // 错误(自交)线颜色
+	};
+
+	// ============ 发布订阅机制 ============
+
+	// 外部订阅数据变化
+	subscribe (key, listener) {
+		this.listeners.push({ key, listener })
+	}
+
+	// 通知订阅者
+	notify (key, data) {
+		this.listeners
+			.filter(subscriber => subscriber.key === key)
+			.forEach(subscriber => subscriber.listener(data))
+	}
+
+	// ============ 绘制相关 ============
+
+	// 开始绘制
+	startDrawing () {
+		this.drawingMode = true
+		this.curPolygon = new Cesium.PolygonHierarchy()
+		if (!this.prompt && this.promptStyle.show) this.prompt = new Prompt(this.viewer, this.promptStyle)
+
+		// 如果还没有 DataSource,就新建一个
+		if (!this.editPolygonDataSource) {
+			this.editPolygonDataSource = new Cesium.CustomDataSource('editPolygonDataSource')
+			this.viewer?.dataSources.add(this.editPolygonDataSource)
+		}
+
+		if (!this.editPolygonPointDataSource) {
+			this.editPolygonPointDataSource = new Cesium.CustomDataSource('editPolygonPointDataSource')
+			this.viewer?.dataSources.add(this.editPolygonPointDataSource)
+		}
+
+		// 清空之前的点
+		this.editPolygonDataSource?.entities.removeAll()
+		this.editPolygonPointDataSource?.entities.removeAll()
+	}
+
+	// 创建多边形实体(含边界线)
+	createPolygonEntity () {
+		this.polygonEntity = this.editPolygonDataSource.entities.add({
+			name: DrawPolygon.ENTITY_NAMES.POLYGON,
+			id: DrawPolygon.ENTITY_NAMES.POLYGON_ID,
+			polygon: {
+				hierarchy: new Cesium.CallbackProperty(() => this.curPolygon, false),
+				material: DrawPolygon.COLORS.DEFAULT_POLYGON,
+				outline: false,
+				outlineWidth: 2,
+				heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+			},
+			polyline: {
+				width: 2,
+				material: DrawPolygon.COLORS.DEFAULT_LINE,
+				clampToGround: true,
+				positions: new Cesium.CallbackProperty(
+					() => [...this.curPolygon.positions, this.curPolygon.positions[0]], // 闭合线
+					false
+				)
+			},
+		})
+	}
+
+	// 创建端点实体
+	createPointEntity (position, isAdd) {
+		const pointIndex = isAdd ? this.curPolygon.positions.length - 2 : this.curPolygon.positions.length - 1
+
+		this.editPolygonPointDataSource.entities.add({
+			name: DrawPolygon.ENTITY_NAMES.POINT,
+			id: `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${pointIndex}`,
+			position: position.clone(),
+			point: {
+				pixelSize: 14,
+				color: Cesium.Color.WHITE,
+				heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+				disableDepthTestDistance: Number.POSITIVE_INFINITY,
+			},
+			customData: {
+				ind: pointIndex, // 索引记录
+			},
+		})
+	}
+
+	// 添加一个点
+	addPosition (position, isAdd = true) {
+		// 第一个点要重复压入一次,形成动态绘制效果
+		if (this.curPolygon.positions.length === 0 && isAdd) {
+			this.curPolygon.positions.push(position.clone())
+		}
+
+		this.curPolygon.positions.push(position.clone())
+
+		// 如果没有实体则创建
+		if (!this.polygonEntity) {
+			this.createPolygonEntity()
+		}
+
+		// 创建端点实体
+		this.createPointEntity(position, isAdd)
+	}
+
+	// ============ 鼠标事件 ============
+
+	// 鼠标左键按下(选中端点拖动)
+	handleLeftDown (movement) {
+		if (!this.editingMode) return
+
+		const pickedEntity = this.viewer.scene.pick(movement.position)?.id
+		const isPoint = pickedEntity?.name === DrawPolygon.ENTITY_NAMES.POINT
+
+		if (pickedEntity && isPoint) {
+			this.isDragging = true
+			this.draggedEntity = pickedEntity
+			this.currentDragPointPosition = this.curPolygon.positions[this.draggedEntity?.customData.ind]
+			this.disableMapControl() // 禁止地图交互
+		}
+	}
+
+	// 鼠标左键抬起(拖拽结束)
+	handleLeftUp () {
+		if (!(this.editingMode && this.curPolygon?.positions && this.draggedEntity)) return
+
+		if (this.currentDragPointIsValid && this.isDragging) {
+			// 更新点位置
+			this.draggedEntity.position = this.currentDragPointPosition
+			this.curPolygon.positions[this.draggedEntity?.customData.ind] = this.currentDragPointPosition
+
+			// 校验多边形是否自交
+			if (!this.curDragPointIsValid(this.curPolygon.positions)) {
+				this.isShowWaringTip = true
+				this.currentDragPointIsValid = true
+				this.updatePolygonAppearance(DrawPolygon.COLORS.ERROR_POLYGON, DrawPolygon.COLORS.ERROR_LINE)
+			} else {
+				this.isShowWaringTip = false
+				this.currentDragPointIsValid = false
+				this.updatePolygonAppearance(DrawPolygon.COLORS.DEFAULT_POLYGON, DrawPolygon.COLORS.DEFAULT_LINE)
+			}
+
+			this.notify('getShowWaringTip', this.isShowWaringTip)
+		}
+
+		// 拖拽结束,恢复交互
+		if (this.isDragging) {
+			this.notify('getPolygonPositions', this.curPolygon.positions)
+			this.isDragging = false
+			this.draggedEntity = null
+			this.enableMapControl()
+		}
+	}
+
+	// 鼠标移动
+	handleMouseMove (movement) {
+		if (!this.drawingMode && !this.editingMode) {
+			if (this.curPolygon.positions.length > 3) {
+				this.prompt.update(movement.endPosition, "单击面开启编辑")
+			}
+			return
+		}
+
+		const cartesian = this.viewer.scene.pickPosition(movement.endPosition)
+		if (!cartesian) return
+
+		if (this.drawingMode) {
+			if (this.curPolygon.positions.length < 1) {
+				this.prompt.update(movement.endPosition, "单击开始绘制")
+			} else {
+				this.prompt.update(movement.endPosition, "单击增加点,双击结束绘制")
+			}
+		}
+
+		if (this.editingMode) {
+			this.prompt.update(movement.endPosition, "拖拽端点更新位置,右击进行删除")
+		}
+
+		// 编辑模式下,拖拽点实时更新
+		if (this.editingMode && this.draggedEntity?.customData) {
+			this.draggedEntity.position = cartesian
+			this.curPolygon.positions[this.draggedEntity?.customData.ind] = cartesian
+		}
+
+		// 绘制模式下,实时更新最后一个点
+		if (this.drawingMode && this.polygonEntity && this.curPolygon.positions.length >= 1) {
+			this.curPolygon.positions.pop()
+			this.curPolygon.positions.push(cartesian)
+		}
+
+		// 实时检查是否自交
+		if ((this.editingMode && this.draggedEntity?.customData) || (this.drawingMode && this.polygonEntity && this.curPolygon.positions.length >= 3)) {
+			if (!this.curDragPointIsValid(this.curPolygon.positions)) {
+				this.isShowWaringTip = true
+				this.currentDragPointIsValid = true
+				this.updatePolygonAppearance(DrawPolygon.COLORS.ERROR_POLYGON, DrawPolygon.COLORS.ERROR_LINE)
+			} else {
+				this.isShowWaringTip = false
+				this.currentDragPointIsValid = false
+				this.updatePolygonAppearance(DrawPolygon.COLORS.DEFAULT_POLYGON, DrawPolygon.COLORS.DEFAULT_LINE)
+			}
+
+			this.notify('getShowWaringTip', this.isShowWaringTip)
+		}
+	}
+
+	// 鼠标左键点击
+	handleLeftClick (click) {
+		this.removeMenuPopup()
+		this.prompt.showTip()
+
+		const pickedAllEntity = this.viewer.scene.drillPick(click.position).filter(i => i.id)
+		const isStartPoint = pickedAllEntity.find(i => i.id.name === '起飞点')
+		const isPolygonPoint = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POINT)
+		const isPolygon = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POLYGON)
+
+		if (isStartPoint) return
+
+		// 如果不是绘制模式
+		if (!this.drawingMode) {
+			if (!isPolygon) {
+				this.editingMode = false
+				this.editPolygonPointDataSource.entities.show = false
+				this.prompt.update(click.endPosition, "单击面开启编辑")
+				return
+			}
+
+			if (isPolygon) {
+				this.editingMode = true
+				this.editPolygonPointDataSource.entities.show = true
+				this.prompt.update(click.endPosition, "拖拽端点更新位置,右击进行删除")
+			}
+			return
+		}
+
+		// 点击闭合多边形
+		if (this.curPolygon.positions.length < 4 && isPolygonPoint) {
+			return
+		}
+
+		if (this.curPolygon.positions.length >= 4 && isPolygonPoint) {
+			if (!this.drawingMode) return
+
+			this.finishDrawing()
+			return
+		}
+
+		// 添加新的点
+		const cartesian = this.viewer.scene.pickPosition(click.position)
+		if (!cartesian) return
+
+		this.addPosition(cartesian)
+	}
+
+	// 鼠标右键点击(弹出菜单)
+	handleRightClick (click) {
+		const that = this
+		if (that.drawingMode) return
+
+		this.prompt.hideTip()
+		that.removeMenuPopup()
+
+		const pickedAllEntity = that.viewer.scene.drillPick(click.position).filter(i => i.id)
+		const isPolygon = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POLYGON)
+		const isEditPoint = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POINT)
+		const { position: { x, y } } = click
+
+		let pickedEntity, tooltipEvent, menuType
+
+		if (isPolygon) {
+			pickedEntity = isPolygon
+			tooltipEvent = that.delPolygon
+			menuType = 'polygon'
+		} else if (isEditPoint) {
+			pickedEntity = isEditPoint
+			tooltipEvent = that.delPoint
+			menuType = 'edit-point'
+		}
+
+		if (pickedEntity) {
+			that.delPolygonPoint = pickedEntity.id
+			that.menuPopup = that.createMenuPopup(menuType)
+			that.menuPopup.style.transform = `translate3d(${x - 10}px, ${y - 10}px, 0)`
+			that.viewer.container.appendChild(that.menuPopup)
+			that.menuPopup.addEventListener('click', tooltipEvent)
+		}
+	}
+
+	// ============ 删除相关 ============
+
+	// 删除所有实体
+	removeEntities () {
+		if (this.editPolygonDataSource) {
+			this.editPolygonDataSource.entities.removeAll()
+			this.editPolygonDataSource = null
+		}
+
+		if (this.editPolygonPointDataSource) {
+			this.editPolygonPointDataSource.entities.removeAll()
+			this.editPolygonPointDataSource = null
+		}
+
+		this.editingMode = false
+		this.polygonEntity = null
+		this.curPolygon = null
+	}
+
+	// 完成绘制
+	finishDrawing () {
+		this.curPolygon.positions.pop()
+
+		if (this.curPolygon.positions.length >= 3) {
+			this.drawingMode = false
+			this.editingMode = true
+			this.editPolygonPointDataSource.entities.show = true
+
+			if (!this.curDragPointIsValid(this.curPolygon.positions)) {
+				this.isShowWaringTip = true
+				this.currentDragPointIsValid = true
+				this.updatePolygonAppearance(DrawPolygon.COLORS.ERROR_POLYGON, DrawPolygon.COLORS.ERROR_LINE)
+			} else {
+				this.isShowWaringTip = false
+				this.currentDragPointIsValid = false
+				this.updatePolygonAppearance(DrawPolygon.COLORS.DEFAULT_POLYGON, DrawPolygon.COLORS.DEFAULT_LINE)
+			}
+
+			this.notify('getShowWaringTip', this.isShowWaringTip)
+			this.notify('getPolygonPositions', this.curPolygon.positions)
+		}
+	}
+
+	// 删除多边形
+	delPolygon () {
+		this.removeEntities()
+		this.removeMenuPopup()
+		this.notify('getPolygonPositions', [])
+		this.startDrawing()
+		this.prompt.showTip()
+	}
+
+	// 删除端点
+	delPoint () {
+		if (this.curPolygon.positions.length <= 3) {
+			this.removeMenuPopup()
+			return ElMessage.warning('端点不可少于3个')
+		}
+		if (!this.delPolygonPoint) return
+
+		this.curPolygon.positions.splice(this.delPolygonPoint.customData.ind, 1)
+		this.editPolygonPointDataSource.entities.remove(this.delPolygonPoint)
+		this.removeMenuPopup()
+
+		// 更新剩余点索引
+		this.editPolygonPointDataSource.entities.values.forEach((item, index) => {
+			item.customData.ind = index
+		})
+
+		this.notify('getPolygonPositions', this.curPolygon.positions)
+	}
+
+	// ============ 工具方法 ============
+
+	// 创建右键菜单
+	createMenuPopup (type = 'polygon') {
+		const menuPopupVBox = document.createElement('div')
+		menuPopupVBox.id = 'planarPolygonEdit'
+		menuPopupVBox.className = 'planar-polygon-edit-tooltip'
+
+		const menuPopup = document.createElement('div')
+		menuPopup.id = 'planarPolygonEditMenu'
+		menuPopup.className = 'planar-polygon-edit-menu'
+
+		const menuItems = type === 'polygon'
+			? [{ title: '删除测区', class: 'del-planar-polygon' }]
+			: [{ title: '删除端点', class: 'del-planar-point' }]
+
+		menuItems.forEach(item => {
+			const titleDiv = document.createElement('div')
+			titleDiv.innerText = item.title
+			titleDiv.className = item.class
+			menuPopup.appendChild(titleDiv)
+		})
+
+		menuPopupVBox.appendChild(menuPopup)
+		return menuPopupVBox
+	}
+
+	// 移除菜单
+	removeMenuPopup () {
+		const that = this
+		if (that.menuPopup) {
+			that.menuPopup.removeEventListener('click', that.delPolygon)
+			that.menuPopup.removeEventListener('click', that.delPoint)
+			that.viewer.container.removeChild(that.menuPopup)
+			that.menuPopup = null
+		}
+		that.delPolygonPoint = null
+	}
+
+	// 更新多边形样式(正常/错误)
+	updatePolygonAppearance (polygonColor, lineColor) {
+		this.polygonEntity.polygon.material = polygonColor
+		this.polygonEntity.polyline.material = lineColor
+	}
+
+	// 禁用地图交互
+	disableMapControl () {
+		const controller = this.viewer.scene.screenSpaceCameraController
+		controller.enableRotate = false
+		controller.enableTranslate = false
+		controller.enableZoom = false
+	}
+
+	// 启用地图交互
+	enableMapControl () {
+		const controller = this.viewer.scene.screenSpaceCameraController
+		controller.enableRotate = true
+		controller.enableTranslate = true
+		controller.enableZoom = true
+	}
+
+	// 检查多边形是否自交
+	curDragPointIsValid (positions) {
+		if (positions.length < 3) return true
+
+		const cartographics = Cesium.Ellipsoid.WGS84.cartesianArrayToCartographicArray(positions)
+		const latLngPoints = cartographics.map(cartographic => [
+			Cesium.Math.toDegrees(cartographic.longitude),
+			Cesium.Math.toDegrees(cartographic.latitude)
+		])
+
+		// 用 turf.js 检查自交
+		const poly = turf.polygon([[...latLngPoints, latLngPoints[0]]])
+		const intersections = turf.kinks(poly)
+
+		return intersections.features.length === 0
+	}
+
+	// 初始化已有多边形
+	initPolygon (viewer, positions) {
+		this.initHandler(viewer)
+		this.startDrawing()
+
+		positions.forEach(item => {
+			this.addPosition(
+				Cesium.Cartesian3.fromDegrees(Number(item.lng), Number(item.lat), Number(item.height)),
+				false
+			)
+		})
+
+		// 视角飞入区域
+		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,
+		})
+
+		this.drawingMode = false
+		this.editingMode = true
+	}
+
+	// 初始化事件处理器
+	initHandler (viewer) {
+		this.viewer = viewer
+		this.startDrawing()
+
+		if (!this.handler) {
+			this.handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
+
+			// 注册鼠标事件
+			const events = [
+				[Cesium.ScreenSpaceEventType.LEFT_DOWN, this.handleLeftDown],
+				[Cesium.ScreenSpaceEventType.LEFT_UP, this.handleLeftUp],
+				[Cesium.ScreenSpaceEventType.MOUSE_MOVE, this.handleMouseMove],
+				[Cesium.ScreenSpaceEventType.LEFT_CLICK, this.handleLeftClick],
+				[Cesium.ScreenSpaceEventType.RIGHT_CLICK, this.handleRightClick]
+			]
+
+			events.forEach(([type, handler]) => {
+				this.handler.setInputAction(handler, type)
+			})
+		}
+	}
+
+	// 移除事件处理器
+	removeHandler () {
+		if (this.handler) {
+			const eventTypes = [
+				Cesium.ScreenSpaceEventType.LEFT_DOWN,
+				Cesium.ScreenSpaceEventType.LEFT_UP,
+				Cesium.ScreenSpaceEventType.MOUSE_MOVE,
+				Cesium.ScreenSpaceEventType.LEFT_CLICK,
+				Cesium.ScreenSpaceEventType.RIGHT_CLICK
+			]
+
+			eventTypes.forEach(type => {
+				this.handler.removeInputAction(type)
+			})
+
+			this.handler = null
+		}
+	}
+
+	/**
+	 * 销毁实例,释放资源
+	 */
+	destroy () {
+		if (!this.viewer) return
+
+		this.removeMenuPopup()
+		this.removeEntities()
+		this.removeHandler()
+		this.enableMapControl()
+	}
+}
diff --git a/src/utils/mapToolTip/prompt.css b/src/utils/mapToolTip/prompt.css
new file mode 100644
index 0000000..88ac8ab
--- /dev/null
+++ b/src/utils/mapToolTip/prompt.css
@@ -0,0 +1,56 @@
+.easy3d-prompt {
+  position: absolute;
+  top: -9999px;
+  left: -9999px;
+  pointer-events: none;
+}
+
+.prompt-close {
+  position: absolute;
+  top: 0;
+  right: 0;
+  padding: 4px 4px 0 0;
+  border: none;
+  text-align: center;
+  width: 18px;
+  height: 14px;
+  font: 16px/14px Tahoma, Verdana, sans-serif;
+  color: #c3c3c3;
+  text-decoration: none;
+  font-weight: bold;
+  background: transparent;
+}
+
+.prompt-content-container {
+  /* 不给padding撑不开div */
+  max-width: 400px;
+  border-radius: 4px;
+  padding: 1px;
+  background: white;
+  color: #333;
+  box-shadow: 0 3px 14px rgb(0 0 0 / 40%);
+}
+
+.prompt-content {
+  margin: 13px 19px;
+}
+
+.prompt-anchor-container {
+  position: absolute;
+  width: 40px;
+
+  height: 20px;
+  left: 50%;
+  margin-left: -20px;
+  overflow: hidden;
+  pointer-events: none;
+}
+
+.prompt-anchor {
+  margin: -10px auto 0;
+  background: white;
+  width: 17px;
+  height: 17px;
+  -webkit-transform: rotate(45deg);
+  transform: rotate(45deg);
+}
diff --git a/src/utils/mapToolTip/prompt.js b/src/utils/mapToolTip/prompt.js
new file mode 100644
index 0000000..f43c327
--- /dev/null
+++ b/src/utils/mapToolTip/prompt.js
@@ -0,0 +1,308 @@
+/**
+ * 气泡窗类
+ * @class
+ * 
+*/
+import * as Cesium from 'cesium'
+import "./prompt.css"
+class Prompt {
+    /**
+     * @param {Cesium.Viewer} viewer 地图viewer对象 
+     * @param {Object} opt 
+     * @param {Cesium.Cartesian3 | Array} [opt.position] 弹窗坐标 (type=2时生效)
+     * @param {Boolean} opt.show 是否显示 
+     * @param {Function} [opt.success] 创建成功的回调函数
+     * @param {Number} [opt.type=1] 1~位置变化提示框 / 2~固定坐标提示框
+     * @param {Cesium.Cartesian3 | Array} opt.position 固定坐标提示框的坐标( cartesian3 / [101,30] ),type为1时,可不设置此参数
+     * @param {Boolean} [opt.anchor=true] 是否显示锚点
+     * @param {Boolean} [opt.closeBtn=true] 是否显示关闭按钮
+     * @param {String} opt.className 自定义class
+     * @param {String} opt.content 弹窗内容
+     * @param {Function} [opt.close] 关闭弹窗时的回调函数
+     * @param {Object} [opt.offset] 偏移参数
+     * @param {Number} [opt.offset.x] 横坐标偏移像素单位
+     * @param {Number} [opt.offset.y] 纵坐标偏移像素单位
+     * @param {Object} [opt.style] 弹窗面板样式
+     * @param {String} [opt.style.background='white'] 背景色
+     * @param {String} [opt.style.boxShadow] 弹窗阴影(css属性)
+     * @param {String} [opt.style.color] 弹窗颜色
+     * 
+     */
+    constructor(viewer, opt) {
+        this.viewer = viewer
+        if (!this.viewer) return
+        this.type = "prompt"
+        // 默认值
+        opt = opt || {}
+        const promptType = opt.type == undefined ? 1 : opt.type
+        let defaultOpt = {
+            id: (new Date().getTime() + "" + Math.floor(Math.random() * 10000)),
+            type: promptType,
+            anchor: promptType == 2 ? true : false,
+            closeBtn: promptType == 2 ? true : false,
+            offset: promptType == 2 ? { x: 0, y: -20 } : { x: 10, y: 10 },
+            content: "",
+            show: true,
+            style: {
+                background: "rgba(0,0,0,0.5)",
+                color: "white"
+            }
+        }
+
+        this.opt = Object.assign(defaultOpt, opt)
+
+        /**
+         * @property {Object} attr 相关属性
+         */
+        this.attr = this.opt
+        // ====================== 创建弹窗内容 start ======================
+        const mapid = this.viewer.container.id
+
+        /**
+         * @property {Boolearn} isShow 当前显示状态
+         */
+        this.isShow = this.opt.show == undefined ? true : this.opt.show // 是否显示
+        let anchorHtml = ``
+        let closeHtml = ``
+        const background = this.opt.style.background
+        const color = this.opt.style.color
+        if (this.opt.anchor) {
+            anchorHtml += `
+            <div class="prompt-anchor-container">
+                <div class="prompt-anchor" style="background:${background} !important;">
+                </div>
+            </div>
+            `
+        }
+        if (this.opt.closeBtn) { // 移动提示框 不显示关闭按钮
+            closeHtml = `<a class="prompt-close" attr="${this.opt.id}" id="prompt-close-${this.opt.id}">x</a>`
+        }
+        let boxShadow = this.opt.style.boxShadow
+        const promptId = "prompt-" + this.opt.id
+        const promptConenet = `
+                <!-- 文本内容 -->
+                <div class="prompt-content-container" style="background:${background} !important;color:${color} !important;box-shadow:${boxShadow} !important">
+                    <div class="prompt-content" id="prompt-content-${this.opt.id}">
+                        ${this.opt.content}
+                    </div>
+                </div>
+                <!-- 锚 -->
+                ${anchorHtml}
+                <!-- 关闭按钮 -->
+                ${closeHtml}
+        `
+        // 构建弹窗元素 
+        this.promptDiv = window.document.createElement("div")
+        this.promptDiv.className = `easy3d-prompt ${this.opt.className}`
+        this.promptDiv.id = promptId
+        this.promptDiv.innerHTML = promptConenet
+        let mapDom = window.document.getElementById(mapid)
+        mapDom.appendChild(this.promptDiv)
+        const clsBtn = window.document.getElementById(`prompt-close-${this.opt.id}`)
+        let that = this
+        if (clsBtn) {
+            clsBtn.addEventListener("click", (e) => {
+                that.hide()
+                if (that.opt.close) that.opt.close()
+            })
+        }
+
+        /**
+         * @property {Object} promptDom 弹窗div
+         */
+        this.promptDom = window.document.getElementById(promptId)
+
+        this.position = this.transPosition(this.opt.position)
+        // ====================== 创建弹窗内容 end ======================
+
+        if (promptType == 2) this.bindRender() // 固定位置弹窗 绑定实时渲染 当到地球背面时 隐藏
+        if (this.opt.show == false) this.hide()
+        this.containerW = this.viewer.container.offsetWidth
+        this.containerH = this.viewer.container.offsetHeight
+        this.containerLeft = this.viewer.container.offsetLeft
+        this.containerTop = this.viewer.container.offsetTop
+
+        /**
+        * @property {Number} contentW 弹窗宽度
+        */
+        this.contentW = Math.ceil(Number(this.promptDom.offsetWidth)) // 宽度
+
+        /**
+         * @property {Number} contentH 弹窗高度
+         */
+        this.contentH = this.promptDom.offsetHeight // 高度
+
+        if (this.opt.success) this.opt.success()
+    }
+
+    /**
+     * 销毁
+     */
+    destroy () {
+        if (this.promptDiv) {
+            window.document.getElementById(this.viewer.container.id).removeChild(this.promptDiv)
+            this.promptDiv = null
+        }
+        if (this.rendHandler) {
+            this.rendHandler()
+            this.rendHandler = null
+        }
+    }
+    // 实时监听
+    bindRender () {
+        let that = this
+        this.rendHandler = this.viewer.scene.postRender.addEventListener(function () {
+            if (!that.isShow && that.promptDom) {
+                that.promptDom.style.display = "none"
+                return
+            }
+            if (!that.position) return
+            if (that.position instanceof Cesium.Cartesian3) {
+                let px = Cesium.SceneTransforms.wgs84ToWindowCoordinates(that.viewer.scene, that.position)
+                if (!px) return
+                const occluder = new Cesium.EllipsoidalOccluder(that.viewer.scene.globe.ellipsoid, that.viewer.scene.camera.position)
+                // 当前点位是否可见 是否在地球背面
+                const res = occluder.isPointVisible(that.position)
+                if (res) {
+                    if (that.promptDom) that.promptDom.style.display = "block"
+                } else {
+                    if (that.promptDom) that.promptDom.style.display = "none"
+                }
+                that.setByPX({
+                    x: px.x,
+                    y: px.y
+                })
+            } else {
+                that.setByPX({
+                    x: that.position.x,
+                    y: that.position.y
+                })
+            }
+
+        }, this)
+    }
+
+    /**
+     * 
+     * @param {Cesium.Cartesian3 | Object} px 弹窗坐标
+     * @param {String} html 弹窗内容
+     */
+    update (px, html) {
+        if (px instanceof Cesium.Cartesian3) {
+            this.position = px.clone()
+
+            px = Cesium.SceneTransforms.wgs84ToWindowCoordinates(this.viewer.scene, px)
+        }
+        this.contentW = Math.ceil(Number(this.promptDom.offsetWidth)) // 宽度
+        this.contentH = this.promptDom.offsetHeight // 高度
+        if (px) this.setByPX(px)
+        if (html) this.setContent(html)
+    }
+
+    // 判断是否在当前视野内
+    isInView () {
+        if (!this.position) return false
+        let px = null
+        if (this.position instanceof Cesium.Cartesian2) {
+            px = this.position
+        } else {
+            px = Cesium.SceneTransforms.wgs84ToWindowCoordinates(this.viewer.scene, this.position)
+        }
+        const occluder = new Cesium.EllipsoidalOccluder(this.viewer.scene.globe.ellipsoid, this.viewer.scene.camera.position)
+        // 是否在地球背面
+        const res = occluder.isPointVisible(this.position)
+        let isin = false
+        if (!px) return isin
+        if (
+            px.x > this.containerLeft &&
+            px.x < (this.containerLeft + this.containerW) &&
+            px.y > this.containerTop &&
+            px.y < (this.containerTop + this.containerH)
+        ) {
+            isin = true
+        }
+        return res && isin
+    }
+
+    /**
+     * 是否可见
+     * @param {Boolean} isShow true可见,false不可见
+     */
+    setVisible (isShow) {
+        let isin = this.isInView(this.position)
+        if (isin && isShow) {
+            this.isShow = true
+            if (this.promptDom) this.promptDom.style.display = "block"
+        } else {
+            this.isShow = false
+            if (this.promptDom) this.promptDom.style.display = "none"
+        }
+    }
+
+    showTip () {
+        if (this.promptDom) this.promptDom.style.display = "block"
+    }
+
+    hideTip () {
+        if (this.promptDom) this.promptDom.style.display = "none"
+    }
+
+    /**
+     * 显示
+     */
+    show () {
+        this.setVisible(true)
+    }
+
+    /**
+     * 隐藏
+     */
+    hide () {
+        this.setVisible(false)
+    }
+
+    /**
+     * 设置弹窗内容
+     * @param {String} content 内容 
+     */
+    setContent (content) {
+        let pc = window.document.getElementById(`prompt-content-${this.opt.id}`)
+        pc.innerHTML = content
+    }
+
+    /**
+     * 设置弹窗坐标
+     * @param {Object} opt 屏幕坐标
+     */
+    setByPX (opt) {
+        if (!opt) return
+        if (this.promptDom) {
+            const contentW = this.promptDom.offsetWidth // 宽度
+            const contentH = this.promptDom.offsetHeight // 高度
+
+            if (this.opt.type == 1) {
+                this.promptDom.style.left = ((Number(opt.x) + Number(this.opt.offset.x || 0))) + "px"
+                this.promptDom.style.top = ((Number(opt.y) + Number(this.opt.offset.y || 0))) + "px"
+            } else {
+                this.promptDom.style.left = ((Number(opt.x) + Number(this.opt.offset.x || 0)) - Number(this.contentW) / 2) + "px"
+                this.promptDom.style.top = ((Number(opt.y) + Number(this.opt.offset.y || 0)) - Number(this.contentH)) + "px"
+            }
+        }
+    }
+
+    // 坐标转换
+    transPosition (p) {
+        let position
+        if (Array.isArray(p)) {
+            const posi = Cesium.Cartesian3.fromDegrees(p[0], p[1], p[2] || 0)
+            position = posi.clone()
+        } else if (p instanceof Cesium.Cartesian3) {
+            position = p.clone()
+        } else { // 像素类型
+            position = p
+        }
+        return position
+    }
+}
+
+export default Prompt
\ No newline at end of file
diff --git a/src/views/airspace/airspaceEntering.vue b/src/views/airspace/airspaceEntering.vue
index e8bacb2..927c2f9 100644
--- a/src/views/airspace/airspaceEntering.vue
+++ b/src/views/airspace/airspaceEntering.vue
@@ -17,7 +17,7 @@
     </avue-crud>
   </basic-container>
 
-  <AirspaceMap v-model:show="airspaceMapShow" />
+  <AirspaceMap v-model:show="airspaceMapShow" @submitClick="searchReset" />
 </template>
 
 <script setup>
@@ -43,7 +43,7 @@
 const creatorOption = ref([])
 const loading = ref(true)
 const page = ref({
-  pageSize: 20,
+  pageSize: 10,
   currentPage: 1,
   total: 0,
   lotValue: '',
@@ -55,7 +55,7 @@
   addBtn: false,
   tip: false,
   searchShow: true,
-  searchMenuSpan: 12,
+  searchMenuSpan: 8,
   searchMenuPosition: 'right',
   border: true,
   index: true,
@@ -85,6 +85,12 @@
     {
       label: '类型',
       prop: 'patches_type',
+      type: 'select',
+      dicUrl: `/blade-system/dict/dictionary?code=flow`,
+      props: {
+        label: 'dictValue',
+        value: 'dictKey',
+      },
       search: true,
       searchSpan: 4,
       searchLabelWidth: 54,
@@ -102,10 +108,15 @@
     },
     {
       label: '管控时段',
-      prop: 'patches_type',
+      prop: 'daterange',
+      type: 'daterange',
+      format: 'YYYY-MM-DD',
+      valueFormat: 'YYYY-MM-DD',
+      startPlaceholder: '开始日期',
+      endPlaceholder: '结束日期',
       search: true,
-      searchSpan: 4,
-      rules: [{ required: true, message: '请输入管控时间', trigger: 'blur' }],
+      searchSpan: 8,
+      searchRange: true,
     },
     {
       label: '创建人',
@@ -150,7 +161,6 @@
     })
   }
 )
-
 // ---------------- computed ----------------
 const permission = computed(() => store.getters.permission)
 const permissionList = computed(() => ({
@@ -220,7 +230,7 @@
   page.value.userName = ''
   page.value.lotValue = ''
   page.value.currentPage = 1
-  page.value.pageSize = 20
+  page.value.pageSize = 10
   onLoad(page.value)
 }
 
diff --git a/src/views/airspace/components/AirspaceMap.vue b/src/views/airspace/components/AirspaceMap.vue
index cd2d1aa..89707c6 100644
--- a/src/views/airspace/components/AirspaceMap.vue
+++ b/src/views/airspace/components/AirspaceMap.vue
@@ -5,16 +5,35 @@
 			<div class="form-container">
 				<avue-form ref="formEle" :option="option" v-model="form"></avue-form>
 			</div>
+
 			<div class="map-container">
+				<div class="tool-tip warning" v-show="isShowWaringTip">
+					<span class="icon">
+						<el-icon>
+							<WarningFilled />
+						</el-icon>
+					</span>
+					<span>空域不支持交叉面,无法生成空域</span>
+				</div>
 				<div id="AirspaceMap" class="ztzf-cesium"></div>
+			</div>
+
+			<div class="btn-container">
+				<el-button icon="el-icon-check" type="primary" @click="handleSubmit">确定</el-button>
+				<el-button icon="el-icon-close" @click="dialogShow = false">取消</el-button>
 			</div>
 		</div>
 	</el-dialog>
 </template>
 
 <script setup>
+import _, { cloneDeep, throttle } from 'lodash'
+
 import * as Cesium from 'cesium'
+import * as turf from '@turf/turf'
+import { DrawPolygon } from '@/utils/drawPolygon/drawPolygon'
 import { PublicCesium } from '@/utils/cesium/publicCesium'
+import { ElMessageBox, ElMessage } from 'element-plus'
 
 const props = defineProps({
 	title: {
@@ -23,6 +42,8 @@
 	},
 })
 
+const emit = defineEmits(['submitClick'])
+
 const dialogShow = defineModel('show')
 
 const formEle = ref(null)
@@ -30,10 +51,11 @@
 const option = ref({
 	submitBtn: false,
 	emptyBtn: false,
+	menuBtn: false,
 	column: [
 		{
 			label: '名称',
-			prop: 'input',
+			prop: 'name',
 			type: 'input',
 			rules: [
 				{
@@ -46,7 +68,7 @@
 
 		{
 			label: '高度',
-			prop: 'input',
+			prop: 'height',
 			type: 'input',
 			rules: [
 				{
@@ -59,18 +81,13 @@
 
 		{
 			label: '类型',
-			prop: 'select',
+			prop: 'type',
 			type: 'select',
-			dicData: [
-				{
-					label: '字典1',
-					value: 0,
-				},
-				{
-					label: '字典2',
-					value: 1,
-				},
-			],
+			dicUrl: `/blade-system/dict/dictionary?code=flow`,
+			props: {
+				label: 'dictValue',
+				value: 'dictKey',
+			},
 			rules: [
 				{
 					required: true,
@@ -97,21 +114,26 @@
 		},
 
 		{
-			label: '备注',
-			prop: 'input',
+			label: '管控原因',
+			prop: 'remark',
 			type: 'textarea',
 			span: 24,
 			rows: 1,
 			minRows: 1,
 			maxRows: 1,
-			placeholder: '请输入备注',
+			placeholder: '请输入管控原因',
 		},
 	],
 })
 
+let drawPolygonExample = null
 let publicCesiumInstance = null
 let viewer = null
 // 地图
+const isShowWaringTip = ref(false)
+
+const curDrawPolygonData = ref([])
+
 const initMap = () => {
 	if (!document.getElementById('AirspaceMap')) {
 		return
@@ -121,37 +143,155 @@
 		flatMode: false,
 		terrain: true,
 		layerMode: 4,
-		contour: false,
+		contour: true,
+		flyToContour: true,
 	})
 	viewer = publicCesiumInstance.getViewer()
+	drawPolygonExample = new DrawPolygon()
+	drawPolygonExample.initHandler(viewer)
+
+	drawPolygonExample?.subscribe('getShowWaringTip', data => {
+		isShowWaringTip.value = 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) // 纬度
+
+			return {
+				lng: _.round(lng, 6),
+				lat: _.round(lat, 6),
+			}
+		})
+	})
 }
 
 watch(dialogShow, async (show) => {
 	if (show) {
 		await nextTick()
+		cesiumContextMenu()
 		initMap()
 	}
 })
 
+const handleSubmit = () => {
+	if (ElMessage.value) {
+		ElMessage.warning('空域不支持交叉,请重新绘制或调整')
+		return
+	}
+
+	if (curDrawPolygonData.value.length === 0) {
+		ElMessage.warning('请绘制空域区域')
+		return
+	}
+
+	if (formEle.value) {
+		formEle.value.validate((valid, done, msg) => {
+			if (valid) {
+				let disposePolygon = curDrawPolygonData.value.map(i => [i.lng, i.lat])
+
+				let polygon = turf.polygon([
+					[
+						...disposePolygon,
+						disposePolygon[0]
+					]
+				])
+
+				let area = turf.area(polygon)
+
+				emit('submitClick')
+				done()
+			} else {
+				return false
+			}
+		})
+	}
+}
+
 const handleClose = () => {
+	isShowWaringTip.value = false
+	curDrawPolygonData.value = []
+	cesiumContextMenu(false)
+	drawPolygonExample?.destroy()
 	publicCesiumInstance?.viewerDestroy()
 	publicCesiumInstance = null
 	viewer = null
+	drawPolygonExample = null
 	form.value = {}
 	formEle.value.resetForm()
 	dialogShow.value = false
+}
+
+// 阻止右键默认行为
+const preventDefault = event => {
+	event.preventDefault()
+	return
+}
+
+const cesiumContextMenu = (isAdd = true) => {
+	let cesium = document.getElementById('AirspaceMap')
+	if (!cesium) return
+	if (isAdd) {
+		cesium.addEventListener('contextmenu', preventDefault)
+	} else {
+		cesium.removeEventListener('contextmenu', preventDefault)
+	}
 }
 </script>
 
 <style scoped lang="scss">
 .custom-container {
 	.map-container {
-		height: 440px;
+		height: 620px;
 
 		#AirspaceMap {
+			position: relative;
 			width: 100%;
 			height: 100%;
+			overflow: hidden;
 		}
 	}
+
+	.btn-container {
+		margin-top: 16px;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+}
+
+.tool-tip {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	position: absolute;
+	width: 800px;
+	height: 64px;
+	font-family: Source Han Sans CN, Source Han Sans CN;
+	font-size: 18px;
+	color: #ffffff;
+
+	background: rgba(11, 31, 58, 0.7);
+	-moz-user-select: none;
+	-webkit-user-select: none;
+	-ms-user-select: none;
+	user-select: none;
+	z-index: 9;
+}
+
+.warning {
+	top: 234px;
+	left: 50%;
+	transform: translateX(-50%);
+	color: #fff;
+	background: rgba(140, 0, 0, 0.4);
+
+	.icon {
+		display: flex;
+		align-items: center;
+		color: #ff1f1f;
+	}
 }
 </style>

--
Gitblit v1.9.3