吉安感知网项目-前端
罗广辉
2026-01-23 5c925f9d963af25f1663383ef173d16c5b5e8a43
Merge remote-tracking branch 'origin/master'
22 files modified
5 files added
1463 ■■■■■ changed files
applications/drone-command/env/.env.development 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/assets/images/dataCockpit/favorite-n.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/dataCockpit/favorite-y.png patch | view | raw | blame | history
applications/drone-command/src/components/map-container/components/DevicePopup.vue 325 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/components/map-container/components/DronePopup.vue 323 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/components/map-container/device-map-container.vue 565 ●●●● patch | view | raw | blame | history
applications/drone-command/src/components/map-container/device-map-materials.js 188 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/areaStatistics/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/defenseZone/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/precinctInfo/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceScrap/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/DeviceScrapDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/DeviceTrackDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/maintainRecord/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/maintainRecord/MaintenanceDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/templateComponents/RealTemplate.vue 20 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/countermeasureEvaluation/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/detectionRange/DetectionRangeDialog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/deviceAppConfig/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/taskSchedule/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/permissionManage/permissionDept/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/permissionManage/permissionRole/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/permissionManage/permissionUser/FormDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/recordManage/historyTracks/TrajectoryDiaLog.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/env/.env.development
@@ -2,7 +2,7 @@
 # @Author       : yuan
 # @Date         : 2026-01-07 14:58:30
 # @LastEditors  : yuan
 # @LastEditTime : 2026-01-22 15:51:00
 # @LastEditTime : 2026-01-23 17:51:43
 # @FilePath     : \applications\drone-command\env\.env.development
 # @Description  : 
 # Copyright 2026 OBKoro1, All Rights Reserved. 
@@ -16,7 +16,7 @@
#开发环境代理地址(推荐本地新建文件 .env.development.local 来进行覆盖)
# VITE_APP_URL = https://wrj.shuixiongit.com/api
VITE_APP_URL= http://192.168.1.204
VITE_APP_URL= http://192.168.1.33
#新大屏地址
VITE_APP_DASHBOARD_URL = 'https://wrj.shuixiongit.com/command-center-dashboard/'
applications/drone-command/src/assets/images/dataCockpit/favorite-n.png
applications/drone-command/src/assets/images/dataCockpit/favorite-y.png
applications/drone-command/src/components/map-container/components/DevicePopup.vue
New file
@@ -0,0 +1,325 @@
<template>
    <div v-if="visible" class="device-popup" :style="popupStyle" @click.stop>
        <div class="device-popup__header">
            <el-tooltip
                class="device-popup__title-tooltip"
                :content="popupTitle"
                placement="top"
                effect="dark"
                :show-after="200"
            >
                <span class="device-popup__title">{{ popupTitleDisplay }}</span>
            </el-tooltip>
            <img class="device-popup__close" :src="popupClose" alt="" @click.stop="emit('close')" />
        </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>
</template>
<script setup>
import { computed } from 'vue'
import popupClose from '@/assets/images/dataCockpit/map/popup-close.png'
const props = defineProps({
    visible: {
        type: Boolean,
        default: false,
    },
    position: {
        type: Object,
        default: () => ({ x: 0, y: 0 }),
    },
    device: {
        type: Object,
        default: () => ({}),
    },
    titleMax: {
        type: Number,
        default: 12,
    },
})
const emit = defineEmits(['close'])
const popupStyle = computed(() => ({
    left: `${props.position?.x ?? 0}px`,
    top: `${props.position?.y ?? 0}px`,
}))
const popupTitle = computed(() => props.device?.deviceName || props.device?.deviceModel || '-')
const popupTitleOverflow = computed(() => popupTitle.value.length > props.titleMax)
const popupTitleDisplay = computed(() => {
    if (!popupTitleOverflow.value) return popupTitle.value
    return `${popupTitle.value.slice(0, props.titleMax)}……`
})
const popupStatusText = computed(() => {
    const status = props.device?.status
    if (status === 0) return '在线'
    if (status === 1) return '离线'
    if (status === 2) return '故障'
    if (status === 3) return '报废'
    return '-'
})
const popupStatusClass = computed(() => (props.device?.status === 0 ? 'online' : ''))
const popupActionText = computed(() => {
    const workMode = props.device?.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 = props.device?.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 = props.device?.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 = props.device?.batteryPct
    if (val == null) return '-'
    return `${val}%`
})
const popupAzimuthText = computed(() => {
    const val = props.device?.azimuth
    if (val == null) return '-'
    return `${val}°`
})
const popupElevationText = computed(() => {
    const val = props.device?.elevation
    if (val == null) return '-'
    return `${val}°`
})
const popupDetectCountText = computed(() => {
    const val = props.device?.detectTargetCnt
    if (val == null) return '-'
    return `${val}台`
})
const popupRangeText = computed(() => {
    const val = props.device?.effectiveRangeKm
    if (val == null) return '-'
    return `${val}KM`
})
</script>
<style lang="scss" scoped>
.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__title-tooltip {
            flex: 1;
            min-width: 0;
            display: block;
        }
        .device-popup__title {
            display: inline-block;
            max-width: 100%;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .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;
        }
    }
}
</style>
applications/drone-command/src/components/map-container/components/DronePopup.vue
New file
@@ -0,0 +1,323 @@
<template>
    <div v-if="visible" class="drone-popup" :style="popupStyle" @click.stop>
        <div class="drone-popup__header">
            <el-tooltip
                class="drone-popup__title-tooltip"
                :content="popupTitle"
                placement="top"
                effect="dark"
                :show-after="200"
            >
                <span class="drone-popup__title">{{ titleDisplay }}</span>
            </el-tooltip>
            <div class="drone-popup__header-actions">
                <img
                    class="drone-popup__favorite"
                    :src="favoriteIcon"
                    alt="收藏"
                    @click.stop="emit('toggle-favorite')"
                />
                <img class="drone-popup__close" :src="popupClose" alt="" @click.stop="emit('close')" />
            </div>
        </div>
        <div class="drone-popup__serial">{{ serial }}</div>
        <div class="drone-popup__status">
            <span class="drone-popup__status-dot" :class="statusClass"></span>
            <span class="drone-popup__status-text">{{ statusText }}</span>
        </div>
        <div class="drone-popup__row">
            <span class="label">数据源</span>
            <span class="value">{{ dataSource }}</span>
        </div>
        <div class="drone-popup__row drone-popup__row-split">
            <div class="drone-popup__col">
                <span class="label">高度</span>
                <span class="value">{{ height }}</span>
            </div>
            <span class="divider">|</span>
            <div class="drone-popup__col">
                <span class="label">速度</span>
                <span class="value">{{ speed }}</span>
            </div>
        </div>
        <div class="drone-popup__row drone-popup__row-coord">
            <span class="label">经纬度</span>
            <div class="drone-popup__coord">
                <div>{{ longitude }}</div>
                <div>{{ latitude }}</div>
            </div>
        </div>
        <div class="drone-popup__footer">
            <button class="drone-popup__btn ghost" @click.stop="emit('signal')">信号干扰</button>
            <button class="drone-popup__btn primary" @click.stop="emit('counter')">诱导驱离</button>
        </div>
    </div>
</template>
<script setup>
import { computed } from 'vue'
import popupClose from '@/assets/images/dataCockpit/map/popup-close.png'
import favoriteNo from '@/assets/images/dataCockpit/favorite-n.png'
import favoriteYes from '@/assets/images/dataCockpit/favorite-y.png'
const props = defineProps({
    visible: {
        type: Boolean,
        default: false,
    },
    position: {
        type: Object,
        default: () => ({ x: 0, y: 0 }),
    },
    drone: {
        type: Object,
        default: () => ({}),
    },
    titleMax: {
        type: Number,
        default: 12,
    },
    favorite: {
        type: Boolean,
        default: false,
    },
})
const emit = defineEmits(['close', 'toggle-favorite', 'signal', 'counter'])
const popupStyle = computed(() => ({
    left: `${props.position?.x ?? 0}px`,
    top: `${props.position?.y ?? 0}px`,
}))
const favoriteIcon = computed(() => (props.favorite ? favoriteYes : favoriteNo))
const title = computed(() => props.drone?.droneName || '无人机名称')
const titleOverflow = computed(() => title.value.length > props.titleMax)
const titleDisplay = computed(() => {
    if (!titleOverflow.value) return title.value
    return `${title.value.slice(0, props.titleMax)}……`
})
const serial = computed(() => props.drone?.droneSerialNo || '-')
const statusText = computed(() => {
    const status = props.drone?.flightStatus
    if (status === 1 || status === '1') return '侦测中'
    if (status === 2 || status === '2') return '反制中'
    return status || '侦测中'
})
const statusClass = computed(() => (statusText.value.includes('反制') ? 'is-driving' : 'is-detecting'))
const dataSource = computed(() => props.drone?.dataSource || props.drone?.deviceName || props.drone?.areaName || '-')
const counterDevice = computed(() => props.drone?.counterDeviceName || props.drone?.deviceName || '-')
const formatNumber = (value, unit = '', digits = 0) => {
    if (value == null || value === '') return '-'
    const num = Number(value)
    if (!Number.isFinite(num)) return '-'
    return `${num.toFixed(digits)}${unit}`
}
const formatCoord = (value, type) => {
    const num = Number(value)
    if (!Number.isFinite(num)) return '-'
    const absVal = Math.abs(num).toFixed(6)
    const dir = type === 'lng' ? (num >= 0 ? 'E' : 'W') : num >= 0 ? 'N' : 'S'
    return `${absVal} ${dir}`
}
const height = computed(() => formatNumber(props.drone?.flightHeightM, 'm', 0))
const speed = computed(() => formatNumber(props.drone?.flightSpeedMs, 'm/s', 1))
const longitude = computed(() => formatCoord(props.drone?.longitude, 'lng'))
const latitude = computed(() => formatCoord(props.drone?.latitude, 'lat'))
</script>
<style lang="scss" scoped>
.drone-popup {
    position: absolute;
    transform: translate(-50%, calc(-100% - 42px));
    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: 12;
    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);
    }
    .drone-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;
        }
        .drone-popup__title-tooltip {
            flex: 1;
            min-width: 0;
            display: block;
        }
        .drone-popup__title {
            display: inline-block;
            max-width: 100%;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .drone-popup__header-actions {
            display: inline-flex;
            align-items: center;
            gap: 6px;
            color: #C9CBE6;
            font-size: 11px;
        }
        .drone-popup__favorite,
        .drone-popup__close {
            width: 16px;
            height: 16px;
            display: inline-flex;
            align-items: flex-start;
            vertical-align: middle;
            cursor: pointer;
        }
        .drone-popup__favorite {
            width: 12px;
            height: 12px;
        }
    }
}
.drone-popup__serial {
    margin-top: 8px;
    color: #B7BACF;
    font-size: 12px;
    letter-spacing: 0.3px;
}
.drone-popup__status {
    margin-top: 10px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 10px;
    border-radius: 999px;
    background: #303041;
}
.drone-popup__status-dot {
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: #4CC3FF;
}
.drone-popup__status-dot.is-driving {
    background: #FF7A4A;
}
.drone-popup__row {
    margin-top: 10px;
    display: flex;
    align-items: center;
    gap: 10px;
}
.drone-popup__row-split {
    justify-content: flex-start;
    gap: 12px;
}
.drone-popup__col {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.drone-popup__row-coord {
    align-items: flex-start;
}
.drone-popup__coord {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.drone-popup__footer {
    margin-top: 14px;
    display: flex;
    gap: 12px;
}
.drone-popup__btn {
    flex: 1;
    height: 34px;
    border-radius: 8px;
    border: none;
    font-size: 13px;
    cursor: pointer;
    color: #FFFFFF;
    background: #3B3E55;
}
.drone-popup__btn.primary {
    background: #2F5BFF;
}
.drone-popup__btn.ghost {
    background: rgba(59, 62, 85, 0.9);
    border: 1px solid rgba(111, 118, 150, 0.6);
}
</style>
applications/drone-command/src/components/map-container/device-map-container.vue
@@ -20,66 +20,23 @@
            </div>
        </div>
        <div v-if="popupVisible" class="device-popup" :style="popupStyle" @click.stop>
            <div class="device-popup__header">
                <el-tooltip
                    class="device-popup__title-tooltip"
                    :content="popupTitle"
                    placement="top"
                    effect="dark"
                    :show-after="200"
                >
                    <span class="device-popup__title">{{ popupTitleDisplay }}</span>
                </el-tooltip>
        <DevicePopup
            :visible="popupVisible && !isDronePopup"
            :position="popupPosition"
            :device="selectedDevice"
            @close="closePopup"
        />
                <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>
        <DronePopup
            :visible="popupVisible && isDronePopup"
            :position="popupPosition"
            :drone="selectedDevice"
            :favorite="Boolean(selectedDevice?.isFavorite)"
            @close="closePopup"
            @toggle-favorite="toggleDroneFavorite"
            @signal="handleDroneSignal"
            @counter="handleDroneCounter"
        />
    </div>
</template>
@@ -96,8 +53,16 @@
import droneIcon from '@/assets/images/dataCockpit/map/drone.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'
import DevicePopup from './components/DevicePopup.vue'
import DronePopup from './components/DronePopup.vue'
import {
    RING_STYLES,
    addDeviceRings,
    createDroneTrackMaterial,
    createRadialGradientMaterial,
    getTexturedVertexFormat,
} from './device-map-materials'
const props = defineProps({
    onlineDevices: {
@@ -117,6 +82,7 @@
        default: true,
    },
})
const emit = defineEmits(['droneSignal', 'droneCounter'])
const DEFAULT_ZONE_PAGE_SIZE = 999
@@ -124,6 +90,7 @@
let viewer = null
let cockpitPrimitiveLayer = null
const devicePickMap = new Map()
const dronePickMap = new Map()
let deviceBillboardCollection = null
let deviceRingOutlinePrimitives = []
let deviceRingFillPrimitives = []
@@ -144,6 +111,7 @@
const showLayerPanel = ref(false)
const layerWrapRef = ref(null)
const selectedDevice = ref(null)
const selectedTargetType = ref('device')
let selectedDeviceBillboard = null
let deviceClickHandler = null
let popupRenderHandler = null
@@ -371,24 +339,36 @@
const clearDroneTrackEntities = () => {
    if (!viewer) return
    stopDroneTrackAnimation()
    if (droneTrackBillboardCollection) {
        droneTrackBillboardCollection.removeAll()
        removeCockpitPrimitive(droneTrackBillboardCollection)
        if (!droneTrackBillboardCollection.isDestroyed?.()) {
            removeCockpitPrimitive(droneTrackBillboardCollection)
        }
        droneTrackBillboardCollection = null
    }
    if (droneTrackPolylineCollection) {
        droneTrackPolylineCollection.removeAll()
        removeCockpitPrimitive(droneTrackPolylineCollection)
        if (!droneTrackPolylineCollection.isDestroyed?.()) {
            removeCockpitPrimitive(droneTrackPolylineCollection)
        }
        droneTrackPolylineCollection = null
    }
    droneTrackRuntime = []
    stopDroneTrackAnimation()
    dronePickMap.clear()
    if (selectedTargetType.value === 'drone') {
        closePopup()
    }
}
const ensureDroneTrackCollections = () => {
    if (!viewer) return
    ensureCockpitPrimitiveLayer()
    if (droneTrackBillboardCollection?.isDestroyed?.()) {
        droneTrackBillboardCollection = null
    }
    if (droneTrackPolylineCollection?.isDestroyed?.()) {
        droneTrackPolylineCollection = null
    }
    if (!droneTrackBillboardCollection) {
        droneTrackBillboardCollection = new Cesium.BillboardCollection()
        addCockpitPrimitive(droneTrackBillboardCollection)
@@ -404,10 +384,17 @@
    stopDroneTrackAnimation()
    droneTrackStartTime = Cesium.JulianDate.now()
    droneTrackTickHandler = clock => {
        if (!viewer || viewer.isDestroyed?.() || !viewer.scene) {
            stopDroneTrackAnimation()
            return
        }
        if (!droneTrackRuntime.length) return
        const current = clock.currentTime
        const elapsed = Cesium.JulianDate.secondsDifference(current, droneTrackStartTime)
        droneTrackRuntime.forEach(track => {
            if (!track?.billboard || track.billboard.isDestroyed?.()) return
            if (!track.billboard._scene) return
            if (!track.positions || track.positions.length < 2) return
            const duration = track.duration
            if (duration <= 0) return
            const t = ((elapsed % duration) + duration) % duration
@@ -434,33 +421,62 @@
const renderSimulatedDroneTrack = () => {
    if (!viewer) return
    clearDroneTrackEntities()
    ensureCountyCenterMap()
    ensureDroneTrackCollections()
    droneTrackBillboardCollection.removeAll()
    droneTrackPolylineCollection.removeAll()
    droneTrackRuntime = []
    const centers = Array.from(countyCenterMap.values())
    const baseCenters = centers.length ? centers.slice(0, 4) : [{ longitude: 116.397, latitude: 39.908 }]
    const centers = Array.from(countyCenterMap.entries())
    const baseCenters = centers.length
        ? centers.slice(0, 4)
        : [['默认区域', { longitude: 116.397, latitude: 39.908 }]]
    const segmentDuration = 6
    baseCenters.forEach((center, trackIndex) => {
    const baseTrackColor = Cesium.Color.fromCssColorString('red')
    baseCenters.forEach(([centerName, center], trackIndex) => {
        const points = buildSimulatedTrackPoints(center)
        if (points.length < 2) return
        const positions = points.map(point =>
            Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
        )
        let trackMaterial = createDroneTrackMaterial({
            color: baseTrackColor,
            speed: 4.5,
            headWidth: 0.2,
            glowPower: 1.8,
            backgroundAlpha: 0.34,
        })
        if (!trackMaterial) {
            trackMaterial = Cesium.Material.fromType('Color', { color: baseTrackColor })
        }
        droneTrackPolylineCollection.add({
            positions,
            width: 2,
            material: Cesium.Material.fromType('Color', {
                color: Cesium.Color.fromCssColorString('#2AEDBF'),
            }),
            width: 3,
            material: trackMaterial,
        })
        const droneId = `drone-track-${trackIndex}`
        const billboard = droneTrackBillboardCollection.add({
            position: positions[0],
            image: droneIcon,
            width: 36,
            height: 36,
            verticalOrigin: Cesium.VerticalOrigin.CENTER,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
        })
        billboard.id = droneId
        const speedMs = Math.round(Cesium.Cartesian3.distance(positions[0], positions[1]) / segmentDuration)
        dronePickMap.set(droneId, {
            data: {
                droneName: `无人机-${trackIndex + 1}`,
                droneSerialNo: `UAV-${trackIndex + 1}`,
                flightHeightM: points[0].height,
                flightSpeedMs: speedMs,
                longitude: points[0].longitude,
                latitude: points[0].latitude,
                flightStatus: 1,
                deviceName: centerName,
                dataSource: centerName,
                counterDeviceName: centerName,
            },
            billboard,
        })
        droneTrackRuntime.push({
            positions,
@@ -472,128 +488,6 @@
    viewer.clock.multiplier = 1
    startDroneTrackAnimation()
    reorderCockpitPrimitives()
}
const RING_STYLES = [
    { inner: 0, outer: 2000, gradient: ['#FF361C', '#360B00'] }
]
const MATERIAL_TYPE = 'RadialGradientMaterial'
const TEXTURED_VERTEX_FORMAT =
    Cesium.MaterialAppearance?.MaterialSupport?.TEXTURED?.vertexFormat || Cesium.VertexFormat.POSITION_AND_ST
let materialRegistered = false
const registerRadialGradientMaterial = () => {
    if (materialRegistered || !Cesium?.Material) return
    materialRegistered = true
    Cesium.Material._materialCache.addMaterial(MATERIAL_TYPE, {
        fabric: {
            type: MATERIAL_TYPE,
            uniforms: {
                color1: new Cesium.Color(1.0, 1.0, 1.0, 1.0),
                color2: new Cesium.Color(0.0, 0.0, 0.0, 1.0),
            },
            source: `
                czm_material czm_getMaterial(czm_materialInput materialInput) {
                    czm_material material = czm_getDefaultMaterial(materialInput);
                    vec2 st = materialInput.st - vec2(0.5);
                    float t = clamp(length(st) * 2.0, 0.0, 1.0);
                    vec4 color = mix(color1, color2, t);
                    material.diffuse = color.rgb;
                    material.alpha = color.a;
                    return material;
                }
            `,
        },
        translucent: () => true,
    })
}
const buildCirclePositions = (center, radiusMeters, steps = 64) => {
    const positions = []
    const latRad = Cesium.Math.toRadians(center.latitude)
    const metersToLat = 1 / 111320
    const metersToLon = 1 / (111320 * Math.cos(latRad))
    for (let i = 0; i <= steps; i += 1) {
        const angle = Cesium.Math.toRadians((i / steps) * 360)
        const dx = radiusMeters * Math.cos(angle)
        const dy = radiusMeters * Math.sin(angle)
        const lon = center.longitude + dx * metersToLon
        const lat = center.latitude + dy * metersToLat
        positions.push(Cesium.Cartesian3.fromDegrees(lon, lat))
    }
    return positions
}
const addDeviceRings = (center, ringFillInstancesByStyle, ringOutlineInstancesByStyle) => {
    if (!ringFillInstancesByStyle) return
    const centerPosition = Cesium.Cartesian3.fromDegrees(center.longitude, center.latitude, 0)
    RING_STYLES.forEach((ring, index) => {
        if (!ringFillInstancesByStyle[index]) return
        ringFillInstancesByStyle[index].push(
            new Cesium.GeometryInstance({
                geometry: new Cesium.EllipseGeometry({
                    center: centerPosition,
                    semiMajorAxis: ring.outer,
                    semiMinorAxis: ring.outer,
                    vertexFormat: TEXTURED_VERTEX_FORMAT,
                }),
            })
        )
    })
    if (!ringOutlineInstancesByStyle) return
    RING_STYLES.forEach((ring, index) => {
        if (!ringOutlineInstancesByStyle[index]) return
        const positions = buildCirclePositions(center, ring.outer)
        ringOutlineInstancesByStyle[index].push(
            new Cesium.GeometryInstance({
                geometry: new Cesium.GroundPolylineGeometry({
                    positions,
                    width: 2,
                }),
                attributes: {
                    color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                        Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.8)
                    ),
                },
            })
        )
    })
}
class RadialGradientMaterialProperty {
    constructor(color1, color2) {
        this._definitionChanged = new Cesium.Event()
        this.color1 = color1
        this.color2 = color2
    }
    get isConstant () {
        return true
    }
    get definitionChanged () {
        return this._definitionChanged
    }
    getType () {
        return MATERIAL_TYPE
    }
    getValue (time, result) {
        const target = result || {}
        target.color1 = this.color1
        target.color2 = this.color2
        return target
    }
    equals (other) {
        return (
            other instanceof RadialGradientMaterialProperty &&
            Cesium.Color.equals(this.color1, other.color1) &&
            Cesium.Color.equals(this.color2, other.color2)
        )
    }
}
const renderDeviceEntities = devices => {
@@ -628,17 +522,16 @@
        billboard.id = entityId
        devicePickMap.set(entityId, { data: item, billboard })
    })
    if (selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) {
    if (selectedTargetType.value === 'device' && selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) {
        closePopup()
    }
    registerRadialGradientMaterial()
    ringFillInstancesByStyle.forEach((instances, index) => {
        if (!instances.length) return
        const ring = RING_STYLES[index]
        const material = Cesium.Material.fromType(MATERIAL_TYPE, {
            color1: Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.64),
            color2: Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(0.64),
        })
        const material = createRadialGradientMaterial(
            Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.64),
            Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(0.64)
        )
        const primitive = new Cesium.GroundPrimitive({
            geometryInstances: instances,
            appearance: new Cesium.MaterialAppearance({
@@ -679,14 +572,14 @@
    if (!viewer) return { primitive: null, outlinePrimitive: null }
    const polygonInstances = []
    const lineInstances = []
    registerRadialGradientMaterial()
    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: TEXTURED_VERTEX_FORMAT,
            vertexFormat: texturedVertexFormat,
        })
        polygonInstances.push(
            new Cesium.GeometryInstance({
@@ -712,10 +605,7 @@
    if (polygonInstances.length) {
        const baseColor1 = Cesium.Color.fromCssColorString(fillColor1)
        const baseColor2 = Cesium.Color.fromCssColorString(fillColor2)
        const material = Cesium.Material.fromType(MATERIAL_TYPE, {
            color1: baseColor1.withAlpha(0.64),
            color2: baseColor2.withAlpha(0.64),
        })
        const material = createRadialGradientMaterial(baseColor1.withAlpha(0.64), baseColor2.withAlpha(0.64))
        primitive = new Cesium.GroundPrimitive({
            geometryInstances: polygonInstances,
            appearance: new Cesium.MaterialAppearance({
@@ -822,12 +712,15 @@
    })
}
const getPickedDevice = picks => {
const getPickedTarget = picks => {
    for (const pick of picks) {
        const pickId = pick?.id
        const resolvedId = typeof pickId === 'string' ? pickId : pickId?.id
        if (resolvedId && devicePickMap.has(resolvedId)) {
            return devicePickMap.get(resolvedId)
            return { type: 'device', ...devicePickMap.get(resolvedId) }
        }
        if (resolvedId && dronePickMap.has(resolvedId)) {
            return { type: 'drone', ...dronePickMap.get(resolvedId) }
        }
    }
    return null
@@ -858,13 +751,14 @@
const handleDeviceClick = movement => {
    if (!viewer) return
    const picks = viewer.scene.drillPick(movement.position) || []
    const pickedDevice = getPickedDevice(picks)
    if (!pickedDevice) {
    const pickedTarget = getPickedTarget(picks)
    if (!pickedTarget) {
        closePopup()
        return
    }
    selectedDevice.value = pickedDevice.data
    selectedDeviceBillboard = pickedDevice.billboard
    selectedTargetType.value = pickedTarget.type
    selectedDevice.value = pickedTarget.data
    selectedDeviceBillboard = pickedTarget.billboard
    startPopupRender()
}
@@ -883,80 +777,19 @@
function closePopup () {
    selectedDevice.value = null
    selectedDeviceBillboard = null
    selectedTargetType.value = 'device'
    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 POPUP_TITLE_MAX = 12
const popupTitle = computed(() => selectedDevice.value?.deviceName || selectedDevice.value?.deviceModel || '-')
const popupTitleOverflow = computed(() => popupTitle.value.length > POPUP_TITLE_MAX)
const popupTitleDisplay = computed(() => {
    if (!popupTitleOverflow.value) return popupTitle.value
    return `${popupTitle.value.slice(0, POPUP_TITLE_MAX)}……`
})
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 isDronePopup = computed(() => selectedTargetType.value === 'drone')
const toggleDroneFavorite = () => {
    if (!selectedDevice.value) return
    selectedDevice.value.isFavorite = !selectedDevice.value.isFavorite
}
const handleDroneSignal = () => emit('droneSignal', selectedDevice.value)
const handleDroneCounter = () => emit('droneCounter', selectedDevice.value)
const renderCommandPosts = list => {
    if (!viewer) return
@@ -1085,172 +918,6 @@
    left: 0;
    width: 100%;
    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__title-tooltip {
            flex: 1;
            min-width: 0;
            display: block;
        }
        .device-popup__title {
            display: inline-block;
            max-width: 100%;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .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 {
applications/drone-command/src/components/map-container/device-map-materials.js
New file
@@ -0,0 +1,188 @@
import * as Cesium from 'cesium'
export const RING_STYLES = [{ inner: 0, outer: 2000, gradient: ['#FF361C', '#360B00'] }]
export const MATERIAL_TYPE = 'RadialGradientMaterial'
export const DRONE_TRACK_MATERIAL_TYPE = 'DroneTrackFlowMaterial'
let materialRegistered = false
let droneTrackMaterialRegistered = false
let texturedVertexFormat = null
export const getTexturedVertexFormat = () => {
    if (!texturedVertexFormat) {
        texturedVertexFormat =
            Cesium.MaterialAppearance?.MaterialSupport?.TEXTURED?.vertexFormat || Cesium.VertexFormat.POSITION_AND_ST
    }
    return texturedVertexFormat
}
export const registerRadialGradientMaterial = () => {
    if (materialRegistered || !Cesium?.Material) return
    materialRegistered = true
    Cesium.Material._materialCache.addMaterial(MATERIAL_TYPE, {
        fabric: {
            type: MATERIAL_TYPE,
            uniforms: {
                color1: new Cesium.Color(1.0, 1.0, 1.0, 1.0),
                color2: new Cesium.Color(0.0, 0.0, 0.0, 1.0),
            },
            source: `
                czm_material czm_getMaterial(czm_materialInput materialInput) {
                    czm_material material = czm_getDefaultMaterial(materialInput);
                    vec2 st = materialInput.st - vec2(0.5);
                    float t = clamp(length(st) * 2.0, 0.0, 1.0);
                    vec4 color = mix(color1, color2, t);
                    material.diffuse = color.rgb;
                    material.alpha = color.a;
                    return material;
                }
            `,
        },
        translucent: () => true,
    })
}
export const registerDroneTrackMaterial = () => {
    if (droneTrackMaterialRegistered || !Cesium?.Material) return
    droneTrackMaterialRegistered = true
    Cesium.Material._materialCache.addMaterial(DRONE_TRACK_MATERIAL_TYPE, {
        fabric: {
            type: DRONE_TRACK_MATERIAL_TYPE,
            uniforms: {
                color: new Cesium.Color(1.0, 0.28, 0.18, 1.0),
                speed: 4.0,
                headWidth: 0.18,
                glowPower: 1.6,
                backgroundAlpha: 0.22,
            },
            source: `
                uniform vec4 color;
                uniform float speed;
                uniform float headWidth;
                uniform float glowPower;
                uniform float backgroundAlpha;
                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);
                    float flow = fract(st.s - time);
                    float head = smoothstep(headWidth, 0.0, flow) * smoothstep(0.0, headWidth, flow);
                    float alpha = mix(backgroundAlpha, 1.0, head);
                    vec3 rgb = color.rgb;
                    material.diffuse = rgb;
                    material.emission = rgb * alpha * glowPower;
                    material.alpha = color.a * alpha;
                    return material;
                }
            `,
        },
        translucent: () => true,
    })
}
export const createRadialGradientMaterial = (color1, color2) => {
    registerRadialGradientMaterial()
    return Cesium.Material.fromType(MATERIAL_TYPE, { color1, color2 })
}
export const createDroneTrackMaterial = options => {
    registerDroneTrackMaterial()
    return Cesium.Material.fromType(DRONE_TRACK_MATERIAL_TYPE, {
        color: new Cesium.Color(1.0, 0.28, 0.18, 1.0),
        speed: 4.0,
        headWidth: 0.18,
        glowPower: 1.6,
        backgroundAlpha: 0.22,
        ...(options || {}),
    })
}
export const buildCirclePositions = (center, radiusMeters, steps = 64) => {
    const positions = []
    const latRad = Cesium.Math.toRadians(center.latitude)
    const metersToLat = 1 / 111320
    const metersToLon = 1 / (111320 * Math.cos(latRad))
    for (let i = 0; i <= steps; i += 1) {
        const angle = Cesium.Math.toRadians((i / steps) * 360)
        const dx = radiusMeters * Math.cos(angle)
        const dy = radiusMeters * Math.sin(angle)
        const lon = center.longitude + dx * metersToLon
        const lat = center.latitude + dy * metersToLat
        positions.push(Cesium.Cartesian3.fromDegrees(lon, lat))
    }
    return positions
}
export const addDeviceRings = (center, ringFillInstancesByStyle, ringOutlineInstancesByStyle) => {
    if (!ringFillInstancesByStyle) return
    const centerPosition = Cesium.Cartesian3.fromDegrees(center.longitude, center.latitude, 0)
    const vertexFormat = getTexturedVertexFormat()
    RING_STYLES.forEach((ring, index) => {
        if (!ringFillInstancesByStyle[index]) return
        ringFillInstancesByStyle[index].push(
            new Cesium.GeometryInstance({
                geometry: new Cesium.EllipseGeometry({
                    center: centerPosition,
                    semiMajorAxis: ring.outer,
                    semiMinorAxis: ring.outer,
                    vertexFormat,
                }),
            })
        )
    })
    if (!ringOutlineInstancesByStyle) return
    RING_STYLES.forEach((ring, index) => {
        if (!ringOutlineInstancesByStyle[index]) return
        const positions = buildCirclePositions(center, ring.outer)
        ringOutlineInstancesByStyle[index].push(
            new Cesium.GeometryInstance({
                geometry: new Cesium.GroundPolylineGeometry({
                    positions,
                    width: 2,
                }),
                attributes: {
                    color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                        Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.8)
                    ),
                },
            })
        )
    })
}
export class RadialGradientMaterialProperty {
    constructor(color1, color2) {
        this._definitionChanged = new Cesium.Event()
        this.color1 = color1
        this.color2 = color2
    }
    get isConstant () {
        return true
    }
    get definitionChanged () {
        return this._definitionChanged
    }
    getType () {
        return MATERIAL_TYPE
    }
    getValue (time, result) {
        const target = result || {}
        target.color1 = this.color1
        target.color2 = this.color2
        return target
    }
    equals (other) {
        return (
            other instanceof RadialGradientMaterialProperty &&
            Cesium.Color.equals(this.color1, other.color1) &&
            Cesium.Color.equals(this.color2, other.color2)
        )
    }
}
applications/drone-command/src/views/areaManage/areaStatistics/FormDiaLog.vue
@@ -42,7 +42,7 @@
            </el-row>
        </div>
        <template #footer>
            <el-button color="#2B2B4C" @click="handleCancel">关闭</el-button>
            <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">关闭</el-button>
        </template>
    </el-dialog>
</template>
applications/drone-command/src/views/areaManage/defenseZone/FormDiaLog.vue
@@ -85,7 +85,7 @@
                </el-form>
                <div class="footer">
                    <el-button 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">
                        确定
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
@@ -154,7 +154,7 @@
                </el-form>
                <div class="footer">
                    <el-button 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">
                        确定
applications/drone-command/src/views/areaManage/precinctInfo/FormDiaLog.vue
@@ -52,7 +52,7 @@
            </el-row>
        </el-form>
        <template #footer>
            <el-button 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">
                确定
applications/drone-command/src/views/areaManage/sceneConfig/FormDiaLog.vue
@@ -129,7 +129,7 @@
                </el-form>
                <div class="footer">
                    <el-button 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">
                        确定
applications/drone-command/src/views/basicManage/deviceScrap/FormDiaLog.vue
@@ -50,7 +50,7 @@
            </el-row>
        </div>
        <template #footer>
            <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
        </template>
    </el-dialog>
</template>
applications/drone-command/src/views/basicManage/deviceStock/DeviceScrapDiaLog.vue
@@ -16,7 +16,7 @@
            </el-row>
        </el-form>
        <template #footer>
            <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button
                v-if="!dialogReadonly"
                type="primary"
applications/drone-command/src/views/basicManage/deviceStock/DeviceTrackDiaLog.vue
@@ -47,7 +47,7 @@
            </el-row>
        </el-form>
        <template #footer>
            <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button
                v-if="!dialogReadonly"
                type="primary"
applications/drone-command/src/views/basicManage/deviceStock/FormDiaLog.vue
@@ -162,7 +162,7 @@
            </el-row>
        </el-form>
        <template #footer>
            <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button color="#284FE3" v-if="!dialogReadonly" type="primary" :loading="submitting"
                :disabled="submitting" @click="handleSubmit">
                保存
applications/drone-command/src/views/basicManage/maintainRecord/FormDiaLog.vue
@@ -112,7 +112,7 @@
            </el-row>
        </el-form>
        <template #footer>
            <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button color="#284FE3" v-if="!dialogReadonly" type="primary" :loading="submitting"
                :disabled="submitting" @click="handleSubmit">
                保存
applications/drone-command/src/views/basicManage/maintainRecord/MaintenanceDiaLog.vue
@@ -56,7 +56,7 @@
</el-row>
        </el-form>
        <template #footer>
            <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button
                v-if="!dialogReadonly"
                type="primary"
applications/drone-command/src/views/dataCockpit/components/templateComponents/RealTemplate.vue
@@ -18,9 +18,12 @@
            </div>
            <div class="actions">
                <span class="star" @click="emit('favorite')">
                    {{ data.isFavorite ? '★' : '☆' }}
                </span>
                <img
                    class="favorite-icon"
                    :src="favoriteIcon"
                    alt="收藏"
                    @click="emit('favorite')"
                />
            </div>
        </div>
@@ -67,8 +70,11 @@
</template>
<script setup>
import { computed } from 'vue'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
import favoriteNo from '@/assets/images/dataCockpit/favorite-n.png'
import favoriteYes from '@/assets/images/dataCockpit/favorite-y.png'
const props = defineProps({
    data: {
@@ -80,6 +86,7 @@
const route = useRoute()
const emit = defineEmits(['signal', 'counter', 'favorite'])
const favoriteIcon = computed(() => (props.data?.isFavorite ? favoriteYes : favoriteNo))
// 信号干扰操作
const handleSignal = () => {
@@ -250,6 +257,11 @@
            gap: 8px;
            font-size: 16px;
            cursor: pointer;
            .favorite-icon {
                width: 16px;
                height: 16px;
            }
        }
    }
@@ -296,4 +308,4 @@
        }
    }
}
</style>
</style>
applications/drone-command/src/views/detectionCountermeasure/countermeasureEvaluation/FormDiaLog.vue
@@ -67,7 +67,7 @@
            </el-row>
        </div>
        <template #footer>
            <el-button color="#2B2B4C" @click="handleCancel">关闭</el-button>
            <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">关闭</el-button>
        </template>
    </el-dialog>
</template>
applications/drone-command/src/views/detectionCountermeasure/detectionRange/DetectionRangeDialog.vue
@@ -129,7 +129,7 @@
                    </el-form-item>
                </el-form>
                <div class="footer">
                    <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
                    <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
                    <el-button
                        color="#284FE3"
                        v-if="!dialogReadonly"
applications/drone-command/src/views/detectionCountermeasure/deviceAppConfig/FormDiaLog.vue
@@ -152,7 +152,7 @@
            </el-row>
        </el-form>
        <template #footer>
            <el-button 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"
applications/drone-command/src/views/detectionCountermeasure/taskSchedule/FormDiaLog.vue
@@ -142,7 +142,7 @@
        </el-form>
        <template #footer>
            <el-button 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">
                保存
applications/drone-command/src/views/permissionManage/permissionDept/FormDiaLog.vue
@@ -115,7 +115,7 @@
        </el-form>
        <template #footer>
            <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button color="#284FE3" v-if="!dialogReadonly" type="primary" :loading="submitting" :disabled="submitting" @click="handleSubmit">
                保存
            </el-button>
applications/drone-command/src/views/permissionManage/permissionRole/FormDiaLog.vue
@@ -57,7 +57,7 @@
        </el-form>
        <template #footer>
            <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button color="#284FE3" v-if="!dialogReadonly" type="primary" :loading="submitting" :disabled="submitting" @click="handleSubmit">
                保存
            </el-button>
applications/drone-command/src/views/permissionManage/permissionUser/FormDiaLog.vue
@@ -231,7 +231,7 @@
        </el-form>
        <template #footer>
            <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
            <el-button color="#284FE3" v-if="!dialogReadonly" type="primary" :loading="submitting" :disabled="submitting" @click="handleSubmit">
                保存
            </el-button>
applications/drone-command/src/views/recordManage/historyTracks/TrajectoryDiaLog.vue
@@ -56,7 +56,7 @@
                    </div>
                </div>
                <div class="footer">
                    <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
                    <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
                </div>
            </div>
        </div>