吉安感知网项目-前端
罗广辉
2026-02-09 8739df8c235b3c36df0b47ddc20a691dadc7bb4f
Merge remote-tracking branch 'origin/master'

# Conflicts:
# applications/drone-command/src/store/modules/user.js
11 files modified
2 files added
2106 ■■■■■ changed files
applications/drone-command/src/api/dataCockpit/index.js 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/components/map-container/common-cesium-map.vue 15 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/components/map-container/device-map-container.vue 41 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/store/modules/user.js 368 ●●●● 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 319 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/SceneDeployment.vue 326 ●●●●● 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 336 ●●●● patch | view | raw | blame | history
applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue 210 ●●●● 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/components/map-container/common-cesium-map.vue
@@ -203,7 +203,20 @@
const getViewer = () => viewer
const getPublicCesium = () => viewInstance
defineExpose({ getMap, getViewer, getPublicCesium, setAdminBoundaryVisible, zoomToAdminBoundary })
const flyToPoint = (options) => {
    const { longitude, latitude, height } = options.position
    viewer.camera.setView({
        destination: Cesium.Cartesian3.fromDegrees(longitude, latitude, height),
        orientation: {
            heading: Cesium.Math.toRadians(0), // 方向
            pitch: Cesium.Math.toRadians(-90), // 仰角
            roll: Cesium.Math.toRadians(0) // 翻滚角
        }
    })
}
defineExpose({ getMap, getViewer, getPublicCesium, flyToPoint, setAdminBoundaryVisible, zoomToAdminBoundary })
</script>
<style scoped lang="scss">
applications/drone-command/src/components/map-container/device-map-container.vue
@@ -1,4 +1,4 @@
<template>
<template>
    <div class="map-shell">
        <CommonCesiumMap ref="mapRef" class="command-cesium map-container" :dom-id="props.containerId" :active="true"
            :flat-mode="false" :terrain="true" :layer-mode="4" :contour="false" :boundary="false"
@@ -466,7 +466,7 @@
            },
            {
                longitude: 114.963974,
                latitude:  27.132677,
                latitude: 27.132677,
                height: 320
            },
            {
@@ -513,22 +513,22 @@
                height: 400
            },
            {
                longitude: 114.925836,
                longitude: 114.925836,
                latitude: 27.096102,
                height: 400
            },
            {
                longitude: 114.928384,
                longitude: 114.928384,
                latitude: 27.095866,
                height: 400
            },
            {
                longitude: 114.929874,
                longitude: 114.929874,
                latitude: 27.096095,
                height: 400
            },
            {
                longitude: 114.931048,
                longitude: 114.931048,
                latitude: 27.097666,
                height: 400
            },
@@ -629,11 +629,11 @@
        const pos = isEntityPosition
            ? getBillboardPosition(track.billboard)
            : Cesium.Cartesian3.lerp(
                    track.positions[seg],
                    track.positions[seg + 1],
                    ratio,
                    new Cesium.Cartesian3()
              )
                track.positions[seg],
                track.positions[seg + 1],
                ratio,
                new Cesium.Cartesian3()
            )
        if (!pos) return
        if (!isEntityPosition) {
            track.billboard.position = pos
@@ -848,6 +848,7 @@
                width: 40,
                height: 56,
                verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
                disableDepthTestDistance: Number.POSITIVE_INFINITY,
            })
            billboard.id = entry.entityId
            devicePickMap.set(entry.entityId, { data: entry.data, billboard })
@@ -976,7 +977,7 @@
    if (!trimmed) return null
    try {
        return JSON.parse(trimmed)
    } catch (error) {}
    } catch (error) { }
    return null
}
@@ -1029,7 +1030,7 @@
                        .filter(radius => Number.isFinite(radius) && radius > 0)
                    radii.forEach((radius, levelIndex) => {
                        const positions = buildEllipsePositions(centerCartesian, radius, radius)
                    const style = BUFFER_LEVEL_STYLES[levelIndex] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
                        const style = BUFFER_LEVEL_STYLES[levelIndex] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
                        if (positions.length >= 3) {
                            shapes.push({
                                positions,
@@ -1380,6 +1381,20 @@
    viewer = null
    publicCesium = null
})
const getMap = () => {
    return mapRef.value?.getMap()
}
// 暴露给父组件调用
const flyTo = (options) => {
    mapRef.value?.flyToPoint(options)
}
defineExpose({
    getMap,
    flyTo
})
</script>
<style lang="scss" scoped>
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,64 +215,64 @@
          !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`)
@@ -287,110 +287,110 @@
            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,8 +1,8 @@
<!--
<!--
 * @Author       : yuan
 * @Date         : 2026-01-07 15:17:54
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-31 15:47:40
 * @LastEditTime : 2026-02-09 15:20:23
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\components\RightContainer.vue
 * @Description  : 
 * Copyright 2026 OBKoro1, All Rights Reserved. 
@@ -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 @click="(item) => emit('scene-click', item)" />
            </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: {
@@ -68,194 +53,29 @@
    }
})
const emit = defineEmits(['update:collapsed', 'update:allDevices'])
const emit = defineEmits(['update:collapsed', 'update:allDevices', 'scene-click'])
const collapsedModel = computed({
    get: () => props.collapsed,
    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,326 @@
<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" @click="emit('click', item)">
                <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 emit = defineEmits(['click'])
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
@@ -2,7 +2,7 @@
 * @Author       : yuan
 * @Date         : 2026-01-06 16:35:50
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-31 15:40:15
 * @LastEditTime : 2026-02-09 15:40:07
 * @FilePath     : \applications\drone-command\src\views\dataCockpit\index.vue
 * @Description  : 
 * Copyright 2026 OBKoro1, All Rights Reserved. 
@@ -10,201 +10,203 @@
-->
<template>
  <div class="page-container">
    <DeviceMapContainer
      :all-devices="allDevices"
      :alarm-drones="realWarningData"
      :left-collapsed="leftCollapsed"
      container-id="data-cockpit-map"
      @drone-signal="handleAlarmAction('signal', $event)"
      @drone-counter="handleAlarmAction('counter', $event)"
      @drone-favorite="handleAlarmFavorite"
    />
    <LeftContainer
      class="left-container"
      v-model:activeId="leftActiveId"
      v-model:collapsed="leftCollapsed"
      :real-warning-data="realWarningData"
      :real-warning-loading="realWarningLoading"
    <DeviceMapContainer ref="deviceMapRef" :all-devices="allDevices" :alarm-drones="realWarningData"
      :left-collapsed="leftCollapsed" container-id="data-cockpit-map"
      @drone-signal="handleAlarmAction('signal', $event)" @drone-counter="handleAlarmAction('counter', $event)"
      @drone-favorite="handleAlarmFavorite" />
    <LeftContainer class="left-container" v-model:activeId="leftActiveId" v-model:collapsed="leftCollapsed"
      :real-warning-data="realWarningData" :real-warning-loading="realWarningLoading"
      @real-warning-signal="handleAlarmAction('signal', $event)"
      @real-warning-counter="handleAlarmAction('counter', $event)"
      @real-warning-favorite="handleAlarmFavorite"
    />
    <RightContainer class="right-container" v-model:collapsed="rightCollapsed"
      v-model:allDevices="allDevices" />
    <CenterContainer @select-warning="onSelectWarning" @select-equipment="onSelectEquipment" />
      @real-warning-counter="handleAlarmAction('counter', $event)" @real-warning-favorite="handleAlarmFavorite" />
    <RightContainer class="right-container" v-model:collapsed="rightCollapsed" v-model:allDevices="allDevices"
      @scene-click="handleSceneClick" />
    <CenterContainer @select-warning="onSelectWarning" @select-equipment="onSelectEquipment" />
    <LegendBar class="legend-container" />
  </div>
</template>
<script setup>
  import DeviceMapContainer from '@/components/map-container/device-map-container.vue';
  import LeftContainer from './components/LeftContainer.vue';
  import RightContainer from './components/RightContainer.vue';
  import CenterContainer from './components/CenterContainer.vue';
  import LegendBar from './components/LegendBar.vue';
  import {
    alarmLogApi,
    alarmFavoriteRemoveApi,
    alarmFavoriteSaveApi,
    interferenceAndExpulsionApi
  } from '@/api/dataCockpit';
  import { ElMessage } from 'element-plus';
  import { onMounted, ref } from 'vue';
import DeviceMapContainer from '@/components/map-container/device-map-container.vue'
import LeftContainer from './components/LeftContainer.vue'
import RightContainer from './components/RightContainer.vue'
import CenterContainer from './components/CenterContainer.vue'
import LegendBar from './components/LegendBar.vue'
import {
  alarmLogApi,
  alarmFavoriteRemoveApi,
  alarmFavoriteSaveApi,
  interferenceAndExpulsionApi
} from '@/api/dataCockpit'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
  const leftActiveId = ref(1);
  const leftCollapsed = ref(false);
const leftActiveId = ref(1)
const leftCollapsed = ref(false)
  const rightCollapsed = ref(false);
  const allDevices = ref([]);
  const realWarningData = ref([]);
  const realWarningLoading = ref(true);
  const minLoadingMs = 400;
const rightCollapsed = ref(false)
const allDevices = ref([])
const realWarningData = ref([])
const realWarningLoading = ref(false)
const loadingTimer = ref(null)
const deviceMapRef = ref(null)
  const onSelectWarning = (type) => {
    leftActiveId.value = type;
    leftCollapsed.value = false;
  };
const onSelectWarning = (type) => {
  leftActiveId.value = type
  leftCollapsed.value = false
}
  const onSelectEquipment = () => {
    rightCollapsed.value = false;
  };
const onSelectEquipment = () => {
  rightCollapsed.value = false
}
  const actionTextMap = {
    signal: '信号干扰',
    counter: '诱导驱离'
  };
const handleSceneClick = (item) => {
  if (item?.longitude && item?.latitude) {
    deviceMapRef.value?.flyTo({
      position: {
        longitude: Number(item.longitude),
        latitude: Number(item.latitude),
        height: 18000
      },
    })
  }
}
  const getAlarmRecordId = (item) => item?.alarmRecordId ?? item?.id;
  const getFavoriteId = (item) =>
    item?.favoriteId ??
    item?.alarmFavoriteId ??
    item?.fwAlarmFavoriteId ??
    item?.favoriteRecordId ??
    getAlarmRecordId(item);
const actionTextMap = {
  signal: '信号干扰',
  counter: '诱导驱离'
}
  const isFavorited = (item) => {
    const value = item?.favorited ?? item?.isFavorite;
    return value === 1 || value === '1' || value === true;
  };
const getAlarmRecordId = (item) => item?.alarmRecordId ?? item?.id
const getFavoriteId = (item) =>
  item?.favoriteId ??
  item?.alarmFavoriteId ??
  item?.fwAlarmFavoriteId ??
  item?.favoriteRecordId ??
  getAlarmRecordId(item)
  const buildPayload = (type, item) => ({
    counterWay: type === 'signal' ? 1 : type === 'counter' ? 2 : '',
    alarmRecordId: getAlarmRecordId(item)
  });
const isFavorited = (item) => {
  const value = item?.favorited ?? item?.isFavorite
  return value === 1 || value === '1' || value === true
}
  const handleAlarmAction = async (type, item) => {
    const actionText = actionTextMap[type] || '操作';
    try {
      const res = await interferenceAndExpulsionApi(buildPayload(type, item));
      if (res?.data?.success) {
        ElMessage({ type: 'success', message: actionText + '成功' });
        await fetchRealWarning();
      } else {
        ElMessage({ type: 'error', message: res?.data?.msg || actionText + '失败' });
      }
    } catch (error) {
      ElMessage({ type: 'error', message: actionText + '失败' });
const buildPayload = (type, item) => ({
  counterWay: type === 'signal' ? 1 : type === 'counter' ? 2 : '',
  alarmRecordId: getAlarmRecordId(item)
})
const handleAlarmAction = async (type, item) => {
  const actionText = actionTextMap[type] || '操作'
  try {
    const res = await interferenceAndExpulsionApi(buildPayload(type, item))
    if (res?.data?.success) {
      ElMessage({ type: 'success', message: actionText + '成功' })
      await fetchRealWarning()
    } else {
      ElMessage({ type: 'error', message: res?.data?.msg || actionText + '失败' })
    }
  };
  } catch (error) {
    ElMessage({ type: 'error', message: actionText + '失败' })
  }
}
  const handleAlarmFavorite = async (item) => {
    const alarmRecordId = getAlarmRecordId(item);
    if (!alarmRecordId) {
      ElMessage({ type: 'warning', message: '缺少告警记录ID' });
      return;
const handleAlarmFavorite = async (item) => {
  const alarmRecordId = getAlarmRecordId(item)
  if (!alarmRecordId) {
    ElMessage({ type: 'warning', message: '缺少告警记录ID' })
    return
  }
  const favorite = isFavorited(item)
  const actionText = favorite ? '取消关注' : '关注'
  try {
    const res = favorite
      ? await alarmFavoriteRemoveApi(getFavoriteId(item))
      : await alarmFavoriteSaveApi({ alarmRecordId })
    if (res?.data?.success) {
      ElMessage({ type: 'success', message: actionText + '成功' })
      await fetchRealWarning()
    } else {
      ElMessage({ type: 'error', message: res?.data?.msg || actionText + '失败' })
    }
    const favorite = isFavorited(item);
    const actionText = favorite ? '取消关注' : '关注';
    try {
      const res = favorite
        ? await alarmFavoriteRemoveApi(getFavoriteId(item))
        : await alarmFavoriteSaveApi({ alarmRecordId });
      if (res?.data?.success) {
        ElMessage({ type: 'success', message: actionText + '成功' });
        await fetchRealWarning();
      } else {
        ElMessage({ type: 'error', message: res?.data?.msg || actionText + '失败' });
      }
    } catch (error) {
      ElMessage({ type: 'error', message: actionText + '失败' });
    }
  };
  } catch (error) {
    ElMessage({ type: 'error', message: actionText + '失败' })
  }
}
  const fetchRealWarning = async () => {
    const startAt = Date.now();
    realWarningLoading.value = true;
    try {
      const params = { current: 1, size: 9999, alarmType: '1' };
      const res = await alarmLogApi(params);
      const records = res?.data?.data?.records ?? [];
      realWarningData.value = records.map((record) => ({
        ...record,
        isFavorite:
          record?.isFavorite ??
          (record?.favorited === 1 || record?.favorited === '1')
      }));
    } finally {
      const elapsed = Date.now() - startAt;
      if (elapsed < minLoadingMs) {
        await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed));
      }
      realWarningLoading.value = false;
    }
  };
const fetchRealWarning = async () => {
  // 只有超过200ms才显示loading,避免闪烁
  loadingTimer.value = setTimeout(() => {
    realWarningLoading.value = true
  }, 200)
  onMounted(() => {
    fetchRealWarning();
  });
  try {
    const params = { current: 1, size: 9999, alarmType: '1' }
    const res = await alarmLogApi(params)
    const records = res?.data?.data?.records ?? []
    realWarningData.value = records.map((record) => ({
      ...record,
      isFavorite:
        record?.isFavorite ??
        (record?.favorited === 1 || record?.favorited === '1')
    }))
  } finally {
    if (loadingTimer.value) clearTimeout(loadingTimer.value)
    realWarningLoading.value = false
  }
}
onMounted(() => {
  fetchRealWarning()
})
</script>
<style scoped lang="scss">
  .page-container {
    position: relative;
    width: 100%;
    height: 100%;
  }
.page-container {
  position: relative;
  width: 100%;
  height: 100%;
}
  .left-container,
  .right-container {
.left-container,
.right-container {
  position: absolute;
  bottom: 20px;
  width: 300px;
  z-index: 9;
  backdrop-filter: blur(2px);
  border-radius: 10px 10px 10px 10px;
  ::v-deep(.wrapper) {
    display: flex;
    flex-direction: column;
    position: absolute;
    bottom: 20px;
    top: 0;
    bottom: 0;
    padding: 20px;
    width: 300px;
    z-index: 9;
    backdrop-filter: blur(2px);
    background: rgba(0, 0, 0, 0.3);
    backdrop-filter: blur(16px);
    border-radius: 10px 10px 10px 10px;
    ::v-deep(.wrapper) {
      display: flex;
      flex-direction: column;
      position: absolute;
      top: 0;
      bottom: 0;
      padding: 20px;
      width: 300px;
      background: rgba(0,0,0,0.3);
      backdrop-filter: blur(16px);
      border-radius: 10px 10px 10px 10px;
      box-sizing: border-box;
    }
    box-sizing: border-box;
  }
}
  .left-container {
    top: 113px;
    left: 17px;
    background: linear-gradient( 270deg, rgba(17,23,34,0) 0%, rgba(17,23,34,0.56) 50%, rgba(17,23,34,0.96) 100%);
  }
.left-container {
  top: 113px;
  left: 17px;
  background: linear-gradient(270deg, rgba(17, 23, 34, 0) 0%, rgba(17, 23, 34, 0.56) 50%, rgba(17, 23, 34, 0.96) 100%);
}
  .right-container {
    top: 75px;
    right: 17px;
    background: linear-gradient( 270deg, rgba(17,23,34,0.96) 0.16%, rgba(17,23,34,0.56) 57.57%, rgba(4,30,37,0) 100%);
  }
.right-container {
  top: 75px;
  right: 17px;
  background: linear-gradient(270deg, rgba(17, 23, 34, 0.96) 0.16%, rgba(17, 23, 34, 0.56) 57.57%, rgba(4, 30, 37, 0) 100%);
}
  .legend-container {
    position: absolute;
    left: 50%;
    bottom: 22px;
    transform: translateX(-50%);
    z-index: 9;
  }
.legend-container {
  position: absolute;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  z-index: 9;
}
</style>
applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
@@ -1,18 +1,6 @@
<template>
    <div class="call-container" :class="{'incoming-call': state === 'ringing'} ">
        <div class="content-wrapper">
            <!--        <div class="input-group">
                    <div class="input-item">
                        <span class="input-label">我的 userId:</span>
                        <input v-model="uid" class="input-field" />
                    </div>
                    <div class="input-item">
                        <span class="input-label">对方 userId:</span>
                        <input v-model="peerUid" class="input-field" />
                    </div>
                </div>-->
            <div class="button-group" v-if="false">
                <button @click="connectWS" :disabled="connected" class="btn btn-connect">连接WS</button>
            </div>
@@ -20,6 +8,12 @@
            <div class="contactAvatar">
                <img :src="defaultAvatar" alt=""></img>
            </div>
            <!--  联系人名称-->
            <div class="contactName">
                {{ contactName }}
            </div>
            <div class="status-group">
                <strong v-if="state==='ringing'" class="status-text incoming-status">来电中...</strong>
                <strong v-if="state==='calling'" class="status-text calling-status">呼叫中...</strong>
@@ -38,18 +32,6 @@
        </div>
        <!--    <div class="tip-message">
            <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>
            </div>-->
        <!--    <div class="log-container">
            <div class="log-box">
                <div class="log-title">日志信息:</div>
                <pre class="log-content">{{ logText }}</pre>
            </div>
            </div>-->
        <audio ref="remoteAudio" autoplay class="hidden-audio"></audio>
    </div>
</template>
@@ -86,10 +68,7 @@
                
                state.value = 'ringing';
                log('📞 收到来电 call,来自', incomingFrom);
                // 开始来电提示(铃声和震动)
                playRingtone();
                startVibration();
            }
        } catch (e) {
            log('❌ 解析voiceparams参数失败:', String(e));
@@ -97,26 +76,30 @@
    }
    
    // 2. 同时保留原有的URL参数解析逻辑,作为备选
    const url = new URL(window.location.href);
    const paramsStr = url.searchParams.get('params');
    if (paramsStr) {
        try {
            const params = JSON.parse(decodeURIComponent(paramsStr));
            if (params.peerUid) {
                peerUid.value = String(params.peerUid);
                log('🔗 从URL参数获取peerUid:', params.peerUid);
            }
        } catch (e) {
            log('❌ 解析URL参数失败:', String(e));
        }
    }
    // const url = new URL(window.location.href);
    // const paramsStr = url.searchParams.get('params');
    // if (paramsStr) {
    //     try {
    //         const params = JSON.parse(decodeURIComponent(paramsStr));
    //         if (params.peerUid) {
    //             peerUid.value = String(params.peerUid);
    //             log('🔗 从URL参数获取peerUid:', params.peerUid);
    //         }
    //     } catch (e) {
    //         log('❌ 解析URL参数失败:', String(e));
    //     }
    // }
}
// 从URL参数或本地存储获取用户ID
const getUserIdFromStorage = () => {
    // 从url获取用户id
    const contact = JSON.parse(decodeURIComponent(receiveParameters.value)).userId
    console.log('从url获取用户id',contact)
    // 从本地获取用户id
    const userId = localStorage.getItem('userId') || sessionStorage.getItem('userId')
    console.log('从本地存储获取userId:', userId)
    if (userId) {
        log('🔗 从本地存储获取userId:', userId)
        return userId
    }
    return '3' // 默认值
@@ -124,6 +107,8 @@
const uid = ref('3')
const peerUid = ref('2')
// 联系人名称
const contactName = ref('张三')
const connected = ref(false)
const state = ref('idle') // idle | calling | ringing | in-call
@@ -133,19 +118,6 @@
const callEnded = ref(false)
const endMessage = ref('对方已挂断')
const endTimer = ref(null)
/**
 * 根据状态返回对应颜色
 */
const getStateColor = (state) => {
    const colors = {
        'idle': '#666',
        'calling': '#2196F3',
        'ringing': '#FF9800',
        'in-call': '#4CAF50'
    }
    return colors[state] || '#666'
}
let ws = null
let pc = null
@@ -185,76 +157,6 @@
    }, 25000)
}
// 来电提示相关
let ringtoneAudio = null
let vibrationTimer = null
// 播放来电铃声
function playRingtone() {
    // 创建音频对象(使用默认的系统提示音)
    ringtoneAudio = new Audio('data:audio/wav;base64,UklGRigAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQAAAAAAA==');
    ringtoneAudio.loop = true;
    ringtoneAudio.volume = 0.5;
    try {
        ringtoneAudio.play();
        log('🔊 开始播放来电铃声')
    } catch (e) {
        log('❌ 播放来电铃声失败:', String(e))
    }
}
// 停止来电铃声
function stopRingtone() {
    if (ringtoneAudio) {
        try {
            ringtoneAudio.pause();
            ringtoneAudio.currentTime = 0;
            ringtoneAudio = null;
            log('🔇 停止播放来电铃声')
        } catch (e) {
            log('❌ 停止来电铃声失败:', String(e))
        }
    }
}
// 开始震动
function startVibration() {
    // 检查浏览器是否支持震动 API
    if ('vibrate' in navigator) {
        // 震动模式:震动1秒,暂停0.5秒,重复
        const vibrationPattern = [1000, 500];
        try {
            navigator.vibrate(vibrationPattern);
            vibrationTimer = setInterval(() => {
                navigator.vibrate(vibrationPattern);
            }, 1500); // 每1.5秒重复一次震动模式
            log('📳 开始震动提示')
        } catch (e) {
            log('❌ 震动失败:', String(e))
        }
    } else {
        log('ℹ️ 当前浏览器不支持震动功能')
    }
}
// 停止震动
function stopVibration() {
    if (vibrationTimer) {
        clearInterval(vibrationTimer);
        vibrationTimer = null;
    }
    if ('vibrate' in navigator) {
        try {
            navigator.vibrate(0); // 停止震动
            log('📳 停止震动提示')
        } catch (e) {
            log('❌ 停止震动失败:', String(e))
        }
    }
}
function stopPing() {
    if (pingTimer) clearInterval(pingTimer)
    pingTimer = null
@@ -297,7 +199,7 @@
            send('candidate', peerUid.value, { candidate: e.candidate.toJSON ? e.candidate.toJSON() : e.candidate })
        }
    }
// 步骤七:建立连接
    pc.ontrack = (e) => {
        log('✅ 收到远端音频流')
        const stream = e.streams[0]
@@ -312,7 +214,7 @@
        log('pc.connectionState =', pc.connectionState)
    }
}
// 步骤六:连接音频
async function getLocalAudio() {
    if (localStream) return localStream
@@ -330,7 +232,7 @@
        throw e
    }
}
// 步骤5 确保本地音频流已添加
/** ✅ 关键:只 addTrack 一次,避免 “sender already exists” */
async function ensureLocalTracksAdded() {
    if (!pc) createPC()
@@ -351,7 +253,7 @@
    return stream
}
// 步骤4 发送本地音频流
async function flushCandidates() {
    if (!pc || !pc.remoteDescription) return
    while (pendingCandidates.length) {
@@ -391,12 +293,7 @@
        offeredByPeer = false
        state.value = 'ringing'
        log('📞 收到来电 call,来自', incomingFrom)
        // 开始来电提示(铃声和震动)
        playRingtone()
        startVibration()
            log('📞 收到来电 call,来自', incomingFrom)
        return
    }
@@ -426,12 +323,12 @@
            lastOfferSdp = msg.payload?.sdp
            return
        }
  // 步骤2
        // 已接听:直接 answer
        await answerOffer(msg.payload?.sdp, incomingFrom)
        return
    }
// 步骤3 接收 answer
    if (t === 'answer') {
        log('✅ 收到 answer,通话建立')
        if (!pc) createPC()
@@ -500,7 +397,7 @@
        onSignal(msg)
    }
}
// 发送呼叫信息
function requestCall() {
    state.value = 'calling'
    incomingFrom = null
@@ -511,14 +408,10 @@
    send('call', peerUid.value, null)
    log('➡️ 发起呼叫 call 给', peerUid.value)
}
// 接收信息
function acceptCall() {
    if (!incomingFrom) return
    acceptedByMe = true
    // 停止来电提示
    stopRingtone()
    stopVibration()
    // 先告诉对方我接听了(后端会把双方置忙)
    send('accept', incomingFrom, null)
@@ -535,10 +428,6 @@
function rejectCall() {
    if (!incomingFrom) return
    // 停止来电提示
    stopRingtone()
    stopVibration()
    // 你后端没有 reject,用 busy 表示拒绝/不可接听
    send('busy', incomingFrom, null)
@@ -563,14 +452,14 @@
    // ✅ 不会重复 addTrack
    await ensureLocalTracksAdded()
// 步骤1:创建 offer
    const offer = await pc.createOffer()
    await pc.setLocalDescription(offer)
    send('offer', peerUid.value, { sdp: pc.localDescription })
    log('➡️ 已发送 offer 给', peerUid.value)
}
// 步骤2:等待 answer
async function answerOffer(offerSdp, from) {
    if (!offerSdp) {
        log('❌ offer sdp 为空,无法接听')
@@ -605,9 +494,6 @@
    } catch {}
    stopTimer()
    // 停止来电提示
    stopRingtone()
    stopVibration()
    if (pc) {
        pc.getSenders().forEach(s => s.track && s.track.stop())
@@ -650,9 +536,8 @@
        callEnded.value = true
        endMessage.value = '对方已挂断'
        
        // 延迟10秒后跳转页面
        // 延迟5秒后跳转页面
        endTimer.value = setTimeout(() => {
            log('⏰ 通话结束10秒后自动返回')
            // 返回上一页(uni-app的语音通话列表页面)
            if (window.uni) {
                window.uni.navigateBack()
@@ -670,7 +555,7 @@
onMounted(() => {
    console.log('收拾收拾',route.query.voiceparams);
    console.log('接收参数', receiveParameters.value)
    console.log('接收参数', JSON.parse(decodeURIComponent(receiveParameters.value)))
    // 解析并使用contact参数
    if (receiveParameters.value) {
@@ -678,8 +563,9 @@
            const contact = JSON.parse(decodeURIComponent(receiveParameters.value))
            if (contact.friendId) {
                peerUid.value = contact.friendId
                contactName.value = contact.friendNickName || '未知联系人'
                log('🔗 从contact参数获取peerUid:', contact.friendId)
                log('👤 联系人名称:', contact.friendNickName || '未知联系人')
                log('👤 联系人名称:', contactName.value)
            }
        } catch (e) {
            log('❌ 解析contact参数失败:', String(e))
@@ -863,6 +749,17 @@
    margin-bottom: 20px;
}
    /* 联系人名称样式 */
    .contactName {
        font-size: 18px;
        font-weight: bold;
        color: #333;
        text-align: center;
        margin-top: 10px;
            margin-bottom: 40px;
    }
/* 来电状态样式 */
.status-text.incoming-status {
    font-size: 24px;
@@ -939,7 +836,6 @@
    display: flex;
    justify-content: center;
    align-items: center;
    margin-bottom: 40px;
    border-radius: 50%;
    background-color: #f0f0f0;
    overflow: hidden;