吉安感知网项目-前端
shuishen
2026-01-08 8e504b07e0070e50e2e04c46d783ca2df2794657
feat:数据驾驶舱页面搭建
7 files modified
11 files added
1617 ■■■■■ changed files
applications/drone-command/env/.env.development 12 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/assets/images/dataCockpit/fzsb.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/dataCockpit/title-bg.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/dataCockpit/zcsb.png patch | view | raw | blame | history
applications/drone-command/src/styles/element-ui.scss 292 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/CenterContainer.vue 12 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/EquipmentWarning.vue 61 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/HistoryWarning.vue 136 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/LeftContainer.vue 93 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/MapContainer.vue 2 ●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/RealWarning.vue 76 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/RightContainer.vue 202 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/templateComponents/EquipmentTemplate.vue 171 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/templateComponents/HistoryTemplate.vue 110 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/templateComponents/RealEquipmentTemplate.vue 199 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/templateComponents/RealTemplate.vue 204 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/templateComponents/TitleTemplate.vue 43 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/index.vue 4 ●●● patch | view | raw | blame | history
applications/drone-command/env/.env.development
@@ -1,3 +1,13 @@
###
 # @Author       : yuan
 # @Date         : 2026-01-07 14:58:30
 # @LastEditors  : yuan
 # @LastEditTime : 2026-01-08 15:14:52
 # @FilePath     : \applications\drone-command\env\.env.development
 # @Description  :
 # Copyright 2026 OBKoro1, All Rights Reserved.
 # 2026-01-07 14:58:30
###
NODE_ENV = 'development'
#开发环境配置
@@ -6,7 +16,7 @@
#开发环境代理地址(推荐本地新建文件 .env.development.local 来进行覆盖)
# VITE_APP_URL = https://wrj.shuixiongit.com/api
VITE_APP_URL= http://192.168.1.33:81
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/fzsb.png
applications/drone-command/src/assets/images/dataCockpit/title-bg.png
applications/drone-command/src/assets/images/dataCockpit/zcsb.png
applications/drone-command/src/styles/element-ui.scss
@@ -114,6 +114,7 @@
    .el-drawer__header {
      margin-bottom: 0;
    }
    .el-drawer__body {
      display: flex;
      flex-direction: column;
@@ -121,12 +122,16 @@
    }
  
  }
  .ztzf-el-card__body {
  
     .el-card__body {
      .avue-form{
      padding: 0;}}
            padding: 0;
  }
    }
}
  .ztzf-public-general-avue-crud {
    .el-table {
      tr {
@@ -149,6 +154,7 @@
      }
    }
  }
  .ztzf-layer-input {
      .el-input__wrapper {
          padding: 0 8px !important;
@@ -172,15 +178,18 @@
          }
      }
  }
  .ztzf-button {
    background-color: transparent !important;
    color: #fff !important;
  }
  .ztzf-el-tree {
    background-color: transparent !important;
    --el-tree-node-hover-bg-color:transparent !important;
    color: #fff !important;
  }
  .ztzf-layer-search {
    color: #fff !important;
    background-color: transparent !important;
@@ -216,6 +225,7 @@
      }
    }
  }
  .ztzf-select-popper.el-select__popper {
      border:#2D2D2D !important;
  
@@ -265,22 +275,27 @@
          border: 1px solid #fff !important;
      }
  }
  //时间日期选择器
  .ztzf-layer-date-picker {
      box-shadow: none !important;
      background: transparent;
      border: 1px solid #fff;
      border-radius: 5px;
      .el-range-editor.is-disabled input {
          background-color: transparent !important;
      }
      .el-range-editor.is-disabled {
          background-color: transparent !important;
      }
      .el-input__wrapper {
          box-shadow: none !important;
          background-color: transparent !important;
      width: 300px;
          &.is-focus {
              box-shadow: none !important;
          }
@@ -305,14 +320,19 @@
      border: 1px solid !important;
      border-radius: 0px 0px 8px 8px;
      border-image: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(115, 192, 255, 1)) 1 1 !important;
      .el-picker-panel .el-time-panel{
          background: #012350 !important;
      }
      .el-time-spinner__item {
      color: #fff !important;}
        color: #fff !important;
    }
      .el-time-spinner__item:hover:not(.is-disabled):not(.is-active){
          background: transparent !important;
      }
      .el-picker-panel {
          .el-picker-panel__body-wrapper {
              .el-picker-panel__body {
@@ -552,6 +572,7 @@
          }
      }
  }
.ztzf-custom-qrcode-popover {
    //background: url("@/assets/images/home/eventOverviewDetail/qrCodeBg.png") no-repeat center center !important;
    //background-size: cover !important;
@@ -647,6 +668,7 @@
        background: #13c6ff !important;
    }
}
  //======以下针对表格功能 增删查改样式和弹窗=========
  .ztzf-dialog-mange {
    .el-dialog__body {
@@ -655,6 +677,7 @@
      overflow: scroll;
      border-top: 0.1rem solid #f0f0f0;
    }
    .el-dialog__footer {
      z-index: 2;
      display: block;
@@ -669,18 +692,24 @@
      text-align: right;
    }
  }
  .ztzf-search-mange {
    height: 40px;
    .search-contain {
      display: flex;justify-content: space-between;
        display: flex;
        justify-content: space-between;
    }
  }
  .ztzf-form-mange {
    .el-form-item {
      width: 300px;
      .el-form-item__content {
        width: 100%;
      }
      .el-form-item__label {
        width: 120px;
      }
@@ -690,9 +719,11 @@
  .ztzf-form-search {
    .el-form-item {
      width: 100%;
      .el-form-item__label {
        width: 80px;
      }
      .el-form-item__content {
        display: flex;
        flex: 1;
@@ -713,6 +744,7 @@
    border-left: 0.1rem solid #ebeef5;
    margin-left: 0;
    margin-right: 0;
    .el-col {
      padding-left: 0;
      padding-right: 0;
@@ -722,11 +754,13 @@
      border-right: 0.1rem solid #ebeef5;
      text-align: center;
      align-items: center;
      .el-input__wrapper {
        box-shadow: none !important;
        border-bottom: 0.1rem solid #ebeef5;
      }
    }
    .label {
      color: #909399;
      box-sizing: border-box;
@@ -734,3 +768,255 @@
    }
  }
  
.ztzf-data-cockpit-search-input {
    .el-input__wrapper {
        padding: 0 8px !important;
        background: #161B2C;
        box-shadow: none !important;
        border: none;
        .el-input__inner {
            color: #ffffff;
            &::placeholder {
                color: #86909C;
            }
        }
    }
    &.is-disabled {
        .el-input__wrapper {
            background-color: #161B2C;
        }
    }
}
.ztzf-data-cockpit-date-picker {
    box-shadow: none !important;
    background: #161B2C;
    border: none;
    border-radius: 5px;
    box-sizing: border-box !important;
    .el-input__wrapper {
        box-shadow: none !important;
        background-color: transparent !important;
        &.is-focus {
            box-shadow: none !important;
        }
    }
    .el-range-input {
        color: #fff;
        &::placeholder {
            color: #86909C;
        }
    }
    .el-range-separator {
        color: #ffffff;
    }
}
.ztzf-data-cockpit-date-picker-popper {
    background: #161B2C !important;
    border: 1px solid !important;
    border-radius: 0px 0px 8px 8px;
    border-image: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(115, 192, 255, 1)) 1 1 !important;
    .el-date-picker__prev-btn,
    .el-date-picker__next-btn {
        .el-icon {
            color: #fff !important;
        }
    }
    .el-date-picker__header-label {
        color: #fff !important;
    }
    .el-picker-panel {
        .el-picker-panel__body-wrapper {
            .el-picker-panel__body {
                background: #161B2C !important;
                color: #fff !important;
                .el-date-picker__time-header {
                    border: none;
                    .el-date-picker__editor-wrap {
                        .el-time-panel {
                            background: #161B2C !important;
                            border-radius: 0px 0px 8px 8px;
                            border: 1px solid !important;
                            border-image: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(115, 192, 255, 1)) 1 1 !important;
                            // 将 time-picker-popper 的样式移到这里
                            .el-time-panel__header {
                                background: #161B2C !important;
                                color: #ffffff !important;
                                border-bottom: 1px solid rgba(71, 157, 255, 0.3) !important;
                            }
                            .el-time-panel__content {
                                .el-time-spinner__item {
                                    color: #e6e6e6 !important;
                                    &:hover {
                                        background: rgba(71, 157, 255, 0.3) !important;
                                    }
                                    &.active {
                                        color: #479dff !important;
                                        font-weight: bold;
                                    }
                                }
                            }
                            .el-time-panel__footer {
                                background: #012350 !important;
                                border-top: 1px solid rgba(71, 157, 255, 0.3) !important;
                                .el-button {
                                    color: #fff !important;
                                    &:hover {
                                        color: #479dff !important;
                                    }
                                }
                                .el-time-panel__btn.cancel {
                                    color: #fff !important;
                                    &:hover {
                                        color: #fff !important;
                                    }
                                }
                            }
                        }
                    }
                }
                .el-picker-panel__content {
                    .el-date-range-picker__header {
                        .el-picker-panel__icon-btn {
                            color: #fff !important;
                            &:hover {
                                color: var(--el-color-primary) !important;
                            }
                            .el-icon {
                                &::before {
                                    color: inherit !important;
                                }
                            }
                        }
                        &>div {
                            span {
                                color: #fff !important;
                            }
                        }
                    }
                    .el-date-table {
                        th {
                            color: #fff !important;
                        }
                        td.disabled div {
                            background-color: #00193b !important;
                            color: #505050 !important;
                            cursor: not-allowed;
                        }
                        // td:hover,
                        // td.current:not(.disabled) {
                        //     background: rgba(60, 121, 202) !important;
                        //     color: white !important;
                        // }
                        td.start-date span,
                        td.start-date span,
                        td.end-date span {
                            background-color: #012350 !important;
                        }
                        td.in-range div,
                        td.in-range div:hover,
                        &.is-week-mode .el-date-table__row.current div,
                        &.is-week-mode .el-date-table__row:hover div {
                            background-color: rgba(60, 121, 202) !important;
                        }
                    }
                }
            }
        }
    }
    .el-popper__arrow {
        &::before {
            background: #161B2C !important;
            border: 1px solid #161B2C !important;
            box-sizing: border-box;
        }
    }
    .el-picker-panel__sidebar {
        background: #012350;
        color: #fff;
        .el-picker-panel__shortcut {
            color: #ffffff;
        }
    }
    .el-picker-panel__body {
        .el-input.is-disabled .el-input__wrapper {
            background: #012350;
            box-shadow: 0 0 0 0.1rem#409eff;
            color: #fff;
            .el-input__inner {
                color: #fff !important;
            }
        }
        .el-button.is-disabled,
        .el-button.is-disabled:hover {
            background: #012350;
        }
        .el-input__wrapper {
            background: #012350;
            box-shadow: 0 0 0 0.1rem#409eff;
            color: #fff;
            .el-input__inner {
                color: #fff !important;
            }
        }
    }
    .el-picker-panel__footer {
        background: #012350;
        color: #fff;
        border-top: none;
        .el-button--small {
            box-shadow: 0 0 0 0.1rem#409eff;
            background-color: #012350;
            color: #fff;
            border: none;
            &:hover {
                background-color: #012350;
                color: #fff;
                border-color: #409eff;
            }
        }
    }
}
applications/drone-command/src/views/dataCockpit/components/CenterContainer.vue
@@ -1,4 +1,4 @@
<template>
<template>
    <div class="center-container">
        <div class="left-box">
            <div class="logo">
@@ -6,7 +6,7 @@
            </div>
            <div class="list">
                <div class="item" v-for="item, ind in warningList" :key="ind">
                <div class="item" v-for="(item, ind) in warningList" :key="ind">
                    <div class="val" @click="warningTypeClick(item.type)">
                        {{ item.value }}
                    </div>
@@ -24,7 +24,7 @@
            </div>
            <div class="list">
                <div class="item" v-for="item, ind in equipmentList" :key="ind">
                <div class="item" v-for="(item, ind) in equipmentList" :key="ind">
                    <div class="val" :class="{highlight: item.isHighlight}" @click="equipmentClick(item.isHighlight)">
                        {{ item.value }}
                    </div>
@@ -39,9 +39,9 @@
</template>
<script setup>
import { ref } from 'vue'
import warningLogo from '@/assets/images/dataCockpit/warning.png'
import equipmentLogo from '@/assets/images/dataCockpit/equipment.png'
let warningList = ref([
    {
@@ -82,12 +82,12 @@
])
const warningTypeClick = (type) => {
    console.log('点击了告警类型:', type);
    console.log('点击了告警类型:', type)
}
const equipmentClick = (isHighlight) => {
    if (isHighlight) {
        console.log('点击了设备统计');
        console.log('点击了设备统计')
    }
}
</script>
applications/drone-command/src/views/dataCockpit/components/EquipmentWarning.vue
New file
@@ -0,0 +1,61 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-08 09:29:07
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-08 14:49:25
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\EquipmentWarning.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 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>
</template>
<script setup>
import EquipmentTemplate from './templateComponents/EquipmentTemplate.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 onCardClick = (item) => {
    console.log('点击设备卡片:', item.id)
}
</script>
<style lang="scss" scoped>
.equipment-warning {
    width: 100%;
    height: 100%;
}
</style>
applications/drone-command/src/views/dataCockpit/components/HistoryWarning.vue
New file
@@ -0,0 +1,136 @@
<template>
    <div class="history-warning">
        <!-- 搜索区(设计稿红框) -->
        <div class="search-box">
            <!-- 关键字 -->
            <el-input v-model="query.keyword" class="ztzf-data-cockpit-search-input" placeholder="请输入数据名称" clearable
                @keyup.enter="onSearch">
            </el-input>
            <!-- 日期范围 -->
            <el-date-picker v-model="query.dateRange"
                  class="ztzf-data-cockpit-date-picker"
                 popper-class="ztzf-data-cockpit-date-picker-popper"
                type="daterange"
                range-separator="~"
                start-placeholder="创建开始日期" end-placeholder="结束日期" format="YYYY年MM月DD日" value-format="YYYY-MM-DD"
                :clearable="true" @change="onSearch">
            </el-date-picker>
        </div>
        <!-- 列表 -->
        <div class="list-content">
            <HistoryTemplate 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 HistoryTemplate from './templateComponents/HistoryTemplate.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: '诱导驱离'
    }
])
// 查询条件:keyword + dateRange(YYYY-MM-DD数组)
const query = ref({
    keyword: '',
    dateRange: [] // [start, end]
})
const onSearch = () => {
    // 目前是本地过滤:computed 会自动更新
    // 后续如果你要走接口,这里换成 fetchList(query.value) 即可
}
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 filteredList = computed(() => {
    const kw = (query.value.keyword || '').trim().toLowerCase()
    const [start, end] = query.value.dateRange || []
    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)
    })
})
</script>
<style scoped lang="scss">
.history-warning {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    .search-box {
        display: flex;
        flex-direction: column;
        gap: 10px;
        ::v-deep(.ztzf-data-cockpit-date-picker) {
            width: 100% !important;
        }
    }
    .list-content {
        height: 0;
        flex: 1;
    }
}
</style>
applications/drone-command/src/views/dataCockpit/components/LeftContainer.vue
@@ -1,15 +1,108 @@
<template>
    <div class="left-container">
        <div class="wrapper">
            <TitleTemplate>异常情况告警</TitleTemplate>
            
            <div class="category-container">
                <div class="category-item" v-for="(item, ind) in categoryList" :key="ind" @click="activeId = item.id"
                    :class="{ active: activeId === item.id }">
                    {{ item.name }}
                </div>
            </div>
            <div class="content">
                <component :is="currentComponent" />
            </div>
        </div>
    </div>
</template>
<script setup>
import TitleTemplate from './templateComponents/TitleTemplate.vue'
import RealWarning from './RealWarning.vue'
import EquipmentWarning from './EquipmentWarning.vue'
import HistoryWarning from './HistoryWarning.vue'
const activeId = ref(1)
const categoryList = ref([
    {
        id: 1,
        name: '实时告警',
        component: RealWarning
    },
    {
        id: 2,
        name: '设备告警',
        component: EquipmentWarning
    },
    {
        id: 3,
        name: '历史告警',
        component: HistoryWarning
    }
])
const currentComponent = computed(() => {
    return categoryList.value.find(i => i.id === activeId.value)?.component
})
</script>
<style lang="scss" scoped>
.category-container {
    display: flex;
    justify-content: space-between;
    .category-item {
        position: relative;
        width: 0;
        flex: 1;
        margin-left: 26px;
        height: 68px;
        line-height: 68px;
        font-family: Source Code Pro, Source Code Pro;
        font-weight: 400;
        font-size: 14px;
        color: #86909C;
        text-align: center;
        font-style: normal;
        text-transform: none;
        cursor: pointer;
        &:first-child {
            margin-left: 0;
        }
        &.active {
            font-weight: normal;
            color: #FFFFFF;
        }
        &::after {
            content: '';
            position: absolute;
            left: 0;
            bottom: 0;
            width: 100%;
            height: 1px;
            background: #333355;
        }
        &.active::after {
            height: 3px;
            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;
        }
    }
}
.content {
    height: 0;
    flex: 1;
    margin-top: 20px;
    overflow-x: hidden;
    overflow-y: auto;
}
</style>
applications/drone-command/src/views/dataCockpit/components/MapContainer.vue
@@ -22,7 +22,7 @@
onBeforeUnmount(() => {
    if (viewInstance) {
        viewInstance.destroy()
        viewInstance?.destroy()
        viewInstance = null
    }
applications/drone-command/src/views/dataCockpit/components/RealWarning.vue
New file
@@ -0,0 +1,76 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-08 09:29:07
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-08 14:49:09
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\RealWarning.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 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>
</template>
<script setup>
import RealTemplate from './templateComponents/RealTemplate.vue'
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
    }
])
</script>
<style lang="scss" scoped>
.real-warning {
    width: 100%;
    height: 100%;
}
</style>
applications/drone-command/src/views/dataCockpit/components/RightContainer.vue
@@ -1,15 +1,217 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-07 15:17:54
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-08 15:58:15
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\RightContainer.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-07 15:17:54
-->
<template>
    <div class="right-container">
        <div class="wrapper">
            <TitleTemplate>侦测反制设备</TitleTemplate>
            
            <div class="category-container">
                <div class="category-item" v-for="(item, ind) in categoryList" :key="ind">
                    <div class="category-item-logo">
                        <img :src="item.logo" alt="">
                    </div>
                    <div class="category-item-content">
                        <span class="name">{{ item.name }}</span>
                        <span class="val">{{ item.val }}</span>
                    </div>
                </div>
            </div>
            <div class="content">
                <RealEquipmentTemplate v-for="item in counterDeviceStatusList" :key="item.id" :data="item"
                    @history="onHistory" @click="onCardClick" />
            </div>
        </div>
    </div>
</template>
<script setup>
import TitleTemplate from './templateComponents/TitleTemplate.vue'
import RealEquipmentTemplate from './templateComponents/RealEquipmentTemplate.vue'
import zcsbLogo from '@/assets/images/dataCockpit/zcsb.png'
import fzsbLogo from '@/assets/images/dataCockpit/fzsb.png'
const categoryList = ref([
    {
        id: 1,
        name: '侦测设备',
        val: 169,
        logo: zcsbLogo
    },
    {
        id: 2,
        name: '反制设备',
        val: 169,
        logo: fzsbLogo
    }
])
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 onHistory = (item) => {
    console.log('历史数据:', item.id)
}
const onCardClick = (item) => {
    console.log('点击卡片:', item.id)
}
</script>
<style lang="scss" scoped>
.category-container {
    margin-top: 20px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 28px;
    box-sizing: border-box;
    .category-item {
        position: relative;
        width: 0;
        flex: 1;
        display: flex;
        height: 64px;
        &-logo {
            display: flex;
            align-items: center;
            img {
                width: 40px;
                height: 40px;
            }
        }
        &-content {
            width: 0;
            flex: 1;
            display: flex;
            flex-direction: column;
            border-bottom: 1px solid #6C6C89;
            span {
                margin-left: 8px;
            }
            .name {
                font-family: Open Sans, Open Sans;
                font-weight: 400;
                font-size: 16px;
                color: #C3C3DD;
                line-height: 20px;
                text-align: left;
                font-style: normal;
                text-transform: none;
            }
            .val {
                margin-top: 10px;
                font-family: DIN, DIN;
                font-weight: bold;
                font-size: 22px;
                color: #FFFFFF;
                line-height: 20px;
                text-align: left;
                font-style: normal;
                text-transform: none;
            }
        }
    }
}
.content {
    height: 0;
    flex: 1;
    margin-top: 20px;
    overflow-x: hidden;
    overflow-y: auto;
}
.right-container {
    transition: transform 0.3s ease-in-out;
    /* position: relative; Removed to avoid conflict with parent absolute positioning */
    &.collapsed {
        transform: translateX(100%);
    }
}
.collapse-btn {
    position: absolute;
    top: 50%;
    left: -24px;
    width: 24px;
    height: 60px;
    background: rgba(17, 23, 34, 0.8);
    border: 1px solid #333355;
    border-right: none;
    border-radius: 4px 0 0 4px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    transform: translateY(-50%);
    z-index: 10;
    backdrop-filter: blur(5px);
    .arrow {
        border: solid #fff;
        border-width: 0 2px 2px 0;
        display: inline-block;
        padding: 3px;
        transition: transform 0.3s;
        &.left {
            transform: rotate(135deg);
        }
        &.right {
            transform: rotate(-45deg);
        }
    }
    &:hover {
        background: rgba(40, 79, 227, 0.6);
    }
}
</style>
applications/drone-command/src/views/dataCockpit/components/templateComponents/EquipmentTemplate.vue
New file
@@ -0,0 +1,171 @@
<template>
    <div class="device-card" @click="emit('click', data)">
        <div class="header">
            <div class="title">
                <span class="name">{{ data.name }}</span>
                <span class="status">
                    <span :class="data.statusType">
                    </span>
                    <span>
                        {{ data.status }}
                    </span>
                </span>
            </div>
        </div>
        <div class="content">
            <div class="row">
                <div class="col">
                    <span class="label">类型</span>
                    {{ data.type }}
                </div>
                <div class="col">
                    <span class="label">厂商</span>
                    {{ data.vendor }}
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">方位角</span>
                    {{ data.azimuth }}°
                </div>
                <div class="col">
                    <span class="label">俯仰角</span>
                    {{ data.pitch }}°
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">有效范围</span>
                    {{ data.range }}
                </div>
                <div class="col">
                    <span class="label">设备型号</span>
                    {{ data.model }}
                </div>
            </div>
            <div>
                <span class="label">部署位置</span>
                <span class="value">{{ data.longitude }},{{ data.latitude }}</span>
            </div>
        </div>
    </div>
</template>
<script setup>
const props = defineProps({
    data: {
        type: Object,
        required: true,
        default: () => ({})
    }
})
const emit = defineEmits(['click'])
</script>
<style scoped lang="scss">
.device-card {
    margin-top: 10px;
    padding: 10px;
    color: #fff;
    background: #191933;
    border-radius: 6px 6px 6px 6px;
    &:first-child {
        margin-top: 0;
    }
    .header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        .title {
            display: flex;
            align-items: center;
            gap: 6px;
            .name {
                font-family: Source Han Sans CN, Source Han Sans CN;
                font-weight: bold;
                font-size: 14px;
                color: #FFFFFF;
                text-align: left;
                font-style: normal;
                text-transform: none;
            }
            .status {
                padding: 0 10px;
                display: flex;
                align-items: center;
                justify-content: space-between;
                height: 21px;
                background: #303041;
                border-radius: 10px;
                gap: 6px;
                span {
                    font-family: Source Han Sans CN, Source Han Sans CN;
                    font-weight: 400;
                    font-size: 10px;
                    color: #FFFFFF;
                    text-align: center;
                    font-style: normal;
                    text-transform: none;
                }
                span.offline {
                    width: 4px;
                    height: 4px;
                    background: #939393;
                    border-radius: 50%;
                }
                span.fault {
                    width: 4px;
                    height: 4px;
                    background: #FF4444;
                    border-radius: 50%;
                }
            }
        }
    }
    .content {
        font-family: Source Han Sans CN, Source Han Sans CN;
        font-weight: 400;
        font-size: 12px;
        color: #9E9EBA;
        text-align: left;
        font-style: normal;
        text-transform: none;
        &>div {
            margin-top: 10px;
            line-height: 22px;
            span.label {
                margin-right: 10px;
                color: #D4D5D7;
            }
        }
        .row {
            display: flex;
            &>div {
                width: 0;
                flex: 1;
            }
        }
    }
}
</style>
applications/drone-command/src/views/dataCockpit/components/templateComponents/HistoryTemplate.vue
New file
@@ -0,0 +1,110 @@
<template>
    <div class="drone-info-card" @click="emit('click', data)">
        <div class="title">
            <span class="name">
                {{ data.name }}
            </span>
        </div>
        <div class="rows">
            <div class="row">
                <span class="label">序列号</span>
                <span class="value">{{ data.serialNumber }}</span>
            </div>
            <div class="row">
                <span class="label">数据源</span>
                <span class="value">{{ data.dataSource }}</span>
            </div>
            <div class="row">
                <span class="label">信号频段</span>
                <span class="value">{{ data.frequency }}</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>
            </div>
            <div class="row">
                <span class="label">停留时间</span>
                <span class="value">{{ data.stayTime }}</span>
            </div>
            <div class="row">
                <span class="label">反制方式</span>
                <span class="value">{{ data.counterMethod }}</span>
            </div>
        </div>
    </div>
</template>
<script setup>
defineProps({
    data: {
        type: Object,
        required: true,
        default: () => ({})
    }
})
const emit = defineEmits(['click'])
</script>
<style scoped lang="scss">
.drone-info-card {
    margin-top: 10px;
    padding: 10px;
    color: #fff;
    background: #191933;
    border-radius: 6px 6px 6px 6px;
    &:hover {
        filter: brightness(1.05);
    }
    .title {
        display: flex;
        align-items: center;
        gap: 6px;
        .name {
            font-family: Source Han Sans CN, Source Han Sans CN;
            font-weight: bold;
            font-size: 14px;
            color: #FFFFFF;
            text-align: left;
            font-style: normal;
            text-transform: none;
        }
    }
    .rows {
        margin-top: 10px;
        display: flex;
        flex-direction: column;
        gap: 10px;
        font-family: Source Han Sans CN, Source Han Sans CN;
        font-weight: 400;
        font-size: 12px;
        color: #9E9EBA;
        text-align: left;
        font-style: normal;
        text-transform: none;
        .row {
            line-height: 22px;
            .label {
                margin-right: 10px;
                color: #D4D5D7;
            }
        }
    }
}
</style>
applications/drone-command/src/views/dataCockpit/components/templateComponents/RealEquipmentTemplate.vue
New file
@@ -0,0 +1,199 @@
<script setup>
defineProps({
    data: {
        type: Object,
        required: true,
        default: () => ({})
    }
})
const emit = defineEmits(['history', 'click'])
</script>
<template>
    <div class="counter-card" @click="emit('click', data)">
        <!-- 头部 -->
        <div class="header">
            <div class="title">
                <span class="name">{{ data.name }}</span>
                <span class="status">
                    <span :class="data.actionType">
                    </span>
                    <span>
                        {{ data.actionText }}
                    </span>
                </span>
            </div>
        </div>
        <!-- 字段区:两列 -->
        <div class="content">
            <div class="row">
                <div class="col">
                    <span class="label">类型</span>
                    {{ data.type }}
                </div>
                <div class="col">
                    <span class="label">电量</span>
                    {{ data.battery }}%
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">方位角</span>
                    {{ data.azimuth }}°
                </div>
                <div class="col">
                    <span class="label">俯仰角</span>
                    {{ data.pitch }}°
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">状态</span>
                    {{ data.statusText }}
                </div>
                <div class="col">
                    <span class="label">厂商</span>
                    {{ data.vendor }}
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">侦测目标</span>
                    {{ data.targetCount }}台
                </div>
                <div class="col">
                    <span class="label">有效范围</span>
                    {{ data.range }}KM
                </div>
            </div>
        </div>
        <!-- 底部按钮 -->
        <div class="footer">
            <el-button color="#284FE3" type="primary" @click.stop="emit('history', data)">历史数据</el-button>
        </div>
    </div>
</template>
<style scoped lang="scss">
.counter-card {
    margin-top: 10px;
    padding: 10px;
    color: #fff;
    background: #191933;
    border-radius: 6px 6px 6px 6px;
    &:first-child {
        margin-top: 0;
    }
    .header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        .title {
            display: flex;
            align-items: center;
            gap: 6px;
            .name {
                font-family: Source Han Sans CN, Source Han Sans CN;
                font-weight: bold;
                font-size: 14px;
                color: #FFFFFF;
                text-align: left;
                font-style: normal;
                text-transform: none;
            }
            .status {
                padding: 0 10px;
                display: flex;
                align-items: center;
                justify-content: space-between;
                height: 21px;
                background: #303041;
                border-radius: 10px;
                gap: 6px;
                span {
                    font-family: Source Han Sans CN, Source Han Sans CN;
                    font-weight: 400;
                    font-size: 10px;
                    color: #FFFFFF;
                    text-align: center;
                    font-style: normal;
                    text-transform: none;
                }
                span.detecting {
                    width: 4px;
                    height: 4px;
                    background: #18FF4A;
                    border-radius: 50%;
                }
                span.jamming {
                    width: 4px;
                    height: 4px;
                    background: #FF4444;
                    border-radius: 50%;
                }
            }
        }
    }
    .content {
        font-family: Source Han Sans CN, Source Han Sans CN;
        font-weight: 400;
        font-size: 12px;
        color: #9E9EBA;
        text-align: left;
        font-style: normal;
        text-transform: none;
        &>div {
            margin-top: 10px;
            line-height: 22px;
            span.label {
                margin-right: 10px;
                color: #D4D5D7;
            }
        }
        .row {
            display: flex;
            &>div {
                width: 0;
                flex: 1;
            }
        }
    }
    .footer {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 10px;
        margin-top: 10px;
        ::v-deep(.el-button) {
            width: 96px;
            height: 36px;
        }
    }
}
</style>
applications/drone-command/src/views/dataCockpit/components/templateComponents/RealTemplate.vue
New file
@@ -0,0 +1,204 @@
<template>
    <div class="drone-card">
        <div class="header">
            <div class="title">
                <span class="name">{{ data.name }}</span>
                <span class="status">
                    <span :class="data.statusType">
                    </span>
                    <span>
                        {{ data.status }}
                    </span>
                </span>
            </div>
            <div class="actions">
                <span class="star" @click="emit('favorite')">
                    {{ data.isFavorite ? '★' : '☆' }}
                </span>
            </div>
        </div>
        <div class="content">
            <div>
                <span class="label">序列号</span>
                {{ data.serialNumber }}
            </div>
            <div>
                <span class="label">数据源</span>
                {{ data.dataSource }}
            </div>
            <div>
                <span class="label">经纬度</span>
                {{ data.longitude }}, {{ data.latitude }}
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">信号频段</span>
                    {{ data.frequency }}
                </div>
                <div class="col">
                    <span class="label">发现时间</span>
                    {{ data.discoverTime }}
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">飞行高度</span>
                    {{ data.altitude }}
                </div>
                <div class="col">
                    <span class="label">飞行速度</span>
                    {{ data.speed }}
                </div>
            </div>
        </div>
        <div class="footer">
            <el-button class="general" color="#2B2B4C" @click="emit('signal')">信号干扰</el-button>
            <el-button color="#284FE3" type="primary" @click="emit('counter')">诱导驱离</el-button>
        </div>
    </div>
</template>
<script setup>
const props = defineProps({
    data: {
        type: Object,
        required: true,
        default: () => ({})
    }
})
const emit = defineEmits(['signal', 'counter', 'favorite'])
</script>
<style scoped lang="scss">
.drone-card {
    margin-top: 10px;
    padding: 10px;
    color: #fff;
    background: #191933;
    border-radius: 6px 6px 6px 6px;
    &:hover {
        background: #29294D;
        ;
    }
    &:first-child {
        margin-top: 0;
    }
    .header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        .title {
            display: flex;
            align-items: center;
            gap: 6px;
            .name {
                font-family: Source Han Sans CN, Source Han Sans CN;
                font-weight: bold;
                font-size: 14px;
                color: #FFFFFF;
                text-align: left;
                font-style: normal;
                text-transform: none;
            }
            .status {
                padding: 0 10px;
                display: flex;
                align-items: center;
                justify-content: space-between;
                height: 21px;
                background: #303041;
                border-radius: 10px;
                gap: 6px;
                span {
                    font-family: Source Han Sans CN, Source Han Sans CN;
                    font-weight: 400;
                    font-size: 10px;
                    color: #FFFFFF;
                    text-align: center;
                    font-style: normal;
                    text-transform: none;
                }
                span.detecting {
                    width: 4px;
                    height: 4px;
                    background: #18FF4A;
                    border-radius: 50%;
                }
                span.countering {
                    width: 4px;
                    height: 4px;
                    background: #FF4444;
                    border-radius: 50%;
                }
            }
        }
        .actions {
            display: flex;
            gap: 8px;
            font-size: 16px;
            cursor: pointer;
        }
    }
    .content {
        font-family: Source Han Sans CN, Source Han Sans CN;
        font-weight: 400;
        font-size: 12px;
        color: #9E9EBA;
        text-align: left;
        font-style: normal;
        text-transform: none;
        &>div {
            margin-top: 10px;
            line-height: 22px;
            span.label {
                margin-right: 10px;
                color: #D4D5D7;
            }
        }
        .row {
            display: flex;
            justify-content: space-between;
        }
    }
    .footer {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 10px;
        margin-top: 10px;
        ::v-deep(.el-button) {
            width: 96px;
            height: 36px;
            &.general.el-button {
                border-radius: 4px 4px 4px 4px;
                border: 1px solid rgba(255, 255, 255, 0.5);
            }
        }
    }
}
</style>
applications/drone-command/src/views/dataCockpit/components/templateComponents/TitleTemplate.vue
New file
@@ -0,0 +1,43 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-08 09:32:52
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-08 09:43:00
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\templateComponents\TitleTemplate.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-08 09:32:52
-->
<template>
    <div class="title-box">
        <div class="title">
            <slot></slot>
        </div>
    </div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.title-box {
    padding-left: 26px;
    background: url('@/assets/images/dataCockpit/title-bg.png') center / 100% 100% no-repeat;
    height: 34px;
    .title {
        height: 25px;
        line-height: 25px;
        font-family: PangMen;
        font-weight: 400;
        font-size: 18px;
        color: #FFFFFF;
        letter-spacing: 1px;
        text-align: left;
        font-style: normal;
        text-transform: none;
    }
}
</style>
applications/drone-command/src/views/dataCockpit/index.vue
@@ -2,7 +2,7 @@
 * @Author       : yuan
 * @Date         : 2026-01-06 16:35:50
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-07 17:33:02
 * @LastEditTime : 2026-01-08 09:45:25
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\index.vue
 * @Description  : 
 * Copyright 2026 OBKoro1, All Rights Reserved. 
@@ -41,6 +41,8 @@
    backdrop-filter: blur(5px);
    ::v-deep(.wrapper) {
      display: flex;
      flex-direction: column;
      position: absolute;
      top: 17px;
      bottom: 20px;