11 files modified
3 files added
| New file |
| | |
| | | /* |
| | | * @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 |
| | | }) |
| | | } |
| | |
| | | |
| | | |
| | | ## 信号干扰,诱导驱离 |
| | | |
| | | |
| | | **接口地址**:`/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| |
| | | |  alarmRecordId|告警记录ID(ja_fw_drone_alarm_record.id)||false|integer(int64)|| |
| | | |  areaCode|区域编码||false|string|| |
| | | |  counterEffect|反制效果(success/fail)||false|string|| |
| | | |  coverRadiusM|覆盖范围(米)||false|integer(int32)|| |
| | | |  deployLatitude|部署纬度(冗余)||false|number(double)|| |
| | | |  deployLongitude|部署经度(冗余)||false|number(double)|| |
| | | |  deviceCode|反制设备编码(快照)||false|string|| |
| | | |  deviceId|反制设备ID(ja_fw_device.id)||false|integer(int64)|| |
| | | |  deviceModel|反制设备型号(快照)||false|string|| |
| | | |  deviceName|反制设备名称(快照)||false|string|| |
| | | |  deviceType|反制设备类型(快照)||false|string|| |
| | | |  droneDeviceCode|无人机设备编码||false|string|| |
| | | |  droneName|无人机名称||false|string|| |
| | | |  droneType|无人机类型||false|string|| |
| | | |  findTime|发现时间||false|string(date-time)|| |
| | | |  id|主键id||false|integer(int64)|| |
| | | |  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 |
| | | } |
| | | ``` |
| | |
| | | </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, |
| | | } |
| | | ]) |
| | | |
| | |
| | | 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> |
| New file |
| | |
| | | <!-- |
| | | * @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> |
| | |
| | | * 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) |
| | |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | </style> |
| | | |
| | | </style> |
| | |
| | | <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"> |
| | | <!-- 关键字 --> |
| | |
| | | |
| | | <!-- 列表 --> |
| | | <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({ |
| | |
| | | }) |
| | | |
| | | 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> |
| | | |
| | |
| | | flex: 1; |
| | | } |
| | | } |
| | | </style> |
| | | |
| | | </style> |
| | |
| | | <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> |
| | | |
| | |
| | | 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: { |
| | |
| | | { |
| | | 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 |
| | | } |
| | | ]) |
| | |
| | | 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> |
| | |
| | | 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; |
| | |
| | | * 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> |
| | |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | </style> |
| | | |
| | | </style> |
| | |
| | | </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> |
| | | |
| | |
| | | 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: { |
| | |
| | | } |
| | | ]) |
| | | |
| | | 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 |
| | |
| | | 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> |
| | |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | |
| | | .right-container { |
| | | transition: transform 0.3s ease-in-out; |
| | | position: relative; |
| | |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">厂商</span> |
| | | {{ data.vendor }} |
| | | {{ data.manufacturer }} |
| | | </div> |
| | | </div> |
| | | <div class="row"> |
| | |
| | | <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> |
| | |
| | | }) |
| | | |
| | | 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"> |
| | |
| | | <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> |
| | |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">电量</span> |
| | | {{ data.battery }}% |
| | | {{ data.batteryPct }}% |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | </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> |
| | |
| | | }) |
| | | |
| | | 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"> |
| | |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | }) |
| | | |
| | | 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"> |