forked from drone/command-center-dashboard

chenyao
2025-04-18 980017c9508f8b8449e773d8913b3b407f0a3416
Merge branch 'master' of http://139.196.74.78:10010/r/drone/command-center-dashboard
17 files modified
2 files added
1616 ■■■■ changed files
src/api/drc.js 9 ●●●●● patch | view | raw | blame | history
src/api/payload.js 11 ●●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/ControlPanel/BaseControl.vue 1 ●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/ControlPanel/ControlPanel.vue 1 ●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/CurrentTaskDetails.vue 21 ●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/TaskDetailsLeft.vue 21 ●●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/TaskDetailsRight.vue 4 ●●●● patch | view | raw | blame | history
src/hooks/components/EventPopUpBox.vue 16 ●●●● patch | view | raw | blame | history
src/hooks/components/useMapHandlerClick.js 89 ●●●● patch | view | raw | blame | history
src/hooks/controlDrone/useManualControl.js 130 ●●●● patch | view | raw | blame | history
src/hooks/useMapAggregation/useMapAggregation.js 139 ●●●● patch | view | raw | blame | history
src/hooks/useSingleDroneMap/useSingleDroneMap.js 135 ●●●● patch | view | raw | blame | history
src/mqtt/index.js 2 ●●● patch | view | raw | blame | history
src/utils/stateToImageMap/drone.js 32 ●●●●● patch | view | raw | blame | history
src/utils/stateToImageMap/event.js 23 ●●●●● patch | view | raw | blame | history
src/views/Home/SearchBox.vue 442 ●●●● patch | view | raw | blame | history
src/views/SignMachineNest/MachineLeft/InspectionRaskDetails.vue 404 ●●●● patch | view | raw | blame | history
src/views/SignMachineNest/MachineRight/MachineMonitor.vue 135 ●●●● patch | view | raw | blame | history
src/views/SignMachineNest/SignMachineNest.vue 1 ●●●● patch | view | raw | blame | history
src/api/drc.js
@@ -30,6 +30,15 @@
  })
}
// 无人控制
export async function updateDroneQualityApi (data) {
  return request({
    url: '/drone-device-core/manage/api/v1/live/streams/update',
    method: 'post',
    data,
  })
}
// 无人机退出控制
export async function exitController(data) {
  return request({
src/api/payload.js
@@ -53,12 +53,21 @@
// 相机参数调整
export async function cameraParamsChangeApi(data) {
  return await request({
    url:`/drone-device-core/payload-control/payload/zoom/level`,
    url:`${API_PREFIX}/devices/payload-control/payload/zoom/level`,
    method:'post',
    data
  })
}
// 云台重置
export async function ptzResetModeApi(params) {
  return await request({
    url:`${API_PREFIX}/devices/payload-control/payload/reset`,
    method:'get',
    params
  })
}
// 切换直播镜头
src/components/CurrentTaskDetails/ControlPanel/BaseControl.vue
@@ -55,6 +55,7 @@
}
function turnBack() {
    isAutoControl.value = true
    EventBus.emit('controlPanel-returnOrCancelReturn')
}
</script>
src/components/CurrentTaskDetails/ControlPanel/ControlPanel.vue
@@ -88,6 +88,7 @@
                        :style="item.style"
                        class="ptzControlItem"
                        @mousedown="onMouseDown(item.key)"
                        @mouseup="onMouseUp"
                    ></div>
                    <div
src/components/CurrentTaskDetails/CurrentTaskDetails.vue
@@ -18,8 +18,8 @@
            <TaskDetailsRight v-if="isAutoControl" />
            <template v-else>
                <TaskDetailsHead />
                <TaskDetailsLeft />
            </template>
            <TaskDetailsLeft />
            <!--    控制面板,里面有方法需要立即执行,不可用v-if        -->
            <ControlPanel v-show="!isAutoControl" />
@@ -45,7 +45,7 @@
import amplifyImg from '@/assets/images/taskManagement/taskIntermediateContent/amplifyImg.png'
import { ElMessage } from 'element-plus'
import EventBus from '@/event-bus'
import { updateDroneQualityApi } from '@/api/drc'
const isAutoControl = ref(true)
const lineQuality = ref(1) //1流畅,2标清
@@ -67,7 +67,7 @@
const currentLiveUrl = ref('')
const isTakeOff = ref(false)
const isMaxMap = ref(false)
let droneWebSocket  //WS实例
let droneWebSocket //WS实例
// 机巢直播
const getDeviceLiveUrl = async () => {
@@ -75,10 +75,19 @@
    currentLiveUrl.value = res.data.data.rtcs_url
}
// 无人机直播画质切换
const changeLineQuality = async () => {
const video_id = ref('')
// 获取无人机直播url
async function getDroneLiveUrl() {
    const res = await liveStart(droneSn.value, lineQuality.value)
    currentLiveUrl.value = res.data.data.rtcs_url
    video_id.value = res.data.data.video_id
}
// 无人机直播画质切换
const changeLineQuality = async () => {
    const res = await updateDroneQualityApi({ video_id: video_id.value, video_quality: lineQuality.value })
    ElMessage.success('切换画质成功')
}
// 设置当前直播地址
@@ -89,7 +98,7 @@
    // 如果还是之前的状态,不切换
    if (isTakeOff.value === currentIsTakeOff) return
    isTakeOff.value = currentIsTakeOff
    isTakeOff.value ? await changeLineQuality() : await getDeviceLiveUrl()
    isTakeOff.value ? await getDroneLiveUrl() : await getDeviceLiveUrl()
}
// 获取任务详情获取航线文件
src/components/CurrentTaskDetails/TaskDetailsLeft.vue
@@ -11,8 +11,8 @@
            {{ item.name }}相机
        </div>
        <div class="multiCol">
            <div @click="faceToCenter">云台回中</div>
            <div @click="faceToDown">云台朝下</div>
            <div @click="ptzResetMode(0)">云台回中</div>
            <div @click="ptzResetMode(1)">云台朝下</div>
        </div>
        <el-select class="qualityChange" v-model="lineQuality" @change="qualityChange">
            <el-option v-for="item in qualityList" :key="item.id" :label="item.name" :value="item.id" />
@@ -30,8 +30,9 @@
            <el-slider
                v-model="cameraParams.zoom_factor"
                vertical
                :max="cameraParams.camera_type === 'ir' ? 20 : 50"
                @input="sliderChange"
                :min="cameraParams.camera_type === 'ir' ? 2 : 0"
                :max="cameraParams.camera_type === 'ir' ? 200 : 200"
                @change="sliderChange"
            />
        </div>
    </div>
@@ -39,7 +40,7 @@
<script setup>
import EventBus from '@/event-bus'
import { callPhotoAndVideoCmd, cameraParamsChangeApi } from '@/api/payload'
import { callPhotoAndVideoCmd, cameraParamsChangeApi, ptzResetModeApi } from '@/api/payload'
import { ElMessage } from 'element-plus'
import { throttle } from 'lodash'
@@ -65,9 +66,9 @@
    { name: '超清', id: 4 },
]
function faceToCenter() {}
function faceToDown() {}
function ptzResetMode(resetMode) {
    ptzResetModeApi({ sn: dockSn.value, resetMode })
}
// 倍率调整
function sliderChange(val) {
@@ -82,9 +83,11 @@
// 修改相机参数
function cameraParamsChange() {
    const { camera_type, zoom_factor} = cameraParams.value
    cameraParamsChangeApi({
        sn: dockSn.value,
        ...cameraParams.value,
        zoom_factor: camera_type === 'wide' ? undefined : zoom_factor,
    })
}
@@ -97,7 +100,7 @@
        cameraParams.value.zoom_factor = 5
    }
    if (row.key === 'ir') {
        cameraParams.value.zoom_factor = 0
        cameraParams.value.zoom_factor = 2
    }
    cameraParamsChange()
}
src/components/CurrentTaskDetails/TaskDetailsRight.vue
@@ -54,6 +54,10 @@
    gap: 25px 0;
    align-items: center;
    .manualControl{
        background: transparent !important;
    }
    .titleImg {
        width: 224px;
        height: 131px;
src/hooks/components/EventPopUpBox.vue
@@ -31,7 +31,7 @@
    </div>
</template>
<script setup>
import { ElImage } from 'element-plus'
import { ElImage, ElIcon } from 'element-plus'
// src="@/assets/images/home/useEventOperate/eventErr.png"
import { Close } from '@element-plus/icons-vue'
@@ -54,10 +54,20 @@
    loading.value = true
    const res = await getEventDetails({ id: props.data.eventId })
    info.value = res.data.data
    info.value.url = import.meta.env.VITE_APP_AIRLINE_URL + info.value.url
    info.value.url = disposeUrl(info.value)
    loading.value = false
})
const disposeUrl = ({ url, media_type }) => {
    if (media_type == 'PHOTO_TYPE') {
        const parts = url.split('.')
        parts[parts.length - 2] += '_small' // 在最后一个点前插入 _small
        return import.meta.env.VITE_APP_AIRLINE_URL + parts.join('.')
    }
    return import.meta.env.VITE_APP_AIRLINE_URL + url
}
</script>
<style scoped lang="scss">
src/hooks/components/useMapHandlerClick.js
@@ -2,33 +2,72 @@
 * @Author: shuishen 1109946754@qq.com
 * @Date: 2025-04-15 22:41:40
 * @LastEditors: shuishen 1109946754@qq.com
 * @LastEditTime: 2025-04-16 19:02:39
 * @LastEditTime: 2025-04-17 19:43:36
 * @FilePath: \command-center-dashboard\src\hooks\components\useMapHandlerClick.js
 * @Description: 
 * 
 * Copyright (c) 2025 by shuishen, All Rights Reserved. 
 */
import * as Cesium from 'cesium'
import { render } from 'vue'
import { useStore } from 'vuex'
import DevicePopUpBox from '@/hooks/components/DevicePopUpBox.vue'
import EventPopUpBox from '@/hooks/components/EventPopUpBox.vue'
/**
 * 
 * @param {Object} options - 配置选项
 */
export function useMapHandlerClick (viewer, options = {
  type: 'single-drone-event',
  customDom: EventPopUpBox
}) {
  const { type } = options
export function useMapHandlerClick (options = {}) {
  const {
    popupType = 'event-popup',
    eventType = 'single-drone-event',
    styleTransform = 'translate(-50%,-110%)',
    getViewer
  } = options
  const currentClickEntity = ref(null)
  const store = useStore()
  const popupData = {
    'event-popup': EventPopUpBox,
    'device-popup': DevicePopUpBox
  }
  const MapPopUpBox = popupData[popupType]
  let viewer = null
  let handler = null
  let currentClickEntity = null
  // 查找特定类型的实体
  const findEntityByType = (entities, type) => {
    return entities.find(entity =>
      entity?.properties?.customData?._value?.data?.type === type
    )
    let types = []
    Array.isArray(type) ? types = type : types = [type]
    return types.reduce((pre, curType) => {
      let entity = entities.find(entity => entity?.properties?.customData?._value?.data?.type === curType)
      return entity ? (pre.push({
        type: curType,
        entity
      }), pre) : pre
    }, [])
  }
  const publicEvent = (entity) => {
    viewer.scene.postRender.addEventListener(labelBoxRender)
  }
  const typeEvent = {
    'deviceAggregation': publicEvent,
    'single-drone-event': publicEvent,
    'event': publicEvent,
    'device': (entity) => {
      const device = entity.properties.customData._value.data
      store.commit('setSingleUavHome', device)
    }
  }
  // 左键单机事件
@@ -36,17 +75,21 @@
    let clickedEntities = viewer?.scene.drillPick(click.position).map(item => item.id)
    if (!clickedEntities.length) return
    currentClickEntity.value = findEntityByType(clickedEntities, type)
    let curClick = findEntityByType(clickedEntities, eventType)
    removeLabel()
    if (currentClickEntity.value) {
      viewer?.scene.postRender.addEventListener(labelBoxRender)
    if (curClick.length > 0 && typeEvent[curClick[0].type]) {
      currentClickEntity = curClick[0].entity
      typeEvent[curClick[0].type](currentClickEntity)
    }
  }
  // 事件初始化
  const handlerInit = () => {
    !viewer && (viewer = getViewer())
    if (handler) return
    handler = new Cesium.ScreenSpaceEventHandler(viewer?.scene.canvas)
@@ -62,7 +105,7 @@
  // 获取弹框box
  const getLabelDom = data => {
    const vNode = h(customDom, { data, removeLabel })
    const vNode = h(MapPopUpBox, { data, removeLabel })
    const tooltipContainer = document.createElement('div')
    tooltipContainer.id = 'mapPopUpBox'
    tooltipContainer.style.position = 'absolute'
@@ -75,12 +118,12 @@
  // 弹框位置刷新
  const labelBoxRender = () => {
    if (!currentEntity) return
    if (!currentClickEntity) return
    let dom = document.querySelector('#mapPopUpBox')
    if (!dom) {
      dom = getLabelDom(currentEntity.properties.customData._value.data)
      dom = getLabelDom(currentClickEntity.properties.customData._value.data)
    }
    const screenPosition = viewer?.scene.cartesianToCanvasCoordinates(currentEntity?.position?._value)
    const screenPosition = viewer?.scene.cartesianToCanvasCoordinates(currentClickEntity?.position?._value)
    if (screenPosition) {
      dom.style.left = `${screenPosition.x}px`
      dom.style.top = `${screenPosition.y}px`
@@ -101,15 +144,19 @@
    removeDom()
  }
  // 自动清理
  onUnmounted(() => {
  const removeAll = () => {
    removeHandler()
    removeLabel()
  }
  // 自动清理
  onUnmounted(() => {
    removeAll()
  })
  return {
    currentClickEntity,
    handlerInit,
    removeHandler
    removeAll,
    removeLabel
  }
}
src/hooks/controlDrone/useManualControl.js
@@ -36,11 +36,18 @@
    }
export function useManualControl(mqttState,deviceTopicInfo, isCurrentFlightController,paramsRef) {
    const activeCodeKey = ref(null)
    let activeCodeKey = null
    let genPortOne = true //是一代机场
    const mqttHooks = useMqtt(mqttState,deviceTopicInfo)
    let seq = 0
    let throttledPtz = null
    const keysPressed = [
        {key: 'KeyQ', value: false},
        {key: 'KeyW', value: false},
        {key: 'KeyE', value: false},
        {key: 'KeyA', value: false},
        {key: 'KeyS', value: false},
        {key: 'KeyD', value: false},
    ]
    function handlePublish(params) {
        const body = {
@@ -51,7 +58,7 @@
        myInterval = setInterval(() => {
            body.data.seq = seq++
            seq++
            window.console.log('keyCode>>>>', activeCodeKey.value, body)
            window.console.log('keyCode>>>>', activeCodeKey, body)
            mqttHooks?.publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 0 })
        }, 50)
    }
@@ -71,84 +78,137 @@
        const HEIGHT = genPortOne ? 5 : 500; //  check
        const W_SPEED = genPortOne ? 20 : 500 // 机头角速度
        let arrList = []
        if (genPortOne) {
            arrList = [
                { Key: 'KeyQ', category: 'w', value: -W_SPEED },{ Key: 'KeyW',category: 'x', value: SPEED },
                { Key: 'KeyE', category: 'w', value: W_SPEED},{ Key: 'KeyA', category: 'y', value: -SPEED},
                { Key: 'KeyS', category: 'x', value: -SPEED},{ Key: 'KeyD', category: 'y', value: SPEED}
            ]
        } else {
            arrList = [
                { Key: 'KeyQ', category: 'yaw', value: 1024-W_SPEED },{ Key: 'KeyW',category: 'pitch', value: 1024+SPEED },
                { Key: 'KeyE', category: 'yaw', value: 1024+W_SPEED},{ Key: 'KeyA', category: 'roll', value: 1024-SPEED},
                { Key: 'KeyS', category: 'pitch', value: 1024-SPEED},{ Key: 'KeyD', category: 'roll', value: 1024+SPEED}
            ]
        }
        let resultObj = []
        let resultArr = []
        // 遍历被选中得数组
        if (keyCode === 'assemblyKeys') {
            keysPressed.map(key => {
                if (key.value) {
                    let result = arrList.filter(item =>item.Key===key.key)
                    resultObj.push(result[0])
                }
            })
            // 相同属性值进行计算
            resultArr = resultObj.reduce((acc, item) => {
                // 如果累加器中已经有这个 category,则累加 value
                if (acc[item.category]) {
                    // acc[item.category] += item.value;
                    acc[item.category] = genPortOne ? 0 : 1024;
                } else {
                    // 否则,初始化这个 category 的 value
                    acc[item.category] = item.value;
                }
                return acc;
            }, {});
        }
        seq = 0
        switch (keyCode) {
            case 'assemblyKeys': // 针对Q W E A S D 组合键
                if (activeCodeKey === keyCode) return
                handlePublish(resultArr)
                activeCodeKey = keyCode
                break
            case 'KeyQ':
                if (activeCodeKey.value === keyCode) return
                if (activeCodeKey === keyCode) return
                let objQ = { yaw: 1024-W_SPEED }
                if (genPortOne) {objQ = {w: -W_SPEED}}
                handlePublish(objQ)
                activeCodeKey.value = keyCode
                activeCodeKey = keyCode
                break
            case 'KeyW':
                if (activeCodeKey.value === keyCode) return
                if (activeCodeKey === keyCode) return
                let objW = { pitch: 1024+SPEED }
                if (genPortOne) {objW = {x: SPEED}}
                handlePublish(objW)
                activeCodeKey.value = keyCode
                activeCodeKey = keyCode
                break
            case 'KeyE':
                if (activeCodeKey.value === keyCode) return
                if (activeCodeKey === keyCode) return
                let objE = { yaw: 1024+W_SPEED }
                if (genPortOne) {objE = {w: W_SPEED}}
                handlePublish(objE)
                activeCodeKey.value = keyCode
                activeCodeKey = keyCode
                break
            case 'KeyA':
                if (activeCodeKey.value === keyCode) return
                if (activeCodeKey === keyCode) return
                let objA = { roll: 1024-SPEED }
                if (genPortOne) {objA = {y: -SPEED}}
                handlePublish(objA)
                activeCodeKey.value = keyCode
                activeCodeKey = keyCode
                break
            case 'KeyS':
                if (activeCodeKey.value === keyCode) return
                if (activeCodeKey === keyCode) return
                let objS = { pitch: 1024-SPEED }
                if (genPortOne) {objS = {x: -SPEED}}
                handlePublish(objS)
                activeCodeKey.value = keyCode
                activeCodeKey = keyCode
                break
            case 'KeyD':
                if (activeCodeKey.value === keyCode) return
                if (activeCodeKey === keyCode) return
                let objD = { roll: 1024+SPEED }
                if (genPortOne) {objD = {y: SPEED}}
                handlePublish(objD)
                activeCodeKey.value = keyCode
                activeCodeKey = keyCode
                break
            case 'KeyZ':
                if (activeCodeKey.value === keyCode) return
                if (activeCodeKey === keyCode) return
                let objZ = { throttle: 1024-HEIGHT }
                if (genPortOne) {objZ = {h: -HEIGHT}}
                handlePublish(objZ)
                activeCodeKey.value = keyCode
                activeCodeKey = keyCode
                break
            // case 'ArrowDown':
            case 'KeyC':
                if (activeCodeKey.value === keyCode) return
                if (activeCodeKey === keyCode) return
                let objC = { throttle: 1024+HEIGHT }
                if (genPortOne) {objC = {h: HEIGHT}}
                handlePublish(objC)
                activeCodeKey.value = keyCode
                activeCodeKey = keyCode
                break
            case 'ArrowUp':
                ptzThrottle[0]()
                if (activeCodeKey === keyCode) return
                clearInterval(myInterval)
                myInterval = setInterval(()=>ptzControl('up'), 200)
                activeCodeKey = keyCode
                break
            case 'ArrowDown':
                ptzThrottle[1]()
                if (activeCodeKey === keyCode) return
                clearInterval(myInterval)
                myInterval = setInterval(()=>ptzControl('down'), 200)
                activeCodeKey = keyCode
                break
            case 'ArrowLeft':
                ptzThrottle[2]()
                if (activeCodeKey === keyCode) return
                clearInterval(myInterval)
                myInterval = setInterval(()=>ptzControl('left'), 200)
                activeCodeKey = keyCode
                break
            case 'ArrowRight':
                ptzThrottle[3]()
                if (activeCodeKey === keyCode) return
                clearInterval(myInterval)
                myInterval = setInterval(()=>ptzControl('right'), 200)
                activeCodeKey = keyCode
                break
            default:
                break
        }
    }
    // 云台控制节流list
    const ptzThrottle = ['up','down','left','right'].map(key => throttle(()=>ptzControl(key), 500))
    // 云台上下左右
    function ptzControl(key) {
@@ -175,7 +235,8 @@
    }
    function resetControlState() {
        activeCodeKey.value = null
        keysPressed.map(item => item.value = false)
        activeCodeKey = null
        seq = 0
        handleClearInterval()
    }
@@ -185,7 +246,16 @@
    }
    function onKeydown(e) {
        handleKeyup(e.code)
        e.preventDefault()
        keysPressed.forEach(item => {
            if(e.code === item.key) item.value = true
        })
        let result = keysPressed.filter(item=>item.value===true)
        if (result && result.length >= 2) {
            handleKeyup('assemblyKeys')
        } else {
            handleKeyup(e.code)
        }
    }
    function startKeyboardManualControl() {
@@ -211,6 +281,10 @@
        { immediate: true }
    )
    onBeforeUnmount(()=>{
        mqttHooks?.d
    })
    onUnmounted(() => {
        closeKeyboardManualControl()
    })
src/hooks/useMapAggregation/useMapAggregation.js
@@ -3,30 +3,19 @@
import eventAggregationImg from '@/assets/images/home/useUavHome/eventAggregationImg.png'
import uavImg from '@/assets/images/home/useUavHome/uavImg.png'
// 事件图标
import eventSingle from '@/assets/images/home/useEventOperate/eventSingle.png'
import { getEventImage } from '@/utils/stateToImageMap/event'
import { getDroneStatusImage } from '@/utils/stateToImageMap/drone'
import eventPending from '@/assets/images/home/useEventOperate/eventPending.png' // 待处理 0
import eventWaitAudit from '@/assets/images/home/useEventOperate/eventWaitAudit.png' // 待审核 2
import eventProcessing from '@/assets/images/home/useEventOperate/eventProcessing.png' // 处理中 3
import eventCompleted from '@/assets/images/home/useEventOperate/eventCompleted.png' // 已完成 4
import eventClosed from '@/assets/images/home/useEventOperate/eventClosed.png' // 已完结 5
import DevicePopUpBox from '@/hooks/components/DevicePopUpBox.vue'
import EventPopUpBox from '@/hooks/components/EventPopUpBox.vue'
// 机巢图标
import endingImg from '@/assets/images/aiNowFly/ending.png'
import endingHighImg from '@/assets/images/aiNowFly/ending-high.png'
import { render } from 'vue'
import { useStore } from 'vuex'
import { getCenterPoint } from '@/utils/cesium/mapUtil'
import cesiumOperation from '@/utils/cesium-tsa'
import { getDeviceRegion, getDeviceRegionCount, getEventDetails, getMapEvents } from '@/api/home/aggregation'
import { PolyGradientMaterial } from '@/utils/cesium/Material'
import { start } from 'nprogress'
// hook
import { useMapHandlerClick } from '@/hooks/components/useMapHandlerClick'
/**
 * 机巢聚合功能
 */
@@ -51,8 +40,21 @@
  const { flyTo } = cesiumOperation()
  const mergeImg = type === 'device' ? aggregationImg : eventAggregationImg
  const MapPopUpBox = type === 'device' ? DevicePopUpBox : EventPopUpBox
  const styleTransform = type === 'device' ? 'translateY(-50%)' : 'translate(-50%,-110%)'
  const eventType = type === 'device' ? ['deviceAggregation', 'device'] : 'event'
  const {
    handlerInit: mapHandlerInit,
    removeAll: removeMapHandlerAll,
    removeLabel: removeMapLabel
  } = useMapHandlerClick({
    popupType: `${type}-popup`,
    eventType: eventType,
    styleTransform,
    getViewer () {
      return viewer
    }
  })
  let scalingJudgment = [
    { name: '县', splashedList: [], gJson: null, show: false, outline: {}, value: [0, 48651], height: 31753 },
@@ -69,8 +71,6 @@
  ]
  let viewer = null
  let active = null
  let handler = null
  let currentEntity = null
  const store = useStore()
  const userAreaCode = computed(() => store.state.user.userInfo.detail.areaCode)
@@ -104,7 +104,7 @@
        if (active === item.name) return
        active = item.name
        removeEntities()
        removeLabel()
        removeMapLabel()
        renderOutline(item)
        if (!item.gJson && !item.splashedList?.length) return
        item.gJson ? aggregation(item) : splashed(item)
@@ -288,7 +288,7 @@
      needFly = true
      if (!viewer) return
      handlerInit()
      mapHandlerInit()
      viewer.scene.postRender.removeEventListener(determineScaling)
@@ -302,15 +302,7 @@
  //散点机巢
  function splashed (row) {
    row.splashedList.forEach((item, index) => {
      const eventImage = {
        0: eventPending,
        2: eventWaitAudit,
        3: eventProcessing,
        4: eventCompleted,
        5: eventClosed
      }
      const image = type === 'device' ? (item.status === "OFFLINE" ? endingHighImg : endingImg) : eventImage[item.status] || eventSingle
      const image = type === 'device' ? getDroneStatusImage(item.status) : getEventImage(item.status)
      viewer.entities.add({
        id: `aggregation-splashed-${index}`,
@@ -470,33 +462,6 @@
    })
  }
  // 获取弹框box
  const getLabelDom = data => {
    const vNode = h(MapPopUpBox, { data, removeLabel })
    const tooltipContainer = document.createElement('div')
    tooltipContainer.id = 'mapPopUpBox'
    tooltipContainer.style.position = 'absolute'
    tooltipContainer.style.transform = styleTransform
    tooltipContainer.style.pointerEvents = 'none'
    document.querySelector('.page-index').append(tooltipContainer)
    render(vNode, tooltipContainer)
    return tooltipContainer
  }
  // 弹框位置刷新
  const labelBoxRender = () => {
    if (!currentEntity) return
    let dom = document.querySelector('#mapPopUpBox')
    if (!dom) {
      dom = getLabelDom(currentEntity.properties.customData._value.data)
    }
    const screenPosition = viewer.scene.cartesianToCanvasCoordinates(currentEntity?.position?._value)
    if (screenPosition) {
      dom.style.left = `${screenPosition.x}px`
      dom.style.top = `${screenPosition.y}px`
      dom.style.display = 'block'
    }
  }
  /**
   * 根据条件获取项
@@ -506,41 +471,6 @@
   */
  const findTypeItem = (arr, condition) => {
    return arr.find(item => condition(item))
  }
  // 左键单机事件
  const singleMachineEvent = async click => {
    let clickTargets = viewer.scene.drillPick(click.position).map(item => item.id)
    if (!clickTargets.length) return
    let deviceAggregationFind = findTypeItem(
      clickTargets,
      item => item?.properties?.customData?._value?.data?.type === 'deviceAggregation'
    )
    let deviceFind = findTypeItem(clickTargets, item => item?.properties?.customData?._value?.data?.type === 'device')
    // "event"
    let eventFind = findTypeItem(clickTargets, item => item?.properties?.customData?._value?.data?.type === 'event')
    // let eventFind = findTypeItem(clickTargets, (item) => item?.properties?.customData?._value?.data?.type === 'eventAggregation')
    currentEntity = deviceAggregationFind || deviceFind || eventFind
    if (!currentEntity) return
    if (!currentEntity?.position?._value) return
    // 一定要移除
    removeLabel()
    if (deviceAggregationFind || eventFind) {
      viewer.scene.postRender.addEventListener(labelBoxRender)
    }
    if (deviceFind) {
      const device = deviceFind.properties.customData._value.data
      store.commit('setSingleUavHome', device)
    }
  }
  const removeDom = () => {
    const dom = document.querySelector('#mapPopUpBox')
    if (dom && dom.parentNode) {
      dom.parentNode.removeChild(dom)
    }
  }
  // 移除 点 和 gjson 实体
@@ -556,11 +486,7 @@
      item.includes('aggregation-') && viewer.entities.removeById(item)
    })
  }
  // 移除弹框标签
  const removeLabel = () => {
    viewer?.scene.postRender.removeEventListener(labelBoxRender)
    removeDom()
  }
  // 移除所有监听事件,变量置空
  const removeAll = () => {
@@ -571,14 +497,12 @@
  const clearMapEntity = () => {
    if (!viewer) return
    removeEntities()
    removeLabel()
    removeMapHandlerAll()
    // viewer.camera.moveEnd.removeEventListener(determineScaling);
    viewer.scene.postRender.removeEventListener(determineScaling)
    handler?.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK)
    handler?.destroy()
    active = null
    handler = null
    currentEntity = null
  }
  const init = () => {
@@ -588,14 +512,7 @@
      viewer.scene.postRender.addEventListener(determineScaling)
    })
    handlerInit()
  }
  const handlerInit = () => {
    if (handler) return
    handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
    handler.setInputAction(singleMachineEvent, Cesium.ScreenSpaceEventType.LEFT_CLICK)
    mapHandlerInit()
  }
  onBeforeUnmount(() => { })
src/hooks/useSingleDroneMap/useSingleDroneMap.js
@@ -2,25 +2,18 @@
 * @Author: shuishen 1109946754@qq.com
 * @Date: 2025-04-15 22:41:40
 * @LastEditors: shuishen 1109946754@qq.com
 * @LastEditTime: 2025-04-17 15:05:32
 * @LastEditTime: 2025-04-17 20:24:19
 * @FilePath: \command-center-dashboard\src\hooks\useSingleDroneMap\useSingleDroneMap.js
 * @Description: 
 * 
 * Copyright (c) 2025 by shuishen, All Rights Reserved. 
 */
import * as Cesium from 'cesium'
import { render } from 'vue'
import endingImg from '@/assets/images/aiNowFly/ending.png'
import endingHighImg from '@/assets/images/aiNowFly/ending-high.png'
import EventPopUpBox from '@/hooks/components/EventPopUpBox.vue'
import eventSingle from '@/assets/images/home/useEventOperate/eventSingle.png'
import { getEventImage } from '@/utils/stateToImageMap/event'
import { getDroneFlagImage } from '@/utils/stateToImageMap/drone'
import eventPending from '@/assets/images/home/useEventOperate/eventPending.png' // 待处理 0
import eventWaitAudit from '@/assets/images/home/useEventOperate/eventWaitAudit.png' // 待审核 2
import eventProcessing from '@/assets/images/home/useEventOperate/eventProcessing.png' // 处理中 3
import eventCompleted from '@/assets/images/home/useEventOperate/eventCompleted.png' // 已完成 4
import eventClosed from '@/assets/images/home/useEventOperate/eventClosed.png' // 已完结 5
import { useMapHandlerClick } from '@/hooks/components/useMapHandlerClick'
/**
 * 
@@ -31,20 +24,18 @@
  eventApi: null,
  eventApiParams: {}
}) {
  const styleTransform = 'translate(-50%,-110%)'
  const { eventPositions, eventApi, eventApiParams } = options
  const eventImage = {
    0: eventPending,
    2: eventWaitAudit,
    3: eventProcessing,
    4: eventCompleted,
    5: eventClosed
  }
  let viewer = null
  let handler = null
  let currentEntity = null
  const {
    handlerInit: mapHandlerInit,
    removeAll: removeMapHandlerAll
  } = useMapHandlerClick({
    getViewer () {
      return viewer
    }
  })
  // 初始化机场位置
  const initDroneEntity = (dronePosition) => {
@@ -52,7 +43,7 @@
    if (!lng || !lat) return
    const markerImg = status ? endingHighImg : endingImg
    const markerImg = getDroneFlagImage(status)
    const position = Cesium.Cartesian3.fromDegrees(+lng, +lat, 0)
    const droneEntity = viewer?.entities.add({
@@ -93,7 +84,7 @@
    eventData.length && eventData.forEach((item, index) => {
      const { longitude, latitude, status, id } = item
      const curImg = eventImage[status] || eventSingle
      const curImg = getEventImage(status)
      const position = Cesium.Cartesian3.fromDegrees(+longitude, +latitude, 0)
@@ -109,7 +100,7 @@
          customData: {
            data: {
              ...item,
              eventId: item.id,
              eventId: id,
              type: 'single-drone-event'
            }
          }
@@ -155,104 +146,20 @@
    })
  }
  // 查找特定类型的实体
  const findEntityByType = (entities, type) => {
    return entities.find(entity =>
      entity?.properties?.customData?._value?.data?.type === type
    )
  }
  // 左键单机事件
  const singleMachineEvent = async click => {
    currentEntity = null
    let clickedEntities = viewer?.scene.drillPick(click.position).map(item => item.id)
    if (!clickedEntities?.length) return
    currentEntity = findEntityByType(clickedEntities, 'single-drone-event')
    removeLabel()
    if (currentEntity) {
      viewer?.scene.postRender.addEventListener(labelBoxRender)
    }
  }
  // 事件初始化
  const handlerInit = () => {
    if (handler) return
    handler = new Cesium.ScreenSpaceEventHandler(viewer?.scene.canvas)
    handler.setInputAction(singleMachineEvent, Cesium.ScreenSpaceEventType.LEFT_CLICK)
  }
  // 事件移除
  const removeHandler = () => {
    handler?.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK)
    handler?.destroy()
    handler = null
  }
  // 获取弹框box
  const getLabelDom = data => {
    const vNode = h(EventPopUpBox, { data, removeLabel })
    const tooltipContainer = document.createElement('div')
    tooltipContainer.id = 'mapPopUpBox'
    tooltipContainer.style.position = 'absolute'
    tooltipContainer.style.transform = styleTransform
    tooltipContainer.style.pointerEvents = 'none'
    document.querySelector('.page-index').append(tooltipContainer)
    render(vNode, tooltipContainer)
    return tooltipContainer
  }
  // 弹框位置刷新
  const labelBoxRender = () => {
    if (!currentEntity) return
    let dom = document.querySelector('#mapPopUpBox')
    if (!dom) {
      dom = getLabelDom(currentEntity.properties.customData._value.data)
    }
    const screenPosition = viewer?.scene.cartesianToCanvasCoordinates(currentEntity?.position?._value)
    if (screenPosition) {
      dom.style.left = `${screenPosition.x}px`
      dom.style.top = `${screenPosition.y}px`
      dom.style.display = 'block'
    }
  }
  const removeDom = () => {
    const dom = document.querySelector('#mapPopUpBox')
    if (dom && dom.parentNode) {
      dom.parentNode.removeChild(dom)
    }
  }
  // 移除弹框标签
  const removeLabel = () => {
    viewer?.scene.postRender.removeEventListener(labelBoxRender)
    removeDom()
  }
  const init = () => {
    viewer = window.$viewer
    handlerInit()
    mapHandlerInit()
  }
  const removeAll = () => {
    removeLayer()
    removeHandler()
    removeLabel()
    removeMapHandlerAll()
  }
  onMounted(() => {
    nextTick(() => {
      init()
    })
  onMounted(async () => {
    await nextTick()
    init()
  })
  // 自动清理
src/mqtt/index.js
@@ -65,7 +65,7 @@
    // 关闭 mqtt 客户端
    destroyed = () => {
        window.console.log('mqtt destroyed')
        window.console.log('mqtt 销毁')
        this._client?.end()
    }
src/utils/stateToImageMap/drone.js
New file
@@ -0,0 +1,32 @@
/*
 * @Author: shuishen 1109946754@qq.com
 * @Date: 2025-04-17 20:17:12
 * @LastEditors: shuishen 1109946754@qq.com
 * @LastEditTime: 2025-04-17 20:28:08
 * @FilePath: \command-center-dashboard\src\utils\stateToImageMap\drone.js
 * @Description:
 *
 * Copyright (c) 2025 by shuishen, All Rights Reserved.
 */
import endingImg from '@/assets/images/aiNowFly/ending.png'
import endingHighImg from '@/assets/images/aiNowFly/ending-high.png'
const droneImage = {
  'OFFLINE': endingImg,
  'WORKING': endingHighImg,
  'LEISURE': endingHighImg
}
/**
 * 根据机巢状态获取图片
 * @param {string} status 状态
 * @returns
 */
export const getDroneStatusImage = (status) => droneImage[status] || endingHighImg
/**
 * 根据机巢状态获取图片
 * @param {boolean} isOnline 状态
 * @returns
 */
export const getDroneFlagImage = (isOnline) => isOnline ? endingHighImg : endingImg
src/utils/stateToImageMap/event.js
New file
@@ -0,0 +1,23 @@
import eventSingle from '@/assets/images/home/useEventOperate/eventSingle.png'
import eventPending from '@/assets/images/home/useEventOperate/eventPending.png' // 待处理 0
import eventWaitAudit from '@/assets/images/home/useEventOperate/eventWaitAudit.png' // 待审核 2
import eventProcessing from '@/assets/images/home/useEventOperate/eventProcessing.png' // 处理中 3
import eventCompleted from '@/assets/images/home/useEventOperate/eventCompleted.png' // 已完成 4
import eventClosed from '@/assets/images/home/useEventOperate/eventClosed.png' // 已完结 5
const eventImage = {
  0: eventPending,
  2: eventWaitAudit,
  3: eventProcessing,
  4: eventCompleted,
  5: eventClosed
}
/**
 * 根据事件状态获取图片资源
 * @param {*} status 状态
 * @returns
 */
export const getEventImage = (status) => eventImage[status] || eventSingle
src/views/Home/SearchBox.vue
@@ -1,277 +1,281 @@
<template>
  <div class="searchBox" ref="searchBoxRef">
    <div class="searchInput">
      <el-select v-model="optionsValue" @change="optionChange" placeholder="请选择查询">
        <el-option
          v-for="item in options"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        />
      </el-select>
    <div class="searchBox" ref="searchBoxRef">
        <div class="searchInput">
            <el-select v-model="optionsValue" @change="optionChange" placeholder="请选择查询">
                <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
            </el-select>
      <el-input v-model="searchKey"  @focus="handleFocus" placeholder="请输入搜索关键字"></el-input>
    </div>
    <div class="searchBtn" @click="searchClick"></div>
    <div class="region">
      <el-tree-select
        v-model="treeValue"
        check-strictly
        lazy
        :load="load"
        :props="props"
        style="width: 240px"
        @node-click="handleNodeClick"
      />
    </div>
  </div>
  <div class="select-down-list" ref="selectDownRef" v-show="isSelectDown">
    <div class="item" v-for="item in downList" @click="selectedValue(item)">{{ item.nickname || item.name }}</div>
  </div>
            <el-input
                v-model="searchKey"
                @input="handlerInput"
                @focus="handleFocus"
                placeholder="请输入搜索关键字"
            ></el-input>
        </div>
        <div class="searchBtn" @click="searchClick"></div>
        <div class="region">
            <el-tree-select
                v-model="treeValue"
                check-strictly
                lazy
                :load="load"
                :props="props"
                style="width: 240px"
                @node-click="handleNodeClick"
            />
        </div>
    </div>
    <div class="select-down-list" ref="selectDownRef" v-show="isSelectDown">
        <div class="item" v-for="item in downList" @click="selectedValue(item)">{{ item.nickname || item.name }}</div>
    </div>
</template>
<script setup>
import { getRegion } from '@/api/home';
import { searchByKeyword, selectDeviceList } from '@/api/home/common';
import { useStore } from 'vuex';
import _ from 'lodash'
import { getRegion } from '@/api/home'
import { searchByKeyword, selectDeviceList } from '@/api/home/common'
import { useStore } from 'vuex'
import cesiumOperation from '@/utils/cesium-tsa'
const { flyTo } = cesiumOperation()
const store = useStore();
const searchKey = ref('');
const userAreaCode = computed(() => store.state.user.userInfo.detail.areaCode);
const selectedAreaCode = computed(() => store.state.user.selectedAreaCode);
const areaValue = ref('江西省');
const treeValue = ref(userAreaCode.value);
let first = true;
const searchBoxRef = ref(null);
const selectDownRef = ref(null);
const store = useStore()
const searchKey = ref('')
const userAreaCode = computed(() => store.state.user.userInfo.detail.areaCode)
const selectedAreaCode = computed(() => store.state.user.selectedAreaCode)
const areaValue = ref('江西省')
const treeValue = ref(userAreaCode.value)
let first = true
const searchBoxRef = ref(null)
const selectDownRef = ref(null)
function handleNodeClick(data) {
  areaValue.value = data.name;
  store.commit('setSelectedAreaCode', data.code);
    areaValue.value = data.name
    store.commit('setSelectedAreaCode', data.code)
}
const props = {
  label: 'name',
  value: 'code',
  children: 'children',
};
    label: 'name',
    value: 'code',
    children: 'children',
}
const load = async (node, resolve) => {
  if (first) {
    first = false;
    const res = await getRegion(userAreaCode.value);
    const { provinceCode, provinceName } = res?.data?.data?.[0] || {};
    resolve([{ code: provinceCode, name: provinceName }]);
    return;
  }
  const code = node?.data?.code || userAreaCode.value;
  if ((node?.data?.regionLevel || 0) > 2) return resolve([]);
  getRegion(code).then(res => {
    resolve(res?.data?.data || []);
  });
};
    if (first) {
        first = false
        const res = await getRegion(userAreaCode.value)
        const { provinceCode, provinceName } = res?.data?.data?.[0] || {}
        resolve([{ code: provinceCode, name: provinceName }])
        return
    }
    const code = node?.data?.code || userAreaCode.value
    if ((node?.data?.regionLevel || 0) > 2) return resolve([])
    getRegion(code).then(res => {
        resolve(res?.data?.data || [])
    })
}
const optionsValue = ref('1');
const optionsValue = ref('1')
const options = [
  {
    value: '1',
    label: '机巢',
  },
  {
    value: '2',
    label: '地址',
  },
];
    {
        value: '1',
        label: '机巢',
    },
    {
        value: '2',
        label: '地址',
    },
]
const isSelectDown = ref(false);
// 下拉数据列表
const machineNestList = ref([]);
const isSelectDown = ref(false)
// 地址搜索结果
const downList = ref([]);
const position = ref({});
const downList = ref([])
const position = ref({})
// 获取机巢搜索结果
const getDeviceList = async () => {
  const res = await selectDeviceList({nickname: searchKey.value});
  if (res.data.code !== 0) return;
  machineNestList.value = res?.data?.data || [];
  downList.value = res?.data?.data || [];
};
    const res = await selectDeviceList({ nickname: searchKey.value })
    if (res.data.code !== 0) return
    downList.value = res?.data?.data || []
}
// 获取地址搜索结果
const getAddressList = async () => {
  const res = await searchByKeyword(encodeURIComponent(`${areaValue.value}+${searchKey.value}`));
  if (res.data.code !== 0) return;
  downList.value = res?.data?.data.tips || [];
  if (downList.value.length > 0) {
    isSelectDown.value = true;
  }
};
    const res = await searchByKeyword(encodeURIComponent(`${areaValue.value}+${searchKey.value}`))
    if (res.data.code !== 0) return
    downList.value = res?.data?.data.tips || []
    if (downList.value.length > 0) {
        isSelectDown.value = true
    }
}
// 搜索结果
const searchClick = () => {
    const longitude = Number(position.value.longitude)
    const latitude = Number(position.value.latitude)
    flyTo({ longitude, latitude }, 1, 1000)
};
}
// 地址和机巢的切换
const optionChange = () => {
  searchKey.value = '';
  downList.value = [];
};
    searchKey.value = ''
    downList.value = []
    inputSelect()
}
// input对应下拉数据初始化
const inputSelect = () => {
    if (optionsValue.value === '2') {
        getAddressList()
    } else {
        getDeviceList()
    }
}
inputSelect()
// 输入框input事件
const handlerInput = _.debounce(inputSelect, 1000)
// 输入框获取焦点
const handleFocus = () => {
  if (optionsValue.value === '1') {
    isSelectDown.value = true;
    downList.value = machineNestList.value;
  }
};
    isSelectDown.value = true
}
// 机巢下拉获取值
const selectedValue = item => {
  searchKey.value = item.nickname || item.name;
  if (optionsValue.value === '1') {
    position.value = item;
  } else {
    position.value = { longitude: item.location.split(',')[0], latitude: item.location.split(',')[1] };
  }
  isSelectDown.value = false;
};
    searchKey.value = item.nickname || item.name
    if (optionsValue.value === '1') {
        position.value = item
    } else {
        position.value = { longitude: item.location.split(',')[0], latitude: item.location.split(',')[1] }
    }
// 监听搜索关键字变化
watch(searchKey, async (newVal) => {
  if (optionsValue.value === '2') {
    await getAddressList();
  } else {
    await getDeviceList();
  }
}, { immediate: false });
    isSelectDown.value = false
}
onMounted(() => {
  document.addEventListener('click', handleClickOutside);
});
    document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
  document.removeEventListener('click', handleClickOutside);
});
    document.removeEventListener('click', handleClickOutside)
})
const handleClickOutside = (event) => {
  if (!searchBoxRef.value?.contains(event.target) && !selectDownRef.value?.contains(event.target)) {
    isSelectDown.value = false;
  }
};
const handleClickOutside = event => {
    if (!searchBoxRef.value?.contains(event.target) && !selectDownRef.value?.contains(event.target)) {
        isSelectDown.value = false
    }
}
</script>
<style scoped lang="scss">
.select-down-list {
  position: absolute;
  top: 188px;
  left: 44%;
  transform: translateX(-40%);
  width: 220px;
  height: 256px;
  overflow-y: auto;
  background: linear-gradient( 180deg, #0D3556 0%, #012350 100%);
  border-radius: 0px 0px 8px 8px;
  border: 1px solid;
  border-image: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(115, 192, 255, 1)) 1 1;
  opacity: 0.8;
  &::-webkit-scrollbar {
    width: 0;
    display: none;
  }
  -ms-overflow-style: none;  /* IE and Edge */
  scrollbar-width: none;  /* Firefox */
  .item {
    color: #FFFFFF;
    height: 32px;
    line-height: 32px;
    text-align: center;
    cursor: pointer;
    &:hover {
      background: linear-gradient( 90deg, rgba(0,122,255,0) 0%, rgba(0,98,204,0.6) 50%, rgba(0,73,153,0) 100%);
      border: 1px solid;
      border-image: linear-gradient(90deg, rgba(0, 199, 190, 0), rgba(48, 176, 199, 1), rgba(0, 199, 190, 0)) 1 1;
    }
  }
    position: absolute;
    top: 188px;
    left: 44%;
    transform: translateX(-40%);
    width: 220px;
    height: 256px;
    overflow-y: auto;
    background: linear-gradient(180deg, #0d3556 0%, #012350 100%);
    border-radius: 0px 0px 8px 8px;
    border: 1px solid;
    border-image: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(115, 192, 255, 1)) 1 1;
    opacity: 0.8;
    &::-webkit-scrollbar {
        width: 0;
        display: none;
    }
    -ms-overflow-style: none; /* IE and Edge */
    scrollbar-width: none; /* Firefox */
    .item {
        color: #ffffff;
        height: 32px;
        line-height: 32px;
        text-align: center;
        cursor: pointer;
        &:hover {
            background: linear-gradient(90deg, rgba(0, 122, 255, 0) 0%, rgba(0, 98, 204, 0.6) 50%, rgba(0, 73, 153, 0) 100%);
            border: 1px solid;
            border-image: linear-gradient(90deg, rgba(0, 199, 190, 0), rgba(48, 176, 199, 1), rgba(0, 199, 190, 0)) 1 1;
        }
    }
}
.searchBox {
  width: 420px;
  height: 43px;
  position: absolute;
  top: 145px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
    width: 420px;
    height: 43px;
    position: absolute;
    top: 145px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
  .el-select {
    width: 130px;
    height: 100%;
    .el-select {
        width: 130px;
        height: 100%;
    :deep() {
      .el-select__wrapper {
        background: transparent;
        border: none;
        box-shadow: none;
        height: 100%;
        padding-left: 20px;
      }
        :deep() {
            .el-select__wrapper {
                background: transparent;
                border: none;
                box-shadow: none;
                height: 100%;
                padding-left: 20px;
            }
      .el-select__selected-item {
        font-family: Source Han Sans CN, Source Han Sans CN, serif;
        font-weight: 400;
        font-size: 14px;
        color: #ffffff;
        line-height: 18px;
      }
    }
  }
            .el-select__selected-item {
                font-family: Source Han Sans CN, Source Han Sans CN, serif;
                font-weight: 400;
                font-size: 14px;
                color: #ffffff;
                line-height: 18px;
            }
        }
    }
  .searchInput {
    width: 243px;
    height: 100%;
    background: url('@/assets/images/home/searchBox/searchBg1.png') no-repeat center / 100% 100%;
    display: flex;
    .searchInput {
        width: 243px;
        height: 100%;
        background: url('@/assets/images/home/searchBox/searchBg1.png') no-repeat center / 100% 100%;
        display: flex;
    .el-input {
      height: 100%;
        .el-input {
            height: 100%;
      :deep() {
        .el-input__wrapper {
          background: transparent;
          border: none;
          box-shadow: none;
          height: 100%;
        }
            :deep() {
                .el-input__wrapper {
                    background: transparent;
                    border: none;
                    box-shadow: none;
                    height: 100%;
                }
        .el-input__inner {
          font-family: Source Han Sans CN, Source Han Sans CN, serif;
          font-weight: 400;
          font-size: 14px;
          color: #ffffff;
          line-height: 18px;
        }
      }
    }
  }
                .el-input__inner {
                    font-family: Source Han Sans CN, Source Han Sans CN, serif;
                    font-weight: 400;
                    font-size: 14px;
                    color: #ffffff;
                    line-height: 18px;
                }
            }
        }
    }
  .searchBtn {
    width: 67px;
    height: 100%;
    background: url('@/assets/images/home/searchBox/searchBg2.png') no-repeat center / 100% 100%;
    cursor: pointer;
  }
    .searchBtn {
        width: 67px;
        height: 100%;
        background: url('@/assets/images/home/searchBox/searchBg2.png') no-repeat center / 100% 100%;
        cursor: pointer;
    }
  .region {
    width: 0;
    flex-grow: 1;
    background: url('@/assets/images/home/searchBox/searchBg3.png') no-repeat center / 100% 100%;
    .region {
        width: 0;
        flex-grow: 1;
        background: url('@/assets/images/home/searchBox/searchBg3.png') no-repeat center / 100% 100%;
    :deep() {
      .el-select__wrapper {
        width: 100px;
        padding-left: 20px;
      }
    }
  }
        :deep() {
            .el-select__wrapper {
                width: 100px;
                padding-left: 20px;
            }
        }
    }
}
</style>
src/views/SignMachineNest/MachineLeft/InspectionRaskDetails.vue
@@ -1,228 +1,230 @@
<!-- 任务事件概况 -->
<template>
    <common-title title="任务事件概况" :style="{ marginLeft: pxToRem(14) }"></common-title>
    <div class="inspection-rask-details">
        <CommonDateTime :style="{ top: pxToRem(14),marginLeft: pxToRem(10) }" v-model="newTime" @change="getData"></CommonDateTime>
        <div class="chart-container" ref="chartRef"></div>
        <div class="pie-container" ref="pieRef"></div>
    </div>
    <common-title title="任务事件概况" :style="{ marginLeft: pxToRem(14) }"></common-title>
    <div class="inspection-rask-details">
        <CommonDateTime
            :style="{ top: pxToRem(14), marginLeft: pxToRem(10) }"
            v-model="newTime"
            @change="getData"
        ></CommonDateTime>
        <div class="chart-container" ref="chartRef"></div>
        <div class="pie-container" ref="pieRef"></div>
    </div>
</template>
<script setup>
import * as echarts from 'echarts';
import dayjs from 'dayjs';
import { pxToRem } from '@/utils/rem';
import CommonTitle from '@/components/CommonTitle.vue';
import CommonDateTime from '@/components/CommonDateTime.vue';
import * as echarts from 'echarts'
import dayjs from 'dayjs'
import { pxToRem } from '@/utils/rem'
import CommonTitle from '@/components/CommonTitle.vue'
import CommonDateTime from '@/components/CommonDateTime.vue'
import { jobNumBar, eventNumPie } from '@/api/home'
import useEchartsResize from '@/hooks/useEchartsResize'
import { useStore } from 'vuex'
const store = useStore()
// 单个机巢信息
const singleUavHome = computed(() => store.state.home.singleUavHome)
// 日期
const currenDate = dayjs().format('YYYY-MM-DD');
const newTime = ref([currenDate, currenDate]);
const currenDate = dayjs().format('YYYY-MM-DD')
const newTime = ref([currenDate, currenDate])
// 统计图
const chartRef = ref(null);
const chartRef = ref(null)
let { chart } = useEchartsResize(chartRef)
const pieRef = ref(null);
const pieRef = ref(null)
let { chart: pieChart } = useEchartsResize(pieRef)
const option = {
    tooltip: {
        trigger: 'axis',
        axisPointer: {
            type: 'shadow'
        }
    },
    grid: {
        top: '15%',
        left: '3%',
        right: '4%',
        bottom: '3%',
        containLabel: true
    },
    legend: {
        data: ['任务', '事件'],
        textStyle: {
            color: '#fff'
        }
    },
    xAxis: {
        type: 'category',
        // data: ['1月', '2月', '3月', '4月', '5月', '6月'],
        axisLine: {
            lineStyle: {
                color: '#fff'
            }
        },
        axisLabel: {
            color: '#fff'
        }
    },
    yAxis: {
        type: 'value',
        axisLine: {
            lineStyle: {
                color: '#fff'
            }
        },
        splitLine: {
            lineStyle: {
                color: 'rgba(255, 255, 255, 0.1)'
            }
        },
        axisLabel: {
            color: '#fff'
        }
    },
    series: [
        {
            name: '任务',
            type: 'bar',
            barWidth: '20%',
            // data: [10, 15, 20, 25, 30, 35],
            itemStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                    { offset: 0, color: '#1EE7E7' },
                    { offset: 1, color: 'rgba(30, 231, 231, 0.1)' }
                ])
            }
        },
        {
            name: '事件',
            type: 'bar',
            barWidth: '20%',
            // data: [15, 20, 25, 30, 35, 40],
            itemStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                    { offset: 0, color: '#0070FF' },
                    { offset: 1, color: 'rgba(0, 112, 255, 0.1)' }
                ])
            }
        }
    ]
};
    tooltip: {
        trigger: 'axis',
        axisPointer: {
            type: 'shadow',
        },
    },
    grid: {
        top: '15%',
        left: '3%',
        right: '4%',
        bottom: '3%',
        containLabel: true,
    },
    legend: {
        data: ['任务', '事件'],
        textStyle: {
            color: '#fff',
        },
    },
    xAxis: {
        type: 'category',
        // data: ['1月', '2月', '3月', '4月', '5月', '6月'],
        axisLine: {
            lineStyle: {
                color: '#fff',
            },
        },
        axisLabel: {
            color: '#fff',
        },
    },
    yAxis: {
        type: 'value',
        axisLine: {
            lineStyle: {
                color: '#fff',
            },
        },
        splitLine: {
            lineStyle: {
                color: 'rgba(255, 255, 255, 0.1)',
            },
        },
        axisLabel: {
            color: '#fff',
        },
    },
    series: [
        {
            name: '任务',
            type: 'bar',
            barWidth: '20%',
            // data: [10, 15, 20, 25, 30, 35],
            itemStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                    { offset: 0, color: '#1EE7E7' },
                    { offset: 1, color: 'rgba(30, 231, 231, 0.1)' },
                ]),
            },
        },
        {
            name: '事件',
            type: 'bar',
            barWidth: '20%',
            // data: [15, 20, 25, 30, 35, 40],
            itemStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                    { offset: 0, color: '#0070FF' },
                    { offset: 1, color: 'rgba(0, 112, 255, 0.1)' },
                ]),
            },
        },
    ],
}
// chart.setOption(option);
const pieOption = {
    tooltip: {
        trigger: 'item',
        formatter: '{b}: {c} ({d}%)'
    },
    legend: {
        orient: 'vertical',
        right: '5%',
        top: 'middle',
        textStyle: {
            color: '#fff',
            fontSize: 12
        },
        itemWidth: 10,
        itemHeight: 10,
        itemGap: 10,
        formatter: function(name) {
            let data = pieOption.series[0].data;
            let total = 0;
            let tarValue = 0;
            for (let i = 0; i < data.length; i++) {
                total += data[i].value;
                if (data[i].name === name) {
                    tarValue = data[i].value;
                }
            }
            let percentage = ((tarValue / total) * 100).toFixed(1);
            return `${name} ${percentage}%`;
        }
    },
    series: [
        {
            name: '任务状态',
            type: 'pie',
            radius: ['10%', '90%'],
            center: ['35%', '50%'],  // 将图表向左偏移
            roseType: 'radius',
            itemStyle: {
                borderRadius: 4
            },
            label: {
                show: true,
                position: 'inside',
                formatter: '{c}%',
                fontSize: 12,
                color: '#fff'
            },
            data: [
                { value: 0, name: '待审核', itemStyle: { color: '#1860EC' } },
                { value: 0, name: '待处理', itemStyle: { color: '#47D107' } },
                { value: 0, name: '已完成', itemStyle: { color: '#F29509' } },
                // { value: 0, name: '待分拨', itemStyle: { color: '#E9C81A' } },
                { value: 0, name: '处理中', itemStyle: { color: '#0FC1E8' } },
                { value: 0, name: '已完结', itemStyle: { color: '#FE577F' } }
            ]
        }
    ]
};
    tooltip: {
        trigger: 'item',
        formatter: '{b}: {c} ({d}%)',
    },
    legend: {
        orient: 'vertical',
        right: '5%',
        top: 'middle',
        textStyle: {
            color: '#fff',
            fontSize: 12,
        },
        itemWidth: 10,
        itemHeight: 10,
        itemGap: 10,
        formatter: function (name) {
            let data = pieOption.series[0].data
            let total = 0
            let tarValue = 0
            for (let i = 0; i < data.length; i++) {
                total += data[i].value
                if (data[i].name === name) {
                    tarValue = data[i].value
                }
            }
            let percentage = ((tarValue / total) * 100).toFixed(1)
            return `${name} ${percentage}%`
        },
    },
    series: [
        {
            name: '任务状态',
            type: 'pie',
            radius: ['10%', '90%'],
            center: ['35%', '50%'], // 将图表向左偏移
            roseType: 'radius',
            itemStyle: {
                borderRadius: 4,
            },
            label: {
                show: true,
                position: 'inside',
                formatter: '{c}%',
                fontSize: 12,
                color: '#fff',
            },
            data: [
                { value: 0, name: '待审核', itemStyle: { color: '#1860EC' } },
                { value: 0, name: '待处理', itemStyle: { color: '#47D107' } },
                { value: 0, name: '已完成', itemStyle: { color: '#F29509' } },
                // { value: 0, name: '待分拨', itemStyle: { color: '#E9C81A' } },
                { value: 0, name: '处理中', itemStyle: { color: '#0FC1E8' } },
                { value: 0, name: '已完结', itemStyle: { color: '#FE577F' } },
            ],
        },
    ],
}
// TODAY,CURRENT_WEEK,CURRENT_MONTH,CURRENT_YEAR
const getData = (value,date_enum) => {
    const params = {
        device_sn: '',
        date_enum,
        // start_date: newTime.value[0],
        // end_date: newTime.value[1]
    };
    // 获取柱状图
    jobNumBar(params).then(res => {
        if (res.data.code !== 0) return;
        option.xAxis.data = res.data?.data.map(item => item.name);
        option.series[0].data = res.data?.data.map(item => item.data[0].value);
        option.series[1].data = res.data?.data.map(item => item.data[1].value);
        chart.value.setOption(option);
    })
    // 获取饼状图
    eventNumPie(params).then(res => {
        if (res.data.code!== 0) return;
        // 更新饼图数据
        const updatedPieData = pieOption.series[0].data.map(item => {
            const matchedData = res.data?.data.find(dataItem => dataItem.name === item.name);
            return {
                ...item,
                value: matchedData ? matchedData.value : item.value
            };
        });
        pieOption.series[0].data = updatedPieData;
        pieChart.value.setOption(pieOption);
    })
};
const getData = (value, date_enum) => {
    const params = {
        device_sn: singleUavHome.value.device_sn,
        date_enum,
        // start_date: newTime.value[0],
        // end_date: newTime.value[1]
    }
    // 获取柱状图
    jobNumBar(params).then(res => {
        if (res.data.code !== 0) return
        option.xAxis.data = res.data?.data.map(item => item.name)
        option.series[0].data = res.data?.data.map(item => item.data[0].value)
        option.series[1].data = res.data?.data.map(item => item.data[1].value)
        chart.value.setOption(option)
    })
    // 获取饼状图
    eventNumPie(params).then(res => {
        if (res.data.code !== 0) return
        // 更新饼图数据
        const updatedPieData = pieOption.series[0].data.map(item => {
            const matchedData = res.data?.data.find(dataItem => dataItem.name === item.name)
            return {
                ...item,
                value: matchedData ? matchedData.value : item.value,
            }
        })
        pieOption.series[0].data = updatedPieData
        pieChart.value.setOption(pieOption)
    })
}
onMounted(() => {
    getData(newTime.value, 'TODAY');
});
    getData(newTime.value, 'TODAY')
})
</script>
<style lang="scss" scoped>
.inspection-rask-details {
    margin-left: 29px;
    // padding: 16px 16px;
    width: 390px;
    height: 526px;
    background: linear-gradient(
        270deg,
        rgba(31, 62, 122, 0) 0%,
        rgba(31, 62, 122, 0.35) 21%,
        #1f3e7a 100%
    );
    border-radius: 0px 0px 0px 0px;
    opacity: 0.85;
    .chart-container {
        width: 100%;
        height: 220px;
        margin-top: 20px;
    }
    .pie-container {
        width: 100%;
        height: 212px;
        margin-top: 10px;
    }
    margin-left: 29px;
    // padding: 16px 16px;
    width: 390px;
    height: 526px;
    background: linear-gradient(270deg, rgba(31, 62, 122, 0) 0%, rgba(31, 62, 122, 0.35) 21%, #1f3e7a 100%);
    border-radius: 0px 0px 0px 0px;
    opacity: 0.85;
    .chart-container {
        width: 100%;
        height: 220px;
        margin-top: 20px;
    }
    .pie-container {
        width: 100%;
        height: 212px;
        margin-top: 10px;
    }
}
</style>
src/views/SignMachineNest/MachineRight/MachineMonitor.vue
@@ -1,81 +1,90 @@
<!--
 * @Author: shuishen 1109946754@qq.com
 * @Date: 2025-04-15 17:19:35
 * @LastEditors: shuishen 1109946754@qq.com
 * @LastEditTime: 2025-04-17 21:16:50
 * @FilePath: \command-center-dashboard\src\views\SignMachineNest\MachineRight\MachineMonitor.vue
 * @Description:
 *
 * Copyright (c) 2025 by shuishen, All Rights Reserved.
-->
<!-- 机巢监控 -->
<template>
  <CommonTitle title="直播监控" />
  <div :style="{ marginLeft: pxToRem(14) }">
    <div class="machine-monitor">
      <LiveVideo :videoUrl="airPortUrl" />
    </div>
  </div>
    <CommonTitle title="直播监控" />
    <div :style="{ marginLeft: pxToRem(14) }">
        <div class="machine-monitor">
            <LiveVideo :videoUrl="airPortUrl" />
        </div>
    </div>
</template>
<script setup>
import CommonTitle from '@/components/CommonTitle.vue';
import LiveVideo from '@/components/LiveVideo.vue';
import { liveStart } from '@/api/home/machineNest';
import CommonTitle from '@/components/CommonTitle.vue'
import LiveVideo from '@/components/LiveVideo.vue'
import { liveStart } from '@/api/home/machineNest'
// import { CURRENT_CONFIG as config } from '@/utils/http/config'
import { useStore } from 'vuex';
import { useStore } from 'vuex'
const store = useStore();
const store = useStore()
// 单个机巢信息
const singleUavHome = computed(() => store.state.home.singleUavHome);
const singleUavHome = computed(() => store.state.home.singleUavHome)
// 直播地址
let airPortUrl = ref('');
let airPortUrl = ref('')
// 获取直播地址
const getVideoUrl = (sn,quality) => {
  liveStart(sn, quality).then(res => {
    if (res.data.code !== 0) return;
    airPortUrl.value = res.data.data.rtcs_url;
  });
};
let isCurrentSn = ref(false);
let CurrentSn = ref('');
const getVideoUrl = (sn, quality) => {
    liveStart(sn, quality).then(res => {
        if (res.data.code !== 0) return
        airPortUrl.value = res.data.data.rtcs_url
    })
}
let isCurrentSn = ref(false)
let CurrentSn = ref('')
// 监听ws消息
watch(
  () => store.state.home.deviceState.deviceInfo,
  (newValue) => {
    CurrentSn.value = Object.keys(newValue)[0];
    const currentDevice = newValue[CurrentSn.value];
    if (currentDevice && currentDevice?.mode_code > 0) {
      isCurrentSn.value = true;
    } else if (currentDevice && currentDevice?.mode_code === 14) {
      isCurrentSn.value = false;
    } else {
      isCurrentSn.value = false;
    }
  },
  {
    immediate: true,
    deep: true,
  }
);
    () => store.state.home.deviceState.deviceInfo,
    newValue => {
        CurrentSn.value = Object.keys(newValue)[0]
        const currentDevice = newValue[CurrentSn.value]
        if (currentDevice && currentDevice?.mode_code > 0) {
            isCurrentSn.value = true
        } else if (currentDevice && currentDevice?.mode_code === 14) {
            isCurrentSn.value = false
        } else {
            isCurrentSn.value = false
        }
    },
    {
        immediate: true,
        deep: true,
    }
)
// 监听 isCurrentSn
watch(isCurrentSn, (newVal) => {
  if (newVal) {
    getVideoUrl(CurrentSn.value, 2);
  } else {
    getVideoUrl(singleUavHome.value.device_sn,1);
  }
}, { immediate: true,deep: true });
watch(
    isCurrentSn,
    newVal => {
        if (newVal) {
            getVideoUrl(CurrentSn.value, 2)
        } else {
            getVideoUrl(singleUavHome.value.device_sn, 1)
        }
    },
    { immediate: true, deep: true }
)
onMounted(() => {
  getVideoUrl(singleUavHome.value.device_sn,1);
});
    // getVideoUrl(singleUavHome.value.device_sn,1);
})
</script>
<style lang="scss" scoped>
  .machine-monitor {
    width: 390px;
    height: 228px;
    background: linear-gradient(
      270deg,
      #1f3e7a 0%,
      rgba(31, 62, 122, 0.35) 79%,
      rgba(31, 62, 122, 0) 100%
    );
    opacity: 0.85;
    margin: 2px 0 13 0;
    display: flex;
    justify-content: space-between;
    padding: 12px 10px 0;
 }
.machine-monitor {
    width: 390px;
    height: 228px;
    background: linear-gradient(270deg, #1f3e7a 0%, rgba(31, 62, 122, 0.35) 79%, rgba(31, 62, 122, 0) 100%);
    opacity: 0.85;
    margin: 2px 0 13 0;
    display: flex;
    justify-content: space-between;
    padding: 12px 10px 0;
}
</style>
src/views/SignMachineNest/SignMachineNest.vue
@@ -19,7 +19,6 @@
let connectWs = ref(null)
// 单个机巢信息
const singleUavHome = computed(() => store.state.home.singleUavHome)
const selectedAreaCode = computed(() => store.state.user.selectedAreaCode)
let osdVisible = ref({})