吉安感知网项目-前端
shuishen
2026-01-19 cf2ab1935f8b6cd113db09142ed8b8dd6f3070ab
feat:数据驾驶舱显示相关调整处理
15 files modified
2 files added
793 ■■■■ changed files
applications/drone-command/src/api/dataCockpit/index.js 8 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/assets/images/dataCockpit/legend/command-post.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/dataCockpit/legend/equipment.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/dataCockpit/map/aggregation.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/dataCockpit/map/command-post.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/dataCockpit/map/equipment.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/dataCockpit/map/popup-close.png patch | view | raw | blame | history
applications/drone-command/src/components/map-container/device-map-container.vue 575 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/api.txt 86 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/DeviceHistoryDialog.vue 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/EquipmentWarning.vue 27 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/HistoryWarning.vue 2 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/LeftContainer.vue 3 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/RealWarning.vue 36 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/RightContainer.vue 1 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/templateComponents/RealEquipmentTemplate.vue 32 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/templateComponents/RealTemplate.vue 19 ●●●● patch | view | raw | blame | history
applications/drone-command/src/api/dataCockpit/index.js
@@ -53,6 +53,14 @@
    })
}
// 数据驾驶舱地图聚合
export const cockpitAggregationApi = () => {
    return request({
        url: '/drone-fw/device/fwDevice/statisticalDeviceOut',
        method: 'get'
    })
}
// 区域列表
export const areaDivideListApi = (params) => {
    return request({
applications/drone-command/src/assets/images/dataCockpit/legend/command-post.png

applications/drone-command/src/assets/images/dataCockpit/legend/equipment.png

applications/drone-command/src/assets/images/dataCockpit/map/aggregation.png
applications/drone-command/src/assets/images/dataCockpit/map/command-post.png

applications/drone-command/src/assets/images/dataCockpit/map/equipment.png

applications/drone-command/src/assets/images/dataCockpit/map/popup-close.png
applications/drone-command/src/components/map-container/device-map-container.vue
@@ -1,35 +1,74 @@
<template>
    <CommonCesiumMap
        ref="mapRef"
        class="command-cesium map-container"
        :dom-id="props.containerId"
        :active="true"
        :flat-mode="false"
        :terrain="false"
        :layer-mode="4"
        :contour="false"
        :boundary="false"
        :show-admin-boundary="true"
        :zoom-to-boundary="true"
        @ready="handleMapReady"
    />
    <div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }">
        <div class="layer-control-wrap" ref="layerWrapRef">
            <div class="layer-control" @click="toggleLayerPanel">
                <img :src="layerControlIcon" alt="图层控制" />
            </div>
            <div v-if="showLayerPanel" class="layer-panel">
                <div class="panel-title">图层管理</div>
    <div class="map-shell">
        <CommonCesiumMap ref="mapRef" class="command-cesium map-container" :dom-id="props.containerId" :active="true"
            :flat-mode="false" :terrain="false" :layer-mode="4" :contour="false" :boundary="false"
            :show-admin-boundary="true" :zoom-to-boundary="true" :enable-stage-emit="true" :cluster-height="100000"
            @ready="handleMapReady" @stage-change="handleStageChange" />
        <div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }">
            <div class="layer-control-wrap" ref="layerWrapRef">
                <div class="layer-control" @click="toggleLayerPanel">
                    <img :src="layerControlIcon" alt="图层控制" />
                </div>
                <div v-if="showLayerPanel" class="layer-panel">
                    <div class="panel-title">图层管理</div>
                <div class="panel-content">
                    <el-tree
                        :data="layerTree"
                        show-checkbox
                        default-expand-all
                        node-key="key"
                        :props="layerTreeProps"
                        :default-checked-keys="defaultCheckedKeys"
                    />
                    <div class="panel-content">
                        <el-tree :data="layerTree" show-checkbox default-expand-all node-key="key"
                            :props="layerTreeProps" :default-checked-keys="defaultCheckedKeys" />
                    </div>
                </div>
            </div>
        </div>
        <div v-if="popupVisible" class="device-popup" :style="popupStyle" @click.stop>
            <div class="device-popup__header">
                <span class="device-popup__title">{{ popupTitle }}</span>
                <img class="device-popup__close" :src="popupClose" alt="" @click.stop="closePopup">
            </div>
            <div class="device-popup__type">
                <span class="value">{{ popupTypeText }}</span>
            </div>
            <div class="device-popup__subtitle">
                <span class="device-popup__dot" :class="popupActionClass"></span>
                <span class="device-popup__action">{{ popupActionText }}</span>
            </div>
            <div class="device-popup__rows">
                <div class="device-popup__row">
                    <div class="device-popup__col">
                        <span class="label">状态</span>
                        <span class="value" :class="popupStatusClass">{{ popupStatusText }}</span>
                    </div>
                    <span class="divider">|</span>
                    <div class="device-popup__col">
                        <span class="label">电量</span>
                        <span class="value">{{ popupBatteryText }}</span>
                    </div>
                </div>
                <div class="device-popup__row">
                    <div class="device-popup__col">
                        <span class="label">方位角</span>
                        <span class="value">{{ popupAzimuthText }}</span>
                    </div>
                    <span class="divider">|</span>
                    <div class="device-popup__col">
                        <span class="label">俯仰角</span>
                        <span class="value">{{ popupElevationText }}</span>
                    </div>
                </div>
                <div class="device-popup__row">
                    <span class="label">侦测目标</span>
                    <span class="value">{{ popupDetectCountText }}</span>
                </div>
                <div class="device-popup__row">
                    <span class="label">有效范围</span>
                    <span class="value">{{ popupRangeText }}</span>
                </div>
            </div>
        </div>
@@ -42,8 +81,14 @@
import { geomAnalysis } from '@ztzf/utils'
import { fwDefenseZonePageApi } from '@/views/areaManage/defenseZone/defenseZoneApi'
import { fwAreaDividePageApi } 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'
import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
import aggregationIcon from '@/assets/images/dataCockpit/map/aggregation.png'
import commandPostIcon from '@/assets/images/dataCockpit/map/command-post.png'
import popupClose from '@/assets/images/dataCockpit/map/popup-close.png'
import jaGeojsonRaw from '@/assets/geojson/ja.geojson?raw'
const props = defineProps({
    onlineDevices: {
@@ -69,10 +114,20 @@
const mapRef = ref(null)
let viewer = null
const deviceEntityIds = new Set()
const deviceEntityMap = new Map()
let defenseZoneSource = null
let partitionSource = null
let aggregationSource = null
let commandPostSource = null
const detailVisible = ref(true)
const clusterVisible = ref(false)
const countyCenterMap = new Map()
const showLayerPanel = ref(false)
const layerWrapRef = ref(null)
const selectedDevice = ref(null)
let selectedDeviceEntity = null
let deviceClickHandler = null
let popupRenderHandler = null
const layerTreeProps = {
    label: 'label',
    children: 'children',
@@ -123,6 +178,7 @@
        viewer.entities.removeById(id)
    })
    deviceEntityIds.clear()
    deviceEntityMap.clear()
}
const clearDefenseZoneEntities = () => {
@@ -133,6 +189,44 @@
const clearPartitionEntities = () => {
    if (!partitionSource) return
    partitionSource.entities.removeAll()
}
const clearAggregationEntities = () => {
    if (!aggregationSource) return
    aggregationSource.entities.removeAll()
}
const clearCommandPostEntities = () => {
    if (!commandPostSource) return
    commandPostSource.entities.removeAll()
}
const setDetailVisibility = visible => {
    detailVisible.value = visible
    if (defenseZoneSource) defenseZoneSource.show = visible
    if (partitionSource) partitionSource.show = visible
    if (commandPostSource) commandPostSource.show = visible
    if (!visible) closePopup()
    deviceEntityIds.forEach(id => {
        const entity = viewer?.entities?.getById(id)
        if (entity) entity.show = visible
    })
}
const setClusterVisibility = visible => {
    clusterVisible.value = visible
    if (aggregationSource) aggregationSource.show = visible
}
const ensureCountyCenterMap = () => {
    if (countyCenterMap.size) return
    const geojson = JSON.parse(jaGeojsonRaw)
    geojson.features?.forEach(feature => {
        const name = feature?.properties?.name
        const center = feature?.properties?.centroid || feature?.properties?.center
        if (!name || !Array.isArray(center) || center.length < 2) return
        countyCenterMap.set(name, { longitude: center[0], latitude: center[1] })
    })
}
@@ -185,7 +279,7 @@
    return positions
}
const addDeviceRings = (center, baseId) => {
const addDeviceRings = (center, baseId, visible) => {
    RING_STYLES.forEach((ring, index) => {
        const outerPositions = buildCirclePositions(center, ring.outer)
        const holes = ring.inner
@@ -200,6 +294,7 @@
        const material = new RadialGradientMaterialProperty(color1, color2)
        viewer.entities.add({
            id: entityId,
            show: visible,
            polygon: {
                hierarchy: new Cesium.PolygonHierarchy(outerPositions, holes),
                material,
@@ -215,26 +310,26 @@
        this.color2 = color2
    }
    get isConstant() {
    get isConstant () {
        return true
    }
    get definitionChanged() {
    get definitionChanged () {
        return this._definitionChanged
    }
    getType() {
    getType () {
        return MATERIAL_TYPE
    }
    getValue(time, result) {
    getValue (time, result) {
        const target = result || {}
        target.color1 = this.color1
        target.color2 = this.color2
        return target
    }
    equals(other) {
    equals (other) {
        return (
            other instanceof RadialGradientMaterialProperty &&
            Cesium.Color.equals(this.color1, other.color1) &&
@@ -251,9 +346,10 @@
        if (!position) return
        const entityId = `online-device-${item.id ?? index}`
        deviceEntityIds.add(entityId)
        addDeviceRings(position, entityId)
        viewer.entities.add({
        addDeviceRings(position, entityId, detailVisible.value)
        const entity = viewer.entities.add({
            id: entityId,
            show: detailVisible.value,
            position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
            billboard: {
                image: equipmentIcon,
@@ -262,7 +358,11 @@
                verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
            },
        })
        deviceEntityMap.set(entityId, { data: item, entity })
    })
    if (selectedDeviceEntity && !deviceEntityMap.has(selectedDeviceEntity.id)) {
        closePopup()
    }
}
const getDefenseZonePositions = geom => {
@@ -316,6 +416,7 @@
        viewer.dataSources.add(defenseZoneSource)
    }
    clearDefenseZoneEntities()
    defenseZoneSource.show = detailVisible.value
    renderZonePolygons({
        zones,
        source: defenseZoneSource,
@@ -332,6 +433,7 @@
        viewer.dataSources.add(partitionSource)
    }
    clearPartitionEntities()
    partitionSource.show = detailVisible.value
    renderZonePolygons({
        zones,
        source: partitionSource,
@@ -361,6 +463,225 @@
    }
}
const renderAggregation = list => {
    if (!viewer) return
    ensureCountyCenterMap()
    if (!aggregationSource) {
        aggregationSource = new Cesium.CustomDataSource('aggregationSource')
        viewer.dataSources.add(aggregationSource)
    }
    clearAggregationEntities()
    aggregationSource.show = clusterVisible.value
    const countMap = new Map()
        ; (list || []).forEach(item => {
            if (!item?.type) return
            countMap.set(item.type, Number(item.count ?? 0))
        })
    Array.from(countyCenterMap.entries()).forEach(([name, center], index) => {
        const position = Cesium.Cartesian3.fromDegrees(center.longitude, center.latitude, 0)
        const count = countMap.get(name) ?? 0
        aggregationSource.entities.add({
            id: `aggregation-${name}-${index}`,
            position,
            billboard: {
                image: aggregationIcon,
                width: 82.5,
                height: 59.25,
                verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
            label: {
                text: `${count}`,
                fillColor: Cesium.Color.WHITE,
                style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                font: '12pt Source Han Sans CN',
                eyeOffset: new Cesium.Cartesian3(0, 0, -20), // 让label "浮" 在广告牌前面
                pixelOffset: new Cesium.Cartesian2(2, -11),
                pixelOffset: new Cesium.Cartesian2(0, -48),
                disableDepthTestDistance: Number.POSITIVE_INFINITY,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        })
    })
}
const getPickedDevice = picks => {
    for (const pick of picks) {
        const entityId = typeof pick?.id === 'string' ? pick.id : pick?.id?.id
        if (entityId && deviceEntityMap.has(entityId)) {
            return deviceEntityMap.get(entityId)
        }
    }
    return null
}
const updatePopupPosition = () => {
    if (!viewer || !selectedDeviceEntity) return
    const cartesian =
        selectedDeviceEntity.position?.getValue?.(Cesium.JulianDate.now()) ||
        selectedDeviceEntity.position?._value
    if (!cartesian) return
    const screenPosition = viewer.scene.cartesianToCanvasCoordinates(cartesian)
    if (!screenPosition) return
    popupPosition.value = { x: screenPosition.x, y: screenPosition.y }
}
const startPopupRender = () => {
    if (!viewer || popupRenderHandler) return
    popupRenderHandler = () => updatePopupPosition()
    viewer.scene.postRender.addEventListener(popupRenderHandler)
    updatePopupPosition()
}
const stopPopupRender = () => {
    if (!viewer || !popupRenderHandler) return
    viewer.scene.postRender.removeEventListener(popupRenderHandler)
    popupRenderHandler = null
}
const handleDeviceClick = movement => {
    if (!viewer) return
    const picks = viewer.scene.drillPick(movement.position) || []
    const pickedDevice = getPickedDevice(picks)
    if (!pickedDevice) {
        closePopup()
        return
    }
    selectedDevice.value = pickedDevice.data
    selectedDeviceEntity = pickedDevice.entity
    startPopupRender()
}
const initDeviceClickHandler = () => {
    if (deviceClickHandler || !viewer) return
    deviceClickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
    deviceClickHandler.setInputAction(handleDeviceClick, Cesium.ScreenSpaceEventType.LEFT_CLICK)
}
const destroyDeviceClickHandler = () => {
    deviceClickHandler?.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK)
    deviceClickHandler?.destroy()
    deviceClickHandler = null
}
function closePopup () {
    selectedDevice.value = null
    selectedDeviceEntity = null
    stopPopupRender()
}
const popupPosition = ref({ x: 0, y: 0 })
const popupVisible = computed(() => Boolean(selectedDevice.value))
const popupStyle = computed(() => ({
    left: `${popupPosition.value.x}px`,
    top: `${popupPosition.value.y}px`,
}))
const popupTitle = computed(() => selectedDevice.value?.deviceName || selectedDevice.value?.deviceModel || '-')
const popupStatusText = computed(() => {
    const status = selectedDevice.value?.status
    if (status === 0) return '在线'
    if (status === 1) return '离线'
    if (status === 2) return '故障'
    if (status === 3) return '报废'
    return '-'
})
const popupStatusClass = computed(() => (selectedDevice.value?.status === 0 ? 'online' : ''))
const popupActionText = computed(() => {
    const workMode = selectedDevice.value?.workMode
    if (workMode === 1 || workMode === '1') return '侦测中'
    if (workMode === 2 || workMode === '2') return '信号干扰中'
    if (workMode === 3 || workMode === '3') return '诱导驱离中'
    if (workMode === 4 || workMode === '4') return '待机'
    return workMode || '-'
})
const popupActionClass = computed(() => {
    const workMode = selectedDevice.value?.workMode
    if (workMode === 1 || workMode === '1') return 'is-detecting'
    if (workMode === 2 || workMode === '2') return 'is-jamming'
    if (workMode === 3 || workMode === '3') return 'is-driving'
    if (workMode === 4 || workMode === '4') return 'is-standby'
    return 'is-standby'
})
const popupTypeText = computed(() => {
    const type = selectedDevice.value?.deviceType
    if (type === 1 || type === '1') return '便捷侦测箱'
    if (type === 2 || type === '2') return '反制枪'
    if (type === 3 || type === '3') return '察打一体'
    return type || '-'
})
const popupBatteryText = computed(() => {
    const val = selectedDevice.value?.batteryPct
    if (val == null) return '-'
    return `${val}%`
})
const popupAzimuthText = computed(() => {
    const val = selectedDevice.value?.azimuth
    if (val == null) return '-'
    return `${val}°`
})
const popupElevationText = computed(() => {
    const val = selectedDevice.value?.elevation
    if (val == null) return '-'
    return `${val}°`
})
const popupDetectCountText = computed(() => {
    const val = selectedDevice.value?.detectTargetCnt
    if (val == null) return '-'
    return `${val}台`
})
const popupRangeText = computed(() => {
    const val = selectedDevice.value?.effectiveRangeKm
    if (val == null) return '-'
    return `${val}KM`
})
const renderCommandPosts = list => {
    if (!viewer) return
    if (!commandPostSource) {
        commandPostSource = new Cesium.CustomDataSource('commandPostSource')
        viewer.dataSources.add(commandPostSource)
    }
    clearCommandPostEntities()
    commandPostSource.show = detailVisible.value
        ; (list || []).forEach((item, index) => {
            const position = getDevicePosition(item)
            if (!position) return
            commandPostSource.entities.add({
                id: `command-post-${item.id ?? index}`,
                position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
                billboard: {
                    image: commandPostIcon,
                    width: 40,
                    height: 56,
                    verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
                    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                },
            })
        })
}
const loadAggregation = async () => {
    try {
        const res = await cockpitAggregationApi()
        renderAggregation(res?.data?.data ?? [])
    } catch (error) {
        renderAggregation([])
    }
}
const loadCommandPosts = async () => {
    try {
        const res = await fwDefenseSceneListApi()
        renderCommandPosts(res?.data?.data ?? [])
    } catch (error) {
        renderCommandPosts([])
    }
}
watch(
    () => props.onlineDevices,
@@ -382,11 +703,27 @@
    showLayerPanel.value = !showLayerPanel.value
}
const updateStageDisplay = stage => {
    const showCluster = stage === 'cluster'
    setClusterVisibility(showCluster)
    setDetailVisibility(!showCluster)
}
const handleMapReady = ({ viewer: mapViewer }) => {
    viewer = mapViewer
    const height = viewer?.camera?.positionCartographic?.height
    const stage = height != null && height >= 100000 ? 'cluster' : 'detail'
    updateStageDisplay(stage)
    renderDeviceEntities(props.onlineDevices)
    loadDefenseZones()
    loadPartitions()
    loadAggregation()
    loadCommandPosts()
    initDeviceClickHandler()
}
const handleStageChange = stage => {
    updateStageDisplay(stage)
}
const handleClickOutside = event => {
@@ -407,11 +744,21 @@
    clearDeviceEntities()
    clearDefenseZoneEntities()
    clearPartitionEntities()
    clearAggregationEntities()
    clearCommandPostEntities()
    closePopup()
    destroyDeviceClickHandler()
    viewer = null
})
</script>
<style lang="scss" scoped>
.map-shell {
    position: relative;
    width: 100%;
    height: 100%;
}
.map-container {
    position: absolute;
    top: 0;
@@ -420,6 +767,158 @@
    height: 100%;
}
.device-popup {
    position: absolute;
    transform: translate(-50%, calc(-100% - 72px));
    width: 202px;
    padding: 0 16px 20px;
    border-radius: 16px;
    background: rgba(5, 5, 15, 0.7);
    backdrop-filter: blur(16px);
    color: #C3C3DD;
    font-family: Source Han Sans CN, Source Han Sans CN;
    font-size: 12px;
    pointer-events: auto;
    z-index: 11;
    box-sizing: border-box;
    &::after {
        content: "";
        position: absolute;
        left: 50%;
        bottom: -8px;
        transform: translateX(-50%);
        width: 0;
        height: 0;
        border-left: 8px solid transparent;
        border-right: 8px solid transparent;
        border-top: 8px solid rgba(5, 5, 15, 0.7);
    }
    .device-popup__header {
        height: 40px;
        display: flex;
        align-items: center;
        justify-content: space-between;
        position: relative;
        font-weight: bold;
        font-size: 14px;
        color: #FFFFFF;
        &::after {
            content: "";
            position: absolute;
            left: 0;
            bottom: 0;
            width: 50px;
            height: 2px;
            background: #284FE3;
            box-shadow: 0px 3px 2px 0px rgba(15, 89, 255, 0.1), 0px 7px 5px 0px rgba(15, 89, 255, 0.15), 0px 13px 10px 0px rgba(15, 89, 255, 0.18), 0px 22px 18px 0px rgba(15, 89, 255, 0.21), 0px 42px 33px 0px rgba(15, 89, 255, 0.26), 0px 100px 80px 0px rgba(15, 89, 255, 0.36);
            border-radius: 5px 5px 0px 0px;
        }
        &::before {
            content: "";
            position: absolute;
            left: 55px;
            right: 0;
            bottom: 0;
            height: 2px;
            background: #323245;
            border-radius: 0px 0px 5px 5px;
        }
        .device-popup__close {
            cursor: pointer;
        }
    }
    .device-popup__type {
        display: flex;
        align-items: center;
        gap: 6px;
        font-size: 12px;
        margin-top: 8px;
        margin-bottom: 10px;
        color: #d2d7df;
        .label {
            color: #9aa4b2;
        }
    }
    .device-popup__subtitle {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        padding: 2px 10px;
        border-radius: 999px;
        background: rgba(255, 255, 255, 0.06);
        font-size: 12px;
        margin-bottom: 10px;
        .device-popup__dot {
            width: 6px;
            height: 6px;
            border-radius: 50%;
            background: #6b7685;
            &.is-detecting {
                background: #35f08c;
                box-shadow: 0 0 6px rgba(53, 240, 140, 0.8);
            }
            &.is-jamming {
                background: #ff4444;
                box-shadow: 0 0 6px rgba(255, 68, 68, 0.7);
            }
            &.is-driving {
                background: #ffb020;
                box-shadow: 0 0 6px rgba(255, 176, 32, 0.7);
            }
            &.is-standby {
                background: #9aa4b2;
            }
        }
    }
    .device-popup__rows {
        display: flex;
        flex-direction: column;
        gap: 6px;
        font-size: 12px;
        color: #d2d7df;
        .device-popup__row {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
            .device-popup__col {
                flex: 1;
                display: flex;
                align-items: center;
                &:last-child {
                    justify-content: flex-end;
                }
            }
        }
        .value {
            margin-left: 10px;
        }
        .value.online {
            color: #35f08c;
        }
    }
}
.layer-control-root {
    position: absolute;
    left: 337px;
applications/drone-command/src/views/api.txt
@@ -1,22 +1,33 @@
## 列表
## 信号干扰,诱导驱离
**接口地址**:`/drone-fw/area/fwAreaDivide/list`
**接口地址**:`/drone-fw/cockpit/dataCockpit/interferenceAndExpulsion`
**请求方式**:`GET`
**请求方式**:`POST`
**请求数据类型**:`application/x-www-form-urlencoded`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:<p>传入filterSelected或sceneId</p>
**接口描述**:<p>传入fwEffectEval</p>
**请求示例**:
```javascript
{
  "alarmRecordId": 0,
  "counterWay": ""
}
```
**请求参数**:
@@ -27,8 +38,9 @@
| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|filterSelected|是否过滤已被选择的数据(1过滤 0不过滤)|query|false|integer(int32)||
|sceneId|场景id|query|false|integer(int64)||
|fwEffectEvalParam|fwEffectEvalParam|body|true|FwEffectEvalParam|FwEffectEvalParam|
|&emsp;&emsp;alarmRecordId|告警记录ID(ja_fw_drone_alarm_record.id)||false|integer(int64)||
|&emsp;&emsp;counterWay|反制方式:1.信号干扰/2.诱导驱离/无||false|string||
**响应状态**:
@@ -36,7 +48,8 @@
| 状态码 | 说明 | schema |
| -------- | -------- | ----- | 
|200|OK|R«List«FwAreaDivide对象»»|
|200|OK|R|
|201|Created||
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
@@ -48,32 +61,7 @@
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- | 
|code|状态码|integer(int32)|integer(int32)|
|data|承载数据|array|FwAreaDivide对象|
|&emsp;&emsp;areaCode|区域编码|string||
|&emsp;&emsp;areaName|区域名称|string||
|&emsp;&emsp;areaSize|区域面积(km²)|number||
|&emsp;&emsp;areaType|区域类型:报警区/重点管制区|string||
|&emsp;&emsp;controlLevel|管控级别:日常/重点/严密|string||
|&emsp;&emsp;createDept|创建部门|integer(int64)||
|&emsp;&emsp;createTime|创建时间|string(date-time)||
|&emsp;&emsp;createUser|创建人|integer(int64)||
|&emsp;&emsp;deviceIds|关联设备ID,逗号分隔|string||
|&emsp;&emsp;flyDateEnd|可飞行日期时间-结束|string(date-time)||
|&emsp;&emsp;flyDateStart|可飞行日期时间-开始|string(date-time)||
|&emsp;&emsp;geom|经纬度面(存储地理面数据)|string||
|&emsp;&emsp;id|主键id|integer(int64)||
|&emsp;&emsp;isDeleted|是否已删除|integer(int32)||
|&emsp;&emsp;latitude|区域中心纬度|number(double)||
|&emsp;&emsp;longitude|区域中心经度|number(double)||
|&emsp;&emsp;policeStationContactPerson|派出所联系人|string||
|&emsp;&emsp;policeStationContactPhone|派出所联系电话|string||
|&emsp;&emsp;policeStationId|关联派出所id|integer(int64)||
|&emsp;&emsp;policeStationName|派出所名称|string||
|&emsp;&emsp;responseMechanism|响应机制内容|string||
|&emsp;&emsp;status|业务状态|integer(int32)||
|&emsp;&emsp;triggerCondition|触发条件|string||
|&emsp;&emsp;updateTime|更新时间|string(date-time)||
|&emsp;&emsp;updateUser|更新人|integer(int64)||
|data|承载数据|object||
|msg|返回消息|string||
|success|是否成功|boolean||
@@ -82,35 +70,7 @@
```javascript
{
    "code": 0,
    "data": [
        {
            "areaCode": "",
            "areaName": "",
            "areaSize": 0,
            "areaType": "",
            "controlLevel": "",
            "createDept": 0,
            "createTime": "",
            "createUser": 0,
            "deviceIds": "",
            "flyDateEnd": "",
            "flyDateStart": "",
            "geom": "",
            "id": 0,
            "isDeleted": 0,
            "latitude": 0,
            "longitude": 0,
            "policeStationContactPerson": "",
            "policeStationContactPhone": "",
            "policeStationId": 0,
            "policeStationName": "",
            "responseMechanism": "",
            "status": 0,
            "triggerCondition": "",
            "updateTime": "",
            "updateUser": 0
        }
    ],
    "data": {},
    "msg": "",
    "success": true
}
applications/drone-command/src/views/dataCockpit/components/DeviceHistoryDialog.vue
@@ -9,7 +9,7 @@
            <el-form-item prop="dateRange">
                <el-date-picker class="command-data-cockpit-date-picker"
                    popper-class="command-data-cockpit-tree-select-popper" v-model="searchParams.dateRange"
                    popper-class="command-data-cockpit-date-picker-popper" v-model="searchParams.dateRange"
                    type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
                    value-format="YYYY/MM/DD" @change="handleSearch" />
            </el-form-item>
@@ -96,7 +96,7 @@
})
const dialogTitle = computed(() => {
    return `反制设备名称 ${props.device?.name || ''}`.trim()
    return `${props.device?.name || ''}`.trim()
})
const initSearchParams = () => ({
applications/drone-command/src/views/dataCockpit/components/EquipmentWarning.vue
@@ -15,14 +15,16 @@
        element-loading-background="rgba(5, 5, 15, 0.6)"
        element-loading-text="加载中..."
    >
        <EmptyState v-if="!equipmentWarningData.length" />
        <EquipmentTemplate
            v-else
            v-for="(item, ind) in equipmentWarningData"
            :key="ind"
            :data="item"
            @click="onCardClick"
        />
        <div class="list-content">
            <EmptyState v-if="!equipmentWarningData.length" />
            <EquipmentTemplate
                v-else
                v-for="(item, ind) in equipmentWarningData"
                :key="ind"
                :data="item"
                @click="onCardClick"
            />
        </div>
    </div>
</template>
@@ -94,8 +96,17 @@
<style lang="scss" scoped>
.equipment-warning {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    overflow: hidden;
}
.list-content {
    height: 0;
    flex: 1;
    overflow-y: auto;
}
</style>
applications/drone-command/src/views/dataCockpit/components/HistoryWarning.vue
@@ -103,6 +103,7 @@
    flex-direction: column;
    width: 100%;
    height: 100%;
    overflow: hidden;
    .search-box {
        display: flex;
@@ -117,6 +118,7 @@
    .list-content {
        height: 0;
        flex: 1;
        overflow-y: auto;
    }
}
applications/drone-command/src/views/dataCockpit/components/LeftContainer.vue
@@ -213,7 +213,6 @@
    height: 0;
    flex: 1;
    margin-top: 20px;
    overflow-x: hidden;
    overflow-y: auto;
    overflow: hidden;
}
</style>
applications/drone-command/src/views/dataCockpit/components/RealWarning.vue
@@ -2,7 +2,7 @@
 * @Author       : yuan
 * @Date         : 2026-01-08 09:29:07
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-08 14:49:09
 * @LastEditTime : 2026-01-19 11:31:09
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\RealWarning.vue
 * @Description  : 
 * Copyright 2026 OBKoro1, All Rights Reserved. 
@@ -15,16 +15,18 @@
        element-loading-background="rgba(5, 5, 15, 0.6)"
        element-loading-text="加载中..."
    >
        <EmptyState v-if="!realWarningData.length" />
        <RealTemplate
            v-else
            v-for="(item, ind) in realWarningData"
            :key="ind"
            :data="item"
            @signal="() => handleAction('signal', item)"
            @counter="() => handleAction('counter', item)"
            @favorite="() => console.log('收藏', item.id)"
        />
        <div class="list-content">
            <EmptyState v-if="!realWarningData.length" />
            <RealTemplate
                v-else
                v-for="(item, ind) in realWarningData"
                :key="ind"
                :data="item"
                @signal="() => handleAction('signal', item)"
                @counter="() => handleAction('counter', item)"
                @favorite="() => console.log('收藏', item.id)"
            />
        </div>
    </div>
</template>
@@ -46,8 +48,7 @@
const buildPayload = (type, item) => ({
    counterWay: type === 'signal' ? 1 : type === 'counter' ? 2 : '',
    deviceId: item.deviceId,
    droneDeviceCode: item.droneDeviceCode ?? item.droneSerialNo
    alarmRecordId: item?.alarmRecordId ?? item?.id
})
const handleAction = async (type, item) => {
@@ -56,6 +57,7 @@
        const res = await interferenceAndExpulsionApi(buildPayload(type, item))
        if (res?.data?.success) {
            ElMessage({ type: 'success', message: `${actionText}成功` })
            await fetchRealWarning()
        } else {
            ElMessage({ type: 'error', message: res?.data?.msg || `${actionText}失败` })
        }
@@ -88,8 +90,16 @@
<style lang="scss" scoped>
.real-warning {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    overflow: hidden;
}
.list-content {
    height: 0;
    flex: 1;
    overflow-y: auto;
}
</style>
applications/drone-command/src/views/dataCockpit/components/RightContainer.vue
@@ -134,6 +134,7 @@
        azimuth: item.azimuth ?? 0,
        elevation: item.elevation ?? 0,
        status: item.status ?? '-',
        workMode: item.workMode ?? '-',
        manufacturer: item.manufacturer || '-',
        detectTargetCnt: item.detectTargetCnt ?? 0,
        effectiveRangeKm: item.effectiveRangeKm ?? 0
applications/drone-command/src/views/dataCockpit/components/templateComponents/RealEquipmentTemplate.vue
@@ -6,12 +6,12 @@
                <span class="name">{{ data.name }}</span>
                <span class="status">
                    <span :class="getStatusType(data.status)">
                    <span :class="getWorkModeType(data.workMode)">
                    </span>
                    <span>
                        {{ getStatusText(data.status) }}
                        {{ getWorkModeText(data.workMode) }}
                    </span>
                </span>
            </div>
@@ -90,9 +90,19 @@
    return value || '-'
}
const getStatusType = (value) => {
    if (value === 0 || value === '0') return 'detecting'
const getWorkModeText = (value) => {
    if (value === 1 || value === '1') return '侦测中'
    if (value === 2 || value === '2') return '信号干扰中'
    if (value === 3 || value === '3') return '诱导驱离中'
    if (value === 4 || value === '4') return '待机'
    return value || '-'
}
const getWorkModeType = (value) => {
    if (value === 1 || value === '1') return 'detecting'
    if (value === 2 || value === '2') return 'jamming'
    if (value === 3 || value === '3') return 'driving'
    if (value === 4 || value === '4') return 'standby'
    return ''
}
</script>
@@ -164,6 +174,20 @@
                    background: #FF4444;
                    border-radius: 50%;
                }
                span.driving {
                    width: 4px;
                    height: 4px;
                    background: #FFB020;
                    border-radius: 50%;
                }
                span.standby {
                    width: 4px;
                    height: 4px;
                    background: #9AA4B2;
                    border-radius: 50%;
                }
            }
        }
    }
applications/drone-command/src/views/dataCockpit/components/templateComponents/RealTemplate.vue
@@ -57,8 +57,8 @@
        </div>
        <div class="footer">
            <el-button class="general" color="#2B2B4C" @click="emit('signal')">信号干扰</el-button>
            <el-button color="#284FE3" type="primary" @click="emit('counter')">诱导驱离</el-button>
            <el-button v-if="showSignalBtn(data)" class="general" color="#2B2B4C" @click="emit('signal')">信号干扰</el-button>
            <el-button v-if="showCounterBtn(data)" color="#284FE3" type="primary" @click="emit('counter')">诱导驱离</el-button>
        </div>
    </div>
</template>
@@ -90,6 +90,21 @@
    return 'detecting'
}
const normalizeCounterWay = (value) => {
    if (value == null) return ''
    return String(value).trim()
}
const showSignalBtn = (item) => {
    const counterWay = normalizeCounterWay(item?.counterWay)
    return counterWay !== '1'
}
const showCounterBtn = (item) => {
    const counterWay = normalizeCounterWay(item?.counterWay)
    return counterWay !== '2'
}
const getDataSource = (item) => {
    return item?.deviceName || item?.areaName || '-'
}