吉安感知网项目-前端
shuishen
2026-01-09 6b82e8c63399005e65322f6d5d463f6e33fedbdc
feat:数据驾驶舱接口对接
11 files modified
3 files added
897 ■■■■ changed files
applications/drone-command/src/api/dataCockpit/index.js 46 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/assets/images/no-data-bg.png patch | view | raw | blame | history
applications/drone-command/src/views/api.txt 107 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/CenterContainer.vue 41 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/EmptyState.vue 59 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/EquipmentWarning.vue 104 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/HistoryWarning.vue 117 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/LeftContainer.vue 35 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/RealWarning.vue 135 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/RightContainer.vue 105 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/templateComponents/EquipmentTemplate.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/templateComponents/HistoryTemplate.vue 55 ●●●● 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 59 ●●●● patch | view | raw | blame | history
applications/drone-command/src/api/dataCockpit/index.js
New file
@@ -0,0 +1,46 @@
/*
 * @Author       : yuan
 * @Date         : 2026-01-09 11:14:04
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-09 11:35:14
 * @FilePath     : \applications\drone-command\src\api\dataCockpit\index.js
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-09 11:14:04
 */
import request from '@/axios'
// 设备查询
export const deviceSearchApi = (params) => {
    return request({
        url: '/drone-fw/cockpit/dataCockpit/deviceSearch',
        method: 'get',
        params
    })
}
// 历史告警/实时告警
export const alarmLogApi = (params) => {
    return request({
        url: '/drone-fw/cockpit/dataCockpit/alarmLog',
        method: 'get',
        params
    })
}
// 告警记录统计
export const alarmStatisticsApi = () => {
    return request({
        url: '/drone-fw/cockpit/dataCockpit/alarmStatistics',
        method: 'get'
    })
}
// 信号干扰/诱导驱离
export const interferenceAndExpulsionApi = (data) => {
    return request({
        url: '/drone-fw/cockpit/dataCockpit/interferenceAndExpulsion',
        method: 'post',
        data
    })
}
applications/drone-command/src/assets/images/no-data-bg.png
applications/drone-command/src/views/api.txt
@@ -0,0 +1,107 @@
## 信号干扰,诱导驱离
**接口地址**:`/cockpit/dataCockpit/interferenceAndExpulsion`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:<p>传入fwEffectEval</p>
**请求示例**:
```javascript
{
  "alarmRecordId": 0,
  "areaCode": "",
  "counterEffect": "",
  "coverRadiusM": 0,
  "deployLatitude": 0,
  "deployLongitude": 0,
  "deviceCode": "",
  "deviceId": 0,
  "deviceModel": "",
  "deviceName": "",
  "deviceType": "",
  "droneDeviceCode": "",
  "droneName": "",
  "droneType": "",
  "findTime": "",
  "id": 0,
  "workMode": ""
}
```
**请求参数**:
**请求参数**:
| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|fwEffectEval|fwEffectEval|body|true|FwEffectEvalDTO|FwEffectEvalDTO|
|&emsp;&emsp;alarmRecordId|告警记录ID(ja_fw_drone_alarm_record.id)||false|integer(int64)||
|&emsp;&emsp;areaCode|区域编码||false|string||
|&emsp;&emsp;counterEffect|反制效果(success/fail)||false|string||
|&emsp;&emsp;coverRadiusM|覆盖范围(米)||false|integer(int32)||
|&emsp;&emsp;deployLatitude|部署纬度(冗余)||false|number(double)||
|&emsp;&emsp;deployLongitude|部署经度(冗余)||false|number(double)||
|&emsp;&emsp;deviceCode|反制设备编码(快照)||false|string||
|&emsp;&emsp;deviceId|反制设备ID(ja_fw_device.id)||false|integer(int64)||
|&emsp;&emsp;deviceModel|反制设备型号(快照)||false|string||
|&emsp;&emsp;deviceName|反制设备名称(快照)||false|string||
|&emsp;&emsp;deviceType|反制设备类型(快照)||false|string||
|&emsp;&emsp;droneDeviceCode|无人机设备编码||false|string||
|&emsp;&emsp;droneName|无人机名称||false|string||
|&emsp;&emsp;droneType|无人机类型||false|string||
|&emsp;&emsp;findTime|发现时间||false|string(date-time)||
|&emsp;&emsp;id|主键id||false|integer(int64)||
|&emsp;&emsp;workMode|工作模式(机动/固定)||false|string||
**响应状态**:
| 状态码 | 说明 | schema |
| -------- | -------- | ----- |
|200|OK|R|
|201|Created||
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- |
|code|状态码|integer(int32)|integer(int32)|
|data|承载数据|object||
|msg|返回消息|string||
|success|是否成功|boolean||
**响应示例**:
```javascript
{
    "code": 0,
    "data": {},
    "msg": "",
    "success": true
}
```
applications/drone-command/src/views/dataCockpit/components/CenterContainer.vue
@@ -39,45 +39,46 @@
</template>
<script setup>
import { ref } from 'vue'
import { onMounted, ref } from 'vue'
import warningLogo from '@/assets/images/dataCockpit/warning.png'
import equipmentLogo from '@/assets/images/dataCockpit/equipment.png'
import { alarmStatisticsApi } from '@/api/dataCockpit'
let warningList = ref([
const warningList = ref([
    {
        title: '实时告警',
        value: 56,
        value: 0,
        type: 1
    },
    {
        title: '设备告警',
        value: 127,
        value: 0,
        type: 2
    },
    {
        title: '历史告警',
        value: 619,
        value: 0,
        type: 3
    },
])
let equipmentList = ref([
const equipmentList = ref([
    {
        title: '设备统计',
        value: 18,
        value: 0,
        isHighlight: true,
    },
    {
        title: '无线电',
        value: 9,
        value: 0,
    },
    {
        title: '光电',
        value: 6,
        value: 0,
    },
    {
        title: '雷达',
        value: 3,
        value: 0,
    }
])
@@ -92,6 +93,26 @@
        emit('select-equipment')
    }
}
const updateStats = async () => {
    const res = await alarmStatisticsApi()
    const data = res?.data?.data || {}
    warningList.value = [
        { title: '实时告警', value: data.realTimeAlarmCount ?? 0, type: 1 },
        { title: '设备告警', value: data.deviceAlarmCount ?? 0, type: 2 },
        { title: '历史告警', value: data.historyAlarmCount ?? 0, type: 3 }
    ]
    equipmentList.value = [
        { title: '设备统计', value: data.deviceCount ?? 0, isHighlight: true },
        { title: '无线电', value: data.radioCount ?? 0 },
        { title: '光电', value: data.opticalCount ?? 0 },
        { title: '雷达', value: data.radarCount ?? 0 }
    ]
}
onMounted(() => {
    updateStats()
})
</script>
<style lang="scss" scoped>
applications/drone-command/src/views/dataCockpit/components/EmptyState.vue
New file
@@ -0,0 +1,59 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-09 15:06:02
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-09 15:10:44
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\EmptyState.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-09 15:06:02
-->
<template>
    <div class="empty-state">
        <div class="empty-content">
            <img :src="noDataBg" alt="暂无数据">
            <span class="empty-text">暂无数据</span>
        </div>
    </div>
</template>
<script setup>
import noDataBg from '@/assets/images/no-data-bg.png'
</script>
<style scoped lang="scss">
.empty-state {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1;
    width: 100%;
    height: 100%;
    min-height: 220px;
    .empty-content {
        display: flex;
        align-items: center;
        justify-content: center;
        flex-direction: column;
        text-align: center;
    }
    img {
        max-width: 78%;
        max-height: 78%;
        object-fit: contain;
    }
    .empty-text {
        font-family: Source Han Sans CN, Source Han Sans CN;
        font-weight: 400;
        font-size: 14px;
        color: #9E9EBA;
        line-height: 18px;
        text-align: center;
        font-style: normal;
        text-transform: none;
    }
}
</style>
applications/drone-command/src/views/dataCockpit/components/EquipmentWarning.vue
@@ -9,44 +9,83 @@
 * 2026-01-08 09:29:07
-->
<template>
    <div class="equipment-warning">
        <EquipmentTemplate v-for="(item, ind) in equipmentWarningData" :key="ind" :data="item" @click="onCardClick" />
    <div
        class="equipment-warning"
        v-loading="loading"
        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>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import EquipmentTemplate from './templateComponents/EquipmentTemplate.vue'
import { deviceSearchApi } from '@/api/dataCockpit'
import EmptyState from './EmptyState.vue'
const equipmentWarningData = ref([
    {
        id: 'dev_001',
        name: '反制设备名称',
        status: '离线',
        statusType: 'offline', // offline | fault | online
        type: '察打一体',
        vendor: '厂商名称',
        azimuth: 14,          // 方位角(°)
        pitch: 14,            // 俯仰角(°)
        range: '1.2KM',       // 有效范围
        model: 'TDOAX1A',     // 设备型号
        longitude: 113.877571,
        latitude: 22.547155
    },
    {
        id: 'dev_002',
        name: '反制设备名称',
        status: '故障',
        statusType: 'fault',
        type: '察打一体',
        vendor: '厂商名称',
        azimuth: 14,
        pitch: 14,
        range: '1.2KM',
        model: 'TDOAX1A',
        longitude: 113.877571,
        latitude: 22.547155
const equipmentWarningData = ref([])
const loading = ref(false)
const minLoadingMs = 400
const statusMap = {
    0: { text: '在线', statusType: 'online' },
    1: { text: '离线', statusType: 'offline' },
    2: { text: '故障', statusType: 'fault' },
    3: { text: '报废', statusType: 'offline' }
}
const deviceTypeMap = {
    1: '便捷侦测箱',
    2: '反制枪',
    3: '察打一体'
}
const formatDeviceItem = (item) => {
    const statusInfo = statusMap[item?.status] || { text: '-', statusType: '' }
    return {
        id: item.id,
        name: item.deviceName || item.deviceModel || '-',
        status: statusInfo.text,
        statusType: statusInfo.statusType,
        type: deviceTypeMap[item.deviceType] || item.deviceType || '-',
        manufacturer: item.manufacturer || '-',
        azimuth: item.azimuth ?? 0,
        pitch: item.elevation ?? 0,
        range: item.effectiveRangeKm ?? 0,
        model: item.deviceModel || '-',
        longitude: item.longitude ?? '-',
        latitude: item.latitude ?? '-'
    }
])
}
const fetchDeviceWarning = async () => {
    const startAt = Date.now()
    loading.value = true
    try {
        const params = { current: 1, size: 20, deviceStatusList: '1,2,3' }
        const res = await deviceSearchApi(params)
        const records = res?.data?.data?.records ?? []
        equipmentWarningData.value = records.map(formatDeviceItem)
    } finally {
        const elapsed = Date.now() - startAt
        if (elapsed < minLoadingMs) {
            await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed))
        }
        loading.value = false
    }
}
onMounted(() => {
    fetchDeviceWarning()
})
const onCardClick = (item) => {
    console.log('点击设备卡片:', item.id)
@@ -58,4 +97,5 @@
    width: 100%;
    height: 100%;
}
</style>
</style>
applications/drone-command/src/views/dataCockpit/components/HistoryWarning.vue
@@ -1,5 +1,10 @@
<template>
    <div class="history-warning">
    <div
        class="history-warning"
        v-loading="loading"
        element-loading-background="rgba(5, 5, 15, 0.6)"
        element-loading-text="加载中..."
    >
        <!-- 搜索区(设计稿红框) -->
        <div class="search-box">
            <!-- 关键字 -->
@@ -20,39 +25,27 @@
        <!-- 列表 -->
        <div class="list-content">
            <HistoryTemplate v-for="(item, ind) in filteredList" :key="item.id || ind" :data="item"
                @click="onCardClick" />
            <EmptyState v-if="!filteredList.length" />
            <HistoryTemplate
                v-else
                v-for="(item, ind) in filteredList"
                :key="item.id || ind"
                :data="item"
                @click="onCardClick"
            />
        </div>
    </div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import HistoryTemplate from './templateComponents/HistoryTemplate.vue'
import { alarmLogApi } from '@/api/dataCockpit'
import EmptyState from './EmptyState.vue'
// 原始数据(别动它,过滤用 computed)
const historyWarningData = ref([
    {
        id: 'drone_001',
        name: '无人机名称',
        serialNumber: 'XLH78945645456',
        dataSource: '侦测反制设备名称',
        frequency: '5800MHZ',
        discoveredAt: { date: '2025年12月30日', time: '14:55:06' },
        stayTime: '00:10:50',
        counterMethod: '信号干扰'
    },
    {
        id: 'drone_002',
        name: '无人机名称',
        serialNumber: 'XLH78945645456',
        dataSource: '侦测反制设备名称',
        frequency: '2400MHZ',
        discoveredAt: { date: '2025年12月30日', time: '15:08:12' },
        stayTime: '00:03:21',
        counterMethod: '诱导驱离'
    }
])
const historyWarningData = ref([])
const loading = ref(false)
const minLoadingMs = 400
// 查询条件:keyword + dateRange(YYYY-MM-DD数组)
const query = ref({
@@ -61,53 +54,46 @@
})
const onSearch = () => {
    // 目前是本地过滤:computed 会自动更新
    // 后续如果你要走接口,这里换成 fetchList(query.value) 即可
    fetchHistoryWarning()
}
const onCardClick = (item) => {
    console.log('点击无人机卡片:', item.id)
}
// "2025年12月30日" -> "2025-12-30"
const cnDateToISO = (cn) => {
    if (!cn) return ''
    const m = cn.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/)
    if (!m) return ''
    const y = m[1]
    const mm = String(m[2]).padStart(2, '0')
    const dd = String(m[3]).padStart(2, '0')
    return `${y}-${mm}-${dd}`
}
const inRange = (targetISO, startISO, endISO) => {
    if (!targetISO) return false
    const t = new Date(`${targetISO}T00:00:00`).getTime()
    const s = startISO ? new Date(`${startISO}T00:00:00`).getTime() : null
    const e = endISO ? new Date(`${endISO}T23:59:59`).getTime() : null
    if (s !== null && t < s) return false
    if (e !== null && t > e) return false
    return true
const fetchHistoryWarning = async () => {
    const startAt = Date.now()
    loading.value = true
    try {
        const [startTime, endTime] = query.value.dateRange || []
        const keyword = (query.value.keyword || '').trim()
        const params = {
            current: 1,
            size: 20,
            alarmType: '2',
            startTime,
            endTime,
            droneName: keyword || undefined,
            droneSerialNo: keyword || undefined
        }
        const res = await alarmLogApi(params)
        const records = res?.data?.data?.records ?? []
        historyWarningData.value = records
    } finally {
        const elapsed = Date.now() - startAt
        if (elapsed < minLoadingMs) {
            await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed))
        }
        loading.value = false
    }
}
const filteredList = computed(() => {
    const kw = (query.value.keyword || '').trim().toLowerCase()
    const [start, end] = query.value.dateRange || []
    return historyWarningData.value
})
    return historyWarningData.value.filter((item) => {
        // 1) 关键字:name / serialNumber / dataSource
        const hitKeyword = !kw
            ? true
            : [item.name, item.serialNumber, item.dataSource]
                .filter(Boolean)
                .some((v) => String(v).toLowerCase().includes(kw))
        if (!hitKeyword) return false
        // 2) 日期范围:按 discoveredAt.date
        if (!start && !end) return true
        const itemISO = cnDateToISO(item?.discoveredAt?.date)
        return inRange(itemISO, start, end)
    })
onMounted(() => {
    fetchHistoryWarning()
})
</script>
@@ -133,4 +119,5 @@
        flex: 1;
    }
}
</style>
</style>
applications/drone-command/src/views/dataCockpit/components/LeftContainer.vue
@@ -7,7 +7,7 @@
                <div class="category-item" v-for="(item, ind) in categoryList" :key="ind"
                    @click="activeIdModel = item.id" :class="{ active: activeIdModel === item.id }">
                    {{ item.name }}
                    <span class="badge">{{ item.badge }}</span>
                    <span class="badge" v-if="item.badge">{{ item.badge }}</span>
                </div>
            </div>
@@ -27,6 +27,8 @@
import RealWarning from './RealWarning.vue'
import EquipmentWarning from './EquipmentWarning.vue'
import HistoryWarning from './HistoryWarning.vue'
import { alarmStatisticsApi } from '@/api/dataCockpit'
import { onMounted } from 'vue'
const props = defineProps({
    activeId: {
@@ -55,19 +57,19 @@
    {
        id: 1,
        name: '实时告警',
        badge: 8,
        badge: 0,
        component: RealWarning
    },
    {
        id: 2,
        name: '设备告警',
        badge: 8,
        badge: 0,
        component: EquipmentWarning
    },
    {
        id: 3,
        name: '历史告警',
        badge: 8,
        badge: 0,
        component: HistoryWarning
    }
])
@@ -79,6 +81,24 @@
const toggleCollapse = () => {
    collapsedModel.value = !collapsedModel.value
}
const updateBadges = async () => {
    const res = await alarmStatisticsApi()
    const data = res?.data?.data || {}
    const realTotal = data.realTimeAlarmCount ?? 0
    const historyTotal = data.historyAlarmCount ?? 0
    const deviceTotal = data.deviceAlarmCount ?? 0
    categoryList.value = categoryList.value.map((item) => {
        if (item.id === 1) return { ...item, badge: realTotal }
        if (item.id === 2) return { ...item, badge: deviceTotal }
        if (item.id === 3) return { ...item, badge: historyTotal }
        return item
    })
}
onMounted(() => {
    updateBadges()
})
</script>
<style lang="scss" scoped>
@@ -170,13 +190,12 @@
            position: absolute;
            top: 16px;
            right: 4px;
            min-width: 16px;
            height: 16px;
            line-height: 16px;
            min-width: 18px;
            line-height: 18px;
            font-size: 12px;
            color: #FFFFFF;
            background: #FF3B30;
            border-radius: 9px;
            border-radius: 50%;
            text-align: center;
            box-shadow: 0px 2px 6px rgba(255, 59, 48, 0.6);
            pointer-events: none;
applications/drone-command/src/views/dataCockpit/components/RealWarning.vue
@@ -9,63 +9,95 @@
 * 2026-01-08 09:29:07
-->
<template>
    <div class="real-warning">
        <RealTemplate v-for="(item, ind) in realWarningData" :key="ind" :data="item"
            @signal="() => console.log('信号干扰', item.id)" @counter="() => console.log('诱导驱离', item.id)"
            @favorite="() => console.log('收藏', item.id)" />
    <div
        class="real-warning"
        v-loading="loading"
        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>
</template>
<script setup>
import RealTemplate from './templateComponents/RealTemplate.vue'
import { alarmLogApi, interferenceAndExpulsionApi } from '@/api/dataCockpit'
import { onMounted, ref } from 'vue'
import EmptyState from './EmptyState.vue'
import { ElMessage } from 'element-plus'
const realWarningData = ref([
    {
        "id": "drone_001",
        "name": "无人机A-01",
        "status": "侦测中",
        "statusType": "detecting",
        "serialNumber": "XLH789456456",
        "dataSource": "侦测反制设备名称",
        "longitude": 113.99238,
        "latitude": 22.541819,
        "frequency": "5800MHz",
        "discoverTime": "14:56:04",
        "altitude": "150m",
        "speed": "5.01m/s",
        "isFavorite": false
    },
    {
        "id": "drone_002",
        "name": "无人机B-07",
        "status": "反制中",
        "statusType": "countering",
        "serialNumber": "XLH789456789",
        "dataSource": "反制设备A区",
        "longitude": 113.99312,
        "latitude": 22.542031,
        "frequency": "2400MHz",
        "discoverTime": "15:02:41",
        "altitude": "120m",
        "speed": "3.86m/s",
        "isFavorite": true
    },
    {
        "id": "drone_002",
        "name": "无人机B-07",
        "status": "反制中",
        "statusType": "countering",
        "serialNumber": "XLH789456789",
        "dataSource": "反制设备A区",
        "longitude": 113.99312,
        "latitude": 22.542031,
        "frequency": "2400MHz",
        "discoverTime": "15:02:41",
        "altitude": "120m",
        "speed": "3.86m/s",
        "isFavorite": true
const realWarningData = ref([])
const loading = ref(false)
const minLoadingMs = 400
const actionTextMap = {
    signal: '信号干扰',
    counter: '诱导驱离'
}
const buildPayload = (item) => ({
    alarmRecordId: item.id,
    areaCode: item.areaCode,
    counterEffect: '',
    coverRadiusM: item.coverRadiusM,
    deployLatitude: item.latitude,
    deployLongitude: item.longitude,
    deviceCode: item.deviceCode,
    deviceId: item.deviceId,
    deviceModel: item.deviceModel,
    deviceName: item.deviceName,
    deviceType: item.deviceType,
    droneDeviceCode: item.droneSerialNo,
    droneName: item.droneName,
    droneType: item.droneType,
    findTime: item.alarmTime,
    id: item.id,
    workMode: ''
})
const handleAction = async (type, item) => {
    const actionText = actionTextMap[type] || '操作'
    try {
        const res = await interferenceAndExpulsionApi(buildPayload(item))
        if (res?.data?.success) {
            ElMessage({ type: 'success', message: `${actionText}成功` })
        } else {
            ElMessage({ type: 'error', message: res?.data?.msg || `${actionText}失败` })
        }
    } catch (error) {
        ElMessage({ type: 'error', message: `${actionText}失败` })
    }
])
}
const fetchRealWarning = async () => {
    const startAt = Date.now()
    loading.value = true
    try {
        const params = { current: 1, size: 20, alarmType: '1' }
        const res = await alarmLogApi(params)
        const records = res?.data?.data?.records ?? []
        realWarningData.value = records
    } finally {
        const elapsed = Date.now() - startAt
        if (elapsed < minLoadingMs) {
            await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed))
        }
        loading.value = false
    }
}
onMounted(() => {
    fetchRealWarning()
})
</script>
<style lang="scss" scoped>
@@ -73,4 +105,5 @@
    width: 100%;
    height: 100%;
}
</style>
</style>
applications/drone-command/src/views/dataCockpit/components/RightContainer.vue
@@ -26,9 +26,21 @@
                </div>
            </div>
            <div class="content">
                <RealEquipmentTemplate v-for="item in counterDeviceStatusList" :key="item.id" :data="item"
                    @history="onHistory" @click="onCardClick" />
            <div
                class="content"
                v-loading="loading"
                element-loading-background="rgba(5, 5, 15, 0.6)"
                element-loading-text="加载中..."
            >
                <EmptyState v-if="!counterDeviceStatusList.length" />
                <RealEquipmentTemplate
                    v-else
                    v-for="item in counterDeviceStatusList"
                    :key="item.id"
                    :data="item"
                    @history="onHistory"
                    @click="onCardClick"
                />
            </div>
        </div>
@@ -44,8 +56,10 @@
import TitleTemplate from './templateComponents/TitleTemplate.vue'
import RealEquipmentTemplate from './templateComponents/RealEquipmentTemplate.vue'
import DeviceHistoryDialog from './DeviceHistoryDialog.vue'
import EmptyState from './EmptyState.vue'
import zcsbLogo from '@/assets/images/dataCockpit/zcsb.png'
import fzsbLogo from '@/assets/images/dataCockpit/fzsb.png'
import { deviceSearchApi } from '@/api/dataCockpit'
const props = defineProps({
    collapsed: {
@@ -76,39 +90,12 @@
    }
])
const counterDeviceStatusList = ref([
    {
        id: 'cd_001',
        name: '反制设备名称',
        actionText: '侦测中',
        actionType: 'detecting',       // detecting | jamming | idle
        type: '察打一体',
        battery: 100,                  // 电量 %
        azimuth: 14,                   // 方位角
        pitch: 14,                     // 俯仰角
        statusText: '在线',            // 状态
        vendor: '厂商名称',            // 厂商
        targetCount: 10,               // 侦测目标
        range: '1.2KM'                 // 有效范围
    },
    {
        id: 'cd_002',
        name: '反制设备名称',
        actionText: '信号干扰中',
        actionType: 'jamming',
        type: '察打一体',
        battery: 100,
        azimuth: 14,
        pitch: 14,
        statusText: '在线',
        vendor: '厂商名称',
        targetCount: 10,
        range: '1.2KM'
    }
])
const counterDeviceStatusList = ref([])
const historyVisible = ref(false)
const historyDevice = ref(null)
const loading = ref(false)
const minLoadingMs = 400
const onHistory = (item) => {
    historyDevice.value = item
@@ -122,6 +109,57 @@
const toggleCollapse = () => {
    collapsedModel.value = !collapsedModel.value
}
const statusMap = {
    0: { text: '在线', actionType: 'detecting' },
    1: { text: '离线', actionType: '' },
    2: { text: '故障', actionType: 'jamming' },
    3: { text: '报废', actionType: '' }
}
const deviceTypeMap = {
    1: '便捷侦测箱',
    2: '反制枪',
    3: '察打一体'
}
const formatDeviceItem = (item) => {
    const statusInfo = statusMap[item?.status] || { text: '-', actionType: '' }
    return {
        id: item.id,
        name: item.deviceName || item.deviceModel || '-',
        actionText: statusInfo.text,
        actionType: statusInfo.actionType,
        type: deviceTypeMap[item.deviceType] || item.deviceType || '-',
        batteryPct: item.batteryPct ?? 0,
        azimuth: item.azimuth ?? 0,
        elevation: item.elevation ?? 0,
        status: item.status ?? '-',
        manufacturer: item.manufacturer || '-',
        detectTargetCnt: item.detectTargetCnt ?? 0,
        effectiveRangeKm: item.effectiveRangeKm ?? 0
    }
}
const fetchDeviceList = async () => {
    const startAt = Date.now()
    loading.value = true
    try {
        const params = { current: 1, size: 20, deviceStatusList: '0' }
        const res = await deviceSearchApi(params)
        const records = res?.data?.data?.records ?? []
        counterDeviceStatusList.value = records.map(formatDeviceItem)
    } finally {
        const elapsed = Date.now() - startAt
        if (elapsed < minLoadingMs) {
            await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed))
        }
        loading.value = false
    }
}
onMounted(() => {
    fetchDeviceList()
})
</script>
<style lang="scss" scoped>
@@ -195,6 +233,7 @@
    overflow-y: auto;
}
.right-container {
    transition: transform 0.3s ease-in-out;
    position: relative;
applications/drone-command/src/views/dataCockpit/components/templateComponents/EquipmentTemplate.vue
@@ -24,7 +24,7 @@
                </div>
                <div class="col">
                    <span class="label">厂商</span>
                    {{ data.vendor }}
                    {{ data.manufacturer }}
                </div>
            </div>
            <div class="row">
applications/drone-command/src/views/dataCockpit/components/templateComponents/HistoryTemplate.vue
@@ -2,42 +2,47 @@
    <div class="drone-info-card" @click="emit('click', data)">
        <div class="title">
            <span class="name">
                {{ data.name }}
                {{ data.droneName }}
            </span>
        </div>
        <div class="rows">
            <div class="row">
                <span class="label">序列号</span>
                <span class="value">{{ data.serialNumber }}</span>
                <span class="value">{{ data.droneSerialNo }}</span>
            </div>
            <div class="row">
                <span class="label">数据源</span>
                <span class="value">{{ data.dataSource }}</span>
                <span class="value">{{ getDataSource(data) }}</span>
            </div>
            <div class="row">
                <span class="label">信号频段</span>
                <span class="value">{{ data.frequency }}</span>
                <span class="value">{{ formatFrequency(data.signalFreqMhz) }}</span>
            </div>
            <div class="row">
                <span class="label">发现时间</span>
                <span class="value">
                    <span class="date">{{ data.discoveredAt?.date }}</span>
                    <span class="time">{{ data.discoveredAt?.time }}</span>
                    <span class="date">{{ getAlarmDate(data.alarmTime) }}</span>
                    <span class="time">{{ getAlarmTime(data.alarmTime) }}</span>
                </span>
            </div>
            <div class="row">
                <span class="label">停留时间</span>
                <span class="value">{{ data.stayTime }}</span>
                <span class="value">{{ data.stayDuration }}</span>
            </div>
            <div class="row">
                <span class="label">反制方式</span>
                <span class="value">{{ data.counterMethod }}</span>
                <span class="value">{{ getCounterWayText(data.counterWay) }}</span>
            </div>
            <div class="row">
                <span class="label">飞行状态</span>
                <span class="value">{{ getFlightStatusText(data.flightStatus) }}</span>
            </div>
        </div>
    </div>
@@ -53,6 +58,40 @@
})
const emit = defineEmits(['click'])
const getDataSource = (item) => {
    return item?.deviceName || item?.areaName || '-'
}
const formatFrequency = (value) => {
    if (value == null || value === '') return '-'
    return `${value}MHZ`
}
const getAlarmDate = (value) => {
    if (!value) return '-'
    const parts = value.replace('T', ' ').split(' ')
    return parts[0] || value
}
const getAlarmTime = (value) => {
    if (!value) return '-'
    const parts = value.replace('T', ' ').split(' ')
    return parts[1] || value
}
const getFlightStatusText = (value) => {
    if (value === 1 || value === '1') return '侦测中'
    if (value === 2 || value === '2') return '反制中'
    return value || '-'
}
const getCounterWayText = (value) => {
    if (value === 1 || value === '1') return '信号干扰'
    if (value === 2 || value === '2') return '诱导驱离'
    if (value === 3 || value === '3') return '无'
    return value || '-'
}
</script>
<style scoped lang="scss">
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="data.actionType">
                    <span :class="getStatusType(data.status)">
                    </span>
                    <span>
                        {{ data.actionText }}
                        {{ getStatusText(data.status) }}
                    </span>
                </span>
            </div>
@@ -26,7 +26,7 @@
                </div>
                <div class="col">
                    <span class="label">电量</span>
                    {{ data.battery }}%
                    {{ data.batteryPct }}%
                </div>
            </div>
@@ -37,29 +37,29 @@
                </div>
                <div class="col">
                    <span class="label">俯仰角</span>
                    {{ data.pitch }}°
                    {{ data.elevation }}°
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">状态</span>
                    {{ data.statusText }}
                    {{ getStatusText(data.status) }}
                </div>
                <div class="col">
                    <span class="label">厂商</span>
                    {{ data.vendor }}
                    {{ data.manufacturer }}
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">侦测目标</span>
                    {{ data.targetCount }}台
                    {{ data.detectTargetCnt }}台
                </div>
                <div class="col">
                    <span class="label">有效范围</span>
                    {{ data.range }}KM
                    {{ data.effectiveRangeKm }}KM
                </div>
            </div>
        </div>
@@ -81,6 +81,20 @@
})
const emit = defineEmits(['history', 'click'])
const getStatusText = (value) => {
    if (value === 0 || value === '0') return '在线'
    if (value === 1 || value === '1') return '离线'
    if (value === 2 || value === '2') return '故障'
    if (value === 3 || value === '3') return '报废'
    return value || '-'
}
const getStatusType = (value) => {
    if (value === 0 || value === '0') return 'detecting'
    if (value === 2 || value === '2') return 'jamming'
    return ''
}
</script>
<style scoped lang="scss">
@@ -196,4 +210,4 @@
        }
    }
}
</style>
</style>
applications/drone-command/src/views/dataCockpit/components/templateComponents/RealTemplate.vue
@@ -2,14 +2,14 @@
    <div class="drone-card">
        <div class="header">
            <div class="title">
                <span class="name">{{ data.name }}</span>
                <span class="name">{{ data.droneName }}</span>
                <span class="status">
                    <span :class="data.statusType">
                    <span :class="getStatusType(data)">
                    </span>
                    <span>
                        {{ data.status }}
                        {{ getStatusText(data) }}
                    </span>
                </span>
            </div>
@@ -24,11 +24,11 @@
        <div class="content">
            <div>
                <span class="label">序列号</span>
                {{ data.serialNumber }}
                {{ data.droneSerialNo }}
            </div>
            <div>
                <span class="label">数据源</span>
                {{ data.dataSource }}
                {{ getDataSource(data) }}
            </div>
            <div>
                <span class="label">经纬度</span>
@@ -37,21 +37,21 @@
            <div class="row">
                <div class="col">
                    <span class="label">信号频段</span>
                    {{ data.frequency }}
                    {{ formatFrequency(data.signalFreqMhz) }}
                </div>
                <div class="col">
                    <span class="label">发现时间</span>
                    {{ data.discoverTime }}
                    {{ getAlarmTime(data.alarmTime) }}
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">飞行高度</span>
                    {{ data.altitude }}
                    {{ formatHeight(data.flightHeightM) }}
                </div>
                <div class="col">
                    <span class="label">飞行速度</span>
                    {{ data.speed }}
                    {{ formatSpeed(data.flightSpeedMs) }}
                </div>
            </div>
        </div>
@@ -73,6 +73,47 @@
})
const emit = defineEmits(['signal', 'counter', 'favorite'])
const getStatusText = (item) => {
    const status = item?.flightStatus
    if (status === 1 || status === '1') return '侦测中'
    if (status === 2 || status === '2') return '反制中'
    return status || '侦测中'
}
const getStatusType = (item) => {
    const statusText = getStatusText(item)
    const counterWay = item?.counterWay
    if (statusText.includes('反制') || counterWay === 2 || counterWay === '2' || counterWay === '诱导驱离') {
        return 'countering'
    }
    return 'detecting'
}
const getDataSource = (item) => {
    return item?.deviceName || item?.areaName || '-'
}
const formatFrequency = (value) => {
    if (value == null || value === '') return '-'
    return `${value}MHz`
}
const getAlarmTime = (value) => {
    if (!value) return '-'
    const parts = value.replace('T', ' ').split(' ')
    return parts[1] || value
}
const formatHeight = (value) => {
    if (value == null || value === '') return '-'
    return `${value}m`
}
const formatSpeed = (value) => {
    if (value == null || value === '') return '-'
    return `${value}m/s`
}
</script>
<style scoped lang="scss">