无人机管理后台前端(已迁走)
shuishen
2025-09-12 ea6e49ccb4d3bdb510ccf87844490823a96ebf08
feat:空域录入完善,增删改查
5 files modified
375 ■■■■ changed files
src/api/airspace/airspace.js 49 ●●●● patch | view | raw | blame | history
src/utils/cesium/publicCesium.js 2 ●●●●● patch | view | raw | blame | history
src/utils/drawPolygon/drawPolygon.js 69 ●●●● patch | view | raw | blame | history
src/views/airspace/airspaceEntering.vue 110 ●●●● patch | view | raw | blame | history
src/views/airspace/components/AirspaceMap.vue 145 ●●●● patch | view | raw | blame | history
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',
  });
};
  })
}
// ----------空域录入相关接口调用----------
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
  })
}
// ----------空域录入相关接口调用----------
src/utils/cesium/publicCesium.js
@@ -149,6 +149,8 @@
                    requestWaterMask: true, // 启用水体遮罩效果
                }).then(terrainProvider => {
                    this.viewer.terrainProvider = terrainProvider
                    options?.terrainInit?.()
                })
            } catch (error) {
                console.error('地形加载失败:', error)
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()
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()
  })
}
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">