吉安感知网项目-前端
chenyao
2026-01-29 f083281ab75f61390a04daa2484f330f8df4178f
Merge remote-tracking branch 'origin/master'
6 files modified
14 files added
3248 ■■■■■ changed files
applications/drone-command/env/.env.development 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/FormDiaLog copy.vue 1065 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/DrawManager.js 34 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/EditManager.js 34 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/ToolBase.js 24 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/Tooltip.js 58 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawBufferCircleTool.js 236 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawEllipseTool.js 162 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawPolygonTool.js 244 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawRectangleTool.js 150 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditBufferCircleTool.js 314 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditEllipseTool.js 233 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditPolygonTool.js 218 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditRectangleTool.js 198 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/shapeUtils.js 89 ●●●●● patch | view | raw | blame | history
applications/task-work-order/env/.env.development 4 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/appInnovation/index.vue 9 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/FormDiaLog.vue 168 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/evaluate/index.vue 2 ●●●●● patch | view | raw | blame | history
applications/drone-command/env/.env.development
@@ -2,7 +2,7 @@
 # @Author       : yuan
 # @Date         : 2026-01-07 14:58:30
 # @LastEditors  : yuan
 # @LastEditTime : 2026-01-26 17:29:11
 # @LastEditTime : 2026-01-28 09:29:05
 # @FilePath     : \applications\drone-command\env\.env.development
 # @Description  : 
 # Copyright 2026 OBKoro1, All Rights Reserved. 
@@ -16,7 +16,7 @@
#开发环境代理地址(推荐本地新建文件 .env.development.local 来进行覆盖)
# VITE_APP_URL = https://wrj.shuixiongit.com/api
VITE_APP_URL= http://192.168.1.204
VITE_APP_URL= http://192.168.1.227
#新大屏地址
VITE_APP_DASHBOARD_URL = 'https://wrj.shuixiongit.com/command-center-dashboard/'
applications/drone-command/src/views/areaManage/partition/FormDiaLog copy.vue
New file
@@ -0,0 +1,1065 @@
<template>
    <el-dialog class="command-page-map-view-dialog" v-model="visible" :show-close="false"
        :close-on-click-modal="false">
        <div class="dialog-container">
            <div class="left-container">
                <CommonCesiumMap
                    ref="mapRef"
                    class="leftMap command-cesium"
                    :active="visible"
                    :flat-mode="false"
                    :terrain="true"
                    :layer-mode="4"
                    :boundary="false"
                />
                <div
                    v-if="showTypePanel && !readonly"
                    class="shape-type-panel"
                >
                    <div class="panel-header">
                        <span>{{ drawTypeLabelMap[currentShapeType] || '区域' }}</span>
                        <el-icon class="panel-close" @click.stop="handleTypePanelClose"><Close /></el-icon>
                    </div>
                    <div class="panel-body">
                        <span class="panel-label">区域类型</span>
                        <el-select
                            class="command-select"
                            popper-class="command-select-popper"
                            v-model="formData.areaType"
                            placeholder="请选择"
                            @change="handleAreaTypeChange"
                        >
                            <el-option
                                v-for="item in dictObj.areaType"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="item.dictKey"
                            />
                        </el-select>
                    </div>
                </div>
                <div class="map-toolbar">
                    <button class="tool-btn" type="button" @click="handleDraw('polygon')">
                        <span class="tool-icon polygon"></span>
                        <span class="tool-text">多边形</span>
                    </button>
                    <button class="tool-btn" type="button" @click="handleDraw('rectangle')">
                        <span class="tool-icon rect"></span>
                        <span class="tool-text">矩形</span>
                    </button>
                    <button class="tool-btn" type="button" @click="handleDraw('ellipse')">
                        <span class="tool-icon ellipse"></span>
                        <span class="tool-text">椭圆</span>
                    </button>
                    <button class="tool-btn" type="button" @click="handleDraw('buffer')">
                        <span class="tool-icon buffer"></span>
                        <span class="tool-text">缓冲圆</span>
                    </button>
                    <button class="tool-btn danger" type="button" @click="handleClearShape">
                        <span class="tool-icon trash"></span>
                        <span class="tool-text">删除</span>
                    </button>
                </div>
            </div>
            <div class="right-container">
                <div class="header">
                    <span>{{ titleEnum[dialogMode] }}</span>
                    <el-icon class="close-btn" @click.stop="visible = false"><Close /></el-icon>
                </div>
                <div class="content" v-if="readonly">
                    <div class="detail-title">区域详情</div>
                    <el-row>
                        <el-col :span="24">
                            <div class="label">区域名称</div>
                            <div class="val">{{ formData.areaName }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">区域位置</div>
                            <div class="val">{{ formatLocation(formData) }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">区域面积</div>
                            <div class="val">{{ formData.areaSize || '-' }}k㎡</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">区域类型</div>
                            <div class="val">{{ getDictLabel(formData.areaType, dictObj.areaType) }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">触发条件</div>
                            <div class="val">
                                {{ getDictLabel(formData.triggerCondition, dictObj.triggeringCondition) }}
                            </div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">响应机制</div>
                            <div class="val">{{ formData.responseMechanism }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">管控级别</div>
                            <div class="val">{{ getDictLabel(formData.controlLevel, dictObj.controlLevel) }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">关联派出所</div>
                            <div class="val">{{ formData.policeStationName }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">可飞行时间段</div>
                            <div class="val">{{ formatFlyDate(formData) }}</div>
                        </el-col>
                    </el-row>
                    <div class="detail-title">关联设备</div>
                    <div class="command-table-container">
                        <div class="command-table-content">
                            <el-table class="command-table" ref="deviceTableRef" :data="deviceOptions"
                                row-key="id">
                                <el-table-column prop="deviceName" label="设备名称" />
                                <el-table-column prop="deviceType" label="类型">
                                    <template v-slot="{ row }">
                                        {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                                    </template>
                                </el-table-column>
                            </el-table>
                        </div>
                    </div>
                </div>
                <el-form class="dialog-form" v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly"
                    label-width="96px">
                    <el-form-item label="区域名称" prop="areaName">
                        <el-input class="command-input" v-model="formData.areaName" maxlength="50"
                            placeholder="请输入" clearable />
                    </el-form-item>
                    <el-form-item label="区域位置" prop="longitude">
                        <div class="val">{{ formatLocation(formData) }}</div>
                    </el-form-item>
                    <el-form-item label="区域面积" prop="areaSize">
                        <div class="val">{{ formData.areaSize || '-' }}k㎡</div>
                    </el-form-item>
                    <el-form-item label="区域类型" prop="areaType">
                        <el-select class="command-select" popper-class="command-select-popper"
                            v-model="formData.areaType" placeholder="请选择" clearable>
                            <el-option v-for="item in dictObj.areaType" :key="item.dictKey" :label="item.dictValue"
                                :value="item.dictKey" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="响应机制" prop="responseMechanism">
                        <el-input class="command-input" v-model="formData.responseMechanism"
                            maxlength="200" placeholder="请输入" clearable />
                    </el-form-item>
                    <el-form-item label="触发条件" prop="triggerCondition">
                        <el-select class="command-select" popper-class="command-select-popper"
                            v-model="formData.triggerCondition" placeholder="请选择" clearable>
                            <el-option v-for="item in dictObj.triggeringCondition" :key="item.dictKey"
                                :label="item.dictValue" :value="item.dictKey" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="管控级别" prop="controlLevel">
                        <el-select class="command-select" popper-class="command-select-popper"
                            v-model="formData.controlLevel" placeholder="请选择" clearable>
                            <el-option v-for="item in dictObj.controlLevel" :key="item.dictKey" :label="item.dictValue"
                                :value="item.dictKey" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="可飞行时段" prop="flyDateStart">
                        <el-date-picker class="command-date-picker"
                            popper-class="command-date-picker-popper" v-model="flyDateRange"
                            type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间"
                            :disabled-date="disabledFlyDate" :disabled-time="disabledFlyTime"
                            value-format="YYYY-MM-DD HH:mm:ss" clearable />
                    </el-form-item>
                    <el-form-item label="关联派出所" prop="policeStationId">
                        <el-select class="command-select" popper-class="command-select-popper"
                            v-model="formData.policeStationId" placeholder="请选择" clearable>
                            <el-option v-for="item in policeStationOptions" :key="item.id" :label="item.stationName"
                                :value="item.id" />
                        </el-select>
                    </el-form-item>
                    <div class="shape-table-container">
                        <div class="shape-table-title">绘制区域列表</div>
                        <el-table class="command-table" :data="shapeList" row-key="id" height="360">
                            <el-table-column prop="areaType" label="类型" width="120">
                                <template v-slot="{ row }">
                                    {{ getDictLabel(row.areaType, dictObj.areaType) }}
                                </template>
                            </el-table-column>
                            <el-table-column prop="drawType" label="绘制类型" width="120">
                                <template v-slot="{ row }">
                                    {{ drawTypeLabelMap[row.drawType] || '-' }}
                                </template>
                            </el-table-column>
                            <el-table-column label="操作" class-name="operation-btns">
                                <template v-slot="{ row }">
                                    <el-link @click="handleShapeEdit(row)">编辑</el-link>
                                    <el-link @click="handleShapeDelete(row)">删除</el-link>
                                </template>
                            </el-table-column>
                        </el-table>
                    </div>
                    <div class="search-table-container">
                        <div class="search-box">
                            <div class="label">
                                关联设备
                            </div>
                            <el-input class="command-input" v-model="searchName" placeholder="请输入"
                                clearable></el-input>
                        </div>
                        <el-form-item prop="deviceIds" label-width="0">
                            <el-table class="command-table" ref="deviceTableRef"
                                :data="deviceOptions.filter(item => item.deviceName.includes(searchName))" row-key="id"
                                @selection-change="handleDeviceSelectionChange">
                                <el-table-column type="selection" width="55" :reserve-selection="true" />
                                <el-table-column prop="deviceName" label="设备名称" />
                                <el-table-column prop="deviceType" label="类型">
                                    <template v-slot="{ row }">
                                        {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                                    </template>
                                </el-table-column>
                            </el-table>
                        </el-form-item>
                    </div>
                </el-form>
                <div class="footer">
                    <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button>
                    <el-button color="#284FE3" v-if="!readonly" type="primary" :loading="submitting"
                        :disabled="submitting" @click="handleSubmit">
                        确定
                    </el-button>
                </div>
            </div>
        </div>
    </el-dialog>
</template>
<script setup>
import { Close } from '@element-plus/icons-vue'
import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { fwAreaDivideDetailApi, fwAreaDivideSubmitApi } from './partitionApi'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
import { fieldRules, geomAnalysis, getDictLabel } from '@ztzf/utils'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { DrawManager } from './shapeTools/DrawManager'
import { EditManager } from './shapeTools/EditManager'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
import { buildEllipsePositions } from './shapeTools/shapeUtils'
import * as turf from '@turf/turf'
import * as Cesium from 'cesium'
import { fwPoliceStationListApi } from '@/views/areaManage/precinctInfo/precinctInfoApi'
import { fwDeviceListApi } from '@/views/basicManage/deviceStock/fwDevice'
const initForm = () => ({
    areaName: '', // 区域名称
    areaSize: null, // 区域面积
    areaType: '', // 区域类型
    controlLevel: '', // 管控级别
    deviceIds: '', // 关联设备ID
    flyDateEnd: '', // 可飞行结束
    flyDateStart: '', // 可飞行开始
    latitude: null, // 区域中心纬度
    longitude: null, // 区域中心经度
    policeStationId: '', // 关联派出所id
    responseMechanism: '', // 响应机制
    triggerCondition: '', // 触发条件
})
const emit = defineEmits(['success'])
const formRef = ref(null) // 表单实例
const formData = ref(initForm()) // 表单数据
const visible = defineModel() // 弹框显隐
const dialogMode = ref('add') // 弹框模式
const submitting = ref(false) // 提交中
const readonly = computed(() => dialogMode.value === 'view')
const route = useRoute()
const titleEnum = ref({ edit: '编辑区域', view: '查看区域', add: '新增区域' })
const flyDateRange = ref([]) // 飞行时间
const deviceTableRef = ref(null)
const searchName = ref('')
const selectedDeviceRows = ref([])
const policeStationOptions = ref([]) // 关联派出所
const deviceOptions = ref([]) // 关联设备
const dictObj = inject('dictObj')
const mapRef = ref(null)
const currentShapeType = ref('polygon')
const shapeList = ref([])
const activeShapeId = ref(null)
const drawTypeLabelMap = {
    polygon: '多边形',
    rectangle: '矩形',
    ellipse: '椭圆',
    buffer: '缓冲圆',
}
const showTypePanel = ref(false)
const suppressTypePanelOnce = ref(false)
const areaTypeStyleMap = {
    '1': {
        fill: Cesium.Color.fromBytes(25, 178, 230, 128),
        outline: Cesium.Color.fromBytes(0, 251, 255, 255),
    },
    '2': {
        fill: Cesium.Color.fromBytes(255, 235, 59, 128),
        outline: Cesium.Color.fromBytes(255, 235, 59, 255),
    },
    '3': {
        fill: Cesium.Color.fromBytes(247, 20, 20, 128),
        outline: Cesium.Color.fromBytes(255, 0, 0, 255),
    },
}
let viewer
let drawManager
let editManager
let activeTool
let pointList = []
let viewEntity
let shapeDisplaySource
const activeToolMode = ref('')
const rules = {
    areaName: fieldRules(true, 50),
    longitude: fieldRules(true),
    areaType: fieldRules(true),
    responseMechanism: fieldRules(true, 200),
    triggerCondition: fieldRules(true, 200),
    controlLevel: fieldRules(true),
    flyDateStart: fieldRules(true),
    policeStationId: fieldRules(true),
    deviceIds: fieldRules(false),
}
function isSameDay (left, right) {
    return (
        left.getFullYear() === right.getFullYear() &&
        left.getMonth() === right.getMonth() &&
        left.getDate() === right.getDate()
    )
}
const disabledFlyDate = time => {
    const startOfToday = new Date()
    startOfToday.setHours(0, 0, 0, 0)
    return time.getTime() < startOfToday.getTime()
}
const disabledFlyTime = date => {
    if (!date) return {}
    const now = new Date()
    if (!isSameDay(date, now)) return {}
    const currentHour = now.getHours()
    const currentMinute = now.getMinutes()
    const currentSecond = now.getSeconds()
    return {
        disabledHours: () => Array.from({ length: currentHour }, (_, index) => index),
        disabledMinutes: hour =>
            hour === currentHour ? Array.from({ length: currentMinute }, (_, index) => index) : [],
        disabledSeconds: (hour, minute) =>
            hour === currentHour && minute === currentMinute
                ? Array.from({ length: currentSecond }, (_, index) => index)
                : [],
    }
}
// 关闭弹框
function handleCancel () {
    visible.value = false
}
// 提交新增/编辑
async function handleSubmit () {
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) return
    if (dialogMode.value === 'add' && Number(formData.value.areaSize) < 0.5) {
        ElMessage.warning('区域面积不能小于0.5km²,请重新绘制')
        return
    }
    submitting.value = true
    try {
        let str = [...pointList, pointList[0]].map(item => `${item.longitude} ${item.latitude}`).join(',')
        formData.value.geom = `POLYGON((${str}))`
        await fwAreaDivideSubmitApi(formData.value)
        const actionText = dialogMode.value === 'add' ? '新增' : '编辑'
        saveOperationLog({
            requestUri: route.path,
            title: `${route.name || '区域划分'}-${actionText}`,
            type: 1,
        })
        ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功')
        visible.value = false
        emit('success')
    } finally {
        submitting.value = false
    }
}
watch(
    () => flyDateRange.value,
    () => {
        formData.value.flyDateStart = flyDateRange.value[0] || ''
        formData.value.flyDateEnd = flyDateRange.value[1] || ''
    }
)
watch(
    () => visible.value,
    val => {
        if (!val) {
            viewEntity && viewer?.entities?.remove(viewEntity)
            viewEntity = null
            clearActiveTool()
            hideTypePanel()
            clearShapeDisplay()
        }
    }
)
watch(
    () => formData.value.areaType,
    val => {
        updateActiveShapeAreaType(val)
    }
)
// 加载详情
async function loadDetail () {
    if (!formData.value.id) return
    const res = await fwAreaDivideDetailApi({ id: formData.value.id })
    formData.value = res?.data?.data ?? initForm()
    flyDateRange.value = [formData.value.flyDateStart, formData.value.flyDateEnd].filter(Boolean)
}
// 格式化区域位置
function formatLocation (row) {
    if (row?.longitude == null || row?.latitude == null) return ''
    return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
}
// 格式化可飞行时间段
function formatFlyDate (row) {
    if (!row?.flyDateStart && !row?.flyDateEnd) return ''
    return `${row.flyDateStart || '-'} ~ ${row.flyDateEnd || '-'}`
}
// 初始化地图实例
function initMap () {
    if (viewer) return
    const map = mapRef.value?.getMap()
    viewer = map?.viewer || null
}
function ensureShapeDisplaySource () {
    if (!viewer) return null
    if (!shapeDisplaySource) {
        shapeDisplaySource = new Cesium.CustomDataSource('partition-shape-display')
        viewer.dataSources.add(shapeDisplaySource)
    }
    return shapeDisplaySource
}
function clearShapeDisplay () {
    if (!shapeDisplaySource) return
    shapeDisplaySource.entities.removeAll()
}
function normalizeShapePoint (point) {
    const lng = point?.lng ?? point?.longitude
    const lat = point?.lat ?? point?.latitude
    if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
    return {
        lng: Number(lng),
        lat: Number(lat),
        height: Number.isFinite(Number(point?.height)) ? Number(point.height) : 0,
    }
}
function buildShapePositions (points = []) {
    const normalized = points.map(normalizeShapePoint).filter(Boolean)
    return normalized.map(point =>
        Cesium.Cartesian3.fromDegrees(point.lng, point.lat, point.height)
    )
}
function getAreaTypeStyle (areaType) {
    return areaTypeStyleMap?.[String(areaType)] || {
        fill: Cesium.Color.fromBytes(45, 140, 240, 99),
        outline: Cesium.Color.fromBytes(45, 140, 240, 255),
    }
}
function renderShapeList () {
    if (!viewer || !visible.value || readonly.value) {
        clearShapeDisplay()
        return
    }
    const dataSource = ensureShapeDisplaySource()
    if (!dataSource) return
    dataSource.entities.removeAll()
    shapeList.value.forEach(shape => {
        if (!shape?.points?.length) return
        if (activeToolMode.value === 'edit' && shape.id === activeShapeId.value) return
        if (shape.drawType === 'buffer' && shape.meta?.bufferRadii?.length && shape.meta?.center) {
            const center = shape.meta.center
            const centerCartesian = Cesium.Cartesian3.fromDegrees(center.lng, center.lat, center.height || 0)
            const [radius1, radius2, radius3] = shape.meta.bufferRadii
            const radii = [radius1, radius2, radius3].filter(item => Number.isFinite(item) && item > 0)
            if (radii.length) {
                const styles = [
                    {
                        fill: Cesium.Color.fromBytes(247, 20, 20, 128),
                        outline: Cesium.Color.fromBytes(255, 0, 0, 255),
                    },
                    {
                        fill: Cesium.Color.fromBytes(255, 235, 59, 128),
                        outline: Cesium.Color.fromBytes(255, 235, 59, 255),
                    },
                    {
                        fill: Cesium.Color.fromBytes(25, 178, 230, 128),
                        outline: Cesium.Color.fromBytes(0, 251, 255, 255),
                    },
                ]
                radii.forEach((radius, index) => {
                    const style = styles[index] || styles[styles.length - 1]
                    dataSource.entities.add({
                        name: 'shape-display',
                        customData: { shapeId: shape.id, drawType: shape.drawType, level: index + 1 },
                        position: centerCartesian,
                        ellipse: {
                            semiMajorAxis: radius,
                            semiMinorAxis: radius,
                            material: style.fill,
                            outline: false,
                            heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                        },
                    })
                    const positions = buildEllipsePositions(centerCartesian, radius, radius)
                    dataSource.entities.add({
                        name: 'shape-display',
                        customData: { shapeId: shape.id, drawType: shape.drawType, level: index + 1, outline: true },
                        polyline: {
                            positions: positions.length ? [...positions, positions[0]] : positions,
                            clampToGround: true,
                            width: 2,
                            material: style.outline,
                        },
                    })
                })
            }
            return
        }
        const positions = buildShapePositions(shape.points)
        if (positions.length < 3) return
        const style = getAreaTypeStyle(shape.areaType)
        dataSource.entities.add({
            name: 'shape-display',
            customData: { shapeId: shape.id, drawType: shape.drawType },
            polygon: {
                hierarchy: new Cesium.PolygonHierarchy(positions),
                material: style.fill,
                outline: false,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
            polyline: {
                positions: positions.length ? [...positions, positions[0]] : positions,
                clampToGround: true,
                width: 2,
                material: style.outline,
            },
        })
    })
}
// 绘制完成回调
const drawFinished = async data => {
    const positions = Array.isArray(data?.positions) ? data.positions : data
    if (!Array.isArray(positions) || positions.length < 3) {
        pointList = []
        formData.value.areaSize = null
        formData.value.longitude = null
        formData.value.latitude = null
        hideTypePanel()
        return
    }
    const meta = data?.meta
    const bufferRadii = Array.isArray(meta?.bufferRadii) ? meta.bufferRadii : null
    const centerCartesian = meta?.center
    const centerLngLat = centerCartesian ? cartesian3Convert(centerCartesian, viewer) : null
    pointList = _.cloneDeep(positions).map(item => {
        const val = cartesian3Convert(item, viewer)
        return { ...val, lng: val.longitude, lat: val.latitude }
    })
    applyPolygonMetrics(pointList)
    if (!formData.value.areaType) {
        const detectAreaType = getDetectAreaTypeKey()
        if (detectAreaType) {
            formData.value.areaType = detectAreaType
        }
    }
    if (!suppressTypePanelOnce.value) {
        showTypePanelAtCurrent()
    }
    suppressTypePanelOnce.value = false
    if (!activeShapeId.value) {
        activeShapeId.value = `shape_${Date.now()}_${Math.random().toString(16).slice(2, 6)}`
    }
    const targetIndex = shapeList.value.findIndex(item => item.id === activeShapeId.value)
    const shapePayload = {
        id: activeShapeId.value,
        areaType: formData.value.areaType,
        drawType: currentShapeType.value,
        points: _.cloneDeep(pointList),
        meta: bufferRadii && centerLngLat
            ? {
                bufferRadii,
                center: {
                    lng: centerLngLat.longitude,
                    lat: centerLngLat.latitude,
                    height: centerLngLat.height ?? 0,
                },
            }
            : null,
    }
    if (targetIndex === -1) {
        shapeList.value.push(shapePayload)
    } else {
        shapeList.value.splice(targetIndex, 1, shapePayload)
    }
    renderShapeList()
}
function applyPolygonMetrics(points) {
    if (!points?.length) return
    const polygon = turf.polygon([
        [...points.map(item => [item.longitude, item.latitude]), [points[0].longitude, points[0].latitude]],
    ])
    const center = turf.centerOfMass(polygon)
    const areaSqm = turf.area(polygon) // 平方米
    formData.value.areaSize = _.round(areaSqm / 1_000_000, 2) // 平方千米
    formData.value.longitude = center.geometry.coordinates[0]
    formData.value.latitude = center.geometry.coordinates[1]
}
function getDetectAreaTypeKey () {
    const areaTypeOptions = Array.isArray(dictObj?.value?.areaType)
        ? dictObj.value.areaType
        : dictObj?.areaType
    if (!Array.isArray(areaTypeOptions)) return ''
    const target = areaTypeOptions.find(item => item?.dictKey === '1')
    return target?.dictKey ?? ''
}
function showTypePanelAtCurrent () {
    if (readonly.value) return
    showTypePanel.value = true
}
function hideTypePanel () {
    showTypePanel.value = false
}
function handleTypePanelClose () {
    hideTypePanel()
    if (readonly.value) return
    if (activeToolMode.value !== 'edit') return
    const positions = activeTool?.getPositions?.()
    if (Array.isArray(positions) && positions.length >= 3) {
        suppressTypePanelOnce.value = true
        drawFinished(positions)
    }
    clearActiveTool()
}
function handleAreaTypeChange (value) {
    updateActiveShapeAreaType(value)
}
function updateActiveShapeAreaType (value) {
    if (currentShapeType.value !== 'buffer') {
        activeTool?.setStyle?.(getAreaTypeStyle(value))
    }
    if (!activeShapeId.value) return
    const targetIndex = shapeList.value.findIndex(item => item.id === activeShapeId.value)
    if (targetIndex === -1) return
    const target = shapeList.value[targetIndex]
    shapeList.value.splice(targetIndex, 1, {
        ...target,
        areaType: value,
    })
    renderShapeList()
}
// Tool lifecycle
function clearActiveTool () {
    activeTool?.destroy?.()
    activeTool = null
    drawManager?.destroy()
    editManager?.destroy()
    activeToolMode.value = ''
    renderShapeList()
}
function startDraw (type) {
    currentShapeType.value = type
    activeToolMode.value = 'draw'
    drawManager ||= new DrawManager(viewer)
    activeTool = drawManager.start(type, { style: getAreaTypeStyle(formData.value.areaType) })
    activeTool?.subscribe('getPolygonPositions', drawFinished)
}
function startEdit (type, points) {
    currentShapeType.value = type
    activeToolMode.value = 'edit'
    editManager ||= new EditManager(viewer)
    activeTool = editManager.start(type, points, { style: getAreaTypeStyle(formData.value.areaType) })
    activeTool?.subscribe('getPolygonPositions', drawFinished)
}
function handleDraw (type) {
    if (readonly.value) return
    if (!viewer) return
    clearActiveTool()
    viewEntity && viewer?.entities?.remove(viewEntity)
    viewEntity = null
    hideTypePanel()
    renderShapeList()
    if (dialogMode.value === 'edit' && pointList.length >= 3) {
        const editPoints = _.cloneDeep(pointList)
        if (editPoints.length > 1) {
            const first = editPoints[0]
            const last = editPoints[editPoints.length - 1]
            if (first.longitude === last.longitude && first.latitude === last.latitude) {
                editPoints.pop()
            }
        }
        activeShapeId.value = activeShapeId.value || (shapeList.value[0]?.id ?? null)
        startEdit(type, editPoints)
        return
    }
    const defaultAreaType = getDetectAreaTypeKey()
    if (defaultAreaType) {
        formData.value.areaType = defaultAreaType
    }
    activeShapeId.value = null
    startDraw(type)
}
function handleClearShape () {
    if (readonly.value) return
    clearActiveTool()
    viewEntity && viewer?.entities?.remove(viewEntity)
    viewEntity = null
    pointList = []
    shapeList.value = []
    activeShapeId.value = null
    formData.value.areaSize = null
    formData.value.longitude = null
    formData.value.latitude = null
    hideTypePanel()
    clearShapeDisplay()
}
function handleShapeEdit (row) {
    if (!row?.points?.length) return
    if (activeToolMode.value === 'edit' && activeShapeId.value === row.id) return
    activeShapeId.value = row.id
    currentShapeType.value = row.drawType
    formData.value.areaType = row.areaType
    pointList = _.cloneDeep(row.points)
    clearActiveTool()
    const editPayload = row.drawType === 'buffer' ? { points: row.points, meta: row.meta } : row.points
    startEdit(row.drawType, editPayload)
    applyPolygonMetrics(pointList)
    showTypePanelAtCurrent()
    renderShapeList()
}
function handleShapeDelete (row) {
    if (!row) return
    shapeList.value = shapeList.value.filter(item => item.id !== row.id)
    renderShapeList()
    if (activeShapeId.value === row.id) {
        activeShapeId.value = null
        clearActiveTool()
        pointList = []
        formData.value.areaSize = null
        formData.value.longitude = null
        formData.value.latitude = null
        hideTypePanel()
    }
}
// 查看面
function viewPolygon () {
    if (!formData.value?.geom) return
    pointList = geomAnalysis(formData.value.geom)
    const result = pointList.map(item => [item.longitude, item.latitude]).flat()
    viewEntity = viewer.entities?.add({
        customType: 'control_group',
        position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
        polyline: {
            positions: Cesium.Cartesian3.fromDegreesArray(result),
            clampToGround: true,
            width: 3,
            material: Cesium.Color.RED,
        },
    })
    viewer.flyTo(viewEntity, { duration: 0 })
}
// 获取派出所列表
async function getPoliceStationList () {
    const res = await fwPoliceStationListApi()
    policeStationOptions.value = res?.data?.data ?? []
}
// 获取设备列表
async function getDeviceList () {
    if (deviceOptions.value.length) return
    const res = await fwDeviceListApi({ isAreaSelect: 1, areaId: formData.value.id, isTrack: 1 })
    deviceOptions.value = res?.data?.data ?? []
    if (dialogMode.value === 'view') {
        deviceOptions.value = deviceOptions.value.filter(item => formData.value.deviceIds.includes(item.id))
    }
}
// 关联设备变更
function handleDeviceSelectionChange (rows) {
    selectedDeviceRows.value = rows
    const ids = rows.map(item => item.id)
    formData.value.deviceIds = ids.join(',')
}
// 同步选择状态
function syncDeviceSelection () {
    if (!deviceTableRef.value) return
    deviceTableRef.value.clearSelection()
    const rows = []
    const arr = formData.value.deviceIds.split(',')
    deviceOptions.value.forEach(row => {
        if (arr.includes(row.id)) {
            deviceTableRef.value.toggleRowSelection(row, true)
            rows.push(row)
        }
    })
    selectedDeviceRows.value = rows
}
// 打开弹框
async function open ({ mode, row } = {}) {
    dialogMode.value = mode || 'add'
    formData.value = dialogMode.value === 'add' ? initForm() : row
    selectedDeviceRows.value = []
    shapeList.value = []
    activeShapeId.value = null
    await nextTick()
    initMap()
    clearShapeDisplay()
    viewEntity && viewer?.entities?.remove(viewEntity)
    viewEntity = null
    clearActiveTool()
    await getDeviceList()
    if (dialogMode.value === 'add') {
        // default no draw mode
    } else if (dialogMode.value === 'edit') {
        await loadDetail()
        pointList = geomAnalysis(formData.value.geom) || []
        if (pointList.length) {
            activeShapeId.value = `shape_${Date.now()}_${Math.random().toString(16).slice(2, 6)}`
            shapeList.value = [
                {
                    id: activeShapeId.value,
                    areaType: formData.value.areaType,
                    drawType: currentShapeType.value,
                    points: _.cloneDeep(pointList),
                },
            ]
        }
        viewPolygon()
    } else {
        await loadDetail()
        viewPolygon()
    }
    await nextTick()
    syncDeviceSelection()
}
onMounted(() => {
    getPoliceStationList()
    getDeviceList()
})
onBeforeUnmount(() => {
    hideTypePanel()
})
defineExpose({
    open,
})
</script>
<style scoped lang="scss">
.left-container {
    position: relative;
}
.shape-type-panel {
    position: absolute;
    right: 16px;
    bottom: 16px;
    z-index: 6;
    min-width: 260px;
    background: rgba(22, 33, 44, 0.9);
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 8px;
    box-shadow: 0 10px 24px rgba(0, 0, 0, 0.35);
    backdrop-filter: blur(6px);
    color: #e5e7f0;
}
.panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 12px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.12);
    font-size: 13px;
    font-weight: 600;
}
.panel-close {
    cursor: pointer;
    color: rgba(255, 255, 255, 0.7);
}
.panel-body {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 12px 12px;
}
.panel-label {
    font-size: 12px;
    color: rgba(255, 255, 255, 0.75);
    white-space: nowrap;
}
.map-toolbar {
    position: absolute;
    top: 64px;
    right: 16px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 10px 8px;
    background: rgba(19, 28, 36, 0.75);
    backdrop-filter: blur(4px);
    border-radius: 8px;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.2);
}
.tool-btn {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    width: 64px;
    padding: 8px 6px;
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: 6px;
    color: #ffffff;
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
    transition: all 0.2s ease;
}
.tool-btn:hover {
    background: rgba(255, 255, 255, 0.16);
    border-color: rgba(255, 255, 255, 0.35);
    transform: translateY(-1px);
}
.tool-btn:active {
    transform: translateY(0);
}
.tool-btn.danger {
    border-color: rgba(255, 112, 112, 0.6);
    color: #ffb3b3;
}
.tool-icon {
    position: relative;
    width: 20px;
    height: 20px;
    border: 2px solid #ffffff;
    border-radius: 3px;
}
.tool-icon.polygon {
    border-radius: 4px;
    transform: rotate(15deg);
}
.tool-icon.rect {
    border-radius: 2px;
}
.tool-icon.ellipse {
    border-radius: 50%;
}
.tool-icon.buffer {
    border-radius: 50%;
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5);
}
.tool-icon.trash {
    border: none;
    box-shadow: none;
}
.tool-icon.trash::before,
.tool-icon.trash::after {
    content: "";
    position: absolute;
    left: 4px;
    right: 4px;
    background: #ffffff;
}
.tool-icon.trash::before {
    top: 4px;
    height: 10px;
    border-radius: 2px;
}
.tool-icon.trash::after {
    top: 1px;
    height: 3px;
    border-radius: 2px;
}
.shape-table-container {
    margin-top: 8px;
    padding: 8px;
    border-radius: 8px;
    background: rgba(27, 34, 56, 0.6);
}
.shape-table-title {
    margin-bottom: 8px;
    color: #e5e7f0;
    font-size: 13px;
}
</style>
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
@@ -1,4 +1,4 @@
<template>
<template>
    <el-dialog class="command-page-map-view-dialog" v-model="visible" :show-close="false"
        :close-on-click-modal="false">
        <div class="dialog-container">
applications/drone-command/src/views/areaManage/partition/shapeTools/DrawManager.js
New file
@@ -0,0 +1,34 @@
import { DrawPolygonTool } from './draw/DrawPolygonTool'
import { DrawRectangleTool } from './draw/DrawRectangleTool'
import { DrawEllipseTool } from './draw/DrawEllipseTool'
import { DrawBufferCircleTool } from './draw/DrawBufferCircleTool'
const DRAW_TOOL_MAP = {
    polygon: DrawPolygonTool,
    rectangle: DrawRectangleTool,
    ellipse: DrawEllipseTool,
    buffer: DrawBufferCircleTool,
}
export class DrawManager {
    constructor(viewer) {
        this.viewer = viewer
        this.activeTool = null
    }
    start(type, options = {}) {
        this.destroy()
        const Tool = DRAW_TOOL_MAP[type]
        if (!Tool) return null
        this.activeTool = new Tool(this.viewer, options)
        this.activeTool.start()
        return this.activeTool
    }
    destroy() {
        if (this.activeTool) {
            this.activeTool.destroy()
            this.activeTool = null
        }
    }
}
applications/drone-command/src/views/areaManage/partition/shapeTools/EditManager.js
New file
@@ -0,0 +1,34 @@
import { EditPolygonTool } from './edit/EditPolygonTool'
import { EditRectangleTool } from './edit/EditRectangleTool'
import { EditEllipseTool } from './edit/EditEllipseTool'
import { EditBufferCircleTool } from './edit/EditBufferCircleTool'
const EDIT_TOOL_MAP = {
    polygon: EditPolygonTool,
    rectangle: EditRectangleTool,
    ellipse: EditEllipseTool,
    buffer: EditBufferCircleTool,
}
export class EditManager {
    constructor(viewer) {
        this.viewer = viewer
        this.activeTool = null
    }
    start(type, data, options = {}) {
        this.destroy()
        const Tool = EDIT_TOOL_MAP[type]
        if (!Tool) return null
        this.activeTool = new Tool(this.viewer, options)
        this.activeTool.start(data)
        return this.activeTool
    }
    destroy() {
        if (this.activeTool) {
            this.activeTool.destroy()
            this.activeTool = null
        }
    }
}
applications/drone-command/src/views/areaManage/partition/shapeTools/ToolBase.js
New file
@@ -0,0 +1,24 @@
export class ToolBase {
    constructor(viewer) {
        this.viewer = viewer
        this.handler = null
        this.listeners = []
    }
    subscribe(key, listener) {
        this.listeners.push({ key, listener })
    }
    notify(key, data) {
        this.listeners.filter(item => item.key === key).forEach(item => item.listener(data))
    }
    removeHandler(types) {
        if (!this.handler) return
        if (types?.length) {
            types.forEach(type => this.handler.removeInputAction(type))
        }
        this.handler.destroy()
        this.handler = null
    }
}
applications/drone-command/src/views/areaManage/partition/shapeTools/Tooltip.js
New file
@@ -0,0 +1,58 @@
export class MapTooltip {
    constructor(viewer, options = {}) {
        this.viewer = viewer
        this.options = options
        this.container = viewer?.container || null
        this.tooltipEl = null
        this.isVisible = false
        this.init()
    }
    init() {
        if (!this.container || this.tooltipEl) return
        const el = document.createElement('div')
        el.style.position = 'absolute'
        el.style.pointerEvents = 'none'
        el.style.padding = '6px 8px'
        el.style.background = 'rgba(0, 0, 0, 0.65)'
        el.style.color = '#fff'
        el.style.fontSize = '12px'
        el.style.borderRadius = '4px'
        el.style.whiteSpace = 'nowrap'
        el.style.zIndex = '1000'
        el.style.display = 'none'
        this.container.appendChild(el)
        this.tooltipEl = el
    }
    show(text, position) {
        if (!this.tooltipEl) return
        this.tooltipEl.textContent = text || ''
        this.tooltipEl.style.display = 'block'
        this.isVisible = true
        if (position) this.move(position)
    }
    move(position) {
        if (!this.tooltipEl || !position) return
        const offset = this.options.offset || { x: 12, y: 12 }
        this.tooltipEl.style.left = `${position.x + offset.x}px`
        this.tooltipEl.style.top = `${position.y + offset.y}px`
    }
    hide() {
        if (!this.tooltipEl) return
        this.tooltipEl.style.display = 'none'
        this.isVisible = false
    }
    destroy() {
        this.hide()
        if (this.tooltipEl && this.container) {
            this.container.removeChild(this.tooltipEl)
        }
        this.tooltipEl = null
        this.container = null
        this.viewer = null
    }
}
applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawBufferCircleTool.js
New file
@@ -0,0 +1,236 @@
import * as Cesium from 'cesium'
import { MapTooltip } from '../Tooltip'
import { ToolBase } from '../ToolBase'
import { buildEllipsePositions } from '../shapeUtils'
export class DrawBufferCircleTool extends ToolBase {
    constructor(viewer) {
        super(viewer)
        this.tooltip = new MapTooltip(viewer)
        this.dataSource = null
        this.centerCartesian = null
        this.radiusLevel1 = 0
        this.radiusLevel2 = 0
        this.radiusLevel3 = 0
        this.circleLevel1Entity = null
        this.circleLevel2Entity = null
        this.circleLevel3Entity = null
        this.circleLevel1OutlineEntity = null
        this.circleLevel2OutlineEntity = null
        this.circleLevel3OutlineEntity = null
        this.isDrawing = false
        this.drawStep = 0
        this.isCompleted = false
    }
    start() {
        this.dataSource = new Cesium.CustomDataSource('draw-buffer-circle')
        this.viewer.dataSources.add(this.dataSource)
        this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
        this.handler.setInputAction(click => this.handleLeftClick(click), Cesium.ScreenSpaceEventType.LEFT_CLICK)
        this.handler.setInputAction(movement => this.handleMouseMove(movement), Cesium.ScreenSpaceEventType.MOUSE_MOVE)
        this.drawStep = 0
        this.isCompleted = false
        this.tooltip.show('单击设置中心点', { x: 0, y: 0 })
    }
    handleLeftClick(click) {
        if (this.isCompleted) return
        const position = this.getPositionFromScreen(click.position)
        if (!position) return
        if (this.drawStep === 0) {
            this.centerCartesian = position
            this.radiusLevel1 = 1
            this.radiusLevel2 = 1
            this.radiusLevel3 = 1
            this.isDrawing = true
            this.drawStep = 1
            this.createEntities()
            this.tooltip.show('单击设置一级缓冲圆', click.position)
            return
        }
        if (this.drawStep === 1) {
            const radius = Math.max(1, this.getSurfaceDistance(this.centerCartesian, position))
            this.radiusLevel1 = radius
            this.radiusLevel2 = radius
            this.radiusLevel3 = radius
            this.drawStep = 2
            this.tooltip.show('单击设置二级缓冲圆', click.position)
            return
        }
        if (this.drawStep === 2) {
            const radius = Math.max(1, this.getSurfaceDistance(this.centerCartesian, position))
            this.radiusLevel2 = Math.max(radius, this.radiusLevel1)
            this.radiusLevel3 = this.radiusLevel2
            this.drawStep = 3
            this.tooltip.show('单击设置三级缓冲圆', click.position)
            return
        }
        if (this.drawStep === 3) {
            const radius = Math.max(1, this.getSurfaceDistance(this.centerCartesian, position))
            this.radiusLevel3 = Math.max(radius, this.radiusLevel2)
            this.isDrawing = false
            this.drawStep = 0
            this.isCompleted = true
            const positions = buildEllipsePositions(this.centerCartesian, this.radiusLevel3, this.radiusLevel3)
            this.notify('getPolygonPositions', {
                positions,
                meta: {
                    center: this.centerCartesian,
                    bufferRadii: [this.radiusLevel1, this.radiusLevel2, this.radiusLevel3],
                },
            })
            this.tooltip.hide()
            this.clearPreviewEntities()
        }
    }
    handleMouseMove(movement) {
        if (this.isCompleted) return
        this.tooltip.move(movement.endPosition)
        if (!this.isDrawing) return
        const position = this.getPositionFromScreen(movement.endPosition)
        if (!position) return
        if (this.drawStep === 1) {
            const radius = Math.max(1, this.getSurfaceDistance(this.centerCartesian, position))
            this.radiusLevel1 = radius
            this.radiusLevel2 = radius
            this.radiusLevel3 = radius
            return
        }
        if (this.drawStep === 2) {
            const radius = Math.max(1, this.getSurfaceDistance(this.centerCartesian, position))
            this.radiusLevel2 = Math.max(radius, this.radiusLevel1)
            this.radiusLevel3 = this.radiusLevel2
            return
        }
        if (this.drawStep === 3) {
            const radius = Math.max(1, this.getSurfaceDistance(this.centerCartesian, position))
            this.radiusLevel3 = Math.max(radius, this.radiusLevel2)
        }
    }
    createEntities() {
        if (this.circleLevel1Entity || this.circleLevel2Entity || this.circleLevel3Entity) return
        this.circleLevel1Entity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.centerCartesian, false),
            ellipse: {
                semiMajorAxis: new Cesium.CallbackProperty(() => this.radiusLevel1, false),
                semiMinorAxis: new Cesium.CallbackProperty(() => this.radiusLevel1, false),
                material: Cesium.Color.fromBytes(247, 20, 20, 128),
                outline: false,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        })
        this.circleLevel1OutlineEntity = this.dataSource.entities.add({
            polyline: {
                positions: new Cesium.CallbackProperty(() => {
                    if (!this.centerCartesian || this.radiusLevel1 <= 0) return []
                    const positions = buildEllipsePositions(this.centerCartesian, this.radiusLevel1, this.radiusLevel1)
                    return positions.length ? [...positions, positions[0]] : positions
                }, false),
                clampToGround: true,
                width: 2,
                material: Cesium.Color.fromBytes(255, 0, 0, 255),
            },
        })
        this.circleLevel2Entity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.centerCartesian, false),
            ellipse: {
                semiMajorAxis: new Cesium.CallbackProperty(() => this.radiusLevel2, false),
                semiMinorAxis: new Cesium.CallbackProperty(() => this.radiusLevel2, false),
                material: Cesium.Color.fromBytes(255, 235, 59, 128),
                outline: false,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        })
        this.circleLevel2OutlineEntity = this.dataSource.entities.add({
            polyline: {
                positions: new Cesium.CallbackProperty(() => {
                    if (!this.centerCartesian || this.radiusLevel2 <= 0) return []
                    const positions = buildEllipsePositions(this.centerCartesian, this.radiusLevel2, this.radiusLevel2)
                    return positions.length ? [...positions, positions[0]] : positions
                }, false),
                clampToGround: true,
                width: 2,
                material: Cesium.Color.fromBytes(255, 235, 59, 255),
            },
        })
        this.circleLevel3Entity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.centerCartesian, false),
            ellipse: {
                semiMajorAxis: new Cesium.CallbackProperty(() => this.radiusLevel3, false),
                semiMinorAxis: new Cesium.CallbackProperty(() => this.radiusLevel3, false),
                material: Cesium.Color.fromBytes(25, 178, 230, 128),
                outline: false,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        })
        this.circleLevel3OutlineEntity = this.dataSource.entities.add({
            polyline: {
                positions: new Cesium.CallbackProperty(() => {
                    if (!this.centerCartesian || this.radiusLevel3 <= 0) return []
                    const positions = buildEllipsePositions(this.centerCartesian, this.radiusLevel3, this.radiusLevel3)
                    return positions.length ? [...positions, positions[0]] : positions
                }, false),
                clampToGround: true,
                width: 2,
                material: Cesium.Color.fromBytes(0, 251, 255, 255),
            },
        })
    }
    clearPreviewEntities() {
        if (!this.dataSource) return
        this.dataSource.entities.removeAll()
        this.circleLevel1Entity = null
        this.circleLevel2Entity = null
        this.circleLevel3Entity = null
        this.circleLevel1OutlineEntity = null
        this.circleLevel2OutlineEntity = null
        this.circleLevel3OutlineEntity = null
    }
    getSurfaceDistance(startCartesian, endCartesian) {
        const start = Cesium.Cartographic.fromCartesian(startCartesian)
        const end = Cesium.Cartographic.fromCartesian(endCartesian)
        const geodesic = new Cesium.EllipsoidGeodesic(start, end)
        return geodesic.surfaceDistance
    }
    getPositionFromScreen(screenPosition) {
        const scene = this.viewer.scene
        const cartesian = scene.pickPosition(screenPosition)
        if (cartesian) return cartesian
        return scene.camera.pickEllipsoid(screenPosition, scene.globe.ellipsoid)
    }
    destroy() {
        if (this.dataSource) {
            this.dataSource.entities.removeAll()
            this.viewer.dataSources.remove(this.dataSource)
            this.dataSource = null
        }
        if (this.handler) {
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE)
            this.handler.destroy()
            this.handler = null
        }
        this.tooltip?.destroy()
        this.tooltip = null
        this.centerCartesian = null
        this.radiusLevel1 = 0
        this.radiusLevel2 = 0
        this.radiusLevel3 = 0
        this.isDrawing = false
        this.drawStep = 0
        this.isCompleted = false
    }
}
applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawEllipseTool.js
New file
@@ -0,0 +1,162 @@
import * as Cesium from 'cesium'
import { MapTooltip } from '../Tooltip'
import { ToolBase } from '../ToolBase'
import { buildEllipsePositions } from '../shapeUtils'
const DEFAULT_STYLE = {
    fill: Cesium.Color.fromBytes(45, 140, 240, 99),
    outline: Cesium.Color.fromBytes(45, 140, 240, 255),
}
const resolveStyle = style => ({
    fill: style?.fill || DEFAULT_STYLE.fill,
    outline: style?.outline || DEFAULT_STYLE.outline,
})
export class DrawEllipseTool extends ToolBase {
    constructor(viewer, options = {}) {
        super(viewer)
        this.tooltip = new MapTooltip(viewer)
        this.dataSource = null
        this.ellipseEntity = null
        this.centerCartesian = null
        this.majorPoint = null
        this.minorPoint = null
        this.semiMajor = 0
        this.semiMinor = 0
        this.isDrawing = false
        this.drawStep = 0
        this.isCompleted = false
        this.style = resolveStyle(options?.style)
    }
    start() {
        this.dataSource = new Cesium.CustomDataSource('draw-ellipse')
        this.viewer.dataSources.add(this.dataSource)
        this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
        this.handler.setInputAction(click => this.handleLeftClick(click), Cesium.ScreenSpaceEventType.LEFT_CLICK)
        this.handler.setInputAction(movement => this.handleMouseMove(movement), Cesium.ScreenSpaceEventType.MOUSE_MOVE)
        this.drawStep = 0
        this.isCompleted = false
        this.tooltip.show('单击设置中心点', { x: 0, y: 0 })
    }
    handleLeftClick(click) {
        if (this.isCompleted) return
        const position = this.getPositionFromScreen(click.position)
        if (!position) return
        if (this.drawStep === 0) {
            this.centerCartesian = position
            this.semiMajor = 1
            this.semiMinor = 1
            this.isDrawing = true
            this.drawStep = 1
            this.createEntities()
            this.tooltip.show('单击设置长轴', click.position)
            return
        }
        if (this.drawStep === 1) {
            this.majorPoint = position
            this.semiMajor = Math.max(1, this.getSurfaceDistance(this.centerCartesian, this.majorPoint))
            this.semiMinor = this.semiMajor
            this.drawStep = 2
            this.tooltip.show('单击设置短轴', click.position)
            return
        }
        if (this.drawStep === 2) {
            this.minorPoint = position
            const minorRadius = Math.max(1, this.getSurfaceDistance(this.centerCartesian, this.minorPoint))
            this.semiMinor = Math.min(minorRadius, this.semiMajor)
            this.isDrawing = false
            this.drawStep = 0
            this.isCompleted = true
            const positions = buildEllipsePositions(this.centerCartesian, this.semiMajor, this.semiMinor)
            this.notify('getPolygonPositions', positions)
            this.tooltip.hide()
            this.clearPreviewEntities()
        }
    }
    handleMouseMove(movement) {
        if (this.isCompleted) return
        this.tooltip.move(movement.endPosition)
        if (!this.isDrawing) return
        const position = this.getPositionFromScreen(movement.endPosition)
        if (!position) return
        if (this.drawStep === 1) {
            const radius = Math.max(1, this.getSurfaceDistance(this.centerCartesian, position))
            this.semiMajor = radius
            this.semiMinor = radius
            return
        }
        if (this.drawStep === 2) {
            const minorRadius = Math.max(1, this.getSurfaceDistance(this.centerCartesian, position))
            this.semiMinor = Math.min(minorRadius, this.semiMajor)
        }
    }
    createEntities() {
        if (this.ellipseEntity) return
        this.ellipseEntity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.centerCartesian, false),
            ellipse: {
                semiMajorAxis: new Cesium.CallbackProperty(() => this.semiMajor, false),
                semiMinorAxis: new Cesium.CallbackProperty(() => this.semiMinor, false),
                material: this.style.fill,
                outline: true,
                outlineColor: this.style.outline,
                outlineWidth: 2,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        })
    }
    getSurfaceDistance(startCartesian, endCartesian) {
        const start = Cesium.Cartographic.fromCartesian(startCartesian)
        const end = Cesium.Cartographic.fromCartesian(endCartesian)
        const geodesic = new Cesium.EllipsoidGeodesic(start, end)
        return geodesic.surfaceDistance
    }
    clearPreviewEntities() {
        if (!this.dataSource) return
        this.dataSource.entities.removeAll()
        this.ellipseEntity = null
    }
    getPositionFromScreen(screenPosition) {
        const scene = this.viewer.scene
        const cartesian = scene.pickPosition(screenPosition)
        if (cartesian) return cartesian
        return scene.camera.pickEllipsoid(screenPosition, scene.globe.ellipsoid)
    }
    setStyle(style) {
        this.style = resolveStyle(style)
        if (this.ellipseEntity?.ellipse) {
            this.ellipseEntity.ellipse.material = this.style.fill
            this.ellipseEntity.ellipse.outlineColor = this.style.outline
        }
    }
    destroy() {
        if (this.dataSource) {
            this.dataSource.entities.removeAll()
            this.viewer.dataSources.remove(this.dataSource)
            this.dataSource = null
        }
        if (this.handler) {
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE)
            this.handler.destroy()
            this.handler = null
        }
        this.tooltip?.destroy()
        this.tooltip = null
        this.centerCartesian = null
        this.majorPoint = null
        this.minorPoint = null
        this.isDrawing = false
        this.drawStep = 0
        this.isCompleted = false
    }
}
applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawPolygonTool.js
New file
@@ -0,0 +1,244 @@
import * as Cesium from 'cesium'
import { MapTooltip } from '../Tooltip'
import { ToolBase } from '../ToolBase'
const POINT_ENTITY_NAME = 'draw-polygon-point'
const DEFAULT_STYLE = {
    fill: Cesium.Color.fromBytes(45, 140, 240, 99),
    outline: Cesium.Color.fromBytes(45, 140, 240, 255),
}
const resolveStyle = style => ({
    fill: style?.fill || DEFAULT_STYLE.fill,
    outline: style?.outline || DEFAULT_STYLE.outline,
})
export class DrawPolygonTool extends ToolBase {
    constructor(viewer, options = {}) {
        super(viewer)
        this.tooltip = new MapTooltip(viewer)
        this.dataSource = null
        this.polygonEntity = null
        this.polylineEntity = null
        this.positions = []
        this.floatPosition = null
        this.isDrawing = false
        this.lastMousePosition = null
        this.style = resolveStyle(options?.style)
    }
    start() {
        this.dataSource = new Cesium.CustomDataSource('draw-polygon')
        this.viewer.dataSources.add(this.dataSource)
        this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
        this.handler.setInputAction(click => this.handleLeftClick(click), Cesium.ScreenSpaceEventType.LEFT_CLICK)
        this.handler.setInputAction(click => this.handleRightClick(click), Cesium.ScreenSpaceEventType.RIGHT_CLICK)
        this.handler.setInputAction(movement => this.handleMouseMove(movement), Cesium.ScreenSpaceEventType.MOUSE_MOVE)
        this.isDrawing = true
        this.tooltip.show(this.getTipText(), { x: 0, y: 0 })
    }
    handleLeftClick(click) {
        if (!this.isDrawing) return
        const pickedEntity = this.viewer.scene.pick(click.position)?.id
        if (this.isFinishTrigger(pickedEntity)) {
            this.finishDrawing()
            return
        }
        if (pickedEntity?.name === POINT_ENTITY_NAME) return
        const position = this.getPositionFromScreen(click.position)
        if (!position) return
        this.addPoint(position)
        this.tooltip.show(this.getTipText(), click.position)
    }
    handleRightClick(click) {
        if (!this.isDrawing) return
        const pickedEntity = this.viewer.scene.pick(click.position)?.id
        if (!pickedEntity || pickedEntity.name !== POINT_ENTITY_NAME) return
        const index = pickedEntity.customData?.index
        if (typeof index !== 'number') return
        this.removePoint(index)
        this.tooltip.show(this.getTipText(), click.position)
    }
    handleMouseMove(movement) {
        if (!this.isDrawing) return
        this.tooltip.show(this.getTipText(), movement.endPosition)
        if (this.positions.length === 0) return
        const position = this.getPositionFromScreen(movement.endPosition)
        if (!position) return
        this.lastMousePosition = position
        this.floatPosition = position
    }
    getTipText() {
        const count = this.positions.length
        if (count === 0) {
            return '单击增加点'
        }
        if (count < 3) {
            return '单击增加点,右击删除点'
        }
        return '单击增加点,右击删除点,双击结束绘制'
    }
    isFinishTrigger(pickedEntity) {
        if (!pickedEntity || pickedEntity.name !== POINT_ENTITY_NAME) return false
        if (this.positions.length < 3) return false
        const index = pickedEntity.customData?.index
        return index === this.positions.length - 1
    }
    addPoint(position) {
        this.positions.push(position)
        if (!this.polygonEntity) {
            this.createEntities()
        }
        this.rebuildPointEntities()
    }
    removePoint(index) {
        if (index < 0 || index >= this.positions.length) return
        this.positions.splice(index, 1)
        this.rebuildPointEntities()
        if (this.positions.length === 0) {
            this.floatPosition = null
            this.lastMousePosition = null
            return
        }
        if (this.lastMousePosition) {
            this.floatPosition = this.lastMousePosition
        } else {
            this.floatPosition = this.positions[this.positions.length - 1]
        }
    }
    createEntities() {
        const getPreviewPositions = () => {
            if (this.positions.length === 0) return []
            if (!this.isDrawing || !this.floatPosition) return this.positions
            return [...this.positions, this.floatPosition]
        }
        this.polygonEntity = this.dataSource.entities.add({
            polygon: {
                hierarchy: new Cesium.CallbackProperty(
                    () => new Cesium.PolygonHierarchy(getPreviewPositions()),
                    false
                ),
                material: this.style.fill,
                outline: false,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                show: new Cesium.CallbackProperty(() => getPreviewPositions().length >= 3, false),
            },
        })
        this.polylineEntity = this.dataSource.entities.add({
            polyline: {
                positions: new Cesium.CallbackProperty(() => {
                    const positions = getPreviewPositions()
                    if (positions.length < 2) return positions
                    if (positions.length >= 3) {
                        return [...positions, positions[0]]
                    }
                    return positions
                }, false),
                clampToGround: true,
                width: 2,
                material: this.style.outline,
                show: new Cesium.CallbackProperty(() => getPreviewPositions().length >= 2, false),
            },
        })
    }
    rebuildPointEntities() {
        this.dataSource.entities.values
            .slice()
            .filter(entity => entity?.name === POINT_ENTITY_NAME)
            .forEach(entity => this.dataSource.entities.remove(entity))
        this.positions.forEach((position, index) => {
            this.dataSource.entities.add({
                name: POINT_ENTITY_NAME,
                position: position.clone ? position.clone() : position,
                point: {
                    pixelSize: 12,
                    color: Cesium.Color.WHITE,
                    outlineColor: this.style.outline,
                    outlineWidth: 2,
                    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                },
                customData: {
                    index,
                },
            })
        })
    }
    finishDrawing() {
        if (this.positions.length < 3) return
        this.isDrawing = false
        this.floatPosition = null
        this.notify('getPolygonPositions', this.positions)
        this.tooltip.hide()
        this.clearPreviewEntities()
    }
    setStyle(style) {
        this.style = resolveStyle(style)
        if (this.polygonEntity?.polygon) {
            this.polygonEntity.polygon.material = this.style.fill
        }
        if (this.polylineEntity?.polyline) {
            this.polylineEntity.polyline.material = this.style.outline
        }
        if (this.dataSource) {
            this.dataSource.entities.values
                .filter(entity => entity?.name === POINT_ENTITY_NAME)
                .forEach(entity => {
                    if (entity?.point) {
                        entity.point.outlineColor = this.style.outline
                    }
                })
        }
    }
    clearPreviewEntities() {
        if (!this.dataSource) return
        this.dataSource.entities.removeAll()
        this.polygonEntity = null
        this.polylineEntity = null
    }
    getPositionFromScreen(screenPosition) {
        const scene = this.viewer.scene
        const cartesian = scene.pickPosition(screenPosition)
        if (cartesian) return cartesian
        return scene.camera.pickEllipsoid(screenPosition, scene.globe.ellipsoid)
    }
    destroy() {
        if (this.dataSource) {
            this.dataSource.entities.removeAll()
            this.viewer.dataSources.remove(this.dataSource)
            this.dataSource = null
        }
        if (this.handler) {
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE)
            this.handler.destroy()
            this.handler = null
        }
        this.tooltip?.destroy()
        this.tooltip = null
        this.positions = []
        this.floatPosition = null
        this.isDrawing = false
        this.lastMousePosition = null
    }
}
applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawRectangleTool.js
New file
@@ -0,0 +1,150 @@
import * as Cesium from 'cesium'
import { MapTooltip } from '../Tooltip'
import { ToolBase } from '../ToolBase'
import { getBoundsFromCartesians, rectanglePositionsFromBounds } from '../shapeUtils'
const DEFAULT_STYLE = {
    fill: Cesium.Color.fromBytes(45, 140, 240, 99),
    outline: Cesium.Color.fromBytes(45, 140, 240, 255),
}
const resolveStyle = style => ({
    fill: style?.fill || DEFAULT_STYLE.fill,
    outline: style?.outline || DEFAULT_STYLE.outline,
})
export class DrawRectangleTool extends ToolBase {
    constructor(viewer, options = {}) {
        super(viewer)
        this.tooltip = new MapTooltip(viewer)
        this.dataSource = null
        this.rectangleEntity = null
        this.outlineEntity = null
        this.startPosition = null
        this.endPosition = null
        this.isDrawing = false
        this.style = resolveStyle(options?.style)
    }
    start() {
        this.dataSource = new Cesium.CustomDataSource('draw-rectangle')
        this.viewer.dataSources.add(this.dataSource)
        this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
        this.handler.setInputAction(click => this.handleLeftClick(click), Cesium.ScreenSpaceEventType.LEFT_CLICK)
        this.handler.setInputAction(movement => this.handleMouseMove(movement), Cesium.ScreenSpaceEventType.MOUSE_MOVE)
        this.tooltip.show('单击设置起点', { x: 0, y: 0 })
    }
    handleLeftClick(click) {
        const position = this.getPositionFromScreen(click.position)
        if (!position) return
        if (!this.startPosition) {
            this.startPosition = position
            this.endPosition = position
            this.isDrawing = true
            this.createEntities()
            this.tooltip.show('单击结束绘制', click.position)
            return
        }
        if (this.isDrawing) {
            this.endPosition = position
            this.isDrawing = false
            const positions = this.getRectanglePositions()
            this.notify('getPolygonPositions', positions)
            this.tooltip.hide()
            this.clearPreviewEntities()
        }
    }
    handleMouseMove(movement) {
        if (!this.isDrawing) {
            this.tooltip.move(movement.endPosition)
            return
        }
        const position = this.getPositionFromScreen(movement.endPosition)
        if (!position) return
        this.endPosition = position
        this.tooltip.move(movement.endPosition)
    }
    createEntities() {
        if (this.rectangleEntity) return
        const hierarchy = new Cesium.CallbackProperty(() => {
            const positions = this.getRectanglePositions()
            return new Cesium.PolygonHierarchy(positions)
        }, false)
        this.rectangleEntity = this.dataSource.entities.add({
            polygon: {
                hierarchy,
                material: this.style.fill,
                outline: false,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        })
        this.outlineEntity = this.dataSource.entities.add({
            polyline: {
                positions: new Cesium.CallbackProperty(() => {
                    const positions = this.getRectanglePositions()
                    return positions.length ? [...positions, positions[0]] : []
                }, false),
                clampToGround: true,
                width: 2,
                material: this.style.outline,
            },
        })
    }
    getRectanglePositions() {
        if (!this.startPosition || !this.endPosition) return []
        const bounds = getBoundsFromCartesians([this.startPosition, this.endPosition])
        return rectanglePositionsFromBounds(bounds)
    }
    clearPreviewEntities() {
        if (!this.dataSource) return
        this.dataSource.entities.removeAll()
        this.rectangleEntity = null
        this.outlineEntity = null
    }
    getPositionFromScreen(screenPosition) {
        const scene = this.viewer.scene
        const cartesian = scene.pickPosition(screenPosition)
        if (cartesian) return cartesian
        return scene.camera.pickEllipsoid(screenPosition, scene.globe.ellipsoid)
    }
    setStyle(style) {
        this.style = resolveStyle(style)
        if (this.rectangleEntity?.polygon) {
            this.rectangleEntity.polygon.material = this.style.fill
        }
        if (this.outlineEntity?.polyline) {
            this.outlineEntity.polyline.material = this.style.outline
        }
    }
    destroy() {
        if (this.dataSource) {
            this.dataSource.entities.removeAll()
            this.viewer.dataSources.remove(this.dataSource)
            this.dataSource = null
        }
        if (this.handler) {
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE)
            this.handler.destroy()
            this.handler = null
        }
        this.tooltip?.destroy()
        this.tooltip = null
        this.startPosition = null
        this.endPosition = null
        this.isDrawing = false
    }
}
applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditBufferCircleTool.js
New file
@@ -0,0 +1,314 @@
import * as Cesium from 'cesium'
import { MapTooltip } from '../Tooltip'
import { ToolBase } from '../ToolBase'
import { buildEllipsePositions, cartesianToLocal } from '../shapeUtils'
const normalizePoints = points =>
    (points || []).map(point => ({
        lng: point.lng ?? point.longitude,
        lat: point.lat ?? point.latitude,
    }))
export class EditBufferCircleTool extends ToolBase {
    constructor(viewer) {
        super(viewer)
        this.tooltip = new MapTooltip(viewer)
        this.dataSource = null
        this.centerCartesian = null
        this.radiusLevel1 = 0
        this.radiusLevel2 = 0
        this.radiusLevel3 = 0
        this.dragType = null
        this.circleLevel1Entity = null
        this.circleLevel2Entity = null
        this.circleLevel3Entity = null
        this.circleLevel1OutlineEntity = null
        this.circleLevel2OutlineEntity = null
        this.circleLevel3OutlineEntity = null
        this.centerEntity = null
        this.radiusLevel1Entity = null
        this.radiusLevel2Entity = null
        this.radiusLevel3Entity = null
    }
    start(data = []) {
        const inputPoints = Array.isArray(data?.points) ? data.points : data
        const normalized = normalizePoints(inputPoints)
        if (!normalized.length) return
        const centerLng = normalized.reduce((sum, p) => sum + p.lng, 0) / normalized.length
        const centerLat = normalized.reduce((sum, p) => sum + p.lat, 0) / normalized.length
        this.centerCartesian = Cesium.Cartesian3.fromDegrees(centerLng, centerLat)
        const radii = Array.isArray(data?.meta?.bufferRadii) ? data.meta.bufferRadii : null
        if (radii?.length) {
            const [r1, r2, r3] = radii
            this.radiusLevel1 = Number.isFinite(r1) && r1 > 0 ? r1 : 1
            this.radiusLevel2 = Number.isFinite(r2) && r2 > 0 ? Math.max(r2, this.radiusLevel1) : this.radiusLevel1
            this.radiusLevel3 = Number.isFinite(r3) && r3 > 0 ? Math.max(r3, this.radiusLevel2) : this.radiusLevel2
        } else {
            this.radiusLevel3 = this.estimateRadius(normalized)
            this.radiusLevel2 = this.radiusLevel3
            this.radiusLevel1 = this.radiusLevel3
        }
        this.dataSource = new Cesium.CustomDataSource('edit-buffer-circle')
        this.viewer.dataSources.add(this.dataSource)
        this.createEntities()
        this.initHandler()
    }
    estimateRadius(points) {
        const center = Cesium.Cartesian3.clone(this.centerCartesian)
        let max = 1
        points.forEach(point => {
            const cartesian = Cesium.Cartesian3.fromDegrees(point.lng, point.lat)
            const distance = Cesium.Cartesian3.distance(center, cartesian)
            max = Math.max(max, distance)
        })
        return max
    }
    initHandler() {
        this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
        this.handler.setInputAction(evt => this.handleLeftDown(evt), Cesium.ScreenSpaceEventType.LEFT_DOWN)
        this.handler.setInputAction(evt => this.handleMouseMove(evt), Cesium.ScreenSpaceEventType.MOUSE_MOVE)
        this.handler.setInputAction(() => this.handleLeftUp(), Cesium.ScreenSpaceEventType.LEFT_UP)
        this.tooltip.show('拖动控制点编辑', { x: 0, y: 0 })
    }
    handleLeftDown(evt) {
        const picked = this.viewer.scene.pick(evt.position)?.id
        if (!picked) return
        if (picked.customType === 'circle-center') this.dragType = 'center'
        if (picked.customType === 'circle-radius-1') this.dragType = 'radius-1'
        if (picked.customType === 'circle-radius-2') this.dragType = 'radius-2'
        if (picked.customType === 'circle-radius-3') this.dragType = 'radius-3'
        if (this.dragType) this.disableMapControl()
    }
    handleMouseMove(evt) {
        this.tooltip.move(evt.endPosition)
        if (!this.dragType) return
        const position = this.getPositionFromScreen(evt.endPosition)
        if (!position) return
        if (this.dragType === 'center') {
            this.centerCartesian = position
            this.updateRadiusPoints()
            return
        }
        const local = cartesianToLocal(this.centerCartesian, position)
        const radius = Math.max(1, Math.sqrt(local.x * local.x + local.y * local.y))
        if (this.dragType === 'radius-1') {
            this.radiusLevel1 = radius
            if (this.radiusLevel2 < this.radiusLevel1) this.radiusLevel2 = this.radiusLevel1
            if (this.radiusLevel3 < this.radiusLevel2) this.radiusLevel3 = this.radiusLevel2
        }
        if (this.dragType === 'radius-2') {
            this.radiusLevel2 = Math.max(radius, this.radiusLevel1)
            if (this.radiusLevel3 < this.radiusLevel2) this.radiusLevel3 = this.radiusLevel2
        }
        if (this.dragType === 'radius-3') {
            this.radiusLevel3 = Math.max(radius, this.radiusLevel2)
        }
        this.updateRadiusPoints()
    }
    handleLeftUp() {
        if (!this.dragType) return
        this.dragType = null
        this.enableMapControl()
        const positions = buildEllipsePositions(this.centerCartesian, this.radiusLevel3, this.radiusLevel3)
        this.notify('getPolygonPositions', {
            positions,
            meta: {
                center: this.centerCartesian,
                bufferRadii: [this.radiusLevel1, this.radiusLevel2, this.radiusLevel3],
            },
        })
    }
    createEntities() {
        this.circleLevel1Entity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.centerCartesian, false),
            ellipse: {
                semiMajorAxis: new Cesium.CallbackProperty(() => this.radiusLevel1, false),
                semiMinorAxis: new Cesium.CallbackProperty(() => this.radiusLevel1, false),
                material: Cesium.Color.fromBytes(247, 20, 20, 128),
                outline: false,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        })
        this.circleLevel1OutlineEntity = this.dataSource.entities.add({
            polyline: {
                positions: new Cesium.CallbackProperty(() => {
                    if (!this.centerCartesian || this.radiusLevel1 <= 0) return []
                    const positions = buildEllipsePositions(this.centerCartesian, this.radiusLevel1, this.radiusLevel1)
                    return positions.length ? [...positions, positions[0]] : positions
                }, false),
                clampToGround: true,
                width: 2,
                material: Cesium.Color.fromBytes(255, 0, 0, 255),
            },
        })
        this.circleLevel2Entity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.centerCartesian, false),
            ellipse: {
                semiMajorAxis: new Cesium.CallbackProperty(() => this.radiusLevel2, false),
                semiMinorAxis: new Cesium.CallbackProperty(() => this.radiusLevel2, false),
                material: Cesium.Color.fromBytes(255, 235, 59, 128),
                outline: false,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        })
        this.circleLevel2OutlineEntity = this.dataSource.entities.add({
            polyline: {
                positions: new Cesium.CallbackProperty(() => {
                    if (!this.centerCartesian || this.radiusLevel2 <= 0) return []
                    const positions = buildEllipsePositions(this.centerCartesian, this.radiusLevel2, this.radiusLevel2)
                    return positions.length ? [...positions, positions[0]] : positions
                }, false),
                clampToGround: true,
                width: 2,
                material: Cesium.Color.fromBytes(255, 235, 59, 255),
            },
        })
        this.circleLevel3Entity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.centerCartesian, false),
            ellipse: {
                semiMajorAxis: new Cesium.CallbackProperty(() => this.radiusLevel3, false),
                semiMinorAxis: new Cesium.CallbackProperty(() => this.radiusLevel3, false),
                material: Cesium.Color.fromBytes(25, 178, 230, 128),
                outline: false,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        })
        this.circleLevel3OutlineEntity = this.dataSource.entities.add({
            polyline: {
                positions: new Cesium.CallbackProperty(() => {
                    if (!this.centerCartesian || this.radiusLevel3 <= 0) return []
                    const positions = buildEllipsePositions(this.centerCartesian, this.radiusLevel3, this.radiusLevel3)
                    return positions.length ? [...positions, positions[0]] : positions
                }, false),
                clampToGround: true,
                width: 2,
                material: Cesium.Color.fromBytes(0, 251, 255, 255),
            },
        })
        this.centerEntity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.centerCartesian, false),
            point: {
                pixelSize: 12,
                color: Cesium.Color.WHITE,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                disableDepthTestDistance: Number.POSITIVE_INFINITY,
            },
            customType: 'circle-center',
        })
        this.radiusLevel1Entity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.getRadiusPoint(this.radiusLevel1), false),
            point: {
                pixelSize: 10,
                color: Cesium.Color.fromBytes(255, 255, 255, 200),
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                disableDepthTestDistance: Number.POSITIVE_INFINITY,
            },
            customType: 'circle-radius-1',
        })
        this.radiusLevel2Entity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.getRadiusPoint(this.radiusLevel2), false),
            point: {
                pixelSize: 10,
                color: Cesium.Color.fromBytes(255, 255, 255, 200),
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                disableDepthTestDistance: Number.POSITIVE_INFINITY,
            },
            customType: 'circle-radius-2',
        })
        this.radiusLevel3Entity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.getRadiusPoint(this.radiusLevel3), false),
            point: {
                pixelSize: 10,
                color: Cesium.Color.fromBytes(255, 255, 255, 200),
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                disableDepthTestDistance: Number.POSITIVE_INFINITY,
            },
            customType: 'circle-radius-3',
        })
    }
    updateRadiusPoints() {
        if (this.radiusLevel1Entity) this.radiusLevel1Entity.position = this.getRadiusPoint(this.radiusLevel1)
        if (this.radiusLevel2Entity) this.radiusLevel2Entity.position = this.getRadiusPoint(this.radiusLevel2)
        if (this.radiusLevel3Entity) this.radiusLevel3Entity.position = this.getRadiusPoint(this.radiusLevel3)
    }
    getPositions() {
        if (!this.centerCartesian) return []
        return {
            positions: buildEllipsePositions(this.centerCartesian, this.radiusLevel3, this.radiusLevel3),
            meta: {
                center: this.centerCartesian,
                bufferRadii: [this.radiusLevel1, this.radiusLevel2, this.radiusLevel3],
            },
        }
    }
    getRadiusPoint(radius) {
        const frame = Cesium.Transforms.eastNorthUpToFixedFrame(this.centerCartesian)
        const local = new Cesium.Cartesian3(radius, 0, 0)
        return Cesium.Matrix4.multiplyByPoint(frame, local, new Cesium.Cartesian3())
    }
    getPositionFromScreen(screenPosition) {
        const scene = this.viewer.scene
        const cartesian = scene.pickPosition(screenPosition)
        if (cartesian) return cartesian
        return scene.camera.pickEllipsoid(screenPosition, scene.globe.ellipsoid)
    }
    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
    }
    destroy() {
        if (this.dataSource) {
            this.dataSource.entities.removeAll()
            this.viewer.dataSources.remove(this.dataSource)
            this.dataSource = null
        }
        if (this.handler) {
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOWN)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_UP)
            this.handler.destroy()
            this.handler = null
        }
        this.tooltip?.destroy()
        this.tooltip = null
        this.centerCartesian = null
        this.radiusLevel1 = 0
        this.radiusLevel2 = 0
        this.radiusLevel3 = 0
        this.circleLevel1OutlineEntity = null
        this.circleLevel2OutlineEntity = null
        this.circleLevel3OutlineEntity = null
        this.dragType = null
    }
}
applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditEllipseTool.js
New file
@@ -0,0 +1,233 @@
import * as Cesium from 'cesium'
import { MapTooltip } from '../Tooltip'
import { ToolBase } from '../ToolBase'
import {
    getBoundsFromLngLats,
    getCenterFromBounds,
    getMetersBetween,
    buildEllipsePositions,
    cartesianToLocal,
} from '../shapeUtils'
const DEFAULT_STYLE = {
    fill: Cesium.Color.fromBytes(45, 140, 240, 99),
    outline: Cesium.Color.fromBytes(45, 140, 240, 255),
}
const resolveStyle = style => ({
    fill: style?.fill || DEFAULT_STYLE.fill,
    outline: style?.outline || DEFAULT_STYLE.outline,
})
const normalizePoints = points =>
    (points || []).map(point => ({
        lng: point.lng ?? point.longitude,
        lat: point.lat ?? point.latitude,
    }))
export class EditEllipseTool extends ToolBase {
    constructor(viewer, options = {}) {
        super(viewer)
        this.tooltip = new MapTooltip(viewer)
        this.dataSource = null
        this.centerCartesian = null
        this.semiMajor = 0
        this.semiMinor = 0
        this.dragType = null
        this.centerEntity = null
        this.majorEntity = null
        this.minorEntity = null
        this.ellipseEntity = null
        this.style = resolveStyle(options?.style)
    }
    start(points = []) {
        const normalized = normalizePoints(points)
        const bounds = getBoundsFromLngLats(normalized)
        const center = getCenterFromBounds(bounds)
        if (!center) return
        this.centerCartesian = Cesium.Cartesian3.fromRadians(center.longitude, center.latitude)
        const eastWest = getMetersBetween(
            new Cesium.Cartographic(bounds.west, center.latitude),
            new Cesium.Cartographic(bounds.east, center.latitude)
        )
        const northSouth = getMetersBetween(
            new Cesium.Cartographic(center.longitude, bounds.south),
            new Cesium.Cartographic(center.longitude, bounds.north)
        )
        this.semiMajor = Math.max(1, eastWest / 2)
        this.semiMinor = Math.max(1, northSouth / 2)
        this.dataSource = new Cesium.CustomDataSource('edit-ellipse')
        this.viewer.dataSources.add(this.dataSource)
        this.createEntities()
        this.initHandler()
    }
    initHandler() {
        this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
        this.handler.setInputAction(evt => this.handleLeftDown(evt), Cesium.ScreenSpaceEventType.LEFT_DOWN)
        this.handler.setInputAction(evt => this.handleMouseMove(evt), Cesium.ScreenSpaceEventType.MOUSE_MOVE)
        this.handler.setInputAction(() => this.handleLeftUp(), Cesium.ScreenSpaceEventType.LEFT_UP)
        this.tooltip.show('拖动控制点编辑', { x: 0, y: 0 })
    }
    handleLeftDown(evt) {
        const picked = this.viewer.scene.pick(evt.position)?.id
        if (!picked) return
        if (picked.customType === 'ellipse-center') this.dragType = 'center'
        if (picked.customType === 'ellipse-major') this.dragType = 'major'
        if (picked.customType === 'ellipse-minor') this.dragType = 'minor'
        if (this.dragType) this.disableMapControl()
    }
    handleMouseMove(evt) {
        this.tooltip.move(evt.endPosition)
        if (!this.dragType) return
        const position = this.getPositionFromScreen(evt.endPosition)
        if (!position) return
        if (this.dragType === 'center') {
            this.centerCartesian = position
            this.updateAxisPoints()
            return
        }
        const local = cartesianToLocal(this.centerCartesian, position)
        if (this.dragType === 'major') {
            this.semiMajor = Math.max(1, Math.abs(local.x))
            if (this.semiMajor < this.semiMinor) {
                this.semiMajor = this.semiMinor
            }
        }
        if (this.dragType === 'minor') {
            this.semiMinor = Math.max(1, Math.abs(local.y))
            if (this.semiMinor > this.semiMajor) {
                this.semiMinor = this.semiMajor
            }
        }
        this.updateAxisPoints()
    }
    handleLeftUp() {
        if (!this.dragType) return
        this.dragType = null
        this.enableMapControl()
        const positions = buildEllipsePositions(this.centerCartesian, this.semiMajor, this.semiMinor)
        this.notify('getPolygonPositions', positions)
    }
    createEntities() {
        this.ellipseEntity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.centerCartesian, false),
            ellipse: {
                semiMajorAxis: new Cesium.CallbackProperty(() => this.semiMajor, false),
                semiMinorAxis: new Cesium.CallbackProperty(() => this.semiMinor, false),
                material: this.style.fill,
                outline: true,
                outlineColor: this.style.outline,
                outlineWidth: 2,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        })
        this.centerEntity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.centerCartesian, false),
            point: {
                pixelSize: 12,
                color: Cesium.Color.WHITE,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                disableDepthTestDistance: Number.POSITIVE_INFINITY,
            },
            customType: 'ellipse-center',
        })
        this.majorEntity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.getAxisPoint(this.semiMajor, 0), false),
            point: {
                pixelSize: 10,
                color: Cesium.Color.fromBytes(255, 255, 255, 200),
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                disableDepthTestDistance: Number.POSITIVE_INFINITY,
            },
            customType: 'ellipse-major',
        })
        this.minorEntity = this.dataSource.entities.add({
            position: new Cesium.CallbackProperty(() => this.getAxisPoint(0, this.semiMinor), false),
            point: {
                pixelSize: 10,
                color: Cesium.Color.fromBytes(255, 255, 255, 200),
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                disableDepthTestDistance: Number.POSITIVE_INFINITY,
            },
            customType: 'ellipse-minor',
        })
    }
    updateAxisPoints() {
        if (!this.majorEntity || !this.minorEntity) return
        this.majorEntity.position = this.getAxisPoint(this.semiMajor, 0)
        this.minorEntity.position = this.getAxisPoint(0, this.semiMinor)
    }
    getPositions() {
        if (!this.centerCartesian) return []
        return buildEllipsePositions(this.centerCartesian, this.semiMajor, this.semiMinor)
    }
    getAxisPoint(eastMeters, northMeters) {
        const frame = Cesium.Transforms.eastNorthUpToFixedFrame(this.centerCartesian)
        const local = new Cesium.Cartesian3(eastMeters, northMeters, 0)
        return Cesium.Matrix4.multiplyByPoint(frame, local, new Cesium.Cartesian3())
    }
    getPositionFromScreen(screenPosition) {
        const scene = this.viewer.scene
        const cartesian = scene.pickPosition(screenPosition)
        if (cartesian) return cartesian
        return scene.camera.pickEllipsoid(screenPosition, scene.globe.ellipsoid)
    }
    setStyle(style) {
        this.style = resolveStyle(style)
        if (this.ellipseEntity?.ellipse) {
            this.ellipseEntity.ellipse.material = this.style.fill
            this.ellipseEntity.ellipse.outlineColor = this.style.outline
        }
    }
    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
    }
    destroy() {
        if (this.dataSource) {
            this.dataSource.entities.removeAll()
            this.viewer.dataSources.remove(this.dataSource)
            this.dataSource = null
        }
        if (this.handler) {
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOWN)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_UP)
            this.handler.destroy()
            this.handler = null
        }
        this.tooltip?.destroy()
        this.tooltip = null
        this.centerCartesian = null
        this.semiMajor = 0
        this.semiMinor = 0
        this.dragType = null
    }
}
applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditPolygonTool.js
New file
@@ -0,0 +1,218 @@
import * as Cesium from 'cesium'
import { MapTooltip } from '../Tooltip'
import { ToolBase } from '../ToolBase'
const POINT_ENTITY_NAME = 'edit-polygon-point'
const DEFAULT_STYLE = {
    fill: Cesium.Color.fromBytes(45, 140, 240, 99),
    outline: Cesium.Color.fromBytes(45, 140, 240, 255),
}
const resolveStyle = style => ({
    fill: style?.fill || DEFAULT_STYLE.fill,
    outline: style?.outline || DEFAULT_STYLE.outline,
})
const normalizePoints = points =>
    (points || []).map(point => ({
        lng: point.lng ?? point.longitude,
        lat: point.lat ?? point.latitude,
        height: point.height ?? 0,
    }))
export class EditPolygonTool extends ToolBase {
    constructor(viewer, options = {}) {
        super(viewer)
        this.tooltip = new MapTooltip(viewer)
        this.dataSource = null
        this.polygonEntity = null
        this.polylineEntity = null
        this.positions = []
        this.isDragging = false
        this.draggedIndex = -1
        this.style = resolveStyle(options?.style)
    }
    start(points = []) {
        const normalized = normalizePoints(points)
        this.positions = normalized
            .filter(point => point?.lng !== undefined && point?.lat !== undefined)
            .map(point => Cesium.Cartesian3.fromDegrees(Number(point.lng), Number(point.lat), Number(point.height || 0)))
        this.dataSource = new Cesium.CustomDataSource('edit-polygon')
        this.viewer.dataSources.add(this.dataSource)
        this.createEntities()
        this.rebuildPointEntities()
        this.initHandler()
    }
    initHandler() {
        if (!this.viewer || this.handler) return
        this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
        this.handler.setInputAction(movement => this.handleMouseMove(movement), Cesium.ScreenSpaceEventType.MOUSE_MOVE)
        this.handler.setInputAction(click => this.handleLeftDown(click), Cesium.ScreenSpaceEventType.LEFT_DOWN)
        this.handler.setInputAction(() => this.handleLeftUp(), Cesium.ScreenSpaceEventType.LEFT_UP)
        this.handler.setInputAction(click => this.handleRightClick(click), Cesium.ScreenSpaceEventType.RIGHT_CLICK)
    }
    handleLeftDown(click) {
        const pickedEntity = this.viewer.scene.pick(click.position)?.id
        if (!pickedEntity || pickedEntity.name !== POINT_ENTITY_NAME) return
        const index = pickedEntity.customData?.index
        if (typeof index !== 'number') return
        this.isDragging = true
        this.draggedIndex = index
        this.disableMapControl()
    }
    handleLeftUp() {
        if (!this.isDragging) return
        this.isDragging = false
        this.draggedIndex = -1
        this.enableMapControl()
        this.notify('getPolygonPositions', this.positions)
    }
    handleMouseMove(movement) {
        this.tooltip.show('拖动顶点编辑,右键删除', movement.endPosition)
        if (!this.isDragging || this.draggedIndex < 0) return
        const position = this.getPositionFromScreen(movement.endPosition)
        if (!position) return
        this.positions[this.draggedIndex] = position
        const pointEntity = this.dataSource.entities.values.find(
            entity => entity?.name === POINT_ENTITY_NAME && entity.customData?.index === this.draggedIndex
        )
        if (pointEntity) {
            pointEntity.position = position
        }
    }
    handleRightClick(click) {
        const pickedEntity = this.viewer.scene.pick(click.position)?.id
        if (!pickedEntity || pickedEntity.name !== POINT_ENTITY_NAME) return
        if (this.positions.length <= 3) return
        const index = pickedEntity.customData?.index
        if (typeof index !== 'number') return
        this.positions.splice(index, 1)
        this.rebuildPointEntities()
        this.notify('getPolygonPositions', this.positions)
    }
    createEntities() {
        this.polygonEntity = this.dataSource.entities.add({
            polygon: {
                hierarchy: new Cesium.CallbackProperty(
                    () => new Cesium.PolygonHierarchy(this.positions),
                    false
                ),
                material: this.style.fill,
                outline: false,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                show: new Cesium.CallbackProperty(() => this.positions.length >= 3, false),
            },
        })
        this.polylineEntity = this.dataSource.entities.add({
            polyline: {
                positions: new Cesium.CallbackProperty(() => {
                    if (this.positions.length < 2) return this.positions
                    return [...this.positions, this.positions[0]]
                }, false),
                clampToGround: true,
                width: 2,
                material: this.style.outline,
                show: new Cesium.CallbackProperty(() => this.positions.length >= 2, false),
            },
        })
    }
    rebuildPointEntities() {
        this.dataSource.entities.values
            .slice()
            .filter(entity => entity?.name === POINT_ENTITY_NAME)
            .forEach(entity => this.dataSource.entities.remove(entity))
        this.positions.forEach((position, index) => {
            this.dataSource.entities.add({
                name: POINT_ENTITY_NAME,
                position: position.clone ? position.clone() : position,
                point: {
                    pixelSize: 12,
                    color: Cesium.Color.WHITE,
                    outlineColor: this.style.outline,
                    outlineWidth: 2,
                    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                },
                customData: {
                    index,
                },
            })
        })
    }
    getPositions() {
        return this.positions.slice()
    }
    getPositionFromScreen(screenPosition) {
        const scene = this.viewer.scene
        const cartesian = scene.pickPosition(screenPosition)
        if (cartesian) return cartesian
        return scene.camera.pickEllipsoid(screenPosition, scene.globe.ellipsoid)
    }
    setStyle(style) {
        this.style = resolveStyle(style)
        if (this.polygonEntity?.polygon) {
            this.polygonEntity.polygon.material = this.style.fill
        }
        if (this.polylineEntity?.polyline) {
            this.polylineEntity.polyline.material = this.style.outline
        }
        if (this.dataSource) {
            this.dataSource.entities.values
                .filter(entity => entity?.name === POINT_ENTITY_NAME)
                .forEach(entity => {
                    if (entity?.point) {
                        entity.point.outlineColor = this.style.outline
                    }
                })
        }
    }
    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
    }
    destroy() {
        if (this.dataSource) {
            this.dataSource.entities.removeAll()
            this.viewer.dataSources.remove(this.dataSource)
            this.dataSource = null
        }
        if (this.handler) {
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOWN)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_UP)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK)
            this.handler.destroy()
            this.handler = null
        }
        this.tooltip?.destroy()
        this.tooltip = null
        this.positions = []
        this.isDragging = false
        this.draggedIndex = -1
    }
}
applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditRectangleTool.js
New file
@@ -0,0 +1,198 @@
import * as Cesium from 'cesium'
import { MapTooltip } from '../Tooltip'
import { ToolBase } from '../ToolBase'
import { getBoundsFromLngLats, rectanglePositionsFromBounds } from '../shapeUtils'
const DEFAULT_STYLE = {
    fill: Cesium.Color.fromBytes(45, 140, 240, 99),
    outline: Cesium.Color.fromBytes(45, 140, 240, 255),
}
const resolveStyle = style => ({
    fill: style?.fill || DEFAULT_STYLE.fill,
    outline: style?.outline || DEFAULT_STYLE.outline,
})
const normalizePoints = points =>
    (points || []).map(point => ({
        lng: point.lng ?? point.longitude,
        lat: point.lat ?? point.latitude,
    }))
export class EditRectangleTool extends ToolBase {
    constructor(viewer, options = {}) {
        super(viewer)
        this.tooltip = new MapTooltip(viewer)
        this.dataSource = null
        this.bounds = null
        this.rectangleEntity = null
        this.outlineEntity = null
        this.cornerEntities = []
        this.dragIndex = null
        this.fixedCorner = null
        this.style = resolveStyle(options?.style)
    }
    start(points = []) {
        const normalized = normalizePoints(points)
        this.bounds = getBoundsFromLngLats(normalized)
        if (!this.bounds) return
        this.dataSource = new Cesium.CustomDataSource('edit-rectangle')
        this.viewer.dataSources.add(this.dataSource)
        this.createEntities()
        this.initHandler()
    }
    initHandler() {
        this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
        this.handler.setInputAction(evt => this.handleLeftDown(evt), Cesium.ScreenSpaceEventType.LEFT_DOWN)
        this.handler.setInputAction(evt => this.handleMouseMove(evt), Cesium.ScreenSpaceEventType.MOUSE_MOVE)
        this.handler.setInputAction(() => this.handleLeftUp(), Cesium.ScreenSpaceEventType.LEFT_UP)
        this.tooltip.show('拖动顶点编辑', { x: 0, y: 0 })
    }
    handleLeftDown(evt) {
        const picked = this.viewer.scene.pick(evt.position)?.id
        if (!picked || picked.customType !== 'rect-corner') return
        this.dragIndex = picked.customData.index
        const corners = this.getCornerCartographics()
        this.fixedCorner = corners[(this.dragIndex + 2) % 4]
        this.disableMapControl()
    }
    handleMouseMove(evt) {
        this.tooltip.move(evt.endPosition)
        if (this.dragIndex == null) return
        const position = this.getPositionFromScreen(evt.endPosition)
        if (!position) return
        const moved = Cesium.Cartographic.fromCartesian(position)
        this.bounds = {
            west: Math.min(moved.longitude, this.fixedCorner.longitude),
            east: Math.max(moved.longitude, this.fixedCorner.longitude),
            south: Math.min(moved.latitude, this.fixedCorner.latitude),
            north: Math.max(moved.latitude, this.fixedCorner.latitude),
        }
        this.updateCorners()
    }
    handleLeftUp() {
        if (this.dragIndex == null) return
        this.dragIndex = null
        this.fixedCorner = null
        this.enableMapControl()
        const positions = rectanglePositionsFromBounds(this.bounds)
        this.notify('getPolygonPositions', positions)
    }
    createEntities() {
        const hierarchy = new Cesium.CallbackProperty(() => {
            const positions = rectanglePositionsFromBounds(this.bounds)
            return new Cesium.PolygonHierarchy(positions)
        }, false)
        this.rectangleEntity = this.dataSource.entities.add({
            polygon: {
                hierarchy,
                material: this.style.fill,
                outline: false,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        })
        this.outlineEntity = this.dataSource.entities.add({
            polyline: {
                positions: new Cesium.CallbackProperty(() => {
                    const positions = rectanglePositionsFromBounds(this.bounds)
                    return positions.length ? [...positions, positions[0]] : []
                }, false),
                clampToGround: true,
                width: 2,
                material: this.style.outline,
            },
        })
        this.createCorners()
    }
    createCorners() {
        const positions = rectanglePositionsFromBounds(this.bounds)
        this.cornerEntities = positions.map((position, index) =>
            this.dataSource.entities.add({
                position,
                point: {
                    pixelSize: 12,
                    color: Cesium.Color.WHITE,
                    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                },
                customType: 'rect-corner',
                customData: { index },
            })
        )
    }
    updateCorners() {
        const positions = rectanglePositionsFromBounds(this.bounds)
        this.cornerEntities.forEach((entity, index) => {
            entity.position = positions[index]
        })
    }
    getPositions() {
        return rectanglePositionsFromBounds(this.bounds)
    }
    getCornerCartographics() {
        return rectanglePositionsFromBounds(this.bounds).map(position =>
            Cesium.Cartographic.fromCartesian(position)
        )
    }
    getPositionFromScreen(screenPosition) {
        const scene = this.viewer.scene
        const cartesian = scene.pickPosition(screenPosition)
        if (cartesian) return cartesian
        return scene.camera.pickEllipsoid(screenPosition, scene.globe.ellipsoid)
    }
    setStyle(style) {
        this.style = resolveStyle(style)
        if (this.rectangleEntity?.polygon) {
            this.rectangleEntity.polygon.material = this.style.fill
        }
        if (this.outlineEntity?.polyline) {
            this.outlineEntity.polyline.material = this.style.outline
        }
    }
    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
    }
    destroy() {
        if (this.dataSource) {
            this.dataSource.entities.removeAll()
            this.viewer.dataSources.remove(this.dataSource)
            this.dataSource = null
        }
        if (this.handler) {
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOWN)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE)
            this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_UP)
            this.handler.destroy()
            this.handler = null
        }
        this.tooltip?.destroy()
        this.tooltip = null
        this.bounds = null
        this.dragIndex = null
        this.fixedCorner = null
        this.cornerEntities = []
    }
}
applications/drone-command/src/views/areaManage/partition/shapeTools/shapeUtils.js
New file
@@ -0,0 +1,89 @@
import * as Cesium from 'cesium'
export const getBoundsFromCartesians = positions => {
    const cartographics = positions
        .map(pos => (pos?.longitude ? pos : Cesium.Cartographic.fromCartesian(pos)))
        .filter(Boolean)
    if (!cartographics.length) return null
    let west = cartographics[0].longitude
    let east = cartographics[0].longitude
    let south = cartographics[0].latitude
    let north = cartographics[0].latitude
    cartographics.forEach(carto => {
        west = Math.min(west, carto.longitude)
        east = Math.max(east, carto.longitude)
        south = Math.min(south, carto.latitude)
        north = Math.max(north, carto.latitude)
    })
    return { west, east, south, north }
}
export const getBoundsFromLngLats = points => {
    if (!points?.length) return null
    let west = points[0].lng
    let east = points[0].lng
    let south = points[0].lat
    let north = points[0].lat
    points.forEach(point => {
        west = Math.min(west, point.lng)
        east = Math.max(east, point.lng)
        south = Math.min(south, point.lat)
        north = Math.max(north, point.lat)
    })
    return {
        west: Cesium.Math.toRadians(west),
        east: Cesium.Math.toRadians(east),
        south: Cesium.Math.toRadians(south),
        north: Cesium.Math.toRadians(north),
    }
}
export const rectanglePositionsFromBounds = bounds => {
    if (!bounds) return []
    const { west, east, south, north } = bounds
    return [
        Cesium.Cartesian3.fromRadians(west, south),
        Cesium.Cartesian3.fromRadians(east, south),
        Cesium.Cartesian3.fromRadians(east, north),
        Cesium.Cartesian3.fromRadians(west, north),
    ]
}
export const getCenterFromBounds = bounds => {
    if (!bounds) return null
    return new Cesium.Cartographic(
        (bounds.west + bounds.east) / 2,
        (bounds.south + bounds.north) / 2,
        0
    )
}
export const getMetersBetween = (start, end) => {
    const geodesic = new Cesium.EllipsoidGeodesic(start, end)
    return geodesic.surfaceDistance
}
export const buildEllipsePositions = (centerCartesian, semiMajor, semiMinor, steps = 60) => {
    if (!centerCartesian || !semiMajor || !semiMinor) return []
    const positions = []
    const frame = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian)
    for (let i = 0; i < steps; i += 1) {
        const angle = (i / steps) * Math.PI * 2
        const x = Math.cos(angle) * semiMajor
        const y = Math.sin(angle) * semiMinor
        const local = new Cesium.Cartesian3(x, y, 0)
        const world = Cesium.Matrix4.multiplyByPoint(frame, local, new Cesium.Cartesian3())
        positions.push(world)
    }
    return positions
}
export const cartesianToLocal = (centerCartesian, positionCartesian) => {
    const frame = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian)
    const inverse = Cesium.Matrix4.inverse(frame, new Cesium.Matrix4())
    return Cesium.Matrix4.multiplyByPoint(inverse, positionCartesian, new Cesium.Cartesian3())
}
applications/task-work-order/env/.env.development
@@ -2,7 +2,7 @@
 # @Author       : yuan
 # @Date         : 2026-01-07 14:58:30
 # @LastEditors  : yuan
 # @LastEditTime : 2026-01-16 11:02:24
 # @LastEditTime : 2026-01-28 09:21:21
 # @FilePath     : \applications\task-work-order\env\.env.development
 # @Description  :
 # Copyright 2026 OBKoro1, All Rights Reserved.
@@ -15,7 +15,7 @@
#开发环境代理地址(推荐本地新建文件 .env.development.local 来进行覆盖)
# VITE_APP_URL = https://wrj.shuixiongit.com/api
VITE_APP_URL= http://192.168.1.204
VITE_APP_URL= http://192.168.1.227
applications/task-work-order/src/views/orderView/orderDataManage/appInnovation/index.vue
@@ -64,7 +64,7 @@
                        <template v-slot="{ row }">
                            <el-link  type="primary"  @click="openForm('view', row)">查看</el-link>
<!--                            <el-link  type="primary"  @click="openForm('edit', row)">编辑</el-link>-->
                            <el-link  type="primary"  @click="handleDelete(row)" v-if="!['1', '2'].includes(row.innovationStatus)">删除</el-link>
                            <el-link  type="primary"  @click="handleDelete(row)" v-if="!['1', '2'].includes(row.innovationStatus) && row.isTheSameDepartment">删除</el-link>
                        </template>
                    </el-table-column>
                </el-table>
@@ -96,7 +96,9 @@
    gdApplicationInnovationPageApi,
    gdApplicationInnovationRemoveApi,
} from '@/views/orderView/orderDataManage/appInnovation/appInnovationApi'
import { useStore } from 'vuex'
const store = useStore()
const userDepartment = store.state.user.userInfo?.dept_id
// 初始化查询参数
const initSearchParams = () => ({
    caseName: '', // 案例名称
@@ -128,6 +130,9 @@
    loading.value = true
    try {
        const res = await gdApplicationInnovationPageApi(searchParams.value)
        res?.data?.data?.records.forEach(item => {
            item.isTheSameDepartment = String(item.createDept) === String(userDepartment)
        })
        list.value = res?.data?.data?.records ?? []
        total.value = res?.data?.data?.total ?? 0
    } finally {
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/FormDiaLog.vue
@@ -323,10 +323,21 @@
}
// 文件移除
// const handleFileRemove = (file, fileListVal) => {
// uploadedFiles.value = fileListVal
// }
const handleFileRemove = (file, fileListVal) => {
uploadedFiles.value = fileListVal
  // 从展示列表中移除
  fileList.value = fileListVal
  // 从已上传列表中移除
  const index = uploadedFiles.value.findIndex(item =>
    item.attachId === file.attachId || item.name === file.name
  )
  if (index > -1) {
    uploadedFiles.value.splice(index, 1)
  }
}
// 上传前验证
const beforeFileUpload = file => {
    const allowedExtensions = ['.zip', '.rar', '.7z', '.tar', '.gz']
@@ -364,45 +375,53 @@
// 提交数据的通用函数
async function submitForm(objectionStatus) {
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) return
     // 只在新增时校验附件是否上传
    if (dialogMode.value === 'add' && uploadedFiles.value.length === 0) {
        ElMessage.warning('请上传附件')
        return
    }
    submitting.value = true
  const isValid = await formRef.value?.validate().catch(() => false)
  if (!isValid) return
  // 在编辑模式下,如果已有附件,可以不强制上传新附件
  if (dialogMode.value === 'add' && uploadedFiles.value.length === 0) {
    return ElMessage.warning('请上传附件')
  }
  submitting.value = true
    try {
        // 构建符合接口要求的提交数据
        const submitData = {
            // 编辑模式下传递id
            ...(formData.value.id && { id: formData.value.id }),
            title: formData.value.title,
            objectionType: formData.value.objectionType,
            submitter: formData.value.submitter,
            submitterContact: formData.value.submitterContact,
            catalogResourceName: formData.value.catalogResourceName,
            objectionDesc: formData.value.objectionDesc,
            objectionBasis: formData.value.objectionBasis,
            areaCode: formData.value.areaCode || '',
            handleUnit: formData.value.handleUnit || '',
          // 只在有附件时才构建附件列表
      ...(uploadedFiles.value.length > 0 && {
        attachmentList: uploadedFiles.value.map(file => ({
          attachId: file.attachId,
          attachName: file.originalName,
        })),
      }),
            objectionStatus: objectionStatus
        }
        await gdDataObjectionSubmitApi(submitData)
        ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功')
        visible.value = false
        emit('success')
    } finally {
        submitting.value = false
    }
  try {
    // 构建符合接口要求的提交数据
    const submitData = {
      // 编辑模式下传递id
      ...(formData.value.id && { id: formData.value.id }),
      title: formData.value.title,
      objectionType: formData.value.objectionType,
      submitter: formData.value.submitter,
      submitterContact: formData.value.submitterContact,
      catalogResourceName: formData.value.catalogResourceName,
      objectionDesc: formData.value.objectionDesc,
      objectionBasis: formData.value.objectionBasis,
      areaCode: formData.value.areaCode || '',
      handleUnit: formData.value.handleUnit || '',
      // 构建附件列表 - 使用当前已上传的文件(包括回显的)
            ...(uploadedFiles.value.length > 0 && {
                attachmentList: uploadedFiles.value.map(file => ({
                    attachId: file.attachId,
                    attachName: file.originalName,
                })),
            }),
      objectionStatus: objectionStatus
    }
    // 如果是编辑且没有修改附件,可以保留原有附件ID
    if (dialogMode.value === 'edit' && formData.value.attachIds) {
      // 这里可以根据业务需求决定是否保留原附件ID
      // 如果接口支持更新时重新上传附件ID列表,可以这样做
    }
    await gdDataObjectionSubmitApi(submitData)
    ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功')
    visible.value = false
    emit('success')
  } finally {
    submitting.value = false
  }
}
// 提交新增/编辑(保存)
@@ -439,12 +458,42 @@
}
// 加载详情
async function loadDetail() {
    if (!formData.value.id) return
    const res = await gdDataObjectionDetailApi({ id: formData.value.id })
    formData.value = res?.data?.data ?? {}
    if (formData.value.attachIds) {
        getAttachDetail(formData.value.attachIds)
    }
  if (!formData.value.id) return
  const res = await gdDataObjectionDetailApi({ id: formData.value.id })
  formData.value = res?.data?.data ?? {}
  // 处理附件回显
  if (formData.value.attachIds) {
    await getAttachDetail(formData.value.attachIds)
    // 回显到上传组件
    updateFileListForDisplay()
  }
}
// 更新文件列表用于显示
function updateFileListForDisplay() {
  fileList.value = []
  uploadedFiles.value = []
  if (attachmentDetailsList.value && attachmentDetailsList.value.length > 0) {
    attachmentDetailsList.value.forEach(attach => {
      const fileItem = {
        name: attach.originalName || attach.name,
        url: attach.link, // 用于预览
        attachId: attach.id, // 文件ID
        originalName: attach.originalName,
        link: attach.link
      }
      fileList.value.push({
        name: fileItem.name,
        url: fileItem.url,
        status: 'success' // 已上传状态
      })
      uploadedFiles.value.push(fileItem)
    })
  }
}
// 获取附件详情
async function getAttachDetail(attachIds) {
@@ -478,17 +527,22 @@
// 打开弹框
async function open({ mode = 'add', row } = {}) {
    dialogMode.value = mode
    // 重置文件列表
    fileList.value = []
    uploadedFiles.value = []
    // 重置表单数据
    formData.value = dialogMode.value === 'add' ? initForm() : row
    // 重置反馈表单数据
    feedbackFormData.value = initFeedbackForm()
    if (dialogMode.value !== 'add') {
        await loadDetail()
    }
  dialogMode.value = mode
  // 重置文件列表
  fileList.value = []
  uploadedFiles.value = []
  // 重置表单数据
  formData.value = dialogMode.value === 'add' ? initForm() : row
  // 重置反馈表单数据
  feedbackFormData.value = initFeedbackForm()
  if (dialogMode.value !== 'add') {
    // 如果是编辑,设置ID并加载详情
    if (row && row.id) {
      formData.value.id = row.id
    }
    await loadDetail()
  }
}
defineExpose({ open })
applications/task-work-order/src/views/orderView/orderDataManage/evaluate/index.vue
@@ -127,8 +127,6 @@
            item.isTheSameDepartment = String(item.createDept) === String(userDepartment)
        })
        list.value = res?.data?.data?.records ?? []
        console.log('list',list.value);
        total.value = res?.data?.data?.total ?? 0
    } finally {
        loading.value = false