吉安感知网项目-前端
chenyao
2026-01-30 fb39a7bb69cc55d2e1ce61d47e0677aa40540f95
Merge remote-tracking branch 'origin/master'
29 files modified
13 files renamed
14 files added
5404 ■■■■■ changed files
applications/drone-command/env/.env.development 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/assets/images/areaManage/positionIcon.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/areaMap/buffer.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/areaMap/ellipse.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/areaMap/polygon.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/areaMap/rect.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/areaMap/trash.png patch | view | raw | blame | history
applications/drone-command/src/components/map-container/device-map-container.vue 424 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/styles/common/cockpit.scss 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/deviceRange.js 117 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/DrawManager.js patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/EditManager.js patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/ToolBase.js patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/Tooltip.js patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/draw/DrawBufferCircleTool.js 12 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/draw/DrawEllipseTool.js 11 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/draw/DrawPolygonTool.js patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/draw/DrawRectangleTool.js patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/edit/EditBufferCircleTool.js 45 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/edit/EditEllipseTool.js 105 ●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/edit/EditPolygonTool.js 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/edit/EditRectangleTool.js patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/index.js 13 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/shapeTools/shapeUtils.js patch | view | raw | blame | history
applications/drone-command/src/views/api.txt 148 ●●●●● 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 1042 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/index.vue 30 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/precinctInfo/FormDiaLog.vue 37 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue 439 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneConfig/index.vue 13 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneManage/FormDiaLog.vue 553 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneManage/index.vue 172 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneManage/sceneManageApi.js 38 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/audioCall.vue 600 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/LegendBar.vue 33 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/permissionManage/permissionDept/FormDiaLog.vue 3 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/permissionManage/permissionDept/ShareDiaLog.vue 169 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/permissionManage/permissionDept/fwDevicePerShareApi.js 28 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/permissionManage/permissionDept/index.vue 39 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/appInnovation/FormDiaLog.vue 1 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/FormDiaLog.vue 133 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/index.vue 2 ●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/ApplyViewDialog.vue 26 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/FormDiaLog.vue 90 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/index.vue 22 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/RefuseOrderDialog1.vue 2 ●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue 88 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/index.vue 8 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue 153 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/orderManage/RefuseOrderDialog.vue 2 ●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/orderManage/index.vue 43 ●●●●● patch | view | raw | blame | history
packages/constants/areaStyles.js 36 ●●●●● patch | view | raw | blame | history
packages/constants/index.js 1 ●●●● patch | view | raw | blame | history
uniapps/work-app/src/pages/login/index.vue 4 ●●●● patch | view | raw | blame | history
uniapps/work-app/src/subPackages/workDetail/index.vue 36 ●●●●● 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-30 09:29:25
 # @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.168
#新大屏地址
VITE_APP_DASHBOARD_URL = 'https://wrj.shuixiongit.com/command-center-dashboard/'
applications/drone-command/src/assets/images/areaManage/positionIcon.png
applications/drone-command/src/assets/images/areaMap/buffer.png
applications/drone-command/src/assets/images/areaMap/ellipse.png
applications/drone-command/src/assets/images/areaMap/polygon.png
applications/drone-command/src/assets/images/areaMap/rect.png
applications/drone-command/src/assets/images/areaMap/trash.png
applications/drone-command/src/components/map-container/device-map-container.vue
@@ -47,9 +47,9 @@
<script setup>
import * as Cesium from 'cesium'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { geomAnalysis } from '@ztzf/utils'
import { fwDefenseZonePageApi } from '@/views/areaManage/defenseZone/defenseZoneApi'
import { fwAreaDividePageApi } from '@/views/areaManage/partition/partitionApi'
import { buildEllipsePositions } from '@/utils/cesium/shapeTools'
import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
import { fwAreaDivideListApi } from '@/views/areaManage/partition/partitionApi'
import { fwDefenseSceneListApi } from '@/views/areaManage/sceneConfig/sceneConfigApi'
import { cockpitAggregationApi } from '@/api/dataCockpit'
import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
@@ -61,17 +61,18 @@
import DevicePopup from './components/DevicePopup.vue'
import DronePopup from './components/DronePopup.vue'
import {
    RING_STYLES,
    addDeviceRings,
    createDroneTrackMaterial,
    createRadialGradientMaterial,
    getTexturedVertexFormat,
} from './device-map-materials'
import { getPointPositionsHeight } from '@/utils/cesium/mapUtil'
import { createDeviceRangePrimitiveWithHeight } from '@/utils/cesium/deviceRange'
import dzIcon from '@/assets/images/dataCockpit/map/dz-map-layer.png'
import yxIcon from '@/assets/images/dataCockpit/map/yx-map-layer.png'
const CLUSTER_HEIGHT = 100000
const DETAIL_HEIGHT = 10000
const POLYGON_HEIGHT_M = 0.2
const props = defineProps({
    onlineDevices: {
@@ -97,7 +98,6 @@
})
const emit = defineEmits(['droneSignal', 'droneCounter', 'droneFavorite'])
const DEFAULT_ZONE_PAGE_SIZE = 999
const mapRef = ref(null)
let viewer = null
@@ -108,10 +108,8 @@
let deviceBillboardCollection = null
let deviceRingOutlinePrimitives = []
let deviceRingFillPrimitives = []
let defenseZonePrimitive = null
let defenseZoneOutlinePrimitive = null
let partitionPrimitive = null
let partitionOutlinePrimitive = null
let partitionFillPrimitives = []
let partitionOutlinePrimitives = []
let aggregationSource = null
let commandPostBillboardCollection = null
let droneTrackBillboardCollection = null
@@ -250,6 +248,7 @@
    return range
}
const ensureCockpitPrimitiveLayer = () => {
    if (!viewer) return
    if (!cockpitPrimitiveLayer) {
@@ -280,10 +279,12 @@
    if (!viewer) return
    ensureCockpitPrimitiveLayer()
    cockpitPrimitiveLayer.removeAll(false)
    if (defenseZonePrimitive) cockpitPrimitiveLayer.add(defenseZonePrimitive)
    if (defenseZoneOutlinePrimitive) cockpitPrimitiveLayer.add(defenseZoneOutlinePrimitive)
    if (partitionPrimitive) cockpitPrimitiveLayer.add(partitionPrimitive)
    if (partitionOutlinePrimitive) cockpitPrimitiveLayer.add(partitionOutlinePrimitive)
    if (partitionFillPrimitives.length) {
        partitionFillPrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
    }
    if (partitionOutlinePrimitives.length) {
        partitionOutlinePrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
    }
    if (deviceRingFillPrimitives.length) {
        deviceRingFillPrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
    }
@@ -323,27 +324,16 @@
    }
}
const clearDefenseZoneEntities = () => {
    if (!viewer) return
    if (defenseZonePrimitive) {
        removeCockpitPrimitive(defenseZonePrimitive)
        defenseZonePrimitive = null
    }
    if (defenseZoneOutlinePrimitive) {
        removeCockpitPrimitive(defenseZoneOutlinePrimitive)
        defenseZoneOutlinePrimitive = null
    }
}
const clearPartitionEntities = () => {
    if (!viewer) return
    if (partitionPrimitive) {
        removeCockpitPrimitive(partitionPrimitive)
        partitionPrimitive = null
    if (partitionFillPrimitives.length) {
        partitionFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
        partitionFillPrimitives = []
    }
    if (partitionOutlinePrimitive) {
        removeCockpitPrimitive(partitionOutlinePrimitive)
        partitionOutlinePrimitive = null
    if (partitionOutlinePrimitives.length) {
        partitionOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
        partitionOutlinePrimitives = []
    }
}
@@ -378,10 +368,16 @@
const setDetailVisibility = visible => {
    detailVisible.value = visible
    if (defenseZonePrimitive) defenseZonePrimitive.show = visible
    if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = visible
    if (partitionPrimitive) partitionPrimitive.show = visible
    if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = visible
    if (partitionFillPrimitives.length) {
        partitionFillPrimitives.forEach(primitive => {
            primitive.show = visible
        })
    }
    if (partitionOutlinePrimitives.length) {
        partitionOutlinePrimitives.forEach(primitive => {
            primitive.show = visible
        })
    }
    if (commandPostBillboardCollection) commandPostBillboardCollection.show = visible
    if (deviceBillboardCollection) deviceBillboardCollection.show = visible
    if (deviceRingFillPrimitives.length) {
@@ -701,7 +697,7 @@
    reorderCockpitPrimitives()
}
const renderDeviceEntities = devices => {
const renderDeviceEntities = async devices => {
    if (!viewer) return
    ensureCockpitPrimitiveLayer()
    ensureDeviceCollections()
@@ -716,14 +712,14 @@
    }
    devicePickMap.clear()
    deviceBillboardCollection.show = detailVisible.value
    const ringFillInstancesByStyle = RING_STYLES.map(() => [])
    const ringOutlineInstancesByStyle = RING_STYLES.map(() => [])
    const deviceEntries = []
    const devicePositions = []
    devices.forEach((item, index) => {
        const position = getDevicePosition(item)
        if (!position) return
        const entityId = `online-device-${item.id ?? index}-${index}`
        const rangeMeters = getDeviceRange(item)
        addDeviceRings(position, ringFillInstancesByStyle, ringOutlineInstancesByStyle, rangeMeters)
        devicePositions.push({ lng: position.longitude, lat: position.latitude })
        const billboard = deviceBillboardCollection.add({
            position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
            image: equipmentIcon,
@@ -733,161 +729,249 @@
        })
        billboard.id = entityId
        devicePickMap.set(entityId, { data: item, billboard })
        deviceEntries.push({ position, rangeMeters, billboard })
    })
    if (selectedTargetType.value === 'device' && selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) {
        closePopup()
    }
    ringFillInstancesByStyle.forEach((instances, index) => {
        if (!instances.length) return
        const ring = RING_STYLES[index]
        const alpha = ring.alpha ?? 0.64
        const innerRatio =
            typeof ring.innerRatio === 'number'
                ? ring.innerRatio
                : ring.inner && ring.outer
                    ? Math.min(Math.max(ring.inner / ring.outer, 0), 0.9)
                    : 0
        const material = createRadialGradientMaterial(
            Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(alpha),
            Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(alpha),
            {
                gamma: 1.7,
                innerCutoff: innerRatio,
    if (deviceEntries.length) {
        const heights = await getPointPositionsHeight(devicePositions, viewer)
        deviceEntries.forEach((entry, index) => {
            const height = Number(heights?.[index]?.ASL)
            const centerHeight = Number.isFinite(height) ? height : 0
            if (entry.billboard) {
                entry.billboard.position = Cesium.Cartesian3.fromDegrees(
                    entry.position.longitude,
                    entry.position.latitude,
                    centerHeight
                )
            }
        )
        const primitive = new Cesium.GroundPrimitive({
            geometryInstances: instances,
            appearance: new Cesium.MaterialAppearance({
                material,
                translucent: true,
            }),
            if (!Number.isFinite(entry.rangeMeters) || entry.rangeMeters <= 0) return
            const primitive = createDeviceRangePrimitiveWithHeight(entry.position, entry.rangeMeters, centerHeight)
            if (!primitive) return
            primitive.show = detailVisible.value
            addCockpitPrimitive(primitive)
            deviceRingFillPrimitives.push(primitive)
        })
        primitive.show = detailVisible.value
        addCockpitPrimitive(primitive)
        deviceRingFillPrimitives.push(primitive)
    })
    ringOutlineInstancesByStyle.forEach(instances => {
        if (!instances.length) return
        const primitive = new Cesium.GroundPolylinePrimitive({
            geometryInstances: instances,
            appearance: new Cesium.PolylineColorAppearance(),
        })
        primitive.show = detailVisible.value
        addCockpitPrimitive(primitive)
        deviceRingOutlinePrimitives.push(primitive)
    })
    }
    reorderCockpitPrimitives()
}
const getDefenseZonePositions = geom => {
    const points = geomAnalysis(geom)
    if (points.length < 3) return []
    const first = points[0]
    const last = points[points.length - 1]
    const list =
        first && last && first.longitude === last.longitude && first.latitude === last.latitude
            ? points.slice(0, -1)
            : points
    return list.map(item => Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude))
const elevatePositions = async (positions, height = POLYGON_HEIGHT_M) => {
    if (!Array.isArray(positions) || !positions.length) return []
    if (!viewer) return positions
    const points = positions
        .map(pos => {
            const carto = Cesium.Cartographic.fromCartesian(pos)
            if (!carto) return null
            return {
                lng: Cesium.Math.toDegrees(carto.longitude),
                lat: Cesium.Math.toDegrees(carto.latitude),
            }
        })
        .filter(Boolean)
    if (!points.length) return positions
    const heights = await getPointPositionsHeight(points, viewer)
    return heights.map(item => Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude, (item.ASL ?? 0) + height))
}
const buildZonePrimitives = (zones, lineColor, fillColor1, fillColor2) => {
    if (!viewer) return { primitive: null, outlinePrimitive: null }
    const polygonInstances = []
    const lineInstances = []
    const texturedVertexFormat = getTexturedVertexFormat()
        ; (zones || []).forEach((zone, index) => {
            if (!zone?.geom) return
            const positions = getDefenseZonePositions(zone.geom)
            if (!positions.length) return
            const polygon = new Cesium.PolygonGeometry({
                polygonHierarchy: new Cesium.PolygonHierarchy(positions),
                vertexFormat: texturedVertexFormat,
const getAreaTypeStyle = areaType => {
    return AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
}
const buildPartitionPrimitives = async shapes => {
    if (!viewer) return { primitives: [], outlinePrimitives: [] }
    const vertexFormat = getTexturedVertexFormat()
    const groups = new Map()
    const tasks = []
        ; (shapes || []).forEach(shape => {
            if (!shape?.positions || shape.positions.length < 3) return
            const outlineColor = shape.outlineColor
            const fillColor = shape.fillColor
            const key = `${outlineColor.toCssColorString()}|${fillColor.toCssColorString()}`
            if (!groups.has(key)) {
                groups.set(key, { outlineColor, fillColor, polygonInstances: [], lineInstances: [] })
            }
            const group = groups.get(key)
            const positions = shape.positions
            tasks.push(
                elevatePositions(positions).then(elevatedPositions => {
                    if (!elevatedPositions.length) return
                    const polygon = new Cesium.PolygonGeometry({
                        polygonHierarchy: new Cesium.PolygonHierarchy(elevatedPositions),
                        perPositionHeight: true,
                        vertexFormat,
                    })
                    group.polygonInstances.push(
                        new Cesium.GeometryInstance({
                            geometry: polygon,
                        })
                    )
                    const linePositions =
                        elevatedPositions.length > 1
                            ? [...elevatedPositions, elevatedPositions[0]]
                            : elevatedPositions
                    group.lineInstances.push(
                        new Cesium.GeometryInstance({
                            geometry: new Cesium.PolylineGeometry({
                                positions: linePositions,
                                width: 2,
                            }),
                            attributes: {
                                color: Cesium.ColorGeometryInstanceAttribute.fromColor(outlineColor),
                            },
                        })
                    )
                })
            )
        })
    if (tasks.length) await Promise.all(tasks)
    const primitives = []
    const outlinePrimitives = []
    groups.forEach(group => {
        if (group.polygonInstances.length) {
            const material = createRadialGradientMaterial(group.outlineColor, group.fillColor, {
                gamma: 1.7,
                innerCutoff: 0,
            })
            polygonInstances.push(
                new Cesium.GeometryInstance({
                    geometry: polygon,
                })
            )
            const linePositions = positions.length > 1 ? [...positions, positions[0]] : positions
            lineInstances.push(
                new Cesium.GeometryInstance({
                    geometry: new Cesium.GroundPolylineGeometry({
                        positions: linePositions,
                        width: 2,
                    }),
                    attributes: {
                        color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                            Cesium.Color.fromCssColorString(lineColor)
                        ),
                    },
                })
            )
        })
    let primitive = null
    if (polygonInstances.length) {
        const baseColor1 = Cesium.Color.fromCssColorString(fillColor1)
        const baseColor2 = Cesium.Color.fromCssColorString(fillColor2)
        const material = createRadialGradientMaterial(baseColor1.withAlpha(0.64), baseColor2.withAlpha(0.64))
        primitive = new Cesium.GroundPrimitive({
            geometryInstances: polygonInstances,
            appearance: new Cesium.MaterialAppearance({
                material,
                translucent: true,
            }),
        })
        addCockpitPrimitive(primitive)
    }
    let outlinePrimitive = null
    if (lineInstances.length) {
        outlinePrimitive = new Cesium.GroundPolylinePrimitive({
            geometryInstances: lineInstances,
            appearance: new Cesium.PolylineColorAppearance(),
        })
        addCockpitPrimitive(outlinePrimitive)
    }
    return { primitive, outlinePrimitive }
            const primitive = new Cesium.Primitive({
                geometryInstances: group.polygonInstances,
                appearance: new Cesium.MaterialAppearance({
                    material,
                    translucent: true,
                }),
            })
            addCockpitPrimitive(primitive)
            primitives.push(primitive)
        }
        if (group.lineInstances.length) {
            const outlinePrimitive = new Cesium.Primitive({
                geometryInstances: group.lineInstances,
                appearance: new Cesium.PolylineColorAppearance(),
            })
            addCockpitPrimitive(outlinePrimitive)
            outlinePrimitives.push(outlinePrimitive)
        }
    })
    return { primitives, outlinePrimitives }
}
const renderDefenseZones = zones => {
    if (!viewer) return
    clearDefenseZoneEntities()
    const result = buildZonePrimitives(zones, '#72F3FF', '#72F3FF', '#0A7C88')
    defenseZonePrimitive = result.primitive
    defenseZoneOutlinePrimitive = result.outlinePrimitive
    if (defenseZonePrimitive) defenseZonePrimitive.show = detailVisible.value
    if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = detailVisible.value
    reorderCockpitPrimitives()
const parseGeomJson = geomJson => {
    if (!geomJson) return null
    if (typeof geomJson === 'object') return geomJson
    if (typeof geomJson !== 'string') return null
    const trimmed = geomJson.trim()
    if (!trimmed) return null
    try {
        return JSON.parse(trimmed)
    } catch (error) {}
    return null
}
const renderPartitions = zones => {
const normalizeShapePoint = point => {
    if (!point) return null
    const lng = point?.lng ?? point?.longitude
    const lat = point?.lat ?? point?.latitude
    const height = Number.isFinite(Number(point?.height)) ? Number(point.height) : 0
    if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
    return { lng: Number(lng), lat: Number(lat), height }
}
const buildShapePositions = (points = []) => {
    const normalized = points.map(normalizeShapePoint).filter(Boolean)
    return normalized.map(point => Cesium.Cartesian3.fromDegrees(point.lng, point.lat, point.height))
}
const getShapeDisplayPoints = shape => {
    if (Array.isArray(shape?.displayPoints) && shape.displayPoints.length) {
        return shape.displayPoints
    }
    return shape?.points || []
}
const resolvePartitionShapes = areas => {
    const shapes = []
        ; (areas || []).forEach(area => {
            const extList = Array.isArray(area?.fwAreaDivideExtList) ? area.fwAreaDivideExtList : []
            extList.forEach((item, index) => {
                const isShapePayload = item?.drawType || item?.points
                const parsed = isShapePayload ? item : parseGeomJson(item?.geomJson)
                if (!parsed) return
                const shape = {
                    id: parsed?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
                    drawType: parsed?.drawType ?? 'polygon',
                    areaType: parsed?.areaType ?? item?.areaTypeKey ?? item?.areaType ?? '',
                    points: Array.isArray(parsed?.points) ? parsed.points : [],
                    displayPoints: Array.isArray(parsed?.displayPoints) ? parsed.displayPoints : null,
                    meta: parsed?.meta ?? null,
                }
                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 radii = shape.meta.bufferRadii
                        .map(radius => Number(radius))
                        .filter(radius => Number.isFinite(radius) && radius > 0)
                    radii.forEach((radius, levelIndex) => {
                        const positions = buildEllipsePositions(centerCartesian, radius, radius)
                    const style = BUFFER_LEVEL_STYLES[levelIndex] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
                        if (positions.length >= 3) {
                            shapes.push({
                                positions,
                                fillColor: style.fill,
                                outlineColor: style.outline,
                            })
                        }
                    })
                    return
                }
                const positions = buildShapePositions(getShapeDisplayPoints(shape))
                if (positions.length >= 3) {
                    const style = getAreaTypeStyle(shape.areaType)
                    shapes.push({
                        positions,
                        fillColor: style.fill,
                        outlineColor: style.outline,
                    })
                }
            })
        })
    return shapes
}
const renderPartitions = async zones => {
    if (!viewer) return
    clearPartitionEntities()
    const result = buildZonePrimitives(zones, '#FFD772', '#FFD772', '#A86B00')
    partitionPrimitive = result.primitive
    partitionOutlinePrimitive = result.outlinePrimitive
    if (partitionPrimitive) partitionPrimitive.show = detailVisible.value
    if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = detailVisible.value
    reorderCockpitPrimitives()
}
const loadDefenseZones = async () => {
    if (!viewer) return
    try {
        const res = await fwDefenseZonePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE })
        renderDefenseZones(res?.data?.data?.records ?? [])
    } catch (error) {
        renderDefenseZones([])
    const shapes = resolvePartitionShapes(zones)
    const result = await buildPartitionPrimitives(shapes)
    partitionFillPrimitives = result.primitives
    partitionOutlinePrimitives = result.outlinePrimitives
    if (partitionFillPrimitives.length) {
        partitionFillPrimitives.forEach(primitive => {
            primitive.show = detailVisible.value
        })
    }
    if (partitionOutlinePrimitives.length) {
        partitionOutlinePrimitives.forEach(primitive => {
            primitive.show = detailVisible.value
        })
    }
    reorderCockpitPrimitives()
}
const loadPartitions = async () => {
    if (!viewer) return
    try {
        const res = await fwAreaDividePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE })
        renderPartitions(res?.data?.data?.records ?? [])
        const res = await fwAreaDivideListApi()
        await renderPartitions(res?.data?.data ?? [])
    } catch (error) {
        renderPartitions([])
        await renderPartitions([])
    }
}
@@ -1125,7 +1209,6 @@
    const stage = getStageByHeight(height)
    updateStageDisplay(stage)
    renderDeviceEntities(props.onlineDevices)
    loadDefenseZones()
    loadPartitions()
    loadAggregation()
    loadCommandPosts()
@@ -1153,7 +1236,6 @@
onBeforeUnmount(() => {
    document.removeEventListener('click', handleClickOutside)
    clearDeviceEntities()
    clearDefenseZoneEntities()
    clearPartitionEntities()
    clearAggregationEntities()
    clearCommandPostEntities()
applications/drone-command/src/styles/common/cockpit.scss
@@ -582,6 +582,15 @@
    .el-input__wrapper {
        --el-input-text-color: #161B2C !important;
    }
    .el-select__selection.is-near {
        margin-left: 0;
        .el-tag {
            color: #ffffff;
            background: #161B2C;
        }
    }
}
.command-select-popper.el-select__popper {
applications/drone-command/src/utils/cesium/deviceRange.js
New file
@@ -0,0 +1,117 @@
import * as Cesium from 'cesium'
import { getPointPositionsHeight } from './mapUtil'
const DEVICE_RANGE_MATERIAL_TYPE = 'DeviceRangeEllipsoid'
let deviceRangeMaterialRegistered = false
export const registerDeviceRangeMaterial = () => {
    if (deviceRangeMaterialRegistered || !Cesium?.Material) return
    deviceRangeMaterialRegistered = true
    Cesium.Material._materialCache.addMaterial(DEVICE_RANGE_MATERIAL_TYPE, {
        fabric: {
            type: DEVICE_RANGE_MATERIAL_TYPE,
            uniforms: {
                color: new Cesium.Color(1.0, 0.0, 0.0, 0.7),
                speed: 3.0,
                globalAlpha: 0.9
            },
            source: `
                uniform vec4 color;
                uniform float speed;
                uniform float globalAlpha;
                czm_material czm_getMaterial(czm_materialInput materialInput)
                {
                czm_material material = czm_getDefaultMaterial(materialInput);
                vec2 st = materialInput.st;
                float time = fract(czm_frameNumber * speed / 1000.0);
                if (st.t < 0.5) {
                    discard;
                }
                float alphaMars3D = abs(smoothstep(0.5, 1.0, fract(-st.t - time)));
                alphaMars3D += 0.1;
                material.alpha = alphaMars3D * globalAlpha;
                material.diffuse = color.rgb;
                return material;
                }
            `,
        },
        translucent: () => true,
    })
}
export const createDeviceRangeMaterial = () => {
    registerDeviceRangeMaterial()
    return Cesium.Material.fromType(DEVICE_RANGE_MATERIAL_TYPE, {
        color: Cesium.Color.fromBytes(0, 255, 255, 155),
    })
}
export const getDeviceLngLat = device => {
    const lngRaw = device?.longitude ?? device?.lng ?? device?.lon
    const latRaw = device?.latitude ?? device?.lat
    const lng = Number(lngRaw)
    const lat = Number(latRaw)
    if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null
    return { lng, lat }
}
export const normalizeDeviceRange = value => {
    const range = Number(value)
    if (!Number.isFinite(range) || range <= 0) return 2000
    return range
}
export const resolveDeviceTerrainHeight = async (viewer, position) => {
    if (!viewer || !position) return 0
    const lng = position.lng ?? position.longitude
    const lat = position.lat ?? position.latitude
    if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return 0
    try {
        const results = await getPointPositionsHeight([{ lng: Number(lng), lat: Number(lat) }], viewer)
        const height = Number(results?.[0]?.ASL)
        return Number.isFinite(height) ? height : 0
    } catch (error) {
        return 0
    }
}
export const createDeviceRangePrimitiveWithHeight = (position, rangeMeters, height, material = null) => {
    if (!position || !Number.isFinite(rangeMeters)) return null
    const lng = position.lng ?? position.longitude
    const lat = position.lat ?? position.latitude
    if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
    const center = Cesium.Cartesian3.fromDegrees(Number(lng), Number(lat), Number(height) || 0)
    const radii = new Cesium.Cartesian3(rangeMeters, rangeMeters, rangeMeters)
    const geometry = new Cesium.EllipsoidGeometry({
        radii,
        vertexFormat: Cesium.MaterialAppearance.MaterialSupport.TEXTURED?.vertexFormat
            || Cesium.VertexFormat.POSITION_AND_NORMAL,
    })
    const instance = new Cesium.GeometryInstance({
        geometry,
        modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(center),
    })
    return new Cesium.Primitive({
        geometryInstances: instance,
        appearance: new Cesium.MaterialAppearance({
            material: material || createDeviceRangeMaterial(),
            closed: true,
            translucent: true,
            flat: true,
            faceForward: true,
        }),
        asynchronous: false
    })
}
export const createDeviceRangePrimitive = async (viewer, position, rangeMeters, material = null) => {
    if (!viewer) return null
    const height = await resolveDeviceTerrainHeight(viewer, position)
    return createDeviceRangePrimitiveWithHeight(position, rangeMeters, height, material)
}
applications/drone-command/src/utils/cesium/shapeTools/DrawManager.js
applications/drone-command/src/utils/cesium/shapeTools/EditManager.js
applications/drone-command/src/utils/cesium/shapeTools/ToolBase.js
applications/drone-command/src/utils/cesium/shapeTools/Tooltip.js
applications/drone-command/src/utils/cesium/shapeTools/draw/DrawBufferCircleTool.js
File was renamed from 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/utils/cesium/shapeTools/draw/DrawEllipseTool.js
File was renamed from 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/utils/cesium/shapeTools/draw/DrawPolygonTool.js
applications/drone-command/src/utils/cesium/shapeTools/draw/DrawRectangleTool.js
applications/drone-command/src/utils/cesium/shapeTools/edit/EditBufferCircleTool.js
File was renamed from 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/utils/cesium/shapeTools/edit/EditEllipseTool.js
File was renamed from 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) {
applications/drone-command/src/utils/cesium/shapeTools/edit/EditPolygonTool.js
File was renamed from applications/drone-command/src/views/areaManage/partition/shapeTools/edit/EditPolygonTool.js
@@ -74,7 +74,7 @@
    }
    handleMouseMove(movement) {
        this.tooltip.show('拖动顶点编辑,右键删除', movement.endPosition)
        this.tooltip.show('拖动顶点编辑,右键删除点', movement.endPosition)
        if (!this.isDragging || this.draggedIndex < 0) return
        const position = this.getPositionFromScreen(movement.endPosition)
        if (!position) return
applications/drone-command/src/utils/cesium/shapeTools/edit/EditRectangleTool.js
applications/drone-command/src/utils/cesium/shapeTools/index.js
New file
@@ -0,0 +1,13 @@
export { DrawManager } from './DrawManager'
export { EditManager } from './EditManager'
export { buildEllipsePositions, getBoundsFromCartesians, getBoundsFromLngLats, rectanglePositionsFromBounds, getCenterFromBounds, getMetersBetween, cartesianToLocal } from './shapeUtils'
export { DrawPolygonTool } from './draw/DrawPolygonTool'
export { DrawRectangleTool } from './draw/DrawRectangleTool'
export { DrawEllipseTool } from './draw/DrawEllipseTool'
export { DrawBufferCircleTool } from './draw/DrawBufferCircleTool'
export { EditPolygonTool } from './edit/EditPolygonTool'
export { EditRectangleTool } from './edit/EditRectangleTool'
export { EditEllipseTool } from './edit/EditEllipseTool'
export { EditBufferCircleTool } from './edit/EditBufferCircleTool'
export { ToolBase } from './ToolBase'
export { MapTooltip as Tooltip } from './Tooltip'
applications/drone-command/src/utils/cesium/shapeTools/shapeUtils.js
applications/drone-command/src/views/api.txt
@@ -1,98 +1,21 @@
## 重点关注
## 列表-不带分页
**接口地址**:`/drone-fw/record/fwAlarmFavorite/save`
**接口地址**:`/area/fwAreaDivide/list`
**请求方式**:`POST`
**请求方式**:`GET`
**请求数据类型**:`application/json`
**请求数据类型**:`application/x-www-form-urlencoded`
**响应数据类型**:`*/*`
**接口描述**:<p>传入fwAlarmFavorite</p>
**请求示例**:
```javascript
{
  "alarmRecordId": 0
}
```
**请求参数**:
**请求参数**:
| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|fwAlarmFavorite|告警记录关注表|body|true|FwAlarmFavorite对象|FwAlarmFavorite对象|
|&emsp;&emsp;alarmRecordId|告警记录id||false|integer(int64)||
**响应状态**:
| 状态码 | 说明 | schema |
| -------- | -------- | ----- |
|200|OK|R|
|201|Created||
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- |
|code|状态码|integer(int32)|integer(int32)|
|data|承载数据|object||
|msg|返回消息|string||
|success|是否成功|boolean||
**响应示例**:
```javascript
{
    "code": 0,
    "data": {},
    "msg": "",
    "success": true
}
```
## 取消关注
**接口地址**:`/drone-fw/record/fwAlarmFavorite/remove`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:<p>传入ids</p>
**接口描述**:<p>传入filterSelected或sceneId或areaTypeKeys或isSetSceneManage或flyTime</p>
@@ -104,7 +27,11 @@
| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|ids|主键集合|query|true|string||
|areaTypeKeys|区域类型key集合(逗号分隔)|query|false|string||
|filterSelected|是否过滤已被选择的数据(1过滤 0不过滤)|query|false|integer(int32)||
|flyTime|时间筛选(yyyy-MM-dd HH:mm:ss)|query|false|string(date-time)||
|isSetSceneManage|是否按场景管理过滤(1是 0或空否)|query|false|integer(int32)||
|sceneId|场景id|query|false|integer(int64)||
**响应状态**:
@@ -112,8 +39,7 @@
| 状态码 | 说明 | schema |
| -------- | -------- | ----- | 
|200|OK|R|
|201|Created||
|200|OK|R«List«FwAreaDivideVO»»|
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
@@ -125,7 +51,28 @@
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- | 
|code|状态码|integer(int32)|integer(int32)|
|data|承载数据|object||
|data|承载数据|array|FwAreaDivideVO|
|&emsp;&emsp;areaCode|区域编码|string||
|&emsp;&emsp;areaName|区域名称|string||
|&emsp;&emsp;areaTypeKeys|区域面的类型集合逗号隔开|string||
|&emsp;&emsp;controlLevel|管控级别:日常/重点/严密|string||
|&emsp;&emsp;deviceIds|关联设备ID,逗号分隔|string||
|&emsp;&emsp;flyDateEnd|可飞行日期时间-结束|string(date-time)||
|&emsp;&emsp;flyDateStart|可飞行日期时间-开始|string(date-time)||
|&emsp;&emsp;fwAreaDivideExtList|面数据|array|FwAreaDivideExtVO|
|&emsp;&emsp;&emsp;&emsp;areaCode|区域编码|string||
|&emsp;&emsp;&emsp;&emsp;areaDivideId|关联主表区域划分ID|integer||
|&emsp;&emsp;&emsp;&emsp;areaTypeKey|区域类型数字字典key|string||
|&emsp;&emsp;&emsp;&emsp;areaTypeValue|区域类型数字字典value|string||
|&emsp;&emsp;&emsp;&emsp;geomJson|几何JSON|string||
|&emsp;&emsp;&emsp;&emsp;id|主键id|integer||
|&emsp;&emsp;id|主键id|integer(int64)||
|&emsp;&emsp;policeStationContactPerson|派出所联系人|string||
|&emsp;&emsp;policeStationContactPhone|派出所联系电话|string||
|&emsp;&emsp;policeStationId|关联派出所id|string||
|&emsp;&emsp;policeStationName|派出所名称|string||
|&emsp;&emsp;responseMechanism|响应机制内容|string||
|&emsp;&emsp;triggerCondition|触发条件|string||
|msg|返回消息|string||
|success|是否成功|boolean||
@@ -134,7 +81,34 @@
```javascript
{
    "code": 0,
    "data": {},
    "data": [
        {
            "areaCode": "",
            "areaName": "",
            "areaTypeKeys": "",
            "controlLevel": "",
            "deviceIds": "",
            "flyDateEnd": "",
            "flyDateStart": "",
            "fwAreaDivideExtList": [
                {
                    "areaCode": "",
                    "areaDivideId": 0,
                    "areaTypeKey": "",
                    "areaTypeValue": "",
                    "geomJson": "",
                    "id": 0
                }
            ],
            "id": 0,
            "policeStationContactPerson": "",
            "policeStationContactPhone": "",
            "policeStationId": "",
            "policeStationName": "",
            "responseMechanism": "",
            "triggerCondition": ""
        }
    ],
    "msg": "",
    "success": true
}
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
@@ -1,24 +1,62 @@
<template>
    <el-dialog class="command-page-map-view-dialog" v-model="visible" :show-close="false"
        :close-on-click-modal="false">
    <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"
                />
                <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="activeAreaType"
                            placeholder="请选择" :disabled="currentShapeType === 'buffer'" collapse-tags :multiple="currentShapeType === 'buffer'"
                            @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">
                    <el-tooltip content="多边形" placement="left">
                        <button class="tool-btn" type="button" @click="handleDraw('polygon')">
                            <img class="tool-icon-image" :src="polygonIcon" alt="多边形" />
                        </button>
                    </el-tooltip>
                    <el-tooltip content="矩形" placement="left">
                        <button class="tool-btn" type="button" @click="handleDraw('rectangle')">
                            <img class="tool-icon-image" :src="rectIcon" alt="矩形" />
                        </button>
                    </el-tooltip>
                    <el-tooltip content="椭圆" placement="left">
                        <button class="tool-btn" type="button" @click="handleDraw('ellipse')">
                            <img class="tool-icon-image" :src="ellipseIcon" alt="椭圆" />
                        </button>
                    </el-tooltip>
                    <el-tooltip content="缓冲圆" placement="left">
                        <button class="tool-btn" type="button" @click="handleDraw('buffer')">
                            <img class="tool-icon-image" :src="bufferIcon" alt="缓冲圆" />
                        </button>
                    </el-tooltip>
                    <el-tooltip content="删除" placement="left">
                        <button class="tool-btn danger" type="button" @click="handleClearShape">
                            <img class="tool-icon-image" :src="trashIcon" alt="删除" />
                        </button>
                    </el-tooltip>
                </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>
                    <el-icon class="close-btn" @click.stop="visible = false">
                        <Close />
                    </el-icon>
                </div>
                <div class="content" v-if="readonly">
@@ -27,18 +65,6 @@
                        <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>
@@ -64,11 +90,29 @@
                        </el-col>
                    </el-row>
                    <div class="detail-title">绘制区域列表</div>
                    <div class="command-table-container">
                        <div class="command-table-content">
                            <el-table class="command-table" :data="shapeList" row-key="id">
                                <el-table-column prop="areaType" label="区域类型">
                                    <template v-slot="{ row }">
                                        {{ resolveShapeAreaTypeLabel(row) }}
                                    </template>
                                </el-table-column>
                                <el-table-column prop="drawType" label="绘制类型">
                                    <template v-slot="{ row }">
                                        {{ drawTypeLabelMap[row.drawType] || '-' }}
                                    </template>
                                </el-table-column>
                            </el-table>
                        </div>
                    </div>
                    <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 class="command-table" ref="deviceTableRef" :data="deviceOptions" row-key="id"
                                @row-click="handleDeviceRowClick">
                                <el-table-column prop="deviceName" label="设备名称" />
                                <el-table-column prop="deviceType" label="类型">
                                    <template v-slot="{ row }">
@@ -83,25 +127,12 @@
                <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-input class="command-input" v-model="formData.areaName" maxlength="50" placeholder="请输入"
                            clearable />
                    </el-form-item>
                    <el-form-item label="响应机制" prop="responseMechanism">
                        <el-input class="command-input" v-model="formData.responseMechanism"
                            maxlength="200" placeholder="请输入" clearable />
                        <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"
@@ -118,10 +149,9 @@
                        </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"
                        <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">
@@ -132,20 +162,41 @@
                        </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="auto">
                            <el-table-column prop="areaType" label="区域类型">
                                <template v-slot="{ row }">
                                    {{ resolveShapeAreaTypeLabel(row) }}
                                </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">
                                关联设备
                            </div>
                            <el-input class="command-input" v-model="searchName" placeholder="请输入"
                                clearable></el-input>
                            <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">
                                @selection-change="handleDeviceSelectionChange" @row-click="handleDeviceRowClick">
                                <el-table-column type="selection" width="55" :reserve-selection="true" />
                                <el-table-column prop="deviceName" label="设备名称" />
                                <el-table-column prop="deviceType" label="类型">
@@ -159,7 +210,9 @@
                </el-form>
                <div class="footer">
                    <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button>
                    <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">
                        确定
@@ -173,30 +226,37 @@
<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, EditManager, buildEllipsePositions } from '@/utils/cesium/shapeTools'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
import * as turf from '@turf/turf'
import {
    createDeviceRangePrimitive,
    getDeviceLngLat,
    normalizeDeviceRange,
    resolveDeviceTerrainHeight,
} from '@/utils/cesium/deviceRange'
import * as Cesium from 'cesium'
import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
import { fwPoliceStationListApi } from '@/views/areaManage/precinctInfo/precinctInfoApi'
import { fwDeviceListApi } from '@/views/basicManage/deviceStock/fwDevice'
import polygonIcon from '@/assets/images/areaMap/polygon.png'
import rectIcon from '@/assets/images/areaMap/rect.png'
import ellipseIcon from '@/assets/images/areaMap/ellipse.png'
import bufferIcon from '@/assets/images/areaMap/buffer.png'
import trashIcon from '@/assets/images/areaMap/trash.png'
const initForm = () => ({
    areaName: '', // 区域名称
    areaSize: null, // 区域面积
    areaType: '', // 区域类型
    controlLevel: '', // 管控级别
    deviceIds: '', // 关联设备ID
    flyDateEnd: '', // 可飞行结束
    flyDateStart: '', // 可飞行开始
    latitude: null, // 区域中心纬度
    longitude: null, // 区域中心经度
    policeStationId: '', // 关联派出所id
    responseMechanism: '', // 响应机制
    triggerCondition: '', // 触发条件
@@ -219,14 +279,31 @@
const deviceOptions = ref([]) // 关联设备
const dictObj = inject('dictObj')
const mapRef = ref(null)
const currentShapeType = ref('polygon')
const activeAreaType = ref('')
const shapeList = ref([])
const activeShapeId = ref(null)
const drawTypeLabelMap = {
    polygon: '多边形',
    rectangle: '矩形',
    ellipse: '椭圆',
    buffer: '缓冲圆',
}
const bufferAreaTypeValue = '1,2,3'
const showTypePanel = ref(false)
const suppressTypePanelOnce = ref(false)
let viewer
let drawPolygonExample
let drawManager
let editManager
let activeTool
let pointList = []
let viewEntity
let shapeDisplaySource
const deviceRangePrimitiveMap = new Map()
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),
@@ -274,17 +351,33 @@
// 提交新增/编辑
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²,请重新绘制')
    if (!shapeList.value.length) {
        ElMessage.warning('区域不可为空,请先绘制区域')
        return
    }
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) 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 areaDivide = { ...formData.value }
        delete areaDivide.geom
        const areaDivideExtList = shapeList.value.map(shape => {
            const areaTypeKey = shape?.areaType ?? ''
            const areaTypeValue = getDictLabel(areaTypeKey, resolveAreaTypeOptions())
            return {
                id: shape?.extId,
                areaDivideId: areaDivide?.id ?? '',
                areaCode: areaDivide?.areaCode ?? '',
                areaTypeKey,
                areaTypeValue,
                geomJson: JSON.stringify(shape),
            }
        })
        await fwAreaDivideSubmitApi({
            areaDivide,
            areaDivideExtList,
        })
        const actionText = dialogMode.value === 'add' ? '新增' : '编辑'
        saveOperationLog({
            requestUri: route.path,
@@ -306,19 +399,43 @@
        formData.value.flyDateEnd = flyDateRange.value[1] || ''
    }
)
watch(
    () => visible.value,
    val => {
        if (!val) {
            viewEntity && viewer?.entities?.remove(viewEntity)
            viewEntity = null
            clearActiveTool()
            hideTypePanel()
            clearShapeDisplay()
            clearDeviceRangePrimitives()
        }
    }
)
watch(
    () => activeAreaType.value,
    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()
    const detail = res?.data?.data ?? {}
    const nextForm = initForm()
    Object.keys(nextForm).forEach(key => {
        if (detail[key] !== undefined) {
            nextForm[key] = detail[key]
        }
    })
    if (detail?.id) {
        nextForm.id = detail.id
    }
    formData.value = nextForm
    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)}`
    syncShapeListFromDetail(detail)
}
// 格式化可飞行时间段
@@ -334,66 +451,531 @@
    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 clearDeviceRangePrimitives () {
    if (!viewer || !deviceRangePrimitiveMap.size) return
    for (const primitive of deviceRangePrimitiveMap.values()) {
        viewer.scene.primitives.remove(primitive)
    }
    deviceRangePrimitiveMap.clear()
}
async function renderDeviceRangeSingle (device) {
    if (!viewer) return
    clearDeviceRangePrimitives()
    const position = getDeviceLngLat(device)
    if (!position) return
    const rangeMeters = normalizeDeviceRange(device.effectiveRangeKm)
    const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters)
    if (!primitive) return
    deviceRangePrimitiveMap.set(String(device?.id ?? Date.now()), primitive)
    viewer.scene.primitives.add(primitive)
}
async function renderDeviceRanges (rows) {
    if (!viewer) return
    const selectedRows = Array.isArray(rows) ? rows : []
    const nextIds = new Set(selectedRows.map(row => String(row?.id)))
    for (const [id, primitive] of deviceRangePrimitiveMap.entries()) {
        if (!nextIds.has(id)) {
            viewer.scene.primitives.remove(primitive)
            deviceRangePrimitiveMap.delete(id)
        }
    }
    const pendingRows = selectedRows.filter(row => {
        const id = String(row?.id)
        return id && !deviceRangePrimitiveMap.has(id)
    })
    if (!pendingRows.length) return
    const primitives = await Promise.all(
        pendingRows.map(async row => {
            const position = getDeviceLngLat(row)
            if (!position) return { row, primitive: null }
            const rangeMeters = normalizeDeviceRange(row.effectiveRangeKm)
            const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters)
            return { row, primitive }
        })
    )
    primitives.forEach(({ row, primitive }) => {
        if (!primitive) return
        const id = String(row?.id)
        deviceRangePrimitiveMap.set(id, primitive)
        viewer.scene.primitives.add(primitive)
    })
}
async function flyToDevice (device) {
    if (!viewer) return
    const position = getDeviceLngLat(device)
    if (!position) return
    const height = await resolveDeviceTerrainHeight(viewer, position)
    const rangeMeters = normalizeDeviceRange(device?.effectiveRangeKm)
    const destination = Cesium.Cartesian3.fromDegrees(position.lng, position.lat, height + rangeMeters * 3)
    viewer.camera.flyTo({
        destination,
        orientation: {
            heading: viewer.camera.heading,
            pitch: Cesium.Math.toRadians(-90),
            roll: 0,
        },
        duration: 0.6,
    })
}
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 AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
}
function renderShapeList () {
    if (!viewer || !visible.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) {
                radii.forEach((radius, index) => {
                    const style = BUFFER_LEVEL_STYLES[index] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_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 = []
        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 }
    })
    const polygon = turf.polygon([
        [...pointList.map(item => [item.longitude, item.latitude]), [pointList[0].longitude, pointList[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 addPolygon () {
    drawPolygonExample = new DrawPolygon(viewer)
    drawPolygonExample.initHandler(viewer)
    drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
}
// 编辑面
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)
    const controlPoints = resolveControlPoints(meta, currentShapeType.value)
    if (!activeAreaType.value) {
        const detectAreaType = getDetectAreaTypeKey()
        if (detectAreaType) {
            activeAreaType.value = detectAreaType
        }
    }
    if (currentShapeType.value === 'buffer') {
        activeAreaType.value = bufferAreaTypeValue.split(',')
    }
    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: currentShapeType.value === 'buffer' ? bufferAreaTypeValue : activeAreaType.value,
        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 getDetectAreaTypeKey () {
    const areaTypeOptions = resolveAreaTypeOptions()
    if (!Array.isArray(areaTypeOptions)) return ''
    const target = areaTypeOptions.find(item => item?.dictKey === '1')
    return target?.dictKey ?? ''
}
function resolveAreaTypeOptions () {
    return Array.isArray(dictObj?.value?.areaType)
        ? dictObj.value.areaType
        : dictObj?.areaType
}
function resolveShapeAreaTypeLabel (shape) {
    return getDictLabel(shape?.areaType, resolveAreaTypeOptions())
}
function parseGeomJson (geomJson) {
    if (!geomJson) return null
    if (typeof geomJson === 'object') return geomJson
    if (typeof geomJson !== 'string') return null
    const trimmed = geomJson.trim()
    if (!trimmed) return null
    try {
        return JSON.parse(trimmed)
    } catch (error) {
        const legacyPoints = geomAnalysis(trimmed)
        if (Array.isArray(legacyPoints) && legacyPoints.length >= 3) {
            return { drawType: 'polygon', points: legacyPoints }
        }
    }
    return null
}
function syncShapeListFromDetail (detail) {
    const extList = Array.isArray(detail?.fwAreaDivideExtList) ? detail.fwAreaDivideExtList : []
    if (!extList.length) {
        shapeList.value = []
        activeAreaType.value = ''
        currentShapeType.value = 'polygon'
        return
    }
    shapeList.value = extList.map((item, index) => {
        const isShapePayload = item?.drawType || item?.points
        if (isShapePayload) {
            const areaType = item?.drawType === 'buffer'
                ? bufferAreaTypeValue
                : (item?.areaType ?? '')
            return {
                id: item?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
                extId: item?.extId ?? item?.id,
                areaType,
                drawType: item?.drawType ?? 'polygon',
                points: Array.isArray(item?.points) ? item.points : [],
                displayPoints: Array.isArray(item?.displayPoints) ? item.displayPoints : null,
                meta: item?.meta ?? null,
            }
        }
        const parsed = parseGeomJson(item?.geomJson) || {}
        const drawType = parsed?.drawType || 'polygon'
        const areaType = drawType === 'buffer'
            ? bufferAreaTypeValue
            : (item?.areaTypeKey ?? parsed?.areaType ?? '')
        const points = Array.isArray(parsed?.points) ? parsed.points : []
        const displayPoints = Array.isArray(parsed?.displayPoints) ? parsed.displayPoints : null
        const meta = parsed?.meta ?? null
        return {
            id: parsed?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
            extId: item?.id,
            areaType,
            drawType,
            points,
            displayPoints,
            meta,
        }
    })
    activeAreaType.value = shapeList.value[0]?.drawType === 'buffer'
        ? bufferAreaTypeValue.split(',')
        : (shapeList.value[0]?.areaType ?? '')
    currentShapeType.value = shapeList.value[0]?.drawType ?? 'polygon'
}
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') return
    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(activeAreaType.value) })
    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(activeAreaType.value) })
    activeTool?.subscribe('getPolygonPositions', drawFinished)
}
function handleDraw (type) {
    if (readonly.value) return
    if (!viewer) return
    clearActiveTool()
    viewEntity && viewer?.entities?.remove(viewEntity)
    viewEntity = null
    hideTypePanel()
    renderShapeList()
    pointList = []
    const defaultAreaType = getDetectAreaTypeKey()
    if (type === 'buffer') {
        activeAreaType.value = bufferAreaTypeValue
    } else if (defaultAreaType) {
        activeAreaType.value = 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
    activeAreaType.value = ''
    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
    activeAreaType.value = row.drawType === 'buffer' ? bufferAreaTypeValue.split(',') : 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)
    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 = []
        hideTypePanel()
    }
}
// 查看面
function viewPolygon () {
    if (!viewer) return
    if (shapeList.value.length) {
        renderShapeList()
        return
    }
    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 +985,7 @@
            material: Cesium.Color.RED,
        },
    })
    viewer.flyTo(mian, { duration: 0 })
    viewer.flyTo(viewEntity, { duration: 0 })
}
// 获取派出所列表
@@ -427,21 +1009,42 @@
    selectedDeviceRows.value = rows
    const ids = rows.map(item => item.id)
    formData.value.deviceIds = ids.join(',')
    renderDeviceRanges(rows)
    const target = rows[rows.length - 1]
    if (target) {
        void flyToDevice(target)
    }
}
function handleDeviceRowClick (row) {
    if (readonly.value) {
        renderDeviceRangeSingle(row)
        void flyToDevice(row)
        return
    }
    renderDeviceRanges(selectedDeviceRows.value)
    void flyToDevice(row)
}
// 同步选择状态
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
    if (readonly.value) {
        renderDeviceRanges(selectedDeviceRows.value)
        return
    }
    if (!deviceTableRef.value) return
    deviceTableRef.value.clearSelection()
    rows.forEach(row => {
        deviceTableRef.value.toggleRowSelection(row, true)
    })
}
// 打开弹框
@@ -449,25 +1052,40 @@
    dialogMode.value = mode || 'add'
    formData.value = dialogMode.value === 'add' ? initForm() : row
    selectedDeviceRows.value = []
    shapeList.value = []
    activeShapeId.value = null
    activeAreaType.value = ''
    pointList = []
    await nextTick()
    initMap()
    clearShapeDisplay()
    clearDeviceRangePrimitives()
    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()
        viewPolygon()
    } else {
        await loadDetail()
        viewPolygon()
    }
    await nextTick()
    syncDeviceSelection()
    if (!readonly.value) {
        renderDeviceRanges(selectedDeviceRows.value)
    }
}
onMounted(() => {
    getPoliceStationList()
    getDeviceList()
})
onBeforeUnmount(() => {
    hideTypePanel()
})
defineExpose({
@@ -475,4 +1093,168 @@
})
</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 {
    padding: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: 8px;
    color: #ffffff;
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
    transition: all 0.2s ease;
    overflow: hidden;
}
.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;
}
.tool-icon-image {
    width: 36px;
    height: 36px;
}
.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-bottom: 20px;
    .shape-table-title {
        margin-bottom: 20px;
        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/index.vue
@@ -5,10 +5,9 @@
                <el-input class="command-input" v-model="searchParams.areaName" placeholder="请输入"
                    clearable @clear="handleSearch" />
            </el-form-item>
            <el-form-item label="区域类型" prop="areaType">
            <el-form-item label="区域类型" prop="areaTypeKeys">
                <el-select class="command-select" popper-class="command-select-popper"
                    v-model="searchParams.areaType" placeholder="请选择" clearable @change="handleSearch">
                    v-model="searchParams.areaTypeKeys" placeholder="请选择" clearable @change="handleSearch">
                    <el-option v-for="item in dictObj.areaType" :key="item.dictKey" :label="item.dictValue"
                        :value="item.dictKey" />
                </el-select>
@@ -31,30 +30,24 @@
                    <el-table-column type="selection" width="46" />
                    <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                    <el-table-column prop="areaName" show-overflow-tooltip width="150" label="区域名称" />
                    <el-table-column show-overflow-tooltip width="160" label="区域位置">
                    <el-table-column prop="areaTypeKeys" show-overflow-tooltip width="196" label="区域类型">
                        <template v-slot="{ row }">
                            {{ formatLocation(row) }}
                            {{ getDictLabel(row.areaTypeKeys, dictObj.areaType) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="areaSize" show-overflow-tooltip width="130" label="区域面积(k㎡)" />
                    <el-table-column prop="areaType" show-overflow-tooltip width="120" label="区域类型">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.areaType, dictObj.areaType) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="triggerCondition" show-overflow-tooltip width="130" label="触发条件">
                    <el-table-column prop="triggerCondition" show-overflow-tooltip label="触发条件">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.triggerCondition, dictObj.triggeringCondition) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="responseMechanism" show-overflow-tooltip width="130" label="响应机制" />
                    <el-table-column prop="controlLevel" show-overflow-tooltip width="120" label="管控级别">
                    <el-table-column prop="responseMechanism" show-overflow-tooltip label="响应机制" />
                    <el-table-column prop="controlLevel" show-overflow-tooltip label="管控级别">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.controlLevel, dictObj.controlLevel) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="policeStationName" show-overflow-tooltip label="关联派出所" />
                    <el-table-column show-overflow-tooltip label="可飞行时间段">
                    <el-table-column show-overflow-tooltip width="300" label="可飞行时间段">
                        <template v-slot="{ row }">
                            {{ formatFlyDate(row) }}
                        </template>
@@ -93,7 +86,7 @@
const initSearchParams = () => ({
    areaName: '', // 区域名称
    areaType: '', // 区域类型
    areaTypeKeys: '', // 区域类型
    current: 1, // 当前页
    size: 10, // 每页大小
})
@@ -157,11 +150,6 @@
// 勾选值设置
function handleSelectionChange (rows) {
    selectedIds.value = rows.map(item => item.id)
}
function formatLocation (row) {
    if (row?.longitude == null || row?.latitude == null) return ''
    return `${row.longitude}, ${row.latitude}`
}
function formatFlyDate (row) {
applications/drone-command/src/views/areaManage/precinctInfo/FormDiaLog.vue
@@ -120,6 +120,7 @@
import axios from 'axios'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
import positionIcon from '@/assets/images/areaManage/positionIcon.png'
const initForm = () => ({
    address: '', // 位置
@@ -185,7 +186,7 @@
let redPointEntity
let leftClickBound = false
const labelParams = {
    font: '16px',
    font: '18px',
    fillColor: Cesium.Color.WHITE, // 文字颜色:白色
    backgroundColor: Cesium.Color.BLACK, //背景颜色
    backgroundPadding: new Cesium.Cartesian2(5, 5), // 水平/垂直内边距(像素)
@@ -200,17 +201,17 @@
}
function LeftClickEvent(click) {
    const pos = click.position // Cartesian2 屏幕坐标
    // 屏幕坐标 -> 椭球面坐标(不考虑地形/模型)
    const cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
    if (!cartesian3) {
        return { longitude: 112, latitude: 23 }
    const pos = click.position
    // 优先使用 pickPosition 获取贴地坐标(包含地形高度)
    let cartesian3 = viewer.scene.pickPosition(pos)
    // 如果 pickPosition 失败,回退到 pickEllipsoid
    if (!cartesian3 || !Cesium.defined(cartesian3)) {
        cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
    }
    // 椭球面坐标 -> 经纬度
    if (!cartesian3) return
    const carto = Cesium.Cartographic.fromCartesian(cartesian3)
    const longitude = Cesium.Math.toDegrees(carto.longitude)
    const latitude = Cesium.Math.toDegrees(carto.latitude)
    // 直接更新 formData
    formData.value.longitude = _.round(longitude, 6)
    formData.value.latitude = _.round(latitude, 6)
    getLocationName(longitude, latitude)
@@ -219,14 +220,16 @@
    if (!redPointEntity) {
        redPointEntity = viewer.entities.add({
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
            point: {
                color: Cesium.Color.RED,
                pixelSize: 10,
            billboard: {
                image: positionIcon,
                width: 20,
                height: 20,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
            label: {
                text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
                ...labelParams
                ...labelParams,
            },
        })
    } else {
@@ -240,14 +243,16 @@
    if (!redPointEntity) {
        redPointEntity = viewer.entities.add({
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
            point: {
                color: Cesium.Color.RED,
                pixelSize: 10,
            billboard: {
                image: positionIcon,
                width: 20,
                height: 20,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
            label: {
                text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
                ...labelParams
                ...labelParams,
            },
        })
    } else {
applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue
@@ -15,7 +15,7 @@
            <div class="right-container">
                <div class="header">
                    <span>{{ titleEnum[dialogMode] }}</span>
                    <el-icon class="close-btn" @click.stop="visible = false"><Close /></el-icon>
                </div>
@@ -23,32 +23,16 @@
                    <div class="detail-title">场景详情</div>
                    <el-row>
                        <el-col :span="24">
                            <div class="label">场景名称</div>
                            <div class="label">场景配置名称</div>
                            <div class="val">{{ formData.sceneName }}</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">{{ getDictLabel(formData.sceneType, dictObj.CommandSceneType) }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">负责人</div>
                            <div class="val">{{ formData.defenseLeader }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">负责人电话</div>
                            <div class="val">{{ formData.leaderPhone }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">设备模式</div>
                            <div class="val">{{ getDictLabel(formData.deviceMode, dictObj.deviceMode) }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">防控面积</div>
                            <div class="val">{{ formatDefenseArea(formData.defenseArea) }}</div>
                        </el-col>
                    </el-row>
@@ -57,9 +41,9 @@
                        <div class="command-table-content">
                            <el-table class="command-table" ref="areaTableRef" :data="areaList" row-key="id">
                                <el-table-column prop="areaName" label="区域名称" />
                                <el-table-column prop="areaType" label="区域类型">
                                <el-table-column prop="areaTypeKeys" label="区域类型">
                                    <template v-slot="{ row }">
                                        {{ getDictLabel(row.areaType, dictObj.areaType) }}
                                        {{ getDictLabel(row.areaTypeKeys, dictObj.areaType) }}
                                    </template>
                                </el-table-column>
                            </el-table>
@@ -67,60 +51,77 @@
                    </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="sceneName">
                        <el-input class="command-input" v-model="formData.sceneName" maxlength="50"
                            placeholder="请输入" clearable />
                    </el-form-item>
                    <el-form-item label="指挥点位置" prop="longitude">
                        <div class="val">{{ formatLocation(formData) }}</div>
                <el-form
                    class="dialog-form"
                    v-else
                    ref="formRef"
                    :model="formData"
                    :rules="rules"
                    :disabled="readonly"
                    label-width="96px"
                >
                    <el-form-item label="场景配置名称" prop="sceneName">
                        <el-input
                            class="command-input"
                            v-model="formData.sceneName"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                        />
                    </el-form-item>
                    <el-form-item label="场景类型" prop="sceneType">
                        <el-select class="command-select" popper-class="command-select-popper"
                            v-model="formData.sceneType" placeholder="请选择" clearable>
                            <el-option v-for="item in dictObj.CommandSceneType" :key="item.dictKey" :label="item.dictValue"
                                :value="item.dictKey" />
                        <el-select
                            class="command-select"
                            popper-class="command-select-popper"
                            v-model="formData.sceneType"
                            placeholder="请选择"
                            clearable
                        >
                            <el-option
                                v-for="item in dictObj.CommandSceneType"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="item.dictKey"
                            />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="负责人" prop="defenseLeader">
                        <el-input class="command-input" v-model="formData.defenseLeader" maxlength="50"
                            placeholder="请输入" clearable />
                    </el-form-item>
                    <el-form-item label="负责人电话" prop="leaderPhone">
                        <el-input class="command-input" v-model="formData.leaderPhone" maxlength="50"
                            placeholder="请输入" clearable />
                    </el-form-item>
                    <el-form-item label="设备模式" prop="deviceMode">
                        <el-select class="command-select" popper-class="command-select-popper"
                            v-model="formData.deviceMode" placeholder="请选择" clearable>
                            <el-option v-for="item in dictObj.deviceMode" :key="item.dictKey" :label="item.dictValue"
                                :value="item.dictKey" />
                        <el-select
                            class="command-select"
                            popper-class="command-select-popper"
                            v-model="formData.deviceMode"
                            placeholder="请选择"
                            clearable
                        >
                            <el-option
                                v-for="item in dictObj.deviceMode"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="item.dictKey"
                            />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="防控面积" prop="defenseArea">
                        <div class="val">{{ formatDefenseArea(formData.defenseArea) }}</div>
                    </el-form-item>
                    <div class="search-table-container">
                        <div class="search-box">
                            <div class="label">
                                关联区域
                            </div>
                            <div class="label">关联区域</div>
                            <el-input class="command-input" v-model="searchName" placeholder="请输入"
                                clearable></el-input>
                            <el-input class="command-input" v-model="searchName" placeholder="请输入" clearable></el-input>
                        </div>
                        <el-form-item prop="areaDivideIds" label-width="0">
                            <el-table class="command-table" ref="areaTableRef"
                                :data="areaList.filter(item => item.areaName.includes(searchName))" row-key="id"
                                @selection-change="handleAreaSelectionChange">
                            <el-table
                                class="command-table"
                                ref="areaTableRef"
                                :data="areaList.filter(item => item.areaName.includes(searchName))"
                                row-key="id"
                                @selection-change="handleAreaSelectionChange"
                            >
                                <el-table-column type="selection" width="55" :reserve-selection="true" />
                                <el-table-column prop="areaName" show-overflow-tooltip label="区域名称" />
                                <el-table-column prop="areaType" show-overflow-tooltip label="区域类型">
                                <el-table-column prop="areaTypeKeys" show-overflow-tooltip label="区域类型">
                                    <template v-slot="{ row }">
                                        {{ getDictLabel(row.areaType, dictObj.areaType) }}
                                        {{ getDictLabel(row.areaTypeKeys, dictObj.areaType) }}
                                    </template>
                                </el-table-column>
                            </el-table>
@@ -129,9 +130,17 @@
                </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 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>
@@ -143,29 +152,24 @@
<script setup>
import { Close } from '@element-plus/icons-vue'
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
import { computed, inject, nextTick, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { fwDefenseSceneDetailApi, fwDefenseSceneSubmitApi } from './sceneConfigApi'
import { fieldRules, geomAnalysis, getDictLabel } from '@ztzf/utils'
import { fieldRules, getDictLabel } from '@ztzf/utils'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import * as Cesium from 'cesium'
import { fwAreaDivideDetailApi, fwAreaDivideListApi } from '../partition/partitionApi'
import { DrawPolygon } from '@/utils/cesium/DrawPolygon'
import commandPost from '@/assets/images/dataCockpit/legend/command-post.png'
import { buildEllipsePositions } from '@/utils/cesium/shapeTools'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
const initForm = () => ({
    sceneName: '', // 场景名称
    sceneName: '', // 场景配置名称
    sceneType: '', // 场景类型
    defenseLeader: '', // 负责人
    leaderPhone: '', // 负责人电话
    deviceMode: '', // 设备模式
    areaDivideIds: '', // 关联区域ID
    areaCount: 0, // 区域数量
    defenseArea: 0, // 防控面积
    latitude: null, // 指挥点纬度
    longitude: null, // 指挥点经度
})
const emit = defineEmits(['success'])
@@ -184,26 +188,22 @@
const mapRef = ref(null)
const route = useRoute()
let viewer
let redPointEntity
let leftClickBound = false
let areaDisplaySource
const rules = {
    sceneName: fieldRules(true, 50),
    sceneType: fieldRules(true),
    defenseLeader: fieldRules(true, 50),
    leaderPhone: fieldRules(true, 50),
    deviceMode: fieldRules(true),
    longitude: fieldRules(true),
    areaDivideIds: fieldRules(false),
}
// 关闭弹框
function handleCancel () {
function handleCancel() {
    visible.value = false
}
// 提交新增/编辑
async function handleSubmit () {
async function handleSubmit() {
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) return
    submitting.value = true
@@ -211,7 +211,6 @@
        const ids = selectedAreaRows.value.map(item => item.id)
        formData.value.areaDivideIds = ids.join(',')
        formData.value.areaCount = ids.length
        formData.value.defenseArea = calcDefenseArea(selectedAreaRows.value)
        await fwDefenseSceneSubmitApi(formData.value)
        saveOperationLog({
            requestUri: route.path,
@@ -227,65 +226,190 @@
}
// 加载详情
async function loadDetail () {
async function loadDetail() {
    if (!formData.value.id) return
    const res = await fwDefenseSceneDetailApi({ id: formData.value.id })
    formData.value = res?.data?.data ?? initForm()
}
// 获取区域列表
async function getAreaList () {
async function getAreaList() {
    if (areaList.value.length) return
    const res = await fwAreaDivideListApi({ filterSelected: 1, sceneId: formData.value.id })
    areaList.value = res?.data?.data ?? []
}
// 关联区域变更
async function handleAreaSelectionChange (rows) {
    const selectedRows = await ensureAreaGeom(rows)
async function handleAreaSelectionChange(rows) {
    const selectedRows = await ensureAreaExtList(rows)
    selectedAreaRows.value = selectedRows
    const ids = selectedRows.map(item => item.id)
    formData.value.areaDivideIds = ids.join(',')
    formData.value.areaCount = ids.length
    formData.value.defenseArea = calcDefenseArea(selectedRows)
}
// 渲染面
function renderingSurface () {
    console.log(geometricSource, 11111111)
    if (!geometricSource) {
        initMap()
function ensureAreaDisplaySource() {
    if (!viewer) return null
    if (!areaDisplaySource) {
        areaDisplaySource = new Cesium.CustomDataSource('scene-area-display')
        viewer.dataSources.add(areaDisplaySource)
    }
    if (!geometricSource) return
    geometricSource && geometricSource.entities.removeAll()
    selectedAreaRows.value.forEach(item => {
        if (item.geom) {
            const pointList = geomAnalysis(item.geom)
            const result = pointList.map(item => [item.longitude, item.latitude]).flat()
            geometricSource.entities?.add({
                customType: 'control_group',
                position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
    return areaDisplaySource
}
function clearAreaDisplay() {
    if (!areaDisplaySource) return
    areaDisplaySource.entities.removeAll()
}
function normalizeShapePoint(point) {
    if (!point) return null
    const lng = point?.lng ?? point?.longitude
    const lat = point?.lat ?? point?.latitude
    const height = Number.isFinite(Number(point?.height)) ? Number(point.height) : 0
    if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
    return { lng: Number(lng), lat: Number(lat), height }
}
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 AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
}
function parseGeomJson(geomJson) {
    if (!geomJson) return null
    if (typeof geomJson === 'object') return geomJson
    if (typeof geomJson !== 'string') return null
    const trimmed = geomJson.trim()
    if (!trimmed) return null
    try {
        return JSON.parse(trimmed)
    } catch (error) {
        return null
    }
}
function resolveAreaShapes(area) {
    const extList = Array.isArray(area?.fwAreaDivideExtList) ? area.fwAreaDivideExtList : []
    if (!extList.length) return []
    return extList
        .map((item, index) => {
            const isShapePayload = item?.drawType || item?.points
            if (isShapePayload) {
                return {
                    id: item?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
                    drawType: item?.drawType ?? 'polygon',
                    areaType: item?.areaType ?? item?.areaTypeKey ?? '',
                    points: Array.isArray(item?.points) ? item.points : [],
                    displayPoints: Array.isArray(item?.displayPoints) ? item.displayPoints : null,
                    meta: item?.meta ?? null,
                }
            }
            const parsed = parseGeomJson(item?.geomJson)
            if (!parsed) return null
            return {
                id: parsed?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
                drawType: parsed?.drawType ?? 'polygon',
                areaType: item?.areaTypeKey ?? parsed?.areaType ?? '',
                points: Array.isArray(parsed?.points) ? parsed.points : [],
                displayPoints: Array.isArray(parsed?.displayPoints) ? parsed.displayPoints : null,
                meta: parsed?.meta ?? null,
            }
        })
        .filter(Boolean)
}
function renderSelectedAreas() {
    if (!visible.value) {
        clearAreaDisplay()
        return
    }
    if (!viewer) initMap()
    const dataSource = ensureAreaDisplaySource()
    if (!dataSource) return
    dataSource.entities.removeAll()
    selectedAreaRows.value.forEach(area => {
        const shapes = resolveAreaShapes(area)
        shapes.forEach(shape => {
            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 radii = shape.meta.bufferRadii
                    .map(radius => Number(radius))
                    .filter(radius => Number.isFinite(radius) && radius > 0)
                radii.forEach((radius, index) => {
                    const style = BUFFER_LEVEL_STYLES[index] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
                    dataSource.entities.add({
                        name: 'scene-area-display',
                        customData: { 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: 'scene-area-display',
                        customData: { 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(getShapeDisplayPoints(shape))
            if (positions.length < 3) return
            const style = getAreaTypeStyle(shape.areaType)
            dataSource.entities.add({
                name: 'scene-area-display',
                customData: { drawType: shape.drawType },
                polygon: {
                    hierarchy: new Cesium.PolygonHierarchy(positions),
                    material: style.fill,
                    outline: false,
                    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                },
                polyline: {
                    positions: Cesium.Cartesian3.fromDegreesArray(result),
                    positions: positions.length ? [...positions, positions[0]] : positions,
                    clampToGround: true,
                    width: 3,
                    material: Cesium.Color.RED,
                    width: 2,
                    material: style.outline,
                },
            })
        }
        })
    })
}
watch(() => selectedAreaRows.value, renderingSurface)
watch(() => selectedAreaRows.value, renderSelectedAreas)
watch(
    () => visible.value,
    val => {
        if (!val) clearAreaDisplay()
    }
)
function calcDefenseArea (rows) {
    const total = rows.reduce((sum, item) => sum + (Number(item.areaSize) || 0), 0)
    return _.round(total, 2)
}
async function ensureAreaGeom (rows) {
    const missingRows = rows.filter(row => row?.id && !row.geom)
async function ensureAreaExtList(rows) {
    const missingRows = rows.filter(row => row?.id && !Array.isArray(row?.fwAreaDivideExtList))
    if (!missingRows.length) return rows
    const detailList = await Promise.all(
        missingRows.map(row =>
@@ -294,9 +418,7 @@
                .catch(() => null)
        )
    )
    const detailMap = new Map(
        detailList.filter(Boolean).map(item => [String(item.id), item])
    )
    const detailMap = new Map(detailList.filter(Boolean).map(item => [String(item.id), item]))
    if (!detailMap.size) return rows
    areaList.value = areaList.value.map(item => {
        const detail = detailMap.get(String(item.id))
@@ -309,7 +431,7 @@
}
// 同步关联区域
function syncAreaSelection () {
async function syncAreaSelection() {
    if (!areaTableRef.value) return
    areaTableRef.value.clearSelection()
    const rows = []
@@ -317,99 +439,32 @@
    const arr = Array.isArray(areaDivideIds)
        ? areaDivideIds.map(item => String(item))
        : String(areaDivideIds || '')
            .split(',')
            .filter(Boolean)
                .split(',')
                .filter(Boolean)
    areaList.value.forEach(row => {
        if (arr.includes(String(row.id))) {
            areaTableRef.value.toggleRowSelection(row, true)
            rows.push(row)
        }
    })
    selectedAreaRows.value = rows
    if (!rows.length) return
    formData.value.areaCount = rows.length
    formData.value.defenseArea = calcDefenseArea(rows)
    const ensuredRows = await ensureAreaExtList(rows)
    selectedAreaRows.value = ensuredRows
    if (!ensuredRows.length) return
    formData.value.areaCount = ensuredRows.length
}
function formatDefenseArea (value) {
    if (value == null || value === '') return ''
    return `${value}k㎡`
}
// 格式化指挥点位置
function formatLocation (row) {
    if (row?.longitude == null || row?.latitude == null) return ''
    return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
}
let geometricSource
// 初始化地图实例
function initMap () {
function initMap() {
    if (viewer) return
    const map = mapRef.value?.getMap()
    if (!map?.viewer) return
    viewer = map.viewer
    if (!readonly.value && !leftClickBound) {
        map.publicCesium?.addLeftClickEvent?.(null, LeftClickEvent)
        leftClickBound = true
    }
    if (!geometricSource) {
        geometricSource = new Cesium.CustomDataSource('geometricSource')
        viewer.dataSources.add(geometricSource)
    }
}
function LeftClickEvent (click) {
    const pos = click.position
    const cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
    if (!cartesian3) return
    const carto = Cesium.Cartographic.fromCartesian(cartesian3)
    const longitude = Cesium.Math.toDegrees(carto.longitude)
    const latitude = Cesium.Math.toDegrees(carto.latitude)
    formData.value.longitude = _.round(longitude, 6)
    formData.value.latitude = _.round(latitude, 6)
    setMapPoint(formData.value.longitude, formData.value.latitude)
}
function setMapPoint (longitude, latitude) {
    if (!viewer || longitude == null || latitude == null) return
    if (!redPointEntity) {
        redPointEntity = viewer.entities.add({
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
            billboard: {
                image: commandPost, // 这里替换为你的图片路径或变量 deviceZcImg
                width: 40,
                height: 56,
                verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 设置图片底部对齐坐标点
            },
            label: {
                text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
                font: '14px',
                fillColor: Cesium.Color.WHITE,
                backgroundColor: Cesium.Color.BLACK.withAlpha(0.6),
                backgroundPadding: new Cesium.Cartesian2(5, 5),
                showBackground: true,
                style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                outlineWidth: 1,
                outlineColor: Cesium.Color.WHITE,
                pixelOffset: new Cesium.Cartesian2(0, -72),
                eyeOffset: new Cesium.Cartesian3(0, 0, -20),
            },
        })
    } else {
        redPointEntity.position = Cesium.Cartesian3.fromDegrees(longitude, latitude)
        redPointEntity.label.text = `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`
    }
}
// 打开弹框
async function open ({ mode, row } = {}) {
async function open({ mode, row } = {}) {
    dialogMode.value = mode || 'add'
    formData.value = dialogMode.value === 'add' ? initForm() : row
    selectedAreaRows.value = []
    redPointEntity = null
    await nextTick()
    initMap()
    await getAreaList()
@@ -417,14 +472,8 @@
        await loadDetail()
    }
    await nextTick()
    syncAreaSelection()
    setMapPoint(formData.value.longitude, formData.value.latitude)
    await syncAreaSelection()
}
onMounted(() => {
    getAreaList()
})
defineExpose({
    open,
})
applications/drone-command/src/views/areaManage/sceneConfig/index.vue
@@ -1,7 +1,7 @@
<template>
    <basic-container>
        <el-form ref="queryParamsRef" :model="searchParams" class="command-page-history-search">
            <el-form-item label="场景名称" prop="sceneName">
            <el-form-item label="场景配置名称" prop="sceneName">
                <el-input
                    class="command-input"
                    v-model="searchParams.sceneName"
@@ -46,19 +46,12 @@
                    <el-table-column type="selection" width="46" />
                    <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                    <el-table-column prop="sceneName" show-overflow-tooltip label="场景名称" />
                    <el-table-column prop="sceneName" show-overflow-tooltip label="场景配置名称" />
                    <el-table-column prop="sceneType" show-overflow-tooltip label="场景类型">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.sceneType, dictObj.CommandSceneType) }}
                        </template>
                    </el-table-column>
                    <el-table-column show-overflow-tooltip label="指挥点位置">
                        <template v-slot="{ row }">
                            {{ formatLocation(row) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="defenseLeader" show-overflow-tooltip label="防控负责人" />
                    <el-table-column prop="leaderPhone" show-overflow-tooltip label="防控负责人电话" />
                    <el-table-column prop="areaCount" show-overflow-tooltip label="区域数量" />
                    <el-table-column prop="counterDeviceCount" show-overflow-tooltip label="反制设备数量" />
                    <el-table-column prop="detectDeviceCount" show-overflow-tooltip label="探测设备数量" />
@@ -100,7 +93,7 @@
import { useRoute } from 'vue-router'
const initSearchParams = () => ({
    sceneName: '', // 场景名称
    sceneName: '', // 场景配置名称
    sceneType: '', // 场景类型
    current: 1, // 当前页
    size: 10, // 每页大小
applications/drone-command/src/views/areaManage/sceneManage/FormDiaLog.vue
New file
@@ -0,0 +1,553 @@
<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>
            <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.sceneName }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">负责人</div>
                            <div class="val">{{ formData.defenseLeader }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">负责人电话</div>
                            <div class="val">{{ formData.leaderPhone }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">有效时间</div>
                            <div class="val">{{ formatEffectiveDate(formData) }}</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">{{ getSceneConfigName(formData.defenseSceneId) }}</div>
                        </el-col>
                    </el-row>
                </div>
                <el-form
                    class="dialog-form"
                    v-else
                    ref="formRef"
                    :model="formData"
                    :rules="rules"
                    :disabled="readonly"
                    label-width="110px"
                >
                    <el-form-item label="场景名称" prop="sceneName">
                        <el-input
                            class="command-input"
                            v-model="formData.sceneName"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                        />
                    </el-form-item>
                    <el-form-item label="关联场景配置" prop="defenseSceneId">
                        <el-select
                            class="command-select"
                            popper-class="command-select-popper"
                            v-model="formData.defenseSceneId"
                            placeholder="请选择"
                            clearable
                        >
                            <el-option v-for="item in sceneConfigList" :key="item.id" :label="item.sceneName" :value="item.id" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="负责人" prop="defenseLeader">
                        <el-input
                            class="command-input"
                            v-model="formData.defenseLeader"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                        />
                    </el-form-item>
                    <el-form-item label="负责人电话" prop="leaderPhone">
                        <el-input
                            class="command-input"
                            v-model="formData.leaderPhone"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                        />
                    </el-form-item>
                    <el-form-item label="有效时间范围" prop="effectiveDateStart">
                        <el-date-picker
                            class="command-date-picker"
                            popper-class="command-date-picker-popper"
                            v-model="effectiveDateRange"
                            type="datetimerange"
                            range-separator="至"
                            start-placeholder="开始时间"
                            end-placeholder="结束时间"
                            value-format="YYYY-MM-DD HH:mm:ss"
                            clearable
                        />
                    </el-form-item>
                    <el-form-item label="指挥点位置" prop="longitude">
                        <div class="val">{{ formatLocation(formData) }}</div>
                    </el-form-item>
                </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, nextTick, onMounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { fwDefenseSceneManageDetailApi, fwDefenseSceneManageSubmitApi } from './sceneManageApi'
import { fwDefenseSceneListApi } from '../sceneConfig/sceneConfigApi'
import { fieldRules } from '@ztzf/utils'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import * as Cesium from 'cesium'
import commandPost from '@/assets/images/dataCockpit/legend/command-post.png'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
import { fwAreaDivideDetailApi, fwAreaDivideListApi } from '../partition/partitionApi'
import { buildEllipsePositions } from '@/utils/cesium/shapeTools'
import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
// 初始化表单数据
const initForm = () => ({
    sceneName: '', // 场景名称
    defenseLeader: '', // 防控负责人
    leaderPhone: '', // 防控负责人电话
    effectiveDateStart: '', // 有效时间-开始
    effectiveDateEnd: '', // 有效时间-结束
    longitude: null, // 指挥点经度
    latitude: null, // 指挥点纬度
    defenseSceneId: '', // 关联场景配置ID
})
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 titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' })
const effectiveDateRange = ref([]) // 有效时间范围
const sceneConfigList = ref([]) // 场景配置列表
const areaList = ref([]) // ????
const mapRef = ref(null)
const route = useRoute()
let viewer
let redPointEntity
let areaDisplaySource
let leftClickBound = false
// 表单验证规则
const rules = {
    sceneName: fieldRules(true, 50),
    defenseLeader: fieldRules(true, 50),
    leaderPhone: fieldRules(true, 50),
    effectiveDateStart: fieldRules(true),
    longitude: fieldRules(true),
    defenseSceneId: fieldRules(true),
}
// 关闭弹框
function handleCancel() {
    visible.value = false
}
// 提交新增/编辑
async function handleSubmit() {
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) return
    submitting.value = true
    try {
        await fwDefenseSceneManageSubmitApi(formData.value)
        saveOperationLog({
            requestUri: route.path,
            title: `${route.name || '场景管理'}-${dialogMode.value === 'add' ? '新增' : '编辑'}`,
            type: 1,
        })
        ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功')
        visible.value = false
        emit('success')
    } finally {
        submitting.value = false
    }
}
// 监听有效时间范围变化,同步到表单数据
watch(
    () => effectiveDateRange.value,
    () => {
        formData.value.effectiveDateStart = effectiveDateRange.value?.[0] || ''
        formData.value.effectiveDateEnd = effectiveDateRange.value?.[1] || ''
    }
)
// 加载详情
async function loadDetail() {
    if (!formData.value.id) return
    const res = await fwDefenseSceneManageDetailApi({ id: formData.value.id })
    formData.value = res?.data?.data ?? initForm()
    // 同步有效时间范围
    effectiveDateRange.value = [formData.value.effectiveDateStart, formData.value.effectiveDateEnd].filter(Boolean)
}
// 获取场景配置列表
// ?????
function ensureAreaDisplaySource() {
    if (!viewer) return null
    if (!areaDisplaySource) {
        areaDisplaySource = new Cesium.CustomDataSource('scene-manage-area-display')
        viewer.dataSources.add(areaDisplaySource)
    }
    return areaDisplaySource
}
function clearAreaDisplay() {
    if (!areaDisplaySource) return
    areaDisplaySource.entities.removeAll()
}
function normalizeShapePoint(point) {
    if (!point) return null
    const lng = point?.lng ?? point?.longitude
    const lat = point?.lat ?? point?.latitude
    const height = Number.isFinite(Number(point?.height)) ? Number(point.height) : 0
    if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
    return { lng: Number(lng), lat: Number(lat), height }
}
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 AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
}
function parseGeomJson(geomJson) {
    if (!geomJson) return null
    if (typeof geomJson === 'object') return geomJson
    if (typeof geomJson !== 'string') return null
    const trimmed = geomJson.trim()
    if (!trimmed) return null
    try {
        return JSON.parse(trimmed)
    } catch (error) {
        return null
    }
}
function resolveAreaShapes(area) {
    const extList = Array.isArray(area?.fwAreaDivideExtList) ? area.fwAreaDivideExtList : []
    if (!extList.length) return []
    return extList
        .map((item, index) => {
            const isShapePayload = item?.drawType || item?.points
            if (isShapePayload) {
                return {
                    id: item?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
                    drawType: item?.drawType ?? 'polygon',
                    areaType: item?.areaType ?? item?.areaTypeKey ?? '',
                    points: Array.isArray(item?.points) ? item.points : [],
                    displayPoints: Array.isArray(item?.displayPoints) ? item.displayPoints : null,
                    meta: item?.meta ?? null,
                }
            }
            const parsed = parseGeomJson(item?.geomJson)
            if (!parsed) return null
            return {
                id: parsed?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
                drawType: parsed?.drawType ?? 'polygon',
                areaType: item?.areaTypeKey ?? parsed?.areaType ?? '',
                points: Array.isArray(parsed?.points) ? parsed.points : [],
                displayPoints: Array.isArray(parsed?.displayPoints) ? parsed.displayPoints : null,
                meta: parsed?.meta ?? null,
            }
        })
        .filter(Boolean)
}
function renderSceneAreas() {
    if (!visible.value) {
        clearAreaDisplay()
        return
    }
    if (!viewer) initMap()
    const dataSource = ensureAreaDisplaySource()
    if (!dataSource) return
    dataSource.entities.removeAll()
    areaList.value.forEach(area => {
        const shapes = resolveAreaShapes(area)
        shapes.forEach(shape => {
            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 radii = shape.meta.bufferRadii
                    .map(radius => Number(radius))
                    .filter(radius => Number.isFinite(radius) && radius > 0)
                radii.forEach((radius, index) => {
                    const style = BUFFER_LEVEL_STYLES[index] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
                    dataSource.entities.add({
                        name: 'scene-manage-area-display',
                        customData: { 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: 'scene-manage-area-display',
                        customData: { 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(getShapeDisplayPoints(shape))
            if (positions.length < 3) return
            const style = getAreaTypeStyle(shape.areaType)
            dataSource.entities.add({
                name: 'scene-manage-area-display',
                customData: { 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,
                },
            })
        })
    })
}
async function ensureAreaExtList(rows) {
    const missingRows = rows.filter(row => row?.id && !Array.isArray(row?.fwAreaDivideExtList))
    if (!missingRows.length) return rows
    const detailList = await Promise.all(
        missingRows.map(row =>
            fwAreaDivideDetailApi({ id: row.id })
                .then(res => res?.data?.data)
                .catch(() => null)
        )
    )
    const detailMap = new Map(detailList.filter(Boolean).map(item => [String(item.id), item]))
    if (!detailMap.size) return rows
    return rows.map(item => {
        const detail = detailMap.get(String(item.id))
        return detail ? { ...item, ...detail } : item
    })
}
// ??????????
async function loadAreaList(sceneId) {
    if (!sceneId) {
        areaList.value = []
        clearAreaDisplay()
        return
    }
    const res = await fwAreaDivideListApi({ sceneId })
    const list = res?.data?.data ?? []
    areaList.value = await ensureAreaExtList(list)
    renderSceneAreas()
}
async function getSceneConfigList() {
    if (sceneConfigList.value.length) return
    const res = await fwDefenseSceneListApi({unbound: 1})
    sceneConfigList.value = res?.data?.data ?? []
}
// 获取场景配置名称
function getSceneConfigName(id) {
    if (!id) return ''
    const item = sceneConfigList.value.find(item => item.id === id)
    return item?.sceneName || id
}
// 格式化指挥点位置
function formatLocation(row) {
    if (row?.longitude == null || row?.latitude == null) return ''
    return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
}
// 格式化有效时间
function formatEffectiveDate(row) {
    if (!row?.effectiveDateStart && !row?.effectiveDateEnd) return ''
    return `${row.effectiveDateStart || '-'} ~ ${row.effectiveDateEnd || '-'}`
}
// 初始化地图实例
function initMap() {
    if (viewer) return
    const map = mapRef.value?.getMap()
    if (!map?.viewer) return
    viewer = map.viewer
    if (!readonly.value && !leftClickBound) {
        map.publicCesium?.addLeftClickEvent?.(null, LeftClickEvent)
        leftClickBound = true
    }
}
// 地图左键点击事件
async function LeftClickEvent(click) {
    const pos = click.position
    // 优先使用 pickPosition 获取贴地坐标(包含地形高度)
    let cartesian3 = viewer.scene.pickPosition(pos)
    // 如果 pickPosition 失败,回退到 pickEllipsoid
    if (!cartesian3 || !Cesium.defined(cartesian3)) {
        cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
    }
    if (!cartesian3) return
    const carto = Cesium.Cartographic.fromCartesian(cartesian3)
    const longitude = Cesium.Math.toDegrees(carto.longitude)
    const latitude = Cesium.Math.toDegrees(carto.latitude)
    formData.value.longitude = _.round(longitude, 6)
    formData.value.latitude = _.round(latitude, 6)
    setMapPoint(formData.value.longitude, formData.value.latitude)
    await loadAreaList(formData.value.defenseSceneId)
}
// 设置地图点位
function setMapPoint(longitude, latitude) {
    if (!viewer || longitude == null || latitude == null) return
    if (!redPointEntity) {
        redPointEntity = viewer.entities.add({
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
            billboard: {
                image: commandPost,
                width: 40,
                height: 56,
                verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
            label: {
                text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
                font: '14px',
                fillColor: Cesium.Color.WHITE,
                backgroundColor: Cesium.Color.BLACK.withAlpha(0.6),
                backgroundPadding: new Cesium.Cartesian2(5, 5),
                showBackground: true,
                style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                outlineWidth: 1,
                outlineColor: Cesium.Color.WHITE,
                pixelOffset: new Cesium.Cartesian2(0, -72),
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        })
    } else {
        redPointEntity.position = Cesium.Cartesian3.fromDegrees(longitude, latitude)
        redPointEntity.label.text = `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`
    }
}
// 打开弹框
async function open({ mode, row } = {}) {
    dialogMode.value = mode || 'add'
    formData.value = dialogMode.value === 'add' ? initForm() : { ...row }
    effectiveDateRange.value = []
    redPointEntity = null
    await nextTick()
    initMap()
    await getSceneConfigList()
    if (dialogMode.value !== 'add') {
        await loadDetail()
    }
    await nextTick()
    setMapPoint(formData.value.longitude, formData.value.latitude)
    await loadAreaList(formData.value.defenseSceneId)
}
onMounted(() => {
    getSceneConfigList()
})
watch(
    () => formData.value.defenseSceneId,
    sceneId => {
        if (!visible.value) return
        loadAreaList(sceneId)
    }
)
watch(
    () => visible.value,
    val => {
        if (!val) clearAreaDisplay()
    }
)
defineExpose({
    open,
})
</script>
<style scoped lang="scss"></style>
applications/drone-command/src/views/areaManage/sceneManage/index.vue
New file
@@ -0,0 +1,172 @@
<template>
    <basic-container>
        <el-form ref="queryParamsRef" :model="searchParams" class="command-page-history-search">
            <el-form-item label="场景名称" prop="sceneName">
                <el-input
                    class="command-input"
                    v-model="searchParams.sceneName"
                    placeholder="请输入"
                    clearable
                    @clear="handleSearch"
                />
            </el-form-item>
            <el-form-item class="history-search-actions">
                <el-button :icon="RefreshRight" @click="resetForm"></el-button>
                <el-button class="search-btn" :icon="Search" @click="handleSearch"></el-button>
            </el-form-item>
        </el-form>
        <div class="command-table-toolbar">
            <el-button :icon="Plus" color="#284FE3" type="primary" @click="openForm('add')">新增</el-button>
            <el-button :icon="Delete" color="#1A2652" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>
        </div>
        <div class="command-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
            <div class="command-table-content command-table-content-bg">
                <el-table class="command-table" :data="list" @selection-change="handleSelectionChange">
                    <el-table-column type="selection" width="46" />
                    <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                    <el-table-column prop="sceneName" show-overflow-tooltip label="场景名称" />
                    <el-table-column show-overflow-tooltip label="指挥点经度纬度">
                        <template v-slot="{ row }">
                            {{ formatLocation(row) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="defenseLeader" show-overflow-tooltip label="防控负责人" />
                    <el-table-column prop="leaderPhone" show-overflow-tooltip label="防控负责人电话" />
                    <el-table-column show-overflow-tooltip label="有效时间" width="300">
                        <template v-slot="{ row }">
                            {{ formatEffectiveDate(row) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="defenseSceneName" show-overflow-tooltip label="关联场景配置" />
                    <el-table-column label="操作" class-name="operation-btns">
                        <template v-slot="{ row }">
                            <el-link @click="openForm('view', row)">查看</el-link>
                            <el-link @click="openForm('edit', row)">编辑</el-link>
                            <el-link @click="handleDelete(row)">删除</el-link>
                        </template>
                    </el-table-column>
                </el-table>
            </div>
            <div class="command-table-pagination">
                <el-pagination
                    popper-class="command-select-popper"
                    v-model:current-page="searchParams.current"
                    v-model:page-size="searchParams.size"
                    layout="total, prev, pager, next, sizes"
                    :total="total"
                    @change="getList"
                />
            </div>
        </div>
        <FormDiaLog v-if="dialogVisible" v-model="dialogVisible" ref="dialogRef" @success="getList" />
    </basic-container>
</template>
<script setup>
import { Search, RefreshRight, Plus, Delete } from '@element-plus/icons-vue'
import { nextTick, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fwDefenseSceneManagePageApi, fwDefenseSceneManageRemoveApi } from './sceneManageApi'
import FormDiaLog from './FormDiaLog.vue'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
// 初始化查询参数
const initSearchParams = () => ({
    sceneName: '', // 场景名称
    current: 1, // 当前页
    size: 10, // 每页大小
})
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const selectedIds = ref([]) // 勾选的ID列表
const queryParamsRef = ref(null) // 查询表单实例
const dialogRef = ref(null) // 弹框实例
const dialogVisible = ref(false)
const route = useRoute()
// 获取列表数据
async function getList() {
    loading.value = true
    try {
        const res = await fwDefenseSceneManagePageApi(searchParams.value)
        list.value = res?.data?.data?.records ?? []
        total.value = res?.data?.data?.total ?? 0
    } finally {
        loading.value = false
    }
}
// 查询
function handleSearch() {
    searchParams.value.current = 1
    getList()
}
// 重置查询
function resetForm() {
    queryParamsRef.value?.resetFields()
    searchParams.value.current = 1
    getList()
}
// 删除
async function handleDelete(row) {
    const tips = row ? '该条' : '选中的项'
    await ElMessageBox.confirm(`确认删除${tips}吗?`, '提示', {
        type: 'warning',
        customClass: 'command-page-view-message-box',
        confirmButtonClass: 'command-message-box-confirm',
        cancelButtonClass: 'command-message-box-cancel',
    })
    const ids = row ? row.id : selectedIds.value.join(',')
    await fwDefenseSceneManageRemoveApi({ ids })
    saveOperationLog({
        requestUri: route.path,
        title: `${route.name || '场景管理'}-删除`,
        type: 1,
    })
    ElMessage.success('删除成功')
    selectedIds.value = []
    getList()
}
// 勾选值设置
function handleSelectionChange(rows) {
    selectedIds.value = rows.map(item => item.id)
}
// 格式化指挥点位置
function formatLocation(row) {
    if (row?.longitude == null || row?.latitude == null) return ''
    return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
}
// 格式化有效时间
function formatEffectiveDate(row) {
    if (!row?.effectiveDateStart && !row?.effectiveDateEnd) return ''
    return `${row.effectiveDateStart || '-'} ~ ${row.effectiveDateEnd || '-'}`
}
// 新增/编辑/查看 弹框
function openForm(mode, row) {
    dialogVisible.value = true
    nextTick(() => {
        dialogRef.value?.open({ mode, row })
    })
}
onMounted(() => {
    getList()
})
</script>
<style scoped lang="scss"></style>
applications/drone-command/src/views/areaManage/sceneManage/sceneManageApi.js
New file
@@ -0,0 +1,38 @@
import request from '@/axios'
// 分页查询场景管理列表
export const fwDefenseSceneManagePageApi = params => {
    return request({
        url: `/drone-fw/area/fwDefenseSceneManage/page`,
        method: 'get',
        params: { descs: 'update_time', ...params },
    })
}
// 获取场景管理详情
export const fwDefenseSceneManageDetailApi = params => {
    return request({
        url: `/drone-fw/area/fwDefenseSceneManage/detail`,
        method: 'get',
        params,
    })
}
// 新增或修改场景管理
export const fwDefenseSceneManageSubmitApi = data => {
    return request({
        url: `/drone-fw/area/fwDefenseSceneManage/submit`,
        method: 'post',
        data,
    })
}
// 删除场景管理
export const fwDefenseSceneManageRemoveApi = params => {
    return request({
        url: `/drone-fw/area/fwDefenseSceneManage/remove`,
        method: 'post',
        params,
    })
}
applications/drone-command/src/views/audioCall.vue
@@ -1,439 +1,439 @@
<template>
    <div style="padding:16px">
      <h2>1v1 语音通话 WebRTC Demo(对齐后端 ChatMessage,JS)</h2>
      <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
        <span>我的 userId:</span>
        <input v-model="uid" style="width:120px" />
        <span>对方 userId:</span>
        <input v-model="peerUid" style="width:120px" />
        <button @click="connectWS" :disabled="connected">连接WS</button>
        <button @click="requestCall" :disabled="!connected || state!=='idle'">呼叫</button>
        <button v-if="state==='ringing'" @click="acceptCall">接听</button>
        <button v-if="state==='ringing'" @click="rejectCall">拒绝</button>
        <button @click="hangup" :disabled="!connected || (state!=='calling' && state!=='in-call')">挂断</button>
        <strong>状态:{{ state }}</strong>
        <strong v-if="state==='in-call'">通话时长:{{ durationText }}</strong>
      </div>
      <div style="margin-top:8px;color:#666">
        <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>
      </div>
      <pre style="margin-top:12px;max-height:280px;overflow:auto;">{{ logText }}</pre>
      <audio ref="remoteAudio" autoplay></audio>
        <h2>1v1 语音通话 WebRTC Demo(对齐后端 ChatMessage,JS)</h2>
        <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
            <span>我的 userId:</span>
            <input v-model="uid" style="width:120px" />
            <span>对方 userId:</span>
            <input v-model="peerUid" style="width:120px" />
            <button @click="connectWS" :disabled="connected">连接WS</button>
            <button @click="requestCall" :disabled="!connected || state!=='idle'">呼叫</button>
            <button v-if="state==='ringing'" @click="acceptCall">接听</button>
            <button v-if="state==='ringing'" @click="rejectCall">拒绝</button>
            <button @click="hangup" :disabled="!connected || (state!=='calling' && state!=='in-call')">挂断</button>
            <strong>状态:{{ state }}</strong>
            <strong v-if="state==='in-call'">通话时长:{{ durationText }}</strong>
        </div>
        <div style="margin-top:8px;color:#666">
            <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>
        </div>
        <pre style="margin-top:12px;max-height:280px;overflow:auto;">{{ logText }}</pre>
        <audio ref="remoteAudio" autoplay></audio>
    </div>
  </template>
  <script setup>
  import { ref, computed, onBeforeUnmount } from 'vue'
  /** ✅ 你的 WS 地址前缀(后面拼 userId) */
  const WS_BASE = 'wss://域名/ws/chat?userId='
  const uid = ref('2')
  const peerUid = ref('3')
  const connected = ref(false)
  const state = ref('idle') // idle | calling | ringing | in-call
  const logText = ref('')
  const remoteAudio = ref(null)
  let ws = null
  let pc = null
  let localStream = null
  const pendingCandidates = []
  // 来电暂存
  let incomingFrom = null
  let acceptedByMe = false // 我是否已经点了接听
  let offeredByPeer = false // 是否已收到对方 offer(用于流程判断)
  // 计时
  let timer = null
  const seconds = ref(0)
  const durationText = computed(() => {
</template>
<script setup>
import { ref, computed, onBeforeUnmount } from 'vue'
/** ✅ 你的 WS 地址前缀(后面拼 userId) */
const WS_BASE = 'wss://域名/ws/chat?userId='
const uid = ref('2')
const peerUid = ref('3')
const connected = ref(false)
const state = ref('idle') // idle | calling | ringing | in-call
const logText = ref('')
const remoteAudio = ref(null)
let ws = null
let pc = null
let localStream = null
const pendingCandidates = []
// 来电暂存
let incomingFrom = null
let acceptedByMe = false // 我是否已经点了接听
let offeredByPeer = false // 是否已收到对方 offer(用于流程判断)
// 计时
let timer = null
const seconds = ref(0)
const durationText = computed(() => {
    const m = String(Math.floor(seconds.value / 60)).padStart(2, '0')
    const s = String(seconds.value % 60).padStart(2, '0')
    return `${m}:${s}`
  })
  function startTimer() {
})
function startTimer() {
    stopTimer()
    seconds.value = 0
    timer = setInterval(() => seconds.value++, 1000)
  }
  function stopTimer() {
}
function stopTimer() {
    if (timer) clearInterval(timer)
    timer = null
  }
  // 心跳
  let pingTimer = null
  function startPing() {
}
// 心跳
let pingTimer = null
function startPing() {
    stopPing()
    // 每 25 秒发一次 ping(你后端有 PING/PONG)
    pingTimer = setInterval(() => {
      if (connected.value) send('ping', 'system', null)
        if (connected.value) send('ping', 'system', null)
    }, 25000)
  }
  function stopPing() {
}
function stopPing() {
    if (pingTimer) clearInterval(pingTimer)
    pingTimer = null
  }
  function log(...args) {
}
function log(...args) {
    const line = args.map(v => (typeof v === 'string' ? v : JSON.stringify(v))).join(' ')
    logText.value += `[${new Date().toLocaleTimeString()}] ${line}\n`
    console.log(...args)
  }
  /**
   * ✅ 严格按后端 ChatMessage 字段发:
   * { type, from, to, payload, timestamp }
   * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处
   */
  function send(type, to, payload) {
}
/**
 * ✅ 严格按后端 ChatMessage 字段发:
 * { type, from, to, payload, timestamp }
 * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处
 */
function send(type, to, payload) {
    if (!type) return
    if (!ws || ws.readyState !== WebSocket.OPEN) return
    const msg = {
      type,                 // "call/accept/busy/offer/answer/candidate/hangup/ping"
      from: String(uid.value),
      to: String(to),
      payload: payload ?? null,
      timestamp: Date.now(),
        type,                 // "call/accept/busy/offer/answer/candidate/hangup/ping"
        from: String(uid.value),
        to: String(to),
        payload: payload ?? null,
        timestamp: Date.now(),
    }
    ws.send(JSON.stringify(msg))
  }
  /* ---------------- WebRTC ---------------- */
  function createPC() {
}
/* ---------------- WebRTC ---------------- */
function createPC() {
    pc = new RTCPeerConnection({
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
        iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
    })
    pc.onicecandidate = (e) => {
      if (e.candidate) {
        send('candidate', peerUid.value, { candidate: e.candidate.toJSON ? e.candidate.toJSON() : e.candidate })
      }
        if (e.candidate) {
            send('candidate', peerUid.value, { candidate: e.candidate.toJSON ? e.candidate.toJSON() : e.candidate })
        }
    }
    pc.ontrack = (e) => {
      log('✅ 收到远端音频流')
      const stream = e.streams[0]
      if (remoteAudio.value) {
        remoteAudio.value.srcObject = stream
        remoteAudio.value.play().catch(() => {})
      }
        log('✅ 收到远端音频流')
        const stream = e.streams[0]
        if (remoteAudio.value) {
            remoteAudio.value.srcObject = stream
            remoteAudio.value.play().catch(() => {})
        }
    }
    pc.onconnectionstatechange = () => {
      log('pc.connectionState =', pc.connectionState)
        log('pc.connectionState =', pc.connectionState)
    }
  }
  async function getLocalAudio() {
}
async function getLocalAudio() {
    if (localStream) return localStream
    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
      log('❌ 当前环境不支持 getUserMedia:请使用 https 或 localhost 打开页面')
      throw new Error('getUserMedia not available')
        log('❌ 当前环境不支持 getUserMedia:请使用 https 或 localhost 打开页面')
        throw new Error('getUserMedia not available')
    }
    try {
      localStream = await navigator.mediaDevices.getUserMedia({ audio: true })
      log('✅ 已获取本地麦克风')
      return localStream
        localStream = await navigator.mediaDevices.getUserMedia({ audio: true })
        log('✅ 已获取本地麦克风')
        return localStream
    } catch (e) {
      log('❌ 获取麦克风失败:', String(e))
      throw e
        log('❌ 获取麦克风失败:', String(e))
        throw e
    }
  }
  /** ✅ 关键:只 addTrack 一次,避免 “sender already exists” */
  async function ensureLocalTracksAdded() {
}
/** ✅ 关键:只 addTrack 一次,避免 “sender already exists” */
async function ensureLocalTracksAdded() {
    if (!pc) createPC()
    const stream = await getLocalAudio()
    const existingTrackIds = new Set(
      pc.getSenders().map(s => (s.track ? s.track.id : null)).filter(Boolean)
        pc.getSenders().map(s => (s.track ? s.track.id : null)).filter(Boolean)
    )
    stream.getTracks().forEach(track => {
      if (!existingTrackIds.has(track.id)) {
        pc.addTrack(track, stream)
        log('addTrack ok:', track.id)
      } else {
        // log('skip addTrack:', track.id)
      }
        if (!existingTrackIds.has(track.id)) {
            pc.addTrack(track, stream)
            log('addTrack ok:', track.id)
        } else {
            // log('skip addTrack:', track.id)
        }
    })
    return stream
  }
  async function flushCandidates() {
}
async function flushCandidates() {
    if (!pc || !pc.remoteDescription) return
    while (pendingCandidates.length) {
      const c = pendingCandidates.shift()
      try {
        await pc.addIceCandidate(new RTCIceCandidate(c))
      } catch (e) {
        log('addIceCandidate failed:', String(e))
      }
        const c = pendingCandidates.shift()
        try {
            await pc.addIceCandidate(new RTCIceCandidate(c))
        } catch (e) {
            log('addIceCandidate failed:', String(e))
        }
    }
  }
  /* ---------------- 信令处理 ---------------- */
  async function onSignal(msg) {
}
/* ---------------- 信令处理 ---------------- */
async function onSignal(msg) {
    if (!msg) return
    const t = (msg.type || '').toString()
    // ✅ 忽略后端回显给自己的消息(你的后端会回显 sender)
    if (msg.from && String(msg.from) === String(uid.value)) {
      return
        return
    }
    // system 消息:login/pong/busy 仅记录
    if (msg.from === 'system') {
      log('system msg:', msg)
      return
    }
    if (!t) return
    if (t === 'call') {
      // 收到来电:弹接听按钮
      incomingFrom = String(msg.from)
      peerUid.value = incomingFrom
      acceptedByMe = false
      offeredByPeer = false
      state.value = 'ringing'
      log('📞 收到来电 call,来自', incomingFrom)
      return
    }
    if (t === 'accept') {
      // 主叫收到 accept:开始 offer
      log('✅ 对方接听 accept,开始建立连接(offer)')
      await startOffer()
      return
    }
    if (t === 'busy') {
      log('❌ 对方忙线/拒绝')
      cleanup(false)
      return
    }
    if (t === 'offer') {
      // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”)
      offeredByPeer = true
      incomingFrom = String(msg.from)
      peerUid.value = incomingFrom
      if (!acceptedByMe) {
        state.value = 'ringing'
        log('📩 收到 offer(但未接听),已等待用户点击接听')
        // 暂存 offer 到一个变量
        lastOfferSdp = msg.payload?.sdp
        log('system msg:', msg)
        return
      }
      // 已接听:直接 answer
      await answerOffer(msg.payload?.sdp, incomingFrom)
      return
    }
    if (!t) return
    if (t === 'call') {
        // 收到来电:弹接听按钮
        incomingFrom = String(msg.from)
        peerUid.value = incomingFrom
        acceptedByMe = false
        offeredByPeer = false
        state.value = 'ringing'
        log('📞 收到来电 call,来自', incomingFrom)
        return
    }
    if (t === 'accept') {
        // 主叫收到 accept:开始 offer
        log('✅ 对方接听 accept,开始建立连接(offer)')
        await startOffer()
        return
    }
    if (t === 'busy') {
        log('❌ 对方忙线/拒绝')
        cleanup(false)
        return
    }
    if (t === 'offer') {
        // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”)
        offeredByPeer = true
        incomingFrom = String(msg.from)
        peerUid.value = incomingFrom
        if (!acceptedByMe) {
            state.value = 'ringing'
            log('📩 收到 offer(但未接听),已等待用户点击接听')
            // 暂存 offer 到一个变量
            lastOfferSdp = msg.payload?.sdp
            return
        }
        // 已接听:直接 answer
        await answerOffer(msg.payload?.sdp, incomingFrom)
        return
    }
    if (t === 'answer') {
      log('✅ 收到 answer,通话建立')
      if (!pc) createPC()
      await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp))
      await flushCandidates()
      state.value = 'in-call'
      startTimer()
      return
        log('✅ 收到 answer,通话建立')
        if (!pc) createPC()
        await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp))
        await flushCandidates()
        state.value = 'in-call'
        startTimer()
        return
    }
    if (t === 'candidate') {
      const c = msg.payload?.candidate
      if (!c) return
      if (pc && pc.remoteDescription) {
        await pc.addIceCandidate(new RTCIceCandidate(c))
      } else {
        pendingCandidates.push(c)
      }
      return
        const c = msg.payload?.candidate
        if (!c) return
        if (pc && pc.remoteDescription) {
            await pc.addIceCandidate(new RTCIceCandidate(c))
        } else {
            pendingCandidates.push(c)
        }
        return
    }
    if (t === 'hangup') {
      log('📴 对方挂断')
      cleanup(false)
      return
        log('📴 对方挂断')
        cleanup(false)
        return
    }
    if (t === 'pong') {
      // 心跳响应
      // log('pong')
      return
        // 心跳响应
        // log('pong')
        return
    }
  }
  // 暂存 offer(用于“先点接听再 answer”)
  let lastOfferSdp = null
  /* ---------------- 业务操作 ---------------- */
  function connectWS() {
}
// 暂存 offer(用于“先点接听再 answer”)
let lastOfferSdp = null
/* ---------------- 业务操作 ---------------- */
function connectWS() {
    if (ws) ws.close()
    ws = new WebSocket(WS_BASE + encodeURIComponent(uid.value))
    ws.onopen = () => {
      connected.value = true
      log('WS 已连接 userId=', uid.value)
      startPing()
        connected.value = true
        log('WS 已连接 userId=', uid.value)
        startPing()
    }
    ws.onclose = () => {
      connected.value = false
      log('WS 已关闭')
      stopPing()
        connected.value = false
        log('WS 已关闭')
        stopPing()
    }
    ws.onerror = (e) => log('WS error', e)
    ws.onmessage = (e) => {
      const msg = JSON.parse(e.data)
      log('收到信令', msg)
      onSignal(msg)
        const msg = JSON.parse(e.data)
        log('收到信令', msg)
        onSignal(msg)
    }
  }
  function requestCall() {
}
function requestCall() {
    state.value = 'calling'
    incomingFrom = null
    acceptedByMe = false
    offeredByPeer = false
    lastOfferSdp = null
    send('call', peerUid.value, null)
    log('➡️ 发起呼叫 call 给', peerUid.value)
  }
  function acceptCall() {
}
function acceptCall() {
    if (!incomingFrom) return
    acceptedByMe = true
    // 先告诉对方我接听了(后端会把双方置忙)
    send('accept', incomingFrom, null)
    log('✅ 已点击接听,发送 accept 给', incomingFrom)
    state.value = 'calling'
    // 如果 offer 已经提前到达(我们暂存了),立刻 answer
    if (lastOfferSdp) {
      answerOffer(lastOfferSdp, incomingFrom)
      lastOfferSdp = null
        answerOffer(lastOfferSdp, incomingFrom)
        lastOfferSdp = null
    }
  }
  function rejectCall() {
}
function rejectCall() {
    if (!incomingFrom) return
    // 你后端没有 reject,用 busy 表示拒绝/不可接听
    send('busy', incomingFrom, null)
    log('❌ 已拒绝来电,发送 busy 给', incomingFrom)
    incomingFrom = null
    acceptedByMe = false
    offeredByPeer = false
    lastOfferSdp = null
    state.value = 'idle'
  }
  async function startOffer() {
}
async function startOffer() {
    if (!pc) createPC()
    // ✅ 不会重复 addTrack
    await ensureLocalTracksAdded()
    const offer = await pc.createOffer()
    await pc.setLocalDescription(offer)
    send('offer', peerUid.value, { sdp: pc.localDescription })
    log('➡️ 已发送 offer 给', peerUid.value)
  }
  async function answerOffer(offerSdp, from) {
}
async function answerOffer(offerSdp, from) {
    if (!offerSdp) {
      log('❌ offer sdp 为空,无法接听')
      return
        log('❌ offer sdp 为空,无法接听')
        return
    }
    if (!pc) createPC()
    await pc.setRemoteDescription(new RTCSessionDescription(offerSdp))
    await flushCandidates()
    await ensureLocalTracksAdded()
    const answer = await pc.createAnswer()
    await pc.setLocalDescription(answer)
    send('answer', from, { sdp: pc.localDescription })
    log('⬅️ 已发送 answer 给', from)
    state.value = 'in-call'
    startTimer()
  }
  function hangup() {
}
function hangup() {
    cleanup(true)
  }
  /* ---------------- 清理 ---------------- */
  function cleanup(sendToPeer) {
}
/* ---------------- 清理 ---------------- */
function cleanup(sendToPeer) {
    try {
      if (sendToPeer) send('hangup', peerUid.value, null)
        if (sendToPeer) send('hangup', peerUid.value, null)
    } catch {}
    stopTimer()
    if (pc) {
      pc.getSenders().forEach(s => s.track && s.track.stop())
      pc.close()
        pc.getSenders().forEach(s => s.track && s.track.stop())
        pc.close()
    }
    if (localStream) {
      localStream.getTracks().forEach(t => t.stop())
        localStream.getTracks().forEach(t => t.stop())
    }
    pc = null
    localStream = null
    pendingCandidates.length = 0
    incomingFrom = null
    acceptedByMe = false
    offeredByPeer = false
    lastOfferSdp = null
    state.value = 'idle'
  }
  onBeforeUnmount(() => {
}
onBeforeUnmount(() => {
    cleanup(false)
    stopPing()
    if (ws) ws.close()
  })
  </script>
})
</script>
  
applications/drone-command/src/views/dataCockpit/components/LegendBar.vue
@@ -2,7 +2,7 @@
 * @Author       : yuan
 * @Date         : 2026-01-17 14:51:39
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-17 15:13:50
 * @LastEditTime : 2026-01-30 15:20:56
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\LegendBar.vue
 * @Description  : 
 * Copyright 2026 OBKoro1, All Rights Reserved. 
@@ -12,12 +12,16 @@
    <div class="legend-bar">
        <div class="legend-title" data-text="图例">图例</div>
        <div class="legend-item">
            <span class="legend-color defense"></span>
            <span class="legend-text" data-text="防区">防区</span>
            <span class="legend-color detect-area"></span>
            <span class="legend-text" data-text="侦测区">侦测区</span>
        </div>
        <div class="legend-item">
            <span class="legend-color partition"></span>
            <span class="legend-text" data-text="区域">区域</span>
            <span class="legend-color alarm-area"></span>
            <span class="legend-text" data-text="报警区">报警区</span>
        </div>
        <div class="legend-item">
            <span class="legend-color key-control-area"></span>
            <span class="legend-text" data-text="重点管制区">重点管制区</span>
        </div>
        <div class="legend-item">
            <img class="legend-icon drone" :src="droneIcon" alt="无人机" />
@@ -84,16 +88,27 @@
    border-radius: 0;
}
.legend-color.defense {
    background: radial-gradient(50% 50% at 50% 50%, rgba(114, 243, 255, 0.45) 0%, rgba(10, 124, 136, 0.55) 100%);
    border: 1px solid rgba(114, 243, 255, 0.85);
}
.legend-color.partition {
    background: radial-gradient(50% 50% at 50% 50%, rgba(255, 215, 114, 0.5) 0%, rgba(168, 107, 0, 0.6) 100%);
    border: 1px solid rgba(255, 215, 114, 0.9);
}
.legend-color.detect-area {
    background-color: rgba(25, 178, 230, 0.5);
    border: 1px solid rgb(0, 251, 255);
}
.legend-color.alarm-area {
    background-color: rgba(255, 235, 59, 0.5);
    border: 1px solid rgb(255, 235, 59);
}
.legend-color.key-control-area {
    background-color: rgba(247, 20, 20, 0.5);
    border: 1px solid rgb(255, 0, 0);
}
.legend-icon {
    width: 30px;
    height: 42px;
applications/drone-command/src/views/permissionManage/permissionDept/FormDiaLog.vue
@@ -175,7 +175,7 @@
const emit = defineEmits(['success'])
const formRef = ref(null)
const formData = ref(initForm())
const visible = ref(false)
const visible = defineModel()
const dialogMode = ref('add')
const submitting = ref(false)
const isAddChild = ref(false)
@@ -283,7 +283,6 @@
async function open({ mode, row, parent } = {}) {
    dialogMode.value = mode || 'add'
    visible.value = true
    isAddChild.value = mode === 'add' && !!parent?.id
    await getDeptTree().then(res => {
        if (deptTree) {
applications/drone-command/src/views/permissionManage/permissionDept/ShareDiaLog.vue
New file
@@ -0,0 +1,169 @@
<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="right-container" style="width: 100%">
                <div class="header">
                    <span>设备共享</span>
                    <el-icon class="close-btn" @click.stop="visible = false"><Close /></el-icon>
                </div>
                <div class="content">
                    <div class="command-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)">
                        <div class="command-table-content">
                            <el-table
                                ref="tableRef"
                                class="command-table"
                                :data="deviceList"
                                row-key="id"
                                @selection-change="handleSelectionChange"
                            >
                                <el-table-column type="selection" width="55" :reserve-selection="true" />
                                <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                                <el-table-column prop="deviceName" show-overflow-tooltip label="设备名称" />
                                <el-table-column prop="deviceSn" show-overflow-tooltip label="设备SN" />
                                <el-table-column prop="deviceType" show-overflow-tooltip label="设备类型">
                                    <template v-slot="{ row }">
                                        {{ getDictLabel(row.deviceType, dictObj.deviceType) }}
                                    </template>
                                </el-table-column>
                                <el-table-column prop="belongDeptName" show-overflow-tooltip label="所属部门" />
                            </el-table>
                        </div>
                    </div>
                </div>
                <div class="footer">
                    <el-button color="#2B2B4C" @click="handleCancel">取消</el-button>
                    <el-button color="#284FE3" 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 { ref, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { nonDeptAreaDevicesApi, fwDevicePerShareSaveApi, fwDevicePerShareRemoveApi } from './fwDevicePerShareApi'
import { getDictionaryByCode } from '@/api/system/dictbiz'
import { getDictLabel } from '@ztzf/utils'
const emit = defineEmits(['success'])
const visible = defineModel() // 弹框显隐
const loading = ref(false) // 加载中
const submitting = ref(false) // 提交中
const deviceList = ref([]) // 设备列表
const tableRef = ref(null) // 表格实例
const selectedRows = ref([]) // 当前选中的行
const originalSharedIds = ref([]) // 原始已共享的设备ID集合
const originalSharedMap = ref({}) // 原始已共享的设备ID与shareId的映射
const currentDeptId = ref(null) // 当前操作的部门ID
const dictObj = ref({
    deviceType: [], // 设备类型
})
// 获取字典
function getDictList() {
    getDictionaryByCode('deviceType').then(res => {
        dictObj.value = res.data.data
    })
}
// 选中变化
function handleSelectionChange(rows) {
    selectedRows.value = rows
}
// 关闭弹框
function handleCancel() {
    visible.value = false
}
// 获取设备列表
async function getDeviceList() {
    loading.value = true
    try {
        const res = await nonDeptAreaDevicesApi({ loanToDeptId: currentDeptId.value })
        deviceList.value = res?.data?.data ?? []
        // 记录原始已共享的设备ID和shareId映射
        const sharedDevices = deviceList.value.filter(item => item.isShared)
        originalSharedIds.value = sharedDevices.map(item => item.id)
        originalSharedMap.value = sharedDevices.reduce((map, item) => {
            map[item.id] = item.shareId
            return map
        }, {})
        // 回显已共享的设备勾选状态
        nextTick(() => {
            deviceList.value.forEach(item => {
                if (item.isShared) {
                    tableRef.value?.toggleRowSelection(item, true)
                }
            })
        })
    } finally {
        loading.value = false
    }
}
// 提交
async function handleSubmit() {
    submitting.value = true
    try {
        const currentSelectedIds = selectedRows.value.map(item => item.id)
        // 计算需要新增的设备(当前选中但原来未共享)
        const toAddDevices = selectedRows.value.filter(item => !originalSharedIds.value.includes(item.id))
        // 计算需要删除的设备(原来共享但当前未选中),使用shareId
        const toRemoveShareIds = originalSharedIds.value
            .filter(id => !currentSelectedIds.includes(id))
            .map(id => originalSharedMap.value[id])
            .filter(Boolean)
        const promises = []
        // 调用新增接口
        if (toAddDevices.length > 0) {
            const addData = toAddDevices.map(item => ({
                deviceId: item.id,
                deviceSn: item.deviceSn,
                loanFromDeptId: item.belongDept,
                loanToDeptId: currentDeptId.value,
            }))
            promises.push(fwDevicePerShareSaveApi(addData))
        }
        // 调用删除接口
        if (toRemoveShareIds.length > 0) {
            promises.push(fwDevicePerShareRemoveApi({ ids: toRemoveShareIds.join(',') }))
        }
        if (promises.length > 0) {
            await Promise.all(promises)
            ElMessage.success('保存成功')
        } else {
            ElMessage.info('未做任何修改')
        }
        visible.value = false
        emit('success')
    } finally {
        submitting.value = false
    }
}
// 打开弹框
async function open({ row } = {}) {
    currentDeptId.value = row?.id
    selectedRows.value = []
    getDictList()
    await getDeviceList()
}
defineExpose({ open })
</script>
<style scoped lang="scss"></style>
applications/drone-command/src/views/permissionManage/permissionDept/fwDevicePerShareApi.js
New file
@@ -0,0 +1,28 @@
import request from '@/axios'
// 查询非本部门区域的设备
export const nonDeptAreaDevicesApi = params => {
    return request({
        url: `/drone-fw/device/fwDevice/nonDeptAreaDevices`,
        method: 'get',
        params,
    })
}
// 新增设备共享
export const fwDevicePerShareSaveApi = data => {
    return request({
        url: `/drone-fw/device/fwDevicePerShare/save`,
        method: 'post',
        data,
    })
}
// 删除设备共享
export const fwDevicePerShareRemoveApi = params => {
    return request({
        url: `/drone-fw/device/fwDevicePerShare/remove`,
        method: 'post',
        params,
    })
}
applications/drone-command/src/views/permissionManage/permissionDept/index.vue
@@ -62,13 +62,14 @@
                    <el-table-column prop="sort" show-overflow-tooltip width="80" label="排序" />
                    <el-table-column prop="bingId" show-overflow-tooltip width="100" label="组织id" />
                    <el-table-column prop="areaName" show-overflow-tooltip width="180" label="行政区划" />
                    <el-table-column label="操作" class-name="operation-btns" width="220">
                        <template v-slot="{ row }">
                            <el-link @click="handleView(row)">查看</el-link>
                            <el-link @click="handleEdit(row)">编辑</el-link>
                            <el-link @click="handleAddChild(row)">新增子项</el-link>
                            <el-link type="danger" @click="handleDelete(row)">删除</el-link>
                            <el-link type="danger" @click="handleShare(row)">共享</el-link>
                        </template>
                    </el-table-column>
                </el-table>
@@ -86,18 +87,20 @@
            </div>
        </div>
        <FormDiaLog ref="dialogRef" @success="getList" />
        <FormDiaLog ref="dialogRef" @success="getList" v-model="visible" v-if="visible" />
        <ShareDiaLog ref="shareDialogRef" @success="getList" v-model="shareVisible" v-if="shareVisible" />
    </basic-container>
</template>
<script setup>
import { Delete, Plus, RefreshRight, Search } from '@element-plus/icons-vue'
import { onMounted, provide, ref } from 'vue'
import { nextTick, onMounted, provide, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getDictionary } from '@/api/system/dict'
import { getChildLazyTree, getDeptTree, getLazyList, remove } from '@/api/system/dept'
import { getDictLabel } from '@ztzf/utils'
import FormDiaLog from './FormDiaLog.vue'
import ShareDiaLog from './ShareDiaLog.vue'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
@@ -110,11 +113,14 @@
const searchParams = ref(initSearchParams())
const loading = ref(true)
const visible = ref(false)
const list = ref([])
const total = ref(0)
const selectedIds = ref([])
const queryParamsRef = ref(null)
const dialogRef = ref(null)
const shareDialogRef = ref(null)
const shareVisible = ref(false)
const dictObj = ref({
    org_category: [],
    org_nature: [],
@@ -148,19 +154,38 @@
}
function handleAdd() {
    dialogRef.value?.open({ mode: 'add' })
    visible.value = true
    nextTick(() => {
        dialogRef.value?.open({ mode: 'add' })
    })
}
function handleAddChild(row) {
    dialogRef.value?.open({ mode: 'add', parent: row })
    visible.value = true
    nextTick(() => {
        dialogRef.value?.open({ mode: 'add', parent: row })
    })
}
function handleView(row) {
    dialogRef.value?.open({ mode: 'view', row: { ...row } })
    visible.value = true
    nextTick(() => {
        dialogRef.value?.open({ mode: 'view', row: { ...row } })
    })
}
function handleEdit(row) {
    dialogRef.value?.open({ mode: 'edit', row: { ...row } })
    visible.value = true
    nextTick(() => {
        dialogRef.value?.open({ mode: 'edit', row: { ...row } })
    })
}
function handleShare(row) {
    shareVisible.value = true
    nextTick(() => {
        shareDialogRef.value?.open({ row })
    })
}
async function handleDelete(row) {
applications/task-work-order/src/views/orderView/orderDataManage/appInnovation/FormDiaLog.vue
@@ -6,6 +6,7 @@
        @closed="visible = false"
        destroy-on-close
        :close-on-click-modal="false"
        width="800px"
    >
        <div class="detail-container" v-if="dialogReadonly">
            <el-row class="detail-row-view">
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/FormDiaLog.vue
@@ -218,6 +218,42 @@
<!--                <el-button color="#F2F3F5" :loading="submitting" :disabled="submitting" @click="visible = false">关闭</el-button>-->
            </template>
        </template>
        <!-- 申请弹框 -->
        <el-dialog
            class="gd-dialog"
            v-model="showApplyDialog"
            title="申请"
            @closed="handleApplyDialogClosed"
            width="500px"
            destroy-on-close
            :close-on-click-modal="false"
        >
            <el-form class="gd-dialog-form" ref="applyFormRef" :model="applyFormData" :rules="applyRules">
                <el-row>
                    <el-col :span="24">
                        <el-form-item label="申请对象部门" prop="applyTargetDeptId">
                            <el-tree-select
                                class="gd-select"
                                popper-class="gd-tree-select-popper"
                                v-model="applyFormData.applyTargetDeptId"
                                node-key="id"
                                :data="deptTree"
                                :props="treeProps"
                                check-strictly
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
            </el-form>
            <template #footer>
                <el-button color="#F2F3F5" @click="showApplyDialog = false">取消</el-button>
                <el-button class="" color="#4C34FF" :loading="applying" :disabled="applying" @click="handleApplySubmit">
                    确认
                </el-button>
            </template>
        </el-dialog>
    </el-dialog>
</template>
@@ -283,6 +319,14 @@
const attachmentDetailsList = ref([])
const uploading = ref(false) // 文件上传中状态
// 申请弹框相关
const showApplyDialog = ref(false)
const applyFormRef = ref(null)
const applying = ref(false)
const applyFormData = ref({
    applyTargetDeptId: ''
})
// 表单校验规则
const rules = {
    title: fieldRules(true),
@@ -292,7 +336,7 @@
    catalogResourceName: fieldRules(true),
    objectionDesc: fieldRules(true),
    objectionBasis: fieldRules(true),
    // attachId: dialogMode.value === 'add' ? fieldRules(true) : fieldRules(false)
    attachId: fieldRules(true)
}
@@ -300,6 +344,11 @@
const feedbackRules = {
    otherObjectionDetail: fieldRules(true),
    reviewOpinion: fieldRules(true),
}
// 申请弹框校验规则
const applyRules = {
    applyTargetDeptId: fieldRules(true),
}
// 自定义上传请求
@@ -323,9 +372,6 @@
}
// 文件移除
// const handleFileRemove = (file, fileListVal) => {
// uploadedFiles.value = fileListVal
// }
const handleFileRemove = (file, fileListVal) => {
  // 从展示列表中移除
  fileList.value = fileListVal
@@ -375,6 +421,7 @@
// 提交数据的通用函数
async function submitForm(objectionStatus) {
formData.value.attachId = uploadedFiles.value.length > 0 ? uploadedFiles.value.length : ''
  const isValid = await formRef.value?.validate().catch(() => false)
  if (!isValid) return
  
@@ -408,13 +455,6 @@
            }),
      objectionStatus: objectionStatus
    }
    // 如果是编辑且没有修改附件,可以保留原有附件ID
    if (dialogMode.value === 'edit' && formData.value.attachIds) {
      // 这里可以根据业务需求决定是否保留原附件ID
      // 如果接口支持更新时重新上传附件ID列表,可以这样做
    }
    await gdDataObjectionSubmitApi(submitData)
    ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功')
    visible.value = false
@@ -429,9 +469,66 @@
    await submitForm('0')
}
// 申请提交
// 申请弹框关闭处理
function handleApplyDialogClosed() {
    applyFormData.value.applyTargetDeptId = ''
}
// 申请
async function handleApply() {
    await submitForm('1')
     formData.value.attachId = uploadedFiles.value.length > 0 ? uploadedFiles.value.length : ''
     // 先验证主表单
     const mainFormValid = await formRef.value?.validate().catch(() => false)
     if (!mainFormValid) return
     showApplyDialog.value = true
}
// 申请提交
async function handleApplySubmit() {
    const isValid = await applyFormRef.value?.validate().catch(() => false)
    if (!isValid) return
    applying.value = true
    try {
        // 构建申请数据
        const applyData = {
            ...formData.value,
            objectionStatus: '1',
            applyTargetDeptId: applyFormData.value.applyTargetDeptId
        }
        // 构建符合接口要求的提交数据
        const submitData = {
            // 编辑模式下传递id
            ...(applyData.id && { id: applyData.id }),
            title: applyData.title,
            objectionType: applyData.objectionType,
            submitter: applyData.submitter,
            submitterContact: applyData.submitterContact,
            catalogResourceName: applyData.catalogResourceName,
            objectionDesc: applyData.objectionDesc,
            objectionBasis: applyData.objectionBasis,
            areaCode: applyData.areaCode || '',
            handleUnit: applyData.handleUnit || '',
            // 构建附件列表 - 使用当前已上传的文件(包括回显的)
            ...(uploadedFiles.value.length > 0 && {
                attachmentList: uploadedFiles.value.map(file => ({
                    attachId: file.attachId,
                    attachName: file.originalName,
                })),
            }),
            objectionStatus: applyData.objectionStatus,
            applyTargetDeptId: applyData.applyTargetDeptId
        }
        await gdDataObjectionSubmitApi(submitData)
        ElMessage.success('申请成功')
        showApplyDialog.value = false
        visible.value = false
        emit('success')
    } finally {
        applying.value = false
    }
}
// 处理异议反馈提交
@@ -509,8 +606,6 @@
    ElMessage.warning('没有可下载的附件')
    return
  }
  // 顺序下载每个附件
  for (let i = 0; i < attachmentDetailsList.value.length; i++) {
    const attach = attachmentDetailsList.value[i]
@@ -521,10 +616,6 @@
    }
    try {
      // 显示当前下载的文件信息
      const currentFileName = attach.originalName || attach.name || `附件_${i + 1}`
      const response = await fetch(attach.link)
      const blob = await response.blob()
      const url = window.URL.createObjectURL(blob)
@@ -568,8 +659,6 @@
  }
  
}
// 打开弹框
async function open({ mode = 'add', row } = {}) {
  dialogMode.value = mode
@@ -580,6 +669,8 @@
  formData.value = dialogMode.value === 'add' ? initForm() : row
  // 重置反馈表单数据
  feedbackFormData.value = initFeedbackForm()
  // 重置申请弹框数据
  applyFormData.value.applyTargetDeptId = ''
  
  if (dialogMode.value !== 'add') {
    // 如果是编辑,设置ID并加载详情
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/index.vue
@@ -59,7 +59,7 @@
                            {{ getDictLabel(row.objectionStatus, dictObj.objectionStatus) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="catalogResourceName" width="130" show-overflow-tooltip  label="目录/申请资源名称" />
                    <el-table-column prop="catalogResourceName" width="140" show-overflow-tooltip  label="目录/申请资源名称" />
                    <el-table-column prop="otherObjectionDetail" show-overflow-tooltip label="其他异议详情" />
                    <el-table-column prop="submitter" show-overflow-tooltip label="问题提交人" />
                    <el-table-column prop="submitterContact" show-overflow-tooltip width="120" label="提交人联系方式" />
applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/ApplyViewDialog.vue
@@ -68,7 +68,7 @@
                        </el-row>
                </div>
                <!--                申请中-->
                <div v-if="detailDemandStatus === '1' && !requesterProvider">
                <div v-if="detailDemandStatus === '1' && (permissionList.approvedBtn || permissionList.rejectBtn)">
                    <div class="detail-title" :style="{ marginTop:pxToRem(10)}">数据上传</div>
                    <div class="upload-container">
                        <el-upload
@@ -106,16 +106,16 @@
            </div>
        </div>
        <template #footer>
            <template v-if="detailDemandStatus === '1' && !requesterProvider">
                <el-button color="#F2F3F5" :loading="submitting || uploading" :disabled="submitting || uploading" @click="rejectTheApplication">拒绝申请</el-button>
                <el-button class="" color="#4C34FF" :loading="submitting || uploading" :disabled="submitting || uploading" @click="approvedByTheReview">审核通过</el-button>
            <template v-if="detailDemandStatus === '1' ">
                <el-button v-if="permissionList.rejectBtn" color="#F2F3F5" :loading="submitting || uploading" :disabled="submitting || uploading" @click="rejectTheApplication">拒绝申请</el-button>
                <el-button v-if="permissionList.approvedBtn" class="" color="#4C34FF" :loading="submitting || uploading" :disabled="submitting || uploading" @click="approvedByTheReview">审核通过</el-button>
            </template>
            <template v-if="detailDemandStatus === '2'">
                <el-button class="" color="#4C34FF" @click="downloadAllFiles">全部下载</el-button>
            </template>
            <template v-if="detailDemandStatus === '3'">
                <el-button class="" color="#4C34FF" @click="visible =false">关闭</el-button>
            </template>
<!--            <template v-if="detailDemandStatus === '3'">-->
<!--                <el-button class="" color="#4C34FF" @click="visible =false">关闭</el-button>-->
<!--            </template>-->
        </template>
    </el-dialog>
@@ -142,7 +142,7 @@
import AuditRecord from '@/views/orderView/orderDataManage/supplyAdd/auditRecord.vue' // 导入新组件
import { useStore } from 'vuex'
const store = useStore()
const requesterProvider = computed(() => store.state.user.userInfo?.role_id === '2014158512610869250')
const permission = computed(() => store.state.user.permission);
const emit = defineEmits(['success'])
const dictObj = inject('dictObj')
const deptTree = inject('deptTree')
@@ -156,6 +156,16 @@
const responseData = ref(null)
const submitting = ref(false) // 提交中状态
const uploading = ref(false) // 文件上传中状态
function validData(value, defaultValue) {
    return value !== undefined ? value : defaultValue
}
const permissionList = computed(() => {
    return {
        addBtn: validData(permission.value.orderData_addDemand, false),
        rejectBtn: validData(permission.value.orderData_rejectApplication, false),
        approvedBtn: validData(permission.value.orderData_approved, false),
    }
})
// 自定义上传请求函数
const handleHttpRequest = async (options) => {
    uploading.value = true
applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/FormDiaLog.vue
@@ -8,7 +8,7 @@
        :close-on-click-modal="false"
    >
        <div class="detail-container" v-if="dialogReadonly">
            <el-row class="detail-row-view">
            <el-row class="detail-row-view supplyAddLabel">
                <el-col :span="12">
                    <div class="label">需求名称</div>
                    <div class="val">{{ formData.demandName }}</div>
@@ -194,9 +194,45 @@
            >
                保存
            </el-button>
            <el-button color="#4C34FF" :loading="submitting" :disabled="submitting" @click="handleApply">申请</el-button>
            <el-button color="#4C34FF" :loading="submitting" :disabled="submitting" @click="handleApply" >申请</el-button>
        </template>
        <!-- 申请弹框 -->
        <el-dialog
            class="gd-dialog"
            v-model="showApplyDialog"
            title="申请"
            @closed="handleApplyDialogClosed"
            width="500px"
            destroy-on-close
            :close-on-click-modal="false"
        >
            <el-form class="gd-dialog-form" ref="applyFormRef" :model="applyFormData" :rules="applyRules">
                <el-row>
                    <el-col :span="24">
                        <el-form-item label="申请对象部门" prop="applyTargetDeptId">
                            <el-tree-select
                                class="gd-select"
                                popper-class="gd-tree-select-popper"
                                v-model="applyFormData.applyTargetDeptId"
                                node-key="id"
                                :data="deptTree"
                                :props="treeProps"
                                check-strictly
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
            </el-form>
            <template #footer>
                <el-button color="#F2F3F5" @click="showApplyDialog = false">取消</el-button>
                <el-button class="" color="#4C34FF" :loading="applying" :disabled="applying" @click="handleApplySubmit">
                    确认
                </el-button>
            </template>
        </el-dialog>
    </el-dialog>
</template>
@@ -240,6 +276,14 @@
const dialogReadonly = computed(() => dialogMode.value === 'view')
const titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' })
// 申请弹框相关
const showApplyDialog = ref(false)
const applyFormRef = ref(null)
const applying = ref(false)
const applyFormData = ref({
    applyTargetDeptId: ''
})
const rules = {
    demandName: fieldRules(true),
    demandType: fieldRules(true),
@@ -253,6 +297,11 @@
    responsibleDeptId: fieldRules(true),
    dataSource: fieldRules(true),
    demandInfo: fieldRules(true),
}
// 申请弹框校验规则
const applyRules = {
    applyTargetDeptId: fieldRules(true),
}
// 根据部门ID获取部门名称
function getDeptNameById(deptId, deptList) {
@@ -295,19 +344,37 @@
    }
}
// 申请提交
// 申请弹框关闭处理
function handleApplyDialogClosed() {
    applyFormData.value.applyTargetDeptId = ''
}
// 申请
async function handleApply() {
    const isValid = await formRef.value?.validate().catch(() => false)
     // 先验证主表单
     const mainFormValid = await formRef.value?.validate().catch(() => false)
     if (!mainFormValid) return
     showApplyDialog.value = true
 }
// 申请提交
async function handleApplySubmit() {
    const isValid = await applyFormRef.value?.validate().catch(() => false)
    if (!isValid) return
    submitting.value = true
    applying.value = true
    try {
        formData.value.demandStatus = '1'
        await gdSupplyDemandSubmitApi(formData.value)
        // 构建申请数据
        const applyData = {
            ...formData.value,
            demandStatus: '1',
            applyTargetDeptId: applyFormData.value.applyTargetDeptId
        }
        await gdSupplyDemandSubmitApi(applyData)
        ElMessage.success('申请成功')
        showApplyDialog.value = false
        visible.value = false
        emit('success')
    } finally {
        submitting.value = false
        applying.value = false
    }
}
@@ -334,3 +401,10 @@
defineExpose({ open })
</script>
<style scoped lang="scss">
.supplyAddLabel {
    .label {
        width: 120px !important;
    }
}
</style>
applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/index.vue
@@ -35,7 +35,7 @@
            </el-form-item>
        </el-form>
        <div class="gd-table-toolbar">
        <div class="gd-table-toolbar" v-if="permissionList.addBtn">
            <el-button :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')">供需填报新增</el-button>
        </div>
@@ -79,9 +79,8 @@
                    <el-table-column label="操作" class-name="operation-btns" width="150">
                        <template v-slot="{ row }">
                            <el-link  type="primary"  @click="openForm('view', row)">查看</el-link>
                            <!-- 只对非审批状态显示编辑和删除按钮 -->
                            <el-link  type="primary"  @click="openForm('edit', row)" v-if="!['1', '2', '3'].includes(row.demandStatus)">编辑</el-link>
                            <el-link  type="primary"  @click="handleDelete(row)" v-if="!['1', '2', '3'].includes(row.demandStatus)">删除</el-link>
                            <el-link  type="primary"  @click="openForm('edit', row)" v-if="!['1', '2', '3'].includes(row.demandStatus) && permissionList.addBtn">编辑</el-link>
                            <el-link  type="primary"  @click="handleDelete(row)" v-if="!['1', '2', '3'].includes(row.demandStatus) && permissionList.deleteBtn">删除</el-link>
                        </template>
                    </el-table-column>
                </el-table>
@@ -116,7 +115,9 @@
    gdSupplyDemandPageApi,
    gdSupplyDemandRemoveApi,
} from '@/views/orderView/orderDataManage/supplyAdd/supplyAddApi'
import { useStore } from 'vuex'
const store = useStore()
const permission = computed(() => store.state.user.permission);
// 初始化查询参数
const initSearchParams = () => ({
    demandName: '', // 需求名称
@@ -151,6 +152,17 @@
provide('detailDemandStatus', detailDemandStatus)
const reasonForRejection = ref('') //拒绝原因
provide('reasonForRejection', reasonForRejection)
function validData(value, defaultValue) {
    return value !== undefined ? value : defaultValue
}
const permissionList = computed(() => {
    return {
        addBtn: validData(permission.value.orderData_addDemand, false),
        deleteBtn: validData(permission.value.orderData_delete, false),
    }
})
// 根据部门ID获取部门名称
function getDeptNameById(deptId, deptList) {
    if (!deptId || !Array.isArray(deptList)) return ''
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/RefuseOrderDialog1.vue
@@ -2,7 +2,7 @@
    <el-dialog
        class="gd-dialog"
        v-model="visible"
        title="编辑"
        :title="titleEnum[dialogMode]"
        @closed="visible = false"
        destroy-on-close
        :close-on-click-modal="false"
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue
@@ -205,34 +205,35 @@
        </div>
        <template #footer>
            <template v-if="requester">
                <el-button v-if="['1', '4'].includes(taskStatus)" @click="viewDescription" color="#F2F3F5">
                    {{ gdStatusObj[taskStatus].reason }}
                </el-button>
                <el-button @click="statusChange(3)" v-if="taskStatus === '3'" color="#4C34FF">撤回任务</el-button>
            <el-button v-if="['1', '4', '7'].includes(taskStatus)" @click="viewDescription" color="#F2F3F5">
                {{ gdStatusObj[taskStatus].reason }}
            </el-button>
            <el-button @click="statusChange(3)" v-if="taskStatus === '3' && permission.flyOrder_revoke" color="#4C34FF">
                撤回任务
            </el-button>
            <template v-if="permission.flyOrder_controlAcceptance">
                <el-button @click="refusalToAccept" v-if="taskStatus === '6'" color="#F2F3F5">拒绝验收</el-button>
                <el-button @click="statusChange(6)" v-if="taskStatus === '6'" color="#4C34FF">验收通过</el-button>
                <el-button
                    v-if="!dialogReadonly || ['1', '4'].includes(taskStatus)"
                    :loading="submitting"
                    :disabled="submitting"
                    color="#4C34FF"
                    @click="handleSubmit"
                >
                    提交
                </el-button>
            </template>
            <template v-else>
                <el-button v-if="['7'].includes(taskStatus)" @click="viewDescription" color="#F2F3F5">
                    {{ gdStatusObj[taskStatus].reason }}
                </el-button>
            <el-button
                v-if="!dialogReadonly || (['1', '4'].includes(taskStatus) && permission.flyOrder_add)"
                :loading="submitting"
                :disabled="submitting"
                color="#4C34FF"
                @click="handleSubmit"
            >
                提交
            </el-button>
            <template v-if="permission.flyOrder_controlSignFor">
                <el-button v-if="taskStatus === '0'" @click="addDescription" color="#F2F3F5">拒绝签收</el-button>
                <el-button v-if="taskStatus === '0'" @click="statusChange(1)" color="#4C34FF">签收</el-button>
            </template>
            <template v-if="permission.flyOrder_controlReview">
                <el-button v-if="taskStatus === '3'" @click="addDescription" color="#F2F3F5">驳回</el-button>
                <el-button v-if="taskStatus === '3'" @click="statusChange(4)" color="#4C34FF">同意</el-button>
            </template>
        </template>
        <RefuseOrderDialog1
            ref="refuseOrderDialogRef"
            v-if="rejectVisible"
@@ -252,11 +253,11 @@
import { pxToRem } from '@/utils/rem'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { gdTaskResultListApi } from '@/views/orderView/orderManage/clueEvents/achievementApi'
import RefuseOrderDialog from '@/views/orderView/orderManage/orderManage/RefuseOrderDialog.vue'
import RefuseOrderDialog1 from '@/views/orderView/orderManage/inspectionRequest/RefuseOrderDialog1.vue'
import { Check } from '@element-plus/icons-vue'
import { queryAirById } from '@/api/zkxt'
import * as Cesium from 'cesium'
import { useStore } from 'vuex'
// 初始化表单数据
const initForm = () => ({
@@ -270,11 +271,12 @@
    taskDesc: '',
    workOrderId: null,
})
const store = useStore()
const permission = computed(() => store.state.user.permission)
const rejectVisible = ref(false)
const mapRef = ref(null)
const dictObj = inject('dictObj')
const requester = inject('requester')
const emit = defineEmits(['success', 'refusalAccept'])
const formRef = ref(null)
const formData = ref(initForm())
@@ -372,7 +374,7 @@
// 获取飞手列表
async function getFlyerList() {
    const res = await gdFlyerPageApi({ size: 999,current: 1 })
    const res = await gdFlyerPageApi({ size: 999, current: 1 })
    flyerList.value = res?.data?.data?.records ?? []
}
@@ -429,7 +431,14 @@
    } finally {
    }
}
import { ArrowLineMaterialProperty } from '@/utils/cesium/Material'
let arrowLineMaterialProperty = new ArrowLineMaterialProperty({
    color: new Cesium.Color(128 / 255, 215 / 255, 255 / 255, 1),
    directionColor: new Cesium.Color(1, 1, 1, 1),
    outlineColor: new Cesium.Color(1, 1, 1, 1),
    outlineWidth: 0,
    speed: 5,
})
// 获取航线详情
function getAirDetails() {
    queryAirById(formData.value.patrolRouteUrl).then(res => {
@@ -440,8 +449,8 @@
            polyline: {
                positions: Cesium.Cartesian3.fromDegreesArray(result),
                clampToGround: true,
                width: 3,
                material: Cesium.Color.RED,
                width: 4,
                material: arrowLineMaterialProperty,
            },
        })
        // 渲染起点(蓝色)
@@ -514,23 +523,22 @@
</script>
<style lang="scss" scoped>
/* 时间线样式 */
:deep(.gd-timeline) {
 padding-left: 90px;
    padding-left: 90px;
}
:deep(.el-timeline-item) {
  padding-bottom: 20px;
  .item-content {
     position: relative;
            .flowName {
                width: 80px;
                position: absolute;
                left: -120px;
                top: 0px;
            }
  }
    padding-bottom: 20px;
    .item-content {
        position: relative;
        .flowName {
            width: 80px;
            position: absolute;
            left: -120px;
            top: 0px;
        }
    }
}
.imgBox {
    display: flex;
@@ -547,8 +555,8 @@
    }
}
:deep(.el-timeline-item__timestamp) {
  font-size: 12px;
  color: #999;
  margin-top: 2px;
    font-size: 12px;
    color: #999;
    margin-top: 2px;
}
</style>
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/index.vue
@@ -72,7 +72,7 @@
        </el-form>
        <div class="gd-table-toolbar">
            <el-button v-if="requester" :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')">新增</el-button>
            <el-button v-if="permission.flyOrder_add" :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')">拆分工单</el-button>
        </div>
        <div class="gd-table-container" v-loading="loading">
@@ -139,7 +139,8 @@
import { airlineListApi } from '@/api/zkxt'
const store = useStore()
const requester = computed(() => store.state.user.userInfo?.role_id === '2014158512610869250')
const permission = computed(() => store.state.user.permission)
// 初始化查询参数
const initSearchParams = () => ({
    patrolTaskName: '', // 任务名称
@@ -169,7 +170,6 @@
})
const activeName = ref('all')
provide('dictObj', dictObj)
provide('requester', requester)
// 获取列表
async function getList() {
@@ -222,7 +222,7 @@
function viewDiaLogView(row) {
    viewDiaLogVisible.value = true
    nextTick(() => {
        let mode = ['1', '4'].includes(row.taskStatus) && requester.value ? 'edit' : 'view'
        let mode = ['1', '4'].includes(row.taskStatus) && permission.value.flyOrder_add ? 'edit' : 'view'
        viewDiaLogRef.value?.open({ mode, row })
    })
}
applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue
@@ -176,64 +176,80 @@
            </div>
            <div class="rightBox" v-if="!dialogReadonly">
                <div class="detail-title">推荐设备</div>
                <template v-if="formData.deviceLoadDemand && pointList.length">
                    <el-table
                        ref="deviceTableRef"
                        :data="deviceList"
                        row-key="id"
                        @selection-change="handleDeviceSelectionChange"
                    >
                        <el-table-column type="selection" width="55" :reserve-selection="true" />
                        <el-table-column prop="nickname" label="设备名称" />
                        <el-table-column prop="deviceName" label="设备型号" />
                        <el-table-column prop="devicePayload" label="设备负载" />
                    </el-table>
                </template>
                <div v-else class="tipSelect">请先选择设备需求并在地图上绘制工单范围</div>
                <el-table
                    :empty-text="(formData.deviceLoadDemand && pointList.length) ? '暂无数据' : '请先选择设备需求并在地图上绘制工单范围'"
                    ref="deviceTableRef"
                    :data="(formData.deviceLoadDemand && pointList.length) ? deviceList : []"
                    row-key="id"
                    @selection-change="handleDeviceSelectionChange"
                >
                    <el-table-column type="selection" width="55" :reserve-selection="true" />
                    <el-table-column prop="nickname" label="设备名称" />
                    <el-table-column prop="deviceName" label="设备型号" />
                    <el-table-column prop="devicePayload" label="设备负载" />
                </el-table>
            </div>
        </div>
        <template #footer>
            <template v-if="requester">
                <template v-if="!suddenlyEdit">
                    <el-button
                        v-if="['11', '21', '23', '24', '25', '31', '60'].includes(gdStatus)"
                        @click="viewDescription"
                        color="#F2F3F5"
                    >
                        {{ gdStatusObj[gdStatus].reason }}
                    </el-button>
                    <el-button @click="addDescription" v-if="['20', '24', '25'].includes(gdStatus)" color="#F2F3F5">
                        申请取消
                    </el-button>
                    <el-button color="#4C34FF" @click="requestModification" v-if="['20', '25'].includes(gdStatus)">
                        申请修改
                    </el-button>
                </template>
            <el-button
                v-if="['11', '21', '23','31', '60'].includes(gdStatus)"
                @click="viewDescription"
                color="#F2F3F5"
            >
                {{ gdStatusObj[gdStatus].reason }}
            </el-button>
            <template v-if="!suddenlyEdit">
                <el-button
                    v-if="['11'].includes(gdStatus) || suddenlyEdit || (!dialogReadonly && !formData.id)"
                    :loading="submitting"
                    :disabled="submitting"
                    color="#4C34FF"
                    @click="handleSubmit"
                    @click="addDescription"
                    v-if="['20'].includes(gdStatus) && permission.order_applyCancel"
                    color="#F2F3F5"
                >
                    {{ gdStatus === '11' || !gdStatus ? '发布' : '提交修改' }}
                    申请取消
                </el-button>
                <el-button
                    color="#4C34FF"
                    @click="requestModification"
                    v-if="['20'].includes(gdStatus) && permission.order_applyNegotiation"
                >
                    申请修改
                </el-button>
            </template>
            <template v-else>
                <el-button v-if="['21', '23'].includes(gdStatus)" @click="viewDescription" color="#F2F3F5">
                    {{ gdStatusObj[gdStatus].reason }}
                </el-button>
                <el-button v-if="gdStatus === '50'" @click="addDescription" color="#F2F3F5">结算</el-button>
            <el-button
                v-if="(['11'].includes(gdStatus) || suddenlyEdit || !formData.id) && permission.order_release"
                :loading="submitting"
                :disabled="submitting"
                color="#4C34FF"
                @click="handleSubmit"
            >
                {{ gdStatus === '11' || !gdStatus ? '发布' : '提交修改' }}
            </el-button>
            <el-button v-if="gdStatus === '50' && permission.order_settlement" @click="addDescription" color="#F2F3F5">
                结算
            </el-button>
            <template v-if="permission.order_controlOrder">
                <el-button v-if="gdStatus === '10'" @click="addDescription" color="#F2F3F5">拒绝接单</el-button>
                <el-button v-if="gdStatus === '10'" @click="statusChange(1)" color="#4C34FF">接单</el-button>
                <el-button v-if="gdStatus === '22'" @click="addDescription" color="#F2F3F5">不同意修改</el-button>
                <el-button v-if="gdStatus === '22'" @click="statusChange(7)" color="#4C34FF">同意修改</el-button>
                <el-button v-if="gdStatus === '21'" @click="addDescription" color="#F2F3F5">不同意取消</el-button>
                <el-button v-if="gdStatus === '21'" @click="statusChange(5)" color="#4C34FF">同意取消</el-button>
                <el-button v-if="gdStatus === '30'" @click="addDescription" color="#4C34FF">协商修改</el-button>
                <el-button v-if="gdStatus === '31'" @click="statusChange(10)" color="#4C34FF">同意</el-button>
            </template>
            <template v-if="permission.order_controlEdit">
                <el-button v-if="gdStatus === '22'" @click="statusChange(8)" color="#F2F3F5">驳回</el-button>
                <el-button v-if="gdStatus === '22'" @click="statusChange(7)" color="#4C34FF">同意</el-button>
            </template>
            <template v-if="permission.order_controlCancel">
                <el-button v-if="gdStatus === '21'" @click="statusChange(6)" color="#F2F3F5">驳回</el-button>
                <el-button v-if="gdStatus === '21'" @click="statusChange(5)" color="#4C34FF">同意</el-button>
            </template>
            <el-button v-if="gdStatus === '30' && permission.order_applyNegotiation" @click="addDescription" color="#4C34FF">
                协商修改
            </el-button>
            <el-button
                v-if="gdStatus === '31' && permission.order_controlNegotiation"
                @click="statusChange(10)"
                color="#4C34FF"
            >
                同意
            </el-button>
        </template>
        <RefuseOrderDialog
@@ -246,7 +262,7 @@
</template>
<script setup>
import { computed, ref, onMounted, nextTick } from 'vue'
import { computed, ref, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { dateRangeFormat, fieldRules, flyVisual, geomAnalysis, getDictLabel } from '@ztzf/utils'
import {
@@ -265,6 +281,12 @@
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { gdPatrolTaskPageApi } from '@/views/orderView/orderManage/inspectionRequest/inspectionRequestApi'
import { Check } from '@element-plus/icons-vue'
import droneIcon from '@/assets/images/orderView/orderManage/drone.png'
import droneActiveIcon from '@/assets/images/orderView/orderManage/droneActive.png'
import { useStore } from 'vuex'
const store = useStore()
const permission = computed(() => store.state.user.permission)
// 初始化表单数据
const initForm = () => ({
    workOrderName: '',
@@ -297,32 +319,25 @@
let viewer
let drawPolygonExample
let pointList = ref([])
const requester = inject('requester')
const suddenlyEdit = ref(false)
const processList = ref([])
let viewPlane
const hasPatrolTaskList = computed(() => ['30', '40', '50', '60'].includes(String(formData.value.workOrderStatus)))
import droneIcon from '@/assets/images/orderView/orderManage/drone.png'
import droneActiveIcon from '@/assets/images/orderView/orderManage/droneActive.png'
const gdStatusObj = {
    '10': { reason: '拒绝原因', operationType: '2' },
    '11': { reason: '拒绝原因' },
    '10': { reason: '驳回原因', operationType: '2' },
    '11': { reason: '驳回原因' },
    '20': { reason: '取消原因', operationType: '3' },
    '21': { reason: '原因', operationType: '6' },
    '22': { reason: '驳回原因', operationType: '8' },
    '23': { reason: '取消原因' },
    '24': { reason: '驳回原因', operationType: '3' },
    '25': { reason: '驳回原因', operationType: '3' },
    '30': { reason: '修改原因', operationType: '9' },
    '31': { reason: '修改原因' },
    '30': { reason: '协商修改原因', operationType: '9' },
    '31': { reason: '协商修改原因' },
    '40': { reason: '' },
    '50': { reason: '情况说明', operationType: '11' },
    '60': { reason: '情况说明' },
}
// 工单状态:0草稿、10发布中_接单中、11发布中_拒绝接单、20响应中_待拆分、21响应中_申请取消、22响应中_申请修改、23响应中_已取消、
// 24响应中_拒绝取消、25响应中_拒绝修改 30执行中_待全部完成、31执行中_协商修改、40完成待验_待全部验收、50验收通过_待结算、60结算完成_已结算
// 30执行中_待全部完成、31执行中_协商修改、40完成待验_待全部验收、50验收通过_待结算、60结算完成_已结算
const gdStatus = computed(() => formData.value?.workOrderStatus)
const rules = {
@@ -595,9 +610,7 @@
        const image = entity.billboard?.image?._value
        if ((image === droneIcon || image === droneActiveIcon) && entity.label) {
            const device = deviceList.value.find(d => d.nickname === entity.label.text._value)
            entity.billboard.image = device && selectedIds.includes(String(device.id))
                ? droneActiveIcon
                : droneIcon
            entity.billboard.image = device && selectedIds.includes(String(device.id)) ? droneActiveIcon : droneIcon
        }
    })
}
@@ -622,6 +635,18 @@
</script>
<style lang="scss" scoped>
.el-table{
    height: 0;
    flex: 1;
    :deep(){
        .el-scrollbar__view{
            height: 100%;
        }
        .el-table__empty-text{
            line-height: normal;
        }
    }
}
.content {
    display: flex;
    gap: 0 20px;
@@ -643,6 +668,8 @@
    .rightBox {
        width: 350px;
        display: flex;
        flex-direction: column;
        .title {
            font-weight: 500;
            font-size: 14px;
applications/task-work-order/src/views/orderView/orderManage/orderManage/RefuseOrderDialog.vue
@@ -2,7 +2,7 @@
    <el-dialog
        class="gd-dialog"
        v-model="visible"
        title="编辑"
        :title="titleEnum[dialogMode]"
        @closed="visible = false"
        destroy-on-close
        :close-on-click-modal="false"
applications/task-work-order/src/views/orderView/orderManage/orderManage/index.vue
@@ -72,17 +72,31 @@
        </el-form>
        <div class="gd-table-toolbar">
<!--            <el-button color="#F2F3F5" @click="requester = !requester">我是{{ requester ? '需求方' : '服务方' }}</el-button>-->
<!--            <el-button color="#F2F3F5" @click="previewVisible = true">预览</el-button>-->
            <el-button :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')" v-if="requester">新增</el-button>
            <el-button :icon="Delete" color="#4C34FF" :disabled="!selectedIds.length" @click="handleDelete()" v-if="requester">删除</el-button>
            <el-button
                v-if="permission.order_release"
                :icon="Plus"
                color="#4C34FF"
                type="primary"
                @click="openForm('add')"
            >
                新增
            </el-button>
<!--            <el-button
                :icon="Delete"
                color="#4C34FF"
                :disabled="!selectedIds.length"
                @click="handleDelete()"
                v-if="permission.order_delete"
            >
                删除
            </el-button>-->
        </div>
        <div class="gd-table-container" v-loading="loading">
            <div class="gd-table-content gd-table-content-bg">
                <el-table class="gd-table" :data="list" @selection-change="handleSelectionChange">
                    <el-table-column type="selection" width="46" v-if="requester"/>
                    <el-table-column type="selection" width="46" v-if="permission.order_delete" />
                    <el-table-column type="index" width="64" label="序号" />
                    <el-table-column prop="workOrderName" show-overflow-tooltip label="工单名称" />
                    <el-table-column prop="workOrderCode" show-overflow-tooltip label="工单编号" />
@@ -112,9 +126,9 @@
                    <el-table-column prop="createTime" show-overflow-tooltip label="工单创建时间" />
                    <el-table-column label="操作" class-name="operation-btns">
                        <template v-slot="{ row }">
                            <el-link  type="primary"  @click="openFormChange(row)">查看</el-link>
                            <el-link type="primary" @click="openFormChange(row)">查看</el-link>
                            <!--<el-link  type="primary"  @click="openForm('edit', row)">编辑</el-link>-->
                            <el-link  type="primary"  @click="handleDelete(row)" v-if="requester">删除</el-link>
<!--                            <el-link type="primary" @click="handleDelete(row)" v-if="permission.order_delete">删除</el-link>-->
                        </template>
                    </el-table-column>
                </el-table>
@@ -132,7 +146,7 @@
            </div>
        </div>
        <PreviewFiles v-model="previewVisible" :src="testDock" type="docx"/>
        <PreviewFiles v-model="previewVisible" :src="testDock" type="docx" />
        <FormDiaLog ref="dialogRef" @success="getList" v-if="dialogVisible" v-model="dialogVisible" />
    </basic-container>
</template>
@@ -151,8 +165,10 @@
import { useStore } from 'vuex'
const store = useStore()
const requester = computed(() => store.state.user.userInfo?.role_id === '2014158512610869250')
const activeName = ref('all')
const permission = computed(() => store.state.user.permission)
console.log(permission.value, 'permission')
// 初始化查询参数
const initSearchParams = () => ({
@@ -164,7 +180,6 @@
    current: 1, // 当前页
    size: 10, // 每页大小
})
// const requester = ref(true)
const searchParams = ref(initSearchParams()) // 查询参数
const dateRange = ref([]) // 执行时间范围
const total = ref(0) // 总条数
@@ -181,8 +196,6 @@
    workOrderStatus: [], // 工单状态
})
provide('dictObj', dictObj)
provide('requester', requester)
// 获取列表
async function getList() {
@@ -230,13 +243,13 @@
    })
}
// 工单状态:0草稿、10发布中_接单中、11发布中_拒绝接单、20响应中_待拆分、
// 21响应中_申请取消、22响应中_申请修改、23响应中_已取消、24响应中_拒绝取消、
// 25响应中_拒绝修改 30执行中_待全部完成、31执行中_协商修改、40完成待验_待全部验收、
// 21响应中_申请取消、22响应中_申请修改、23响应中_已取消、
// 30执行中_待全部完成、31执行中_协商修改、40完成待验_待全部验收、
// 50验收通过_待结算、60结算完成_已结算
function openFormChange(row) {
    dialogVisible.value = true
    nextTick(() => {
        const mode = ['11'].includes(row.workOrderStatus) && requester.value ? 'edit' : 'view'
        const mode = ['11'].includes(row.workOrderStatus) && permission.value.order_release ? 'edit' : 'view'
        dialogRef.value?.open({ mode, row })
    })
}
packages/constants/areaStyles.js
New file
@@ -0,0 +1,36 @@
import * as Cesium from 'cesium'
export const AREA_TYPE_STYLE_MAP = {
    '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),
    },
}
export const DEFAULT_AREA_STYLE = {
    fill: Cesium.Color.fromBytes(45, 140, 240, 99),
    outline: Cesium.Color.fromBytes(45, 140, 240, 255),
}
export const BUFFER_LEVEL_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),
    },
]
packages/constants/index.js
@@ -6,3 +6,4 @@
export * from './publicEnums';
export * from './workEnums';
export * from './fwDevice';
export * from './areaStyles';
uniapps/work-app/src/pages/login/index.vue
@@ -82,8 +82,8 @@
  });
}
const loginFormGd = ref({
  username: "admin",
  password: "admin",
  username: "zhx",
  password: "123456",
});
async function submit() {
uniapps/work-app/src/subPackages/workDetail/index.vue
@@ -4,7 +4,8 @@
        <!-- 自定义导航栏 -->
        <u-navbar title="工单详情" :is-back="true" back-text="" :back-icon-size="40" @left-click="onBackClick">
            <template #right>
                <div class="share-btn" @click="onShareClick"></div>
        <div v-if="isH5" class="tapShare-icon"><img src="/static/images/work/share.svg"  @click="onShareClick"/></div>
                <div v-else class="share-btn" @click="onShareClick"></div>
            </template>
        </u-navbar>
        <div class="detailTop">
@@ -71,7 +72,7 @@
        <up-button type="primary" color="#1D6FE9" text="确认" @click="confirmTheTicket"></up-button>
      </div>
      <div class="btngroups" v-if="workDetailData.eventStatus === 3">
        <up-button type="primary" color="#1D6FE9" text="已确认" ></up-button>
        <up-button type="primary" color="#1D6FE9" text="已确认" @click="confirmedHandle"></up-button>
      </div>
    </div>
    <!-- 分享弹出层 -->
@@ -102,7 +103,6 @@
<script setup>
// import { getShowImg, getSmallImg } from '@/utils/util'
import { ref, computed } from 'vue'
import {getGddetailedData,backGdApi} from '/src/api/work/index.js'
import dayjs from 'dayjs'
const formatDate = dateString => {
@@ -111,9 +111,12 @@
const eventNum = ref('')
// 工单内容
const workDetailData = ref({})
// 环境判断
const isH5 = ref(false)
onLoad(async options => {
  eventNum.value = options.id
  await getDataList(options.id)
  checkEnvironment()
})
const getDataList = async val => {
  const params = {
@@ -122,6 +125,19 @@
  const res = await getGddetailedData(params)
  const response = res.data.data
  workDetailData.value = response
}
// 检查运行环境
const checkEnvironment = () => {
  const systemInfo = uni.getSystemInfoSync()
  // #ifdef APP-PLUS
  isH5.value = false
  // #endif
  // #ifdef H5
  isH5.value = true
  // #endif
  console.log('当前环境:', isH5.value ? 'H5' : 'App')
}
// 图片预览
const previewImage = index => {
@@ -180,6 +196,10 @@
      uni.navigateBack()
    })
  })
}
// 已确认
const confirmedHandle = () => {
  uni.navigateBack()
}
// 分享模态框状态
const showShareModal = ref(false)
@@ -312,12 +332,22 @@
  }
}  // 分享按钮样式
  .share-btn {
    // #ifdef APP-PLUS
    width: 40rpx;
    height: 40rpx;
    background-image: url('/static/images/work/share.svg');
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
    // #endif
  }
  .tapShare-icon {
    width: 40rpx;
    height: 40rpx;
    img {
      width: 40rpx;
      height: 40rpx;
    }
  }
  .detailTop {
  // #ifdef APP-PLUS