吉安感知网项目-前端
shuishen
2026-02-09 23545fcd763ae8bc87072df7001ccd2682b82cce
feat:侦测反制设备&场景部署调整
8 files modified
2 files added
1522 ■■■■■ changed files
applications/drone-command/src/api/dataCockpit/index.js 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/store/modules/user.js 376 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/api.txt 173 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/DetectionCountermeasure.vue 226 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/EquipmentWarning.vue 16 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/HistoryWarning.vue 45 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/RightContainer.vue 315 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/SceneDeployment.vue 324 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/templateComponents/RealEquipmentTemplate.vue 22 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/index.vue 16 ●●●● patch | view | raw | blame | history
applications/drone-command/src/api/dataCockpit/index.js
@@ -118,3 +118,12 @@
        params,
    })
}
// 场景管理统计
export const sceneManageStatisticsApi = (params) => {
    return request({
        url: '/drone-fw/cockpit/dataCockpit/sceneManageStatistics',
        method: 'get',
        params
    })
}
applications/drone-command/src/store/modules/user.js
@@ -4,10 +4,10 @@
  removeToken,
  removeRefreshToken,
  getRefreshToken,
} from '@/utils/auth';
import { setStore, getStore } from '@/utils/store';
import { validatenull } from '@/utils/validate';
import { deepClone } from '@/utils/util';
} from '@/utils/auth'
import { setStore, getStore } from '@/utils/store'
import { validatenull } from '@/utils/validate'
import { deepClone } from '@/utils/util'
import defaultAva from '@/assets/images/defaultava.png'
import {
  loginByUsername,
@@ -20,12 +20,12 @@
  getButtons,
  registerUser,
  getParentDeptInfo
} from '@/api/user';
import { getRoutes, getTopMenu } from '@/api/system/menu';
import { formatPath } from '@/router/avue-router';
import { ElMessage } from 'element-plus';
import { encrypt } from '@/utils/sm2';
import md5 from 'js-md5';
} from '@/api/user'
import { getRoutes, getTopMenu } from '@/api/system/menu'
import { formatPath } from '@/router/avue-router'
import { ElMessage } from 'element-plus'
import { encrypt } from '@/utils/sm2'
import md5 from 'js-md5'
const user = {
@@ -44,7 +44,7 @@
  },
  actions: {
    //根据用户名登录
    LoginByUsername({ commit }, userInfo = {}) {
    LoginByUsername ({ commit }, userInfo = {}) {
      return new Promise((resolve, reject) => {
        loginByUsername(
          userInfo.tenantId,
@@ -57,82 +57,82 @@
          userInfo.code
        )
          .then(res => {
            const data = res.data;
            const data = res.data
            if (data.error_description) {
              ElMessage({
                message: data.error_description,
                type: 'error',
              });
              })
            } else {
              commit('SET_TOKEN', data.access_token);
              commit('SET_REFRESH_TOKEN', data.refresh_token);
              commit('SET_TENANT_ID', data.tenant_id);
              commit('SET_USER_INFO', data);
              commit('SET_PARENT_DEPT_INFO', data.dept_id);
              commit('DEL_ALL_TAG');
              commit('CLEAR_LOCK');
              commit('SET_TOKEN', data.access_token)
              commit('SET_REFRESH_TOKEN', data.refresh_token)
              commit('SET_TENANT_ID', data.tenant_id)
              commit('SET_USER_INFO', data)
              commit('SET_PARENT_DEPT_INFO', data.dept_id)
              commit('DEL_ALL_TAG')
              commit('CLEAR_LOCK')
            }
            resolve();
            resolve()
          })
          .catch(err => {
            reject(err);
          });
      });
            reject(err)
          })
      })
    },
    //根据第三方信息登录
    LoginBySocial({ commit }, userInfo) {
    LoginBySocial ({ commit }, userInfo) {
      return new Promise((resolve, reject) => {
        loginBySocial(userInfo.tenantId, userInfo.source, userInfo.code, userInfo.state)
          .then(res => {
            const data = res.data;
            const data = res.data
            if (data.error_description) {
              ElMessage({
                message: data.error_description,
                type: 'error',
              });
              })
            } else {
              commit('SET_TOKEN', data.access_token);
              commit('SET_REFRESH_TOKEN', data.refresh_token);
              commit('SET_USER_INFO', data);
              commit('SET_TENANT_ID', data.tenant_id);
              commit('DEL_ALL_TAG');
              commit('CLEAR_LOCK');
              commit('SET_TOKEN', data.access_token)
              commit('SET_REFRESH_TOKEN', data.refresh_token)
              commit('SET_USER_INFO', data)
              commit('SET_TENANT_ID', data.tenant_id)
              commit('DEL_ALL_TAG')
              commit('CLEAR_LOCK')
            }
            resolve();
            resolve()
          })
          .catch(err => {
            reject(err);
          });
      });
            reject(err)
          })
      })
    },
    //根据单点信息登录
    LoginBySso({ commit }, userInfo) {
    LoginBySso ({ commit }, userInfo) {
      return new Promise((resolve, reject) => {
        loginBySso(userInfo.state, userInfo.code)
          .then(res => {
            const data = res.data;
            const data = res.data
            if (data.error_description) {
              ElMessage({
                message: data.error_description,
                type: 'error',
              });
              })
            } else {
              commit('SET_TOKEN', data.access_token);
              commit('SET_REFRESH_TOKEN', data.refresh_token);
              commit('SET_USER_INFO', data);
              commit('SET_TENANT_ID', data.tenant_id);
              commit('DEL_ALL_TAG');
              commit('CLEAR_LOCK');
              commit('SET_TOKEN', data.access_token)
              commit('SET_REFRESH_TOKEN', data.refresh_token)
              commit('SET_USER_INFO', data)
              commit('SET_TENANT_ID', data.tenant_id)
              commit('DEL_ALL_TAG')
              commit('CLEAR_LOCK')
            }
            resolve();
            resolve()
          })
          .catch(err => {
            reject(err);
          });
      });
            reject(err)
          })
      })
    },
    //根据手机信息登录
    LoginByPhone({ commit }, userInfo) {
    LoginByPhone ({ commit }, userInfo) {
      return new Promise((resolve, reject) => {
        loginByPhone(
          userInfo.tenantId,
@@ -141,29 +141,29 @@
          userInfo.codeValue
        )
          .then(res => {
            const data = res.data;
            const data = res.data
            if (data.error_description) {
              ElMessage({
                message: data.error_description,
                type: 'error',
              });
              })
            } else {
              commit('SET_TOKEN', data.access_token);
              commit('SET_REFRESH_TOKEN', data.refresh_token);
              commit('SET_USER_INFO', data);
              commit('SET_TENANT_ID', data.tenant_id);
              commit('DEL_ALL_TAG');
              commit('CLEAR_LOCK');
              commit('SET_TOKEN', data.access_token)
              commit('SET_REFRESH_TOKEN', data.refresh_token)
              commit('SET_USER_INFO', data)
              commit('SET_TENANT_ID', data.tenant_id)
              commit('DEL_ALL_TAG')
              commit('CLEAR_LOCK')
            }
            resolve();
            resolve()
          })
          .catch(err => {
            reject(err);
          });
      });
            reject(err)
          })
      })
    },
    //用户注册
    RegisterUser({ commit }, userInfo = {}) {
    RegisterUser ({ commit }, userInfo = {}) {
      return new Promise((resolve, reject) => {
        registerUser(
          userInfo.tenantId,
@@ -173,40 +173,40 @@
          userInfo.phone,
          userInfo.email
        ).then(res => {
          const data = res.data;
          const data = res.data
          if (data.error_description) {
            ElMessage({
              message: data.error_description,
              type: 'error',
            });
            reject(data.error_description);
            })
            reject(data.error_description)
          } else {
            commit('SET_TOKEN', data.access_token);
            commit('SET_REFRESH_TOKEN', data.refresh_token);
            commit('SET_USER_INFO', data);
            commit('SET_TENANT_ID', data.tenant_id);
            commit('DEL_ALL_TAG');
            commit('CLEAR_LOCK');
            commit('SET_TOKEN', data.access_token)
            commit('SET_REFRESH_TOKEN', data.refresh_token)
            commit('SET_USER_INFO', data)
            commit('SET_TENANT_ID', data.tenant_id)
            commit('DEL_ALL_TAG')
            commit('CLEAR_LOCK')
          }
          resolve();
        });
      });
          resolve()
        })
      })
    },
    GetUserInfo({ commit }) {
    GetUserInfo ({ commit }) {
      return new Promise((resolve, reject) => {
        getUserInfo()
          .then(res => {
            const data = res.data.data;
            commit('SET_ROLES', data.roles);
            resolve(data);
            const data = res.data.data
            commit('SET_ROLES', data.roles)
            resolve(data)
          })
          .catch(err => {
            reject(err);
          });
      });
            reject(err)
          })
      })
    },
    //刷新token
    RefreshToken({ state, commit }, userInfo) {
    RefreshToken ({ state, commit }, userInfo) {
      return new Promise((resolve, reject) => {
        refreshToken(
          getRefreshToken(),
@@ -215,181 +215,181 @@
          !validatenull(userInfo) ? userInfo.roleId : state.userInfo.role_id
        )
          .then(res => {
            const data = res.data;
            commit('SET_TOKEN', data.access_token);
            commit('SET_REFRESH_TOKEN', data.refresh_token);
            commit('SET_USER_INFO', data);
            resolve();
            const data = res.data
            commit('SET_TOKEN', data.access_token)
            commit('SET_REFRESH_TOKEN', data.refresh_token)
            commit('SET_USER_INFO', data)
            resolve()
          })
          .catch(error => {
            reject(error);
          });
      });
            reject(error)
          })
      })
    },
    // 登出
    LogOut({ commit }) {
    LogOut ({ commit }) {
      return new Promise((resolve, reject) => {
        logout()
          .then(() => {
            commit('SET_TOKEN', '');
            commit('SET_MENU_ALL_NULL', []);
            commit('SET_MENU', []);
            commit('SET_ROLES', []);
            commit('DEL_ALL_TAG', []);
            commit('CLEAR_LOCK');
            removeToken();
            removeRefreshToken();
            resolve();
            commit('SET_TOKEN', '')
            commit('SET_MENU_ALL_NULL', [])
            commit('SET_MENU', [])
            commit('SET_ROLES', [])
            commit('DEL_ALL_TAG', [])
            commit('CLEAR_LOCK')
            removeToken()
            removeRefreshToken()
            resolve()
          })
          .catch(error => {
            reject(error);
          });
      });
            reject(error)
          })
      })
    },
    //注销session
    FedLogOut({ commit }) {
    FedLogOut ({ commit }) {
      return new Promise(resolve => {
        commit('SET_TOKEN', '');
        commit('SET_MENU_ALL_NULL', []);
        commit('SET_MENU', []);
        commit('SET_ROLES', []);
        commit('DEL_ALL_TAG', []);
        commit('CLEAR_LOCK');
        removeToken();
        removeRefreshToken();
        removeToken();
        resolve();
      });
        commit('SET_TOKEN', '')
        commit('SET_MENU_ALL_NULL', [])
        commit('SET_MENU', [])
        commit('SET_ROLES', [])
        commit('DEL_ALL_TAG', [])
        commit('CLEAR_LOCK')
        removeToken()
        removeRefreshToken()
        removeToken()
        resolve()
      })
    },
    GetTopMenu() {
    GetTopMenu () {
      return new Promise(resolve => {
        getTopMenu().then(res => {
          const data = res.data.data || [];
          resolve(data);
        });
      });
          const data = res.data.data || []
          resolve(data)
        })
      })
    },
    GetMenu({ commit, dispatch }, tenantId) {
    GetMenu ({ commit, dispatch }, tenantId) {
      const errLogOut = () => {
        dispatch('LogOut').then(()=>{
                    window.location.href = "/drone-command/login"
        dispatch('LogOut').then(() => {
          window.location.href = "/drone-command/login"
          // env === 'development'
          //   ? window.location.href = "/drone-command/login"
          //   : window.location.replace(`${adminUrl}#/login`)
        })
      }
      return new Promise(resolve => {
        getRoutes({tenantId,sysType: 5}).then(res => {
          const data = res.data.data;
          let menu = deepClone(data);
          if (!res.data?.data?.length){
        getRoutes({ tenantId, sysType: 5 }).then(res => {
          const data = res.data.data
          let menu = deepClone(data)
          if (!res.data?.data?.length) {
            errLogOut()
            return
          }
          menu.forEach(ele => formatPath(ele, true));
          commit('SET_MENU', menu);
          commit('SET_MENU_ALL', menu);
          dispatch('GetButtons');
          resolve(menu);
        }).catch(err =>{
          menu.forEach(ele => formatPath(ele, true))
          commit('SET_MENU', menu)
          commit('SET_MENU_ALL', menu)
          dispatch('GetButtons')
          resolve(menu)
        }).catch(err => {
          errLogOut()
        })
      });
      })
    },
    GetButtons({ commit }) {
    GetButtons ({ commit }) {
      return new Promise(resolve => {
        getButtons({sysType: 5}).then(res => {
          const data = res.data.data;
          commit('SET_PERMISSION', data);
          resolve();
        });
      });
        getButtons({ sysType: 5 }).then(res => {
          const data = res.data.data
          commit('SET_PERMISSION', data)
          resolve()
        })
      })
    },
  },
  mutations: {
    SET_PARENT_DEPT_INFO(state, deptId){
      getParentDeptInfo({deptId:deptId}).then(res=>{
        const data = res.data.data;
        state.parentDeptInfo = data;
        setStore({ name: 'parentDeptInfo', content: data });
    SET_PARENT_DEPT_INFO (state, deptId) {
      getParentDeptInfo({ deptId: deptId }).then(res => {
        const data = res.data.data
        state.parentDeptInfo = data
        setStore({ name: 'parentDeptInfo', content: data })
      })
    },
    SET_TOKEN: (state, token) => {
      setToken(token);
      state.token = token;
      setStore({ name: 'token', content: state.token });
      setToken(token)
      state.token = token
      setStore({ name: 'token', content: state.token })
    },
    SET_REFRESH_TOKEN: (state, refreshToken) => {
      setRefreshToken(refreshToken);
      state.refreshToken = refreshToken;
      setStore({ name: 'refreshToken', content: state.refreshToken });
      setRefreshToken(refreshToken)
      state.refreshToken = refreshToken
      setStore({ name: 'refreshToken', content: state.refreshToken })
    },
    SET_MENU_ID(state, menuId) {
      state.menuId = menuId;
    SET_MENU_ID (state, menuId) {
      state.menuId = menuId
    },
    SET_TENANT_ID: (state, tenantId) => {
      state.tenantId = tenantId;
      setStore({ name: 'tenantId', content: state.tenantId });
      state.tenantId = tenantId
      setStore({ name: 'tenantId', content: state.tenantId })
    },
    SET_USER_INFO: (state, userInfo) => {
      if (validatenull(userInfo.user_id) && validatenull(userInfo.account)) {
        state.userInfo = { user_name: 'unauth', role_name: 'unauth' };
        state.userInfo = { user_name: 'unauth', role_name: 'unauth' }
      } else {
        if (validatenull(userInfo.avatar)) {
          userInfo.avatar = defaultAva;
          userInfo.avatar = defaultAva
        }
        state.userInfo = userInfo;
        state.userInfo = userInfo
      }
      setStore({ name: 'userInfo', content: state.userInfo });
      setStore({ name: 'userInfo', content: state.userInfo })
    },
    SET_MENU_ALL: (state, menuAll) => {
      let menu = state.menuAll;
      let menu = state.menuAll
      menuAll.forEach(ele => {
        let index = menu.findIndex(item => item.path === ele.path);
        let index = menu.findIndex(item => item.path === ele.path)
        if (index === -1) {
          menu.push(ele);
          menu.push(ele)
        } else {
          menu[index] = ele;
          menu[index] = ele
        }
      });
      state.menuAll = menu;
      setStore({ name: 'menuAll', content: state.menuAll });
      })
      state.menuAll = menu
      setStore({ name: 'menuAll', content: state.menuAll })
    },
    SET_MENU_ALL_NULL: state => {
      state.menuAll = [];
      setStore({ name: 'menuAll', content: state.menuAll });
      state.menuAll = []
      setStore({ name: 'menuAll', content: state.menuAll })
    },
    SET_MENU: (state, menu) => {
      state.menu = menu;
      setStore({ name: 'menu', content: state.menu });
      state.menu = menu
      setStore({ name: 'menu', content: state.menu })
    },
    SET_ROLES: (state, roles) => {
      state.roles = roles;
      state.roles = roles
    },
    SET_PERMISSION: (state, permission) => {
      let result = [];
      let result = []
      function getCode(list) {
      function getCode (list) {
        list.forEach(ele => {
          if (typeof ele === 'object') {
            const children = ele.children;
            const code = ele.code;
            const children = ele.children
            const code = ele.code
            if (children) {
              getCode(children);
              getCode(children)
            } else {
              result.push(code);
              result.push(code)
            }
          }
        });
        })
      }
      getCode(permission);
      state.permission = {};
      getCode(permission)
      state.permission = {}
      result.forEach(ele => {
        state.permission[ele] = true;
      });
      setStore({ name: 'permission', content: state.permission, type: 'session' });
        state.permission[ele] = true
      })
      setStore({ name: 'permission', content: state.permission, type: 'session' })
    },
  },
};
export default user;
}
export default user
applications/drone-command/src/views/api.txt
@@ -3,7 +3,7 @@
## 列表
**接口地址**:`/device/fwDevice/list`
**接口地址**:`/cockpit/dataCockpit/defenseSceneManageList`
**请求方式**:`GET`
@@ -15,7 +15,7 @@
**响应数据类型**:`*/*`
**接口描述**:<p>传入isAreaSelect、areaId、areaIds(多个用,隔开获取对应区域绑定设备数据)、status</p>
**接口描述**:<p>传入time</p>
@@ -27,11 +27,7 @@
| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|areaId|areaId|query|false|integer(int64)||
|areaIds|areaIds(多个用,隔开)|query|false|string||
|isAreaSelect|isAreaSelect|query|false|integer(int32)||
|isTrack|isTrack|query|false|integer(int32)||
|status|status|query|false|integer(int32)||
|time|时间筛选(yyyy-MM-dd HH:mm:ss)|query|false|string(date-time)||
**响应状态**:
@@ -39,7 +35,7 @@
| 状态码 | 说明 | schema |
| -------- | -------- | ----- | 
|200|OK|R«List«FwDeviceVO»»|
|200|OK|R«List«FwDefenseSceneManageVO»»|
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
@@ -51,41 +47,21 @@
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- | 
|code|状态码|integer(int32)|integer(int32)|
|data|承载数据|array|FwDeviceVO|
|&emsp;&emsp;azimuth|方位角 0-360°|integer(int32)||
|&emsp;&emsp;batteryPct|电量百分比 0-100|integer(int32)||
|&emsp;&emsp;belongDept|所属部门id|integer(int64)||
|&emsp;&emsp;belongDeptName|所属部门名称|string||
|&emsp;&emsp;charger|负责人|string||
|&emsp;&emsp;contactPhone|联系电话|string||
|&emsp;&emsp;createDept|创建部门|integer(int64)||
|&emsp;&emsp;createTime|创建时间|string(date-time)||
|&emsp;&emsp;createUser|创建人|integer(int64)||
|&emsp;&emsp;detectTargetCnt|侦测目标数量|integer(int32)||
|&emsp;&emsp;deviceAtt|设备属性(无线电/光电/雷达)|string||
|&emsp;&emsp;deviceModel|型号|string||
|&emsp;&emsp;deviceName|设备名称|string||
|&emsp;&emsp;deviceSn|设备sn|string||
|&emsp;&emsp;deviceSpecification|规格|string||
|&emsp;&emsp;deviceType|设备类型(察打一体/便捷侦测箱/反制枪)|string||
|&emsp;&emsp;effectiveRangeKm|有效范围 km|number||
|&emsp;&emsp;elevation|俯仰角 -90-+90°|integer(int32)||
|&emsp;&emsp;id|主键|integer(int64)||
|&emsp;&emsp;isShared|是否被共享给当前机构|boolean||
|&emsp;&emsp;latitude|纬度|string||
|&emsp;&emsp;longitude|经度|string||
|&emsp;&emsp;manufacturer|生产厂商|string||
|&emsp;&emsp;outTarget|出库去向|string||
|&emsp;&emsp;purpose|用途|string||
|&emsp;&emsp;remark|备注|string||
|data|承载数据|array|FwDefenseSceneManageVO|
|&emsp;&emsp;areaCode|区域编码|string||
|&emsp;&emsp;areaCount|区域数量|integer(int32)||
|&emsp;&emsp;defenseLeader|防控负责人|string||
|&emsp;&emsp;defenseSceneId|关联场景配置ID|integer(int64)||
|&emsp;&emsp;defenseSceneName|场景配置名称|string||
|&emsp;&emsp;deviceCount|设备数量|integer(int32)||
|&emsp;&emsp;effectiveDateEnd|有效时间-结束|string(date-time)||
|&emsp;&emsp;effectiveDateStart|有效时间-开始|string(date-time)||
|&emsp;&emsp;id||integer(int64)||
|&emsp;&emsp;latitude|指挥点纬度|number(double)||
|&emsp;&emsp;leaderPhone|防控负责人电话|string||
|&emsp;&emsp;longitude|指挥点经度|number(double)||
|&emsp;&emsp;sceneName|场景名称|string||
|&emsp;&emsp;shareId|共享记录ID|integer(int64)||
|&emsp;&emsp;source|来源|string||
|&emsp;&emsp;status|运行状态(0:在线/1:离线/2:故障/3:报废)|integer(int32)||
|&emsp;&emsp;trackStatus|设备出入库状态 0未出库1已出库|integer(int32)||
|&emsp;&emsp;updateTime|更新时间|string(date-time)||
|&emsp;&emsp;updateUser|更新人|integer(int64)||
|&emsp;&emsp;workMode|工作模式:1.侦测中/2.信号干扰中/3.诱导驱离中/4.待机|string||
|&emsp;&emsp;sceneType|场景类型:核心区/学校等|string||
|msg|返回消息|string||
|success|是否成功|boolean||
@@ -96,43 +72,92 @@
    "code": 0,
    "data": [
        {
            "azimuth": 0,
            "batteryPct": 0,
            "belongDept": 0,
            "belongDeptName": "",
            "charger": "",
            "contactPhone": "",
            "createDept": 0,
            "createTime": "",
            "createUser": 0,
            "detectTargetCnt": 0,
            "deviceAtt": "",
            "deviceModel": "",
            "deviceName": "",
            "deviceSn": "",
            "deviceSpecification": "",
            "deviceType": "",
            "effectiveRangeKm": 0,
            "elevation": 0,
            "areaCode": "",
            "areaCount": 0,
            "defenseLeader": "",
            "defenseSceneId": 0,
            "defenseSceneName": "",
            "deviceCount": 0,
            "effectiveDateEnd": "",
            "effectiveDateStart": "",
            "id": 0,
            "isShared": true,
            "latitude": "",
            "longitude": "",
            "manufacturer": "",
            "outTarget": "",
            "purpose": "",
            "remark": "",
            "latitude": 0,
            "leaderPhone": "",
            "longitude": 0,
            "sceneName": "",
            "shareId": 0,
            "source": "",
            "status": 0,
            "trackStatus": 0,
            "updateTime": "",
            "updateUser": 0,
            "workMode": ""
            "sceneType": ""
        }
    ],
    "msg": "",
    "success": true
}
```
## 场景管理统计
**接口地址**:`/cockpit/dataCockpit/sceneManageStatistics`
**请求方式**:`GET`
**请求数据类型**:`application/x-www-form-urlencoded`
**响应数据类型**:`*/*`
**接口描述**:<p>返回场景管理数量与区域数量</p>
**请求参数**:
**请求参数**:
暂无
**响应状态**:
| 状态码 | 说明 | schema |
| -------- | -------- | ----- |
|200|OK|R«SceneManageStatisticsVO»|
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- |
|code|状态码|integer(int32)|integer(int32)|
|data|承载数据|SceneManageStatisticsVO|SceneManageStatisticsVO|
|&emsp;&emsp;areaCount|区域数量|integer(int32)||
|&emsp;&emsp;sceneManageCount|场景管理数量|integer(int32)||
|msg|返回消息|string||
|success|是否成功|boolean||
**响应示例**:
```javascript
{
    "code": 0,
    "data": {
        "areaCount": 0,
        "sceneManageCount": 0
    },
    "msg": "",
    "success": true
}
```
applications/drone-command/src/views/dataCockpit/components/DetectionCountermeasure.vue
New file
@@ -0,0 +1,226 @@
<template>
    <div class="detection-countermeasure">
        <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"
            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>
        <DeviceHistoryDialog v-model="historyVisible" :device="historyDevice" />
    </div>
</template>
<script setup>
import { ref, onMounted } from '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, deviceStatisticsApi } from '@/api/dataCockpit'
const emit = defineEmits(['update:allDevices'])
const categoryList = ref([
    {
        id: 1,
        name: '侦测设备',
        val: 0,
        logo: zcsbLogo
    },
    {
        id: 2,
        name: '反制设备',
        val: 0,
        logo: fzsbLogo
    }
])
const counterDeviceStatusList = ref([])
const historyVisible = ref(false)
const historyDevice = ref(null)
const loading = ref(false)
const loadingTimer = ref(null)
const onHistory = (item) => {
    historyDevice.value = item
    historyVisible.value = true
}
const onCardClick = (item) => {
    console.log('点击卡片:', item.id)
}
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 ?? '-',
        workMode: item.workMode ?? '-',
        manufacturer: item.manufacturer || '-',
        detectTargetCnt: item.detectTargetCnt ?? 0,
        effectiveRangeKm: item.effectiveRangeKm ?? 0
    }
}
const fetchDeviceList = async () => {
    // 只有超过200ms才显示loading,避免闪烁
    loadingTimer.value = setTimeout(() => {
        loading.value = true
    }, 200)
    try {
        const params = { current: 1, size: 9999, deviceStatusList: '0,1,2' }
        const res = await deviceSearchApi(params)
        const records = res?.data?.data?.records ?? []
        let onlineData = records.filter(item => String(item.status) === '0')
        counterDeviceStatusList.value = onlineData.map(formatDeviceItem)
        emit('update:allDevices', records)
    } finally {
        if (loadingTimer.value) clearTimeout(loadingTimer.value)
        loading.value = false
    }
}
const normalizeType = (value) => {
    if (value === 1 || value === '1') return '侦测设备'
    if (value === 2 || value === '2') return '反制设备'
    if (value === 'detect') return '侦测设备'
    if (value === 'counter') return '反制设备'
    return value
}
const fetchDeviceStatistics = async () => {
    const res = await deviceStatisticsApi()
    const list = res?.data?.data ?? []
    const map = new Map(list.map((item) => [normalizeType(item.type), item.count]))
    categoryList.value = categoryList.value.map((item) => ({
        ...item,
        val: map.has(item.name) ? map.get(item.name) : item.val
    }))
}
onMounted(() => {
    fetchDeviceList()
    fetchDeviceStatistics()
})
</script>
<style lang="scss" scoped>
.detection-countermeasure {
    display: flex;
    flex-direction: column;
    height: 100%;
}
.category-container {
    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: DinAB;
                font-weight: bold;
                font-size: 22px;
                color: #FFFFFF;
                line-height: 20px;
                text-align: left;
                font-style: normal;
                text-transform: none;
            }
        }
    }
}
.content {
    flex: 1;
    margin-top: 20px;
    overflow-y: auto;
    overflow-x: hidden;
}
</style>
applications/drone-command/src/views/dataCockpit/components/EquipmentWarning.vue
@@ -35,8 +35,8 @@
import EmptyState from './EmptyState.vue'
const equipmentWarningData = ref([])
const loading = ref(true)
const minLoadingMs = 400
const loading = ref(false)
const loadingTimer = ref(null)
const statusMap = {
    0: { text: '在线', statusType: 'online' },
@@ -69,18 +69,18 @@
}
const fetchDeviceWarning = async () => {
    const startAt = Date.now()
    loading.value = true
    // 只有超过200ms才显示loading,避免闪烁
    loadingTimer.value = setTimeout(() => {
        loading.value = true
    }, 200)
    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))
        }
        if (loadingTimer.value) clearTimeout(loadingTimer.value)
        loading.value = false
    }
}
applications/drone-command/src/views/dataCockpit/components/HistoryWarning.vue
@@ -1,10 +1,6 @@
<template>
    <div
        class="history-warning"
        v-loading="loading"
        element-loading-background="rgba(5, 5, 15, 0.6)"
        element-loading-text="加载中..."
    >
    <div class="history-warning" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)"
        element-loading-text="加载中...">
        <!-- 搜索区(设计稿红框) -->
        <div class="search-box">
            <!-- 关键字 -->
@@ -13,11 +9,8 @@
            </el-input>
            <!-- 日期范围 -->
            <el-date-picker v-model="query.dateRange"
                  class="command-date-picker"
                 popper-class="command-date-picker-popper"
                type="daterange"
                range-separator="~"
            <el-date-picker v-model="query.dateRange" class="command-date-picker"
                popper-class="command-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>
@@ -26,27 +19,22 @@
        <!-- 列表 -->
        <div class="list-content">
            <EmptyState v-if="!filteredList.length" />
            <HistoryTemplate
                v-else
                v-for="(item, ind) in filteredList"
                :key="item.id || ind"
                :data="item"
                @click="onCardClick"
            />
            <HistoryTemplate v-else v-for="(item, ind) in filteredList" :key="item.id || ind" :data="item"
                @click="onCardClick" />
        </div>
    </div>
</template>
<script setup>
    import { dateRangeFormat } from '@ztzf/utils'
import { dateRangeFormat } from '@ztzf/utils'
import { ref, computed, onMounted } from 'vue'
import HistoryTemplate from './templateComponents/HistoryTemplate.vue'
import { alarmLogApi } from '@/api/dataCockpit'
import EmptyState from './EmptyState.vue'
const historyWarningData = ref([])
const loading = ref(true)
const minLoadingMs = 400
const loading = ref(false)
const loadingTimer = ref(null)
// 查询条件:keyword + dateRange(YYYY-MM-DD数组)
const query = ref({
@@ -63,9 +51,12 @@
}
const fetchHistoryWarning = async () => {
    const startAt = Date.now()
    loading.value = true
    // 只有超过200ms才显示loading,避免闪烁
    loadingTimer.value = setTimeout(() => {
        loading.value = true
    }, 200)
    try {
        const [startTime, endTime] = dateRangeFormat(query.value.dateRange) || []
        const keyword = (query.value.keyword || '').trim()
@@ -82,10 +73,7 @@
        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))
        }
        if (loadingTimer.value) clearTimeout(loadingTimer.value)
        loading.value = false
    }
}
@@ -123,5 +111,4 @@
        overflow-y: auto;
    }
}
</style>
applications/drone-command/src/views/dataCockpit/components/RightContainer.vue
@@ -1,4 +1,4 @@
<!--
<!--
 * @Author       : yuan
 * @Date         : 2026-01-07 15:17:54
 * @LastEditors  : yuan
@@ -11,55 +11,40 @@
<template>
    <div class="right-container" :class="{ collapsed: collapsedModel }">
        <div class="wrapper">
            <TitleTemplate>侦测反制设备</TitleTemplate>
            <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 class="tabs-container">
                <div
                    class="tabs-item"
                    v-for="item in tabs"
                    :key="item.id"
                    @click="activeTab = item.id"
                    :class="{ active: activeTab === item.id }"
                >
                    {{ item.name }}
                </div>
            </div>
            <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 class="content">
                <DetectionCountermeasure
                    v-if="activeTab === 1"
                    @update:allDevices="handleUpdateAllDevices"
                />
                <SceneDeployment v-else />
            </div>
        </div>
        <div class="collapse-btn" @click="toggleCollapse">
            <span class="arrow" :class="collapsedModel ? 'left' : 'right'"></span>
        </div>
        <DeviceHistoryDialog v-model="historyVisible" :device="historyDevice" />
    </div>
</template>
<script setup>
import { ref, computed } from 'vue'
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, deviceStatisticsApi } from '@/api/dataCockpit'
import SceneDeployment from './SceneDeployment.vue'
import DetectionCountermeasure from './DetectionCountermeasure.vue'
const props = defineProps({
    collapsed: {
@@ -75,187 +60,22 @@
    set: (val) => emit('update:collapsed', val)
})
const categoryList = ref([
    {
        id: 1,
        name: '侦测设备',
        val: 0,
        logo: zcsbLogo
    },
    {
        id: 2,
        name: '反制设备',
        val: 0,
        logo: fzsbLogo
    }
])
const activeTab = ref(1)
const tabs = [
    { id: 1, name: '侦测反制设备' },
    { id: 2, name: '场景部署' }
]
const counterDeviceStatusList = ref([])
const historyVisible = ref(false)
const historyDevice = ref(null)
const loading = ref(true)
const minLoadingMs = 400
const onHistory = (item) => {
    historyDevice.value = item
    historyVisible.value = true
    console.log('历史数据:', item.id)
}
const onCardClick = (item) => {
    console.log('点击卡片:', item.id)
}
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 ?? '-',
        workMode: item.workMode ?? '-',
        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: 9999, deviceStatusList: '0,1,2' }
        const res = await deviceSearchApi(params)
        const records = res?.data?.data?.records ?? []
        let onlineData = records.filter(item => String(item.status) === '0')
        counterDeviceStatusList.value = onlineData.map(formatDeviceItem)
        emit('update:allDevices', records)
    } finally {
        const elapsed = Date.now() - startAt
        if (elapsed < minLoadingMs) {
            await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed))
        }
        loading.value = false
    }
}
onMounted(() => {
    fetchDeviceList()
    fetchDeviceStatistics()
})
const normalizeType = (value) => {
    if (value === 1 || value === '1') return '侦测设备'
    if (value === 2 || value === '2') return '反制设备'
    if (value === 'detect') return '侦测设备'
    if (value === 'counter') return '反制设备'
    return value
}
const fetchDeviceStatistics = async () => {
    const res = await deviceStatisticsApi()
    const list = res?.data?.data ?? []
    const map = new Map(list.map((item) => [normalizeType(item.type), item.count]))
    categoryList.value = categoryList.value.map((item) => ({
        ...item,
        val: map.has(item.name) ? map.get(item.name) : item.val
    }))
const handleUpdateAllDevices = (devices) => {
    emit('update:allDevices', devices)
}
</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: DinAB;
                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;
@@ -287,4 +107,87 @@
        background: rgba(40, 79, 227, 0.6);
    }
}
.tabs-container {
    position: relative;
    display: flex;
    justify-content: space-between;
    &::after {
        content: '';
        position: absolute;
        left: 0;
        bottom: 0;
        width: 100%;
        height: 1px;
        background: #333355;
    }
    .tabs-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: 0;
        }
        &.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;
            z-index: 9;
        }
        .badge {
            position: absolute;
            top: 16px;
            right: -4px;
            min-width: 18px;
            line-height: 18px;
            font-size: 12px;
            color: #FFFFFF;
            background: #FF3B30;
            border-radius: 50%;
            text-align: center;
            box-shadow: 0px 2px 6px rgba(255, 59, 48, 0.6);
            pointer-events: none;
        }
    }
}
.content {
    height: 0;
    flex: 1;
    margin-top: 20px;
    overflow-x: hidden;
    overflow-y: auto;
}
</style>
applications/drone-command/src/views/dataCockpit/components/SceneDeployment.vue
New file
@@ -0,0 +1,324 @@
<template>
    <div class="scene-deployment">
        <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" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)"
            element-loading-text="加载中...">
            <EmptyState v-if="!sceneList.length && !loading" />
            <div class="scene-card" v-else v-for="(item, index) in sceneList" :key="index">
                <div class="card-header">
                    <el-tooltip class="title-tooltip" :content="item.sceneName || '-'" placement="top" effect="dark"
                        :show-after="200">
                        <span class="name">
                            {{ item.sceneName }}
                        </span>
                    </el-tooltip>
                </div>
                <div class="card-body">
                    <div class="row">
                        <span class="label">指挥点</span>
                        <span class="value">{{ formatPoint(item.longitude, item.latitude) }}</span>
                    </div>
                    <div class="rows">
                        <div class="col">
                            <span class="label">负责人</span>
                            <span class="value">{{ item.defenseLeader || '-' }}</span>
                        </div>
                        <div class="col">
                            <span class="label">电话</span>
                            <span class="value">{{ item.leaderPhone || '-' }}</span>
                        </div>
                    </div>
                    <div class="rows">
                        <div class="col">
                            <span class="label">区域数量</span>
                            <span class="value">{{ item.areaCount || 0 }}</span>
                        </div>
                        <div class="col">
                            <span class="label">设备数量</span>
                            <span class="value">{{ item.deviceCount || 0 }}</span>
                        </div>
                    </div>
                    <div class="row">
                        <span class="label">场景类型</span>
                        <span class="value">{{ getSceneTypeName(item.sceneType) }}</span>
                    </div>
                    <div class="row">
                        <span class="label">有效时间</span>
                        <span class="value">{{ formatTimeRange(item.effectiveDateStart, item.effectiveDateEnd) }}</span>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import dayjs from 'dayjs'
import zcsbLogo from '@/assets/images/dataCockpit/zcsb.png'
import fzsbLogo from '@/assets/images/dataCockpit/fzsb.png'
import EmptyState from './EmptyState.vue'
import { newDefenseSceneManageList, sceneManageStatisticsApi } from '@/api/dataCockpit'
import { getDictionaryByCode } from '@/api/system/dictbiz'
const loading = ref(false)
const sceneTypeDict = ref([])
const loadingTimer = ref(null)
const categoryList = ref([
    {
        name: '场景',
        val: 0,
        logo: zcsbLogo // 暂时复用
    },
    {
        name: '区域',
        val: 0,
        logo: fzsbLogo // 暂时复用
    }
])
const sceneList = ref([])
const formatPoint = (lng, lat) => {
    if (!lng || !lat) return '-'
    return `${lng}, ${lat}`
}
const formatTimeRange = (start, end) => {
    if (!start && !end) return '-'
    const s = start ? dayjs(start).format('YYYY-MM-DD HH:mm:ss') : '-'
    const e = end ? dayjs(end).format('YYYY-MM-DD HH:mm:ss') : '-'
    return `${s} ~ ${e}`
}
const getSceneTypeName = (code) => {
    const item = sceneTypeDict.value.find(i => i.dictKey === String(code))
    return item ? item.dictValue : (code || '-')
}
const fetchDict = async () => {
    try {
        const res = await getDictionaryByCode('CommandSceneType')
        if (res.data.code === 200) {
            sceneTypeDict.value = res.data.data.CommandSceneType || []
        }
    } catch (error) {
        console.error('获取场景类型字典失败:', error)
    }
}
const fetchStatistics = async () => {
    try {
        const res = await sceneManageStatisticsApi()
        if (res.data.code === 200) {
            const { sceneManageCount, areaCount } = res.data.data
            categoryList.value[0].val = sceneManageCount || 0
            categoryList.value[1].val = areaCount || 0
        }
    } catch (error) {
        console.error('获取场景统计失败:', error)
    }
}
const fetchList = async () => {
    // 只有超过200ms才显示loading,避免闪烁
    loadingTimer.value = setTimeout(() => {
        loading.value = true
    }, 200)
    try {
        const params = {
            time: dayjs().format('YYYY-MM-DD HH:mm:ss')
        }
        const res = await newDefenseSceneManageList(params)
        if (res.data.code === 200) {
            sceneList.value = res.data.data || []
        }
    } catch (error) {
        console.error('获取场景列表失败:', error)
    } finally {
        if (loadingTimer.value) clearTimeout(loadingTimer.value)
        loading.value = false
    }
}
onMounted(async () => {
    await fetchDict()
    fetchStatistics()
    fetchList()
})
</script>
<style lang="scss" scoped>
.scene-deployment {
    display: flex;
    flex-direction: column;
    height: 100%;
}
.category-container {
    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: 1rem;
                font-family: DinAB;
                font-weight: bold;
                font-size: 22px;
                color: #FFFFFF;
                line-height: 20px;
                text-align: left;
                font-style: normal;
                text-transform: none;
            }
        }
    }
}
.content {
    flex: 1;
    margin-top: 20px;
    overflow-y: auto;
    overflow-x: hidden;
}
.scene-card {
    margin-top: 10px;
    padding: 10px;
    color: #fff;
    background: #191933;
    border-radius: 6px 6px 6px 6px;
    cursor: pointer;
    &:first-child {
        margin-top: 0;
    }
    &:hover {
        filter: brightness(1.05);
    }
    .card-header {
        display: flex;
        align-items: center;
        gap: 6px;
        min-width: 0;
        .title-tooltip {
            flex: 1;
            min-width: 0;
            display: block;
        }
        .name {
            display: inline-block;
            max-width: 100%;
            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;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
    }
    .card-body {
        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;
            }
        }
        .rows {
            display: flex;
            .col {
                flex: 1;
                .label {
                    margin-right: 10px;
                    color: #D4D5D7;
                }
            }
        }
    }
}
</style>
applications/drone-command/src/views/dataCockpit/components/templateComponents/RealEquipmentTemplate.vue
@@ -3,13 +3,8 @@
        <!-- 头部 -->
        <div class="header">
            <div class="title">
                <el-tooltip
                    class="title-tooltip"
                    :content="data.name || '-'"
                    placement="top"
                    effect="dark"
                    :show-after="200"
                >
                <el-tooltip class="title-tooltip" :content="data.name || '-'" placement="top" effect="dark"
                    :show-after="200">
                    <span class="name">{{ data.name }}</span>
                </el-tooltip>
@@ -55,21 +50,22 @@
                    {{ getStatusText(data.status) }}
                </div>
                <div class="col">
                    <span class="label">厂商</span>
                    {{ data.manufacturer }}
                    <span class="label">侦测目标</span>
                    {{ data.detectTargetCnt }}台
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span class="label">侦测目标</span>
                    {{ data.detectTargetCnt }}台
                </div>
                <div class="col">
                    <span class="label">有效范围</span>
                    {{ Number(data.effectiveRangeKm).toFixed(0) }}m
                </div>
            </div>
            <div class="col">
                <span class="label">厂商</span>
                {{ data.manufacturer }}
            </div>
        </div>
        <!-- 底部按钮 -->
applications/drone-command/src/views/dataCockpit/index.vue
@@ -57,8 +57,8 @@
  const rightCollapsed = ref(false);
  const allDevices = ref([]);
  const realWarningData = ref([]);
  const realWarningLoading = ref(true);
  const minLoadingMs = 400;
  const realWarningLoading = ref(false);
  const loadingTimer = ref(null);
  const onSelectWarning = (type) => {
    leftActiveId.value = type;
@@ -131,8 +131,11 @@
  };
  const fetchRealWarning = async () => {
    const startAt = Date.now();
    realWarningLoading.value = true;
    // 只有超过200ms才显示loading,避免闪烁
    loadingTimer.value = setTimeout(() => {
      realWarningLoading.value = true;
    }, 200);
    try {
      const params = { current: 1, size: 9999, alarmType: '1' };
      const res = await alarmLogApi(params);
@@ -144,10 +147,7 @@
          (record?.favorited === 1 || record?.favorited === '1')
      }));
    } finally {
      const elapsed = Date.now() - startAt;
      if (elapsed < minLoadingMs) {
        await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed));
      }
      if (loadingTimer.value) clearTimeout(loadingTimer.value);
      realWarningLoading.value = false;
    }
  };