无人机管理后台前端(已迁走)
张含笑
2025-10-27 31c6f5d0ff294bf4f1a6cc34ab880d70fcbf2beb
feat:demo
2 files added
695 ■■■■■ changed files
src/views/layerManagement/components/utils.js 624 ●●●●● patch | view | raw | blame | history
src/views/layerManagement/index.vue 71 ●●●●● patch | view | raw | blame | history
src/views/layerManagement/components/utils.js
New file
@@ -0,0 +1,624 @@
import * as Cesium from 'cesium'
import _, { cloneDeep, throttle } from 'lodash'
import * as turf from '@turf/turf'
import { boxTransformScale } from '@/utils/turfFunc'
/**
 * 多边形绘制与编辑工具类
 * 功能:
 *  - 绘制多边形
 *  - 拖动编辑端点
 *  - 删除端点、删除整个多边形
 *  - 多边形自交检查(避免非法几何)
 *  - 外部订阅/通知机制
 */
export class DrawPolygon {
    constructor() {
        // 图斑预览模式标记
        this.isPureSpotPreview = false
        // 是否删除测区
        this.isDeleteTheArea = true
        //是否可以编辑图斑
        this.isPreviewMode = true
        // 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 = []
    }
    // 实体命名常量
    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))
    }
    // ============ 绘制相关 ============
    // 编辑图斑
    editThePatch(data) {
        this.isPreviewMode = data
    }
    // 删除测区
    deleteTheArea(data) {
        this.isDeleteTheArea = data
    }
    // 开始绘制
    startDrawing() {
        this.drawingMode = true
        this.curPolygon = new Cesium.PolygonHierarchy()
        // 如果还没有 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, // 索引记录
            },
        })
    }
    // 清除不在范围内的点
    removeLastInvalidPoint() {
        const posLen = this.curPolygon.positions.length
        if (posLen === 0) return
        let targetPointId = ''
        let removeCount = 0
        if (posLen === 2) {
            removeCount = 2
            targetPointId = `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}0`
        } else if (posLen > 2) {
            removeCount = 1
            const pointIndex = posLen - 2
            targetPointId = `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${pointIndex}`
        }
        this.curPolygon.positions.splice(posLen - removeCount, removeCount)
        const invalidPoint = this.editPolygonPointDataSource.entities.getById(targetPointId)
        if (invalidPoint) {
            this.editPolygonPointDataSource.entities.remove(invalidPoint)
        }
        if (this.curPolygon.positions.length === 0 && this.polygonEntity) {
            this.editPolygonDataSource.entities.remove(this.polygonEntity)
            this.polygonEntity = null
        }
    }
    // 添加一个点
    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()
        }
        // 创建端点实体
        if (this.isPreviewMode) {
            this.createPointEntity(position, isAdd)
        }
        this.notify('getPoints', position)
    }
    // ============ 鼠标事件 ============
    // 鼠标左键按下(选中端点拖动)
    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) return
        const cartesian = this.viewer.scene.pickPosition(movement.endPosition)
        if (!cartesian) return
        // 编辑模式下,拖拽点实时更新
        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()
        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
                return
            }
            if (isPolygon) {
                this.editingMode = true
                this.editPolygonPointDataSource.entities.show = true
            }
            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
        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 && this.isDeleteTheArea) {
            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()
    }
    // 删除图斑
    delSpot() {
        this.removeEntities()
        this.isPreviewMode = true
        this.startDrawing()
    }
    // 删除端点
    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)
        })
        this.isPreviewMode = true
        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, isPurePreview = false) {
        this.initHandler(viewer)
        this.isPureSpotPreview = isPurePreview
        this.startDrawing()
        let newPosition = positions.map(item => {
            return Cesium.Cartesian3.fromDegrees(Number(item.lng), Number(item.lat), Number(item.height))
        })
        // 预览航线的时候调用
        if (!this.isPureSpotPreview) {
            this.notify('getPolygonPositions', newPosition)
        }
        newPosition.forEach(item => {
            this.addPosition(item, 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()
    }
}
src/views/layerManagement/index.vue
New file
@@ -0,0 +1,71 @@
<template>
  <basic-container>
    <div class="layerContainer">
      <!-- 地图 -->
      <div id="layMap" class="ztzf-cesium"></div>
    </div>
  </basic-container>
</template>
<script setup>
import { DrawPolygon } from '@/views/layerManagement/components/utils'
import * as Cesium from 'cesium';
import { PublicCesium } from '@/utils/cesium/publicCesium';
import _, { cloneDeep, throttle } from 'lodash'
let publicCesiumInstance = null;
let viewer = null;
const viewInstance = shallowRef(null);
const homeViewer = shallowRef(null);
// 地图
const initMap = () => {
  publicCesiumInstance = new PublicCesium({
    dom: 'layMap',
    flatMode: false,
    terrain: true,
    layerMode: 4,
    contour: false,
  });
  viewer = publicCesiumInstance.getViewer();
  viewInstance.value = publicCesiumInstance;
};
const drawPolygonExample = new DrawPolygon()
const loadPlanarRoute =async(positions = null, save = false)=>{
}
const throttleLoadPlanarRoute = throttle(loadPlanarRoute, 200)
drawPolygonExample.subscribe('getShowWaringTip', data => {
})
drawPolygonExample.subscribe('getPolygonPositions', data => {
    throttleLoadPlanarRoute(data)
})
// 销毁
const destroyMap = () => {
  if (viewer) {
    viewer.destroy();
    viewer = null;
  }
  publicCesiumInstance = null;
};
onMounted(() => {
  initMap();
});
onBeforeUnmount(() => {
  destroyMap();
});
</script>
<style scoped lang="scss">
.layerContainer {
     width: 100%;
    height: 100vh;
  #layMap {
    width: 100%;
   height: 100%;
  }
}
</style>