吉安感知网项目-前端
shuishen
2026-01-29 76ad0866ab16079b054590cfd0dcc10c439f526b
feat:区域划分调整
7 files modified
1615 ■■■■■ changed files
applications/drone-command/env/.env.development 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/FormDiaLog copy.vue 671 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue 767 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawBufferCircleTool.js 12 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawEllipseTool.js 11 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditBufferCircleTool.js 45 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditEllipseTool.js 105 ●●●● 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-28 09:29:05
 # @LastEditTime : 2026-01-29 09:34:58
 # @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.227
VITE_APP_URL= http://192.168.1.204
#新大屏地址
VITE_APP_DASHBOARD_URL = 'https://wrj.shuixiongit.com/command-center-dashboard/'
applications/drone-command/src/views/areaManage/partition/FormDiaLog copy.vue
@@ -12,54 +12,6 @@
                    :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">
@@ -180,28 +132,6 @@
                        </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">
@@ -243,17 +173,15 @@
<script setup>
import { Close } from '@element-plus/icons-vue'
import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { computed, inject, nextTick, 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 { DrawPolygon } from '@/utils/cesium/DrawPolygon'
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'
@@ -291,39 +219,9 @@
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 drawPolygonExample
let pointList = []
let viewEntity
let shapeDisplaySource
const activeToolMode = ref('')
const rules = {
    areaName: fieldRules(true, 50),
@@ -408,24 +306,6 @@
        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 () {
@@ -454,191 +334,14 @@
    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 => {
    pointList = _.cloneDeep(data).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]],
        [...pointList.map(item => [item.longitude, item.latitude]), [pointList[0].longitude, pointList[0].latitude]],
    ])
    const center = turf.centerOfMass(polygon)
    const areaSqm = turf.area(polygon) // 平方米
@@ -647,152 +350,41 @@
    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 addPolygon () {
    drawPolygonExample = new DrawPolygon(viewer)
    drawPolygonExample.initHandler(viewer)
    drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
}
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()
            }
// 编辑面
function editPolygon () {
    if (!formData.value?.geom) return
    pointList = geomAnalysis(formData.value.geom)
    drawPolygonExample?.destroy()
    drawPolygonExample = new DrawPolygon(viewer)
    let pointList1 = _.cloneDeep(pointList)
    pointList1.pop()
    drawPolygonExample.initPolygon(
        viewer,
        pointList.map(item => ({ lng: item.longitude, lat: item.latitude }))
    )
    drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
    if (pointList1.length) {
        const result = pointList1.map(item => [item.longitude, item.latitude]).flat()
        const tempEntity = viewer.entities?.add({
            position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
            polyline: {
                positions: Cesium.Cartesian3.fromDegreesArray(result),
                clampToGround: true,
                width: 1,
                material: Cesium.Color.TRANSPARENT,
            },
        })
        if (tempEntity) {
            viewer.flyTo(tempEntity, { duration: 0 })
            viewer.entities.remove(tempEntity)
        }
        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()
    }
}
@@ -801,7 +393,7 @@
    if (!formData.value?.geom) return
    pointList = geomAnalysis(formData.value.geom)
    const result = pointList.map(item => [item.longitude, item.latitude]).flat()
    viewEntity = viewer.entities?.add({
    const mian = viewer.entities?.add({
        customType: 'control_group',
        position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
        polyline: {
@@ -811,7 +403,7 @@
            material: Cesium.Color.RED,
        },
    })
    viewer.flyTo(viewEntity, { duration: 0 })
    viewer.flyTo(mian, { duration: 0 })
}
// 获取派出所列表
@@ -857,32 +449,14 @@
    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
        addPolygon()
    } 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()
        editPolygon()
    } else {
        await loadDetail()
        viewPolygon()
@@ -896,170 +470,9 @@
    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>
<style scoped lang="scss"></style>
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
@@ -12,6 +12,54 @@
                    :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">
@@ -132,6 +180,28 @@
                        </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="类型">
                                <template v-slot="{ row }">
                                    {{ getDictLabel(row.areaType, dictObj.areaType) }}
                                </template>
                            </el-table-column>
                            <el-table-column prop="drawType" label="绘制类型">
                                <template v-slot="{ row }">
                                    {{ drawTypeLabelMap[row.drawType] || '-' }}
                                </template>
                            </el-table-column>
                            <el-table-column label="操作" class-name="operation-btns" width="120">
                                <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">
@@ -173,15 +243,17 @@
<script setup>
import { Close } from '@element-plus/icons-vue'
import { computed, inject, nextTick, onMounted, ref, watch } from '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 { DrawPolygon } from '@/utils/cesium/DrawPolygon'
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'
@@ -219,9 +291,39 @@
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 drawPolygonExample
let drawManager
let editManager
let activeTool
let pointList = []
let viewEntity
let shapeDisplaySource
const activeToolMode = ref('')
const rules = {
    areaName: fieldRules(true, 50),
@@ -274,6 +376,13 @@
// 提交新增/编辑
async function handleSubmit () {
    if (!shapeList.value.length) {
        ElMessage.warning('区域不可为空,请先绘制区域')
        return
    }
    console.log(shapeList.value, 1111111)
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) return
    if (dialogMode.value === 'add' && Number(formData.value.areaSize) < 0.5) {
@@ -306,6 +415,24 @@
        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 () {
@@ -334,14 +461,260 @@
    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 resolveLngLatPoint (point) {
    if (!point) return null
    const lng = point?.lng ?? point?.longitude
    const lat = point?.lat ?? point?.latitude
    if (Number.isFinite(Number(lng)) && Number.isFinite(Number(lat))) {
        return {
            lng: Number(lng),
            lat: Number(lat),
            height: Number.isFinite(Number(point?.height)) ? Number(point.height) : 0,
        }
    }
    if (!viewer || !Number.isFinite(point?.x) || !Number.isFinite(point?.y) || !Number.isFinite(point?.z)) {
        return null
    }
    const val = cartesian3Convert(point, viewer)
    return {
        lng: val.longitude,
        lat: val.latitude,
        height: Number.isFinite(val.height) ? val.height : 0,
    }
}
function resolveControlPoints (meta, drawType) {
    if (!meta) return []
    if (Array.isArray(meta?.controlPoints)) {
        return meta.controlPoints.map(resolveLngLatPoint).filter(Boolean)
    }
    if (drawType === 'ellipse') {
        return [meta.center, meta.majorPoint, meta.minorPoint].map(resolveLngLatPoint).filter(Boolean)
    }
    if (drawType === 'buffer') {
        return [meta.center, meta.radiusPoint1, meta.radiusPoint2, meta.radiusPoint3]
            .map(resolveLngLatPoint)
            .filter(Boolean)
    }
    return []
}
function buildShapePositions (points = []) {
    const normalized = points.map(normalizeShapePoint).filter(Boolean)
    return normalized.map(point =>
        Cesium.Cartesian3.fromDegrees(point.lng, point.lat, point.height)
    )
}
function getShapeDisplayPoints (shape) {
    if (Array.isArray(shape?.displayPoints) && shape.displayPoints.length) {
        return shape.displayPoints
    }
    return shape?.points || []
}
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 (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
        }
        if (!shape?.points?.length) return
        const positions = buildShapePositions(getShapeDisplayPoints(shape))
        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 => {
    pointList = _.cloneDeep(data).map(item => {
    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)
    const controlPoints = resolveControlPoints(meta, currentShapeType.value)
    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 displayPoints = ['ellipse', 'buffer'].includes(currentShapeType.value)
        ? _.cloneDeep(pointList)
        : null
    let shapeMeta = null
    if (currentShapeType.value === 'buffer' && bufferRadii?.length && centerLngLat) {
        shapeMeta = {
            bufferRadii,
            center: { lng: centerLngLat.longitude, lat: centerLngLat.latitude, height: centerLngLat.height ?? 0 },
        }
        if (controlPoints.length) {
            shapeMeta.controlPoints = controlPoints
        }
    } else if (currentShapeType.value === 'ellipse') {
        const centerPoint = resolveLngLatPoint(meta?.center)
        const majorPoint = resolveLngLatPoint(meta?.majorPoint)
        const minorPoint = resolveLngLatPoint(meta?.minorPoint)
        if (centerPoint || majorPoint || minorPoint) {
            shapeMeta = {
                center: centerPoint,
                majorPoint,
                minorPoint,
                semiMajor: Number.isFinite(Number(meta?.semiMajor)) ? Number(meta.semiMajor) : null,
                semiMinor: Number.isFinite(Number(meta?.semiMinor)) ? Number(meta.semiMinor) : null,
            }
            if (controlPoints.length) {
                shapeMeta.controlPoints = controlPoints
            }
        }
    }
    const shapePayload = {
        id: activeShapeId.value,
        areaType: formData.value.areaType,
        drawType: currentShapeType.value,
        points: ['ellipse', 'buffer'].includes(currentShapeType.value) && controlPoints.length
            ? controlPoints
            : _.cloneDeep(pointList),
        displayPoints,
        meta: shapeMeta,
    }
    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([
        [...pointList.map(item => [item.longitude, item.latitude]), [pointList[0].longitude, pointList[0].latitude]],
        [...points.map(item => [item.longitude, item.latitude]), [points[0].longitude, points[0].latitude]],
    ])
    const center = turf.centerOfMass(polygon)
    const areaSqm = turf.area(polygon) // 平方米
@@ -350,41 +723,163 @@
    formData.value.latitude = center.geometry.coordinates[1]
}
// 新增模式绘制
function addPolygon () {
    drawPolygonExample = new DrawPolygon(viewer)
    drawPolygonExample.initHandler(viewer)
    drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
function getDetectAreaTypeKey () {
    const areaTypeOptions = Array.isArray(dictObj?.value?.areaType)
        ? dictObj.value.areaType
        : dictObj?.areaType
    console.log(areaTypeOptions, 1)
    if (!Array.isArray(areaTypeOptions)) return ''
    const target = areaTypeOptions.find(item => item?.dictKey === '1')
    return target?.dictKey ?? ''
}
// 编辑面
function editPolygon () {
    if (!formData.value?.geom) return
    pointList = geomAnalysis(formData.value.geom)
    drawPolygonExample?.destroy()
    drawPolygonExample = new DrawPolygon(viewer)
    let pointList1 = _.cloneDeep(pointList)
    pointList1.pop()
    drawPolygonExample.initPolygon(
        viewer,
        pointList.map(item => ({ lng: item.longitude, lat: item.latitude }))
    )
    drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
    if (pointList1.length) {
        const result = pointList1.map(item => [item.longitude, item.latitude]).flat()
        const tempEntity = viewer.entities?.add({
            position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
            polyline: {
                positions: Cesium.Cartesian3.fromDegreesArray(result),
                clampToGround: true,
                width: 1,
                material: Cesium.Color.TRANSPARENT,
            },
        })
        if (tempEntity) {
            viewer.flyTo(tempEntity, { duration: 0 })
            viewer.entities.remove(tempEntity)
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)
    }
    if (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
    const displayPoints = getShapeDisplayPoints(row)
    pointList = _.cloneDeep(displayPoints)
    clearActiveTool()
    const editPayload = ['buffer', 'ellipse'].includes(row.drawType)
        ? { points: displayPoints, 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()
    }
}
@@ -393,7 +888,7 @@
    if (!formData.value?.geom) return
    pointList = geomAnalysis(formData.value.geom)
    const result = pointList.map(item => [item.longitude, item.latitude]).flat()
    const mian = viewer.entities?.add({
    viewEntity = viewer.entities?.add({
        customType: 'control_group',
        position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
        polyline: {
@@ -403,7 +898,7 @@
            material: Cesium.Color.RED,
        },
    })
    viewer.flyTo(mian, { duration: 0 })
    viewer.flyTo(viewEntity, { duration: 0 })
}
// 获取派出所列表
@@ -449,14 +944,32 @@
    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') {
        addPolygon()
        // default no draw mode
    } else if (dialogMode.value === 'edit') {
        await loadDetail()
        editPolygon()
        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()
@@ -467,7 +980,10 @@
onMounted(() => {
    getPoliceStationList()
    getDeviceList()
})
onBeforeUnmount(() => {
    hideTypePanel()
})
defineExpose({
@@ -475,4 +991,169 @@
})
</script>
<style scoped lang="scss"></style>
<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 {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    padding-right: 1.2rem;
    font-family: Source Han Sans CN, Source Han Sans CN;
    font-weight: 500;
    font-size: 1.4rem;
    color: #D4D5D7;
    font-style: normal;
    text-transform: none;
    box-sizing: border-box;
}
</style>
applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawBufferCircleTool.js
@@ -18,6 +18,9 @@
        this.circleLevel1OutlineEntity = null
        this.circleLevel2OutlineEntity = null
        this.circleLevel3OutlineEntity = null
        this.radiusPoint1 = null
        this.radiusPoint2 = null
        this.radiusPoint3 = null
        this.isDrawing = false
        this.drawStep = 0
        this.isCompleted = false
@@ -56,6 +59,7 @@
            this.radiusLevel1 = radius
            this.radiusLevel2 = radius
            this.radiusLevel3 = radius
            this.radiusPoint1 = position
            this.drawStep = 2
            this.tooltip.show('单击设置二级缓冲圆', click.position)
            return
@@ -65,6 +69,7 @@
            const radius = Math.max(1, this.getSurfaceDistance(this.centerCartesian, position))
            this.radiusLevel2 = Math.max(radius, this.radiusLevel1)
            this.radiusLevel3 = this.radiusLevel2
            this.radiusPoint2 = position
            this.drawStep = 3
            this.tooltip.show('单击设置三级缓冲圆', click.position)
            return
@@ -73,6 +78,7 @@
        if (this.drawStep === 3) {
            const radius = Math.max(1, this.getSurfaceDistance(this.centerCartesian, position))
            this.radiusLevel3 = Math.max(radius, this.radiusLevel2)
            this.radiusPoint3 = position
            this.isDrawing = false
            this.drawStep = 0
            this.isCompleted = true
@@ -82,6 +88,9 @@
                meta: {
                    center: this.centerCartesian,
                    bufferRadii: [this.radiusLevel1, this.radiusLevel2, this.radiusLevel3],
                    controlPoints: [this.centerCartesian, this.radiusPoint1, this.radiusPoint2, this.radiusPoint3].filter(
                        Boolean
                    ),
                },
            })
            this.tooltip.hide()
@@ -229,6 +238,9 @@
        this.radiusLevel1 = 0
        this.radiusLevel2 = 0
        this.radiusLevel3 = 0
        this.radiusPoint1 = null
        this.radiusPoint2 = null
        this.radiusPoint3 = null
        this.isDrawing = false
        this.drawStep = 0
        this.isCompleted = false
applications/drone-command/src/views/areaManage/partition/shapeTools/draw/DrawEllipseTool.js
@@ -70,7 +70,16 @@
            this.drawStep = 0
            this.isCompleted = true
            const positions = buildEllipsePositions(this.centerCartesian, this.semiMajor, this.semiMinor)
            this.notify('getPolygonPositions', positions)
            this.notify('getPolygonPositions', {
                positions,
                meta: {
                    center: this.centerCartesian,
                    majorPoint: this.majorPoint,
                    minorPoint: this.minorPoint,
                    semiMajor: this.semiMajor,
                    semiMinor: this.semiMinor,
                },
            })
            this.tooltip.hide()
            this.clearPreviewEntities()
        }
applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditBufferCircleTool.js
@@ -9,6 +9,26 @@
        lat: point.lat ?? point.latitude,
    }))
const resolveLngLat = point => {
    if (!point) return null
    const lng = point.lng ?? point.longitude
    const lat = point.lat ?? point.latitude
    if (Number.isFinite(lng) && Number.isFinite(lat)) {
        return { lng: Number(lng), lat: Number(lat), height: Number(point.height) || 0 }
    }
    return null
}
const resolveCartesian = point => {
    if (!point) return null
    if (Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z)) {
        return new Cesium.Cartesian3(point.x, point.y, point.z)
    }
    const lngLat = resolveLngLat(point)
    if (!lngLat) return null
    return Cesium.Cartesian3.fromDegrees(lngLat.lng, lngLat.lat, lngLat.height || 0)
}
export class EditBufferCircleTool extends ToolBase {
    constructor(viewer) {
        super(viewer)
@@ -34,10 +54,15 @@
    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 metaCenter = resolveCartesian(data?.meta?.center)
        if (metaCenter) {
            this.centerCartesian = metaCenter
        } else {
            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
@@ -124,6 +149,12 @@
            meta: {
                center: this.centerCartesian,
                bufferRadii: [this.radiusLevel1, this.radiusLevel2, this.radiusLevel3],
                controlPoints: [
                    this.centerCartesian,
                    this.getRadiusPoint(this.radiusLevel1),
                    this.getRadiusPoint(this.radiusLevel2),
                    this.getRadiusPoint(this.radiusLevel3),
                ],
            },
        })
    }
@@ -256,6 +287,12 @@
            meta: {
                center: this.centerCartesian,
                bufferRadii: [this.radiusLevel1, this.radiusLevel2, this.radiusLevel3],
                controlPoints: [
                    this.centerCartesian,
                    this.getRadiusPoint(this.radiusLevel1),
                    this.getRadiusPoint(this.radiusLevel2),
                    this.getRadiusPoint(this.radiusLevel3),
                ],
            },
        }
    }
applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditEllipseTool.js
@@ -24,6 +24,36 @@
        lat: point.lat ?? point.latitude,
    }))
const resolveLngLat = point => {
    if (!point) return null
    const lng = point.lng ?? point.longitude
    const lat = point.lat ?? point.latitude
    if (Number.isFinite(lng) && Number.isFinite(lat)) {
        return { lng: Number(lng), lat: Number(lat), height: Number(point.height) || 0 }
    }
    return null
}
const resolveCartesian = point => {
    if (!point) return null
    if (Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z)) {
        return new Cesium.Cartesian3(point.x, point.y, point.z)
    }
    const lngLat = resolveLngLat(point)
    if (!lngLat) return null
    return Cesium.Cartesian3.fromDegrees(lngLat.lng, lngLat.lat, lngLat.height || 0)
}
const resolveCartographic = point => {
    if (!point) return null
    if (Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z)) {
        return Cesium.Cartographic.fromCartesian(point)
    }
    const lngLat = resolveLngLat(point)
    if (!lngLat) return null
    return Cesium.Cartographic.fromDegrees(lngLat.lng, lngLat.lat, lngLat.height || 0)
}
export class EditEllipseTool extends ToolBase {
    constructor(viewer, options = {}) {
        super(viewer)
@@ -40,22 +70,43 @@
        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)
    start(data = []) {
        const inputPoints = Array.isArray(data?.points) ? data.points : data
        const meta = data?.meta || null
        const metaCenter = resolveCartesian(meta?.center)
        if (metaCenter) {
            this.centerCartesian = metaCenter
            const centerCarto = Cesium.Cartographic.fromCartesian(metaCenter)
            const majorFromMeta = Number.isFinite(meta?.semiMajor) ? Number(meta.semiMajor) : 0
            const minorFromMeta = Number.isFinite(meta?.semiMinor) ? Number(meta.semiMinor) : 0
            const majorPoint = resolveCartographic(meta?.majorPoint)
            const minorPoint = resolveCartographic(meta?.minorPoint)
            const majorDistance =
                majorFromMeta || (majorPoint ? getMetersBetween(centerCarto, majorPoint) : 0)
            const minorDistance =
                minorFromMeta || (minorPoint ? getMetersBetween(centerCarto, minorPoint) : 0)
            if (majorDistance > 0) this.semiMajor = Math.max(1, majorDistance)
            if (minorDistance > 0) this.semiMinor = Math.max(1, minorDistance)
            if (this.semiMinor > this.semiMajor) this.semiMinor = this.semiMajor
        }
        if (!this.centerCartesian || !this.semiMajor || !this.semiMinor) {
            const normalized = normalizePoints(inputPoints)
            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)
@@ -113,7 +164,16 @@
        this.dragType = null
        this.enableMapControl()
        const positions = buildEllipsePositions(this.centerCartesian, this.semiMajor, this.semiMinor)
        this.notify('getPolygonPositions', positions)
        this.notify('getPolygonPositions', {
            positions,
            meta: {
                center: this.centerCartesian,
                majorPoint: this.getAxisPoint(this.semiMajor, 0),
                minorPoint: this.getAxisPoint(0, this.semiMinor),
                semiMajor: this.semiMajor,
                semiMinor: this.semiMinor,
            },
        })
    }
    createEntities() {
@@ -172,7 +232,16 @@
    getPositions() {
        if (!this.centerCartesian) return []
        return buildEllipsePositions(this.centerCartesian, this.semiMajor, this.semiMinor)
        return {
            positions: buildEllipsePositions(this.centerCartesian, this.semiMajor, this.semiMinor),
            meta: {
                center: this.centerCartesian,
                majorPoint: this.getAxisPoint(this.semiMajor, 0),
                minorPoint: this.getAxisPoint(0, this.semiMinor),
                semiMajor: this.semiMajor,
                semiMinor: this.semiMinor,
            },
        }
    }
    getAxisPoint(eastMeters, northMeters) {