forked from drone/command-center-dashboard

罗广辉
2025-04-18 75cdd3cdcec1fc22bddebd6352e5e87e0a8d5d33
Merge remote-tracking branch 'origin/master'
17 files modified
3 files added
1502 ■■■■ changed files
.env.development 2 ●●● patch | view | raw | blame | history
.env.production 2 ●●● patch | view | raw | blame | history
.env.test 3 ●●●●● patch | view | raw | blame | history
src/assets/images/home/useUavHome/eventAggregationImg.png patch | view | raw | blame | history
src/assets/images/home/useUavHome/eventAggregationImg1.png 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/useMapAggregation/useMapAggregation.js 139 ●●●● patch | view | raw | blame | history
src/hooks/useSingleDroneMap/useSingleDroneMap.js 135 ●●●● patch | view | raw | blame | history
src/layout/Header.vue 8 ●●●●● patch | view | raw | blame | history
src/page/login/index.vue 4 ●●●● patch | view | raw | blame | history
src/styles/login.scss 19 ●●●●● 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/AINowFly.vue 2 ●●●●● patch | view | raw | blame | history
src/views/Home/HomeLeft/InspectionRaskDetails/InspectionRaskDetailsDialog.vue 46 ●●●● 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
.env.development
@@ -12,4 +12,4 @@
VITE_APP_AIRLINE_URL = https://wrj.shuixiongit.com/minio/cloud-bucket
#系统运维
VITE_APP_ADMIN_URL = 'https://wrj.shuixiongit.com/manage'
VITE_APP_ADMIN_URL = 'https://wrj.shuixiongit.com/manage'
.env.production
@@ -6,4 +6,4 @@
# ws地址
VITE_APP_WS_API_URL = wss://wrj.shuixiongit.com/drone-wss/api/v1/ws
# 管理后台地址
VITE_APP_ADMIN_URL = ' https://aisky.org.cn/manage'
VITE_APP_ADMIN_URL = 'https://aisky.org.cn/manage'
.env.test
@@ -5,3 +5,6 @@
VITE_APP_WS_API_URL = wss://wrj.shuixiongit.com/drone-wss/api/v1/ws
# 航线文件地址
VITE_APP_AIRLINE_URL = https://wrj.shuixiongit.com/minio/cloud-bucket
#系统运维
VITE_APP_ADMIN_URL = 'https://wrj.shuixiongit.com/manage'
src/assets/images/home/useUavHome/eventAggregationImg.png

src/assets/images/home/useUavHome/eventAggregationImg1.png
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/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/layout/Header.vue
@@ -128,14 +128,16 @@
    font-family: YouSheBiaoTiHei, YouSheBiaoTiHei;
    font-weight: 400;
    font-size: 45px;
    color: #ffffff;
    line-height: 53px;
    letter-spacing: 11px;
    text-shadow: 0px 4px 1px rgba(19, 80, 143, 0.66), 0px 0px 18px rgba(130, 165, 255, 0.4),
      inset 0px 0px 2px rgba(255, 255, 255, 0.8);
    text-shadow: 0px 4px 1px rgba(19,80,143,0.66), 0px 0px 18px rgba(130,165,255,0.4), inset 0px 0px 2px rgba(255,255,255,0.8);
    text-align: center;
    font-style: normal;
    text-transform: none;
    background: linear-gradient(180deg, #FFFFFF 23%, #E9F8FF 46%, #77BAFF 76%);
    -webkit-background-clip: text;
    color: transparent;
    background-clip: text;
  }
  .h-right {
src/page/login/index.vue
@@ -3,7 +3,7 @@
    <div class="login-header">
      <div class="title">中图智飞低空智能感知网平台</div>
    </div>
    <div class="login-left-title">中国图强 智领飞跃</div>
    <!-- <div class="login-left-title">中国图强 智领飞跃</div> -->
    <div class="login-left"></div>
    <userLogin v-if="activeName === 'user'"></userLogin>
  </div>
@@ -31,7 +31,7 @@
  data() {
    return {
      login:{
        info: '中图智绘低空无人机监测网平台'
        info: '中图智飞低空智能感知网平台'
      },
      website: website,
      time: '',
src/styles/login.scss
@@ -23,15 +23,17 @@
    text-align: center;
    font-style: normal;
    text-transform: none;
    color: #fff;
    // background: linear-gradient(90deg, #FFFFFF 0%, #B2D5FF 100%);
    background: linear-gradient(180deg, #FFFFFF 0%, #B2D5FF 100%);
    -webkit-background-clip: text;
    color: transparent;
    background-clip: text;
  }
}
.login-left-title-new {
.login-left-title {
  position: absolute;
  border: 1px solid greenyellow;
  left: 147px;
  top: 263px;
  // border: 1px solid greenyellow;
  left: 160px;
  top: 240px;
  width: 926px;
  height: 42px;
  font-family: Source Han Sans CN, Source Han Sans CN;
@@ -44,7 +46,10 @@
  text-align: center;
  font-style: normal;
  text-transform: none;
  // background: linear-gradient(90.00000000000004deg, #FFFFFF 0%, #E3FEFF 63%, #88BAFF 100%);
  background: linear-gradient(90.00000000000004deg, #FFFFFF 0%, #E3FEFF 63%, #88BAFF 100%);
  -webkit-background-clip: text;
  color: transparent;
  background-clip: text;
}
.login-left-new {
  position: absolute;
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/AINowFly.vue
@@ -122,6 +122,8 @@
    await ruleFormRef.value.validate()
    if (!params.value.dock_sns.length) return ElMessage.warning('请选择将要飞行的机巢')
    if (!params.value.longitude) return ElMessage.warning('请选择飞行的点')
    // 先清除,再push
    params.value.action_modes = [{action_actuator_func:'startRecord'}]
    if (isPhoto.value) {
        params.value.action_modes.push({action_actuator_func:'takePhoto'})
    }
src/views/Home/HomeLeft/InspectionRaskDetails/InspectionRaskDetailsDialog.vue
@@ -88,7 +88,9 @@
                                    ? 'finish '
                                    : scope.row.status === 5
                                    ? 'fail '
                                    : ' '">
                                    : ' '
                            "
                        >
                            {{
                                scope.row.status === 1
                                    ? '待执行'
@@ -110,7 +112,11 @@
                        <span>{{ scope.row.event_number ? scope.row.event_number : '/' }}</span>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="80"><div class="ztzf-view">查看</div></el-table-column>
                <el-table-column label="操作" width="80">
                    <template #default="scope">
                        <div class="ztzf-view" @click="viewDetail(scope.row)">查看</div>
                    </template>
                </el-table-column>
            </el-table>
        </div>
        <!-- 分页 -->
@@ -128,6 +134,18 @@
            />
        </div>
    </el-dialog>
    <!-- 当前任务详情 -->
    <CurrentTaskDetails
        v-if="isShowCurrentTaskDetails"
        v-model:show="isShowCurrentTaskDetails"
        :id="currentTaskDetailsId"
    />
    <!-- 历史任务详情 -->
    <DeviceJobDetails
        v-if="deviceJobDetailsShow"
        v-model:show="deviceJobDetailsShow"
        :wayLineJodInfoId="wayLineJodInfoId"
    />
</template>
<script setup>
import { pxToRem } from '@/utils/rem'
@@ -135,6 +153,8 @@
import { getDictionary } from '@/api/system/dict'
import { selectDevicePage } from '@/api/home/machineNest'
import { getMultipleDictionary } from '@/api/system/dictbiz'
import CurrentTaskDetails from '@/components/CurrentTaskDetails/CurrentTaskDetails.vue'
import DeviceJobDetails from '@/components/DeviceJobDetails/DeviceJobDetails.vue'
const isShowDetailsDialog = defineModel('show')
const dateRange = ref('')
const searchForm = reactive({
@@ -148,7 +168,7 @@
const statusOptions = [
    { label: '待执行', value: 1 },
    { label: '执行中', value: 2 },
    { label: '已完成', value: 3 },
    { label: '已执行', value: 3 },
    { label: '执行失败', value: 5 },
]
@@ -215,7 +235,6 @@
}
// 获取任务列表
const getJobList = () => {
    // 事件状态:0 =待处理,1=待分拨,2=待处理,3=处理中,4=已完成 5=已完结
    jobList(taskDetailParams).then(res => {
        if (res.data.code !== 0) return
        taskDetailData.value = res.data.data.records
@@ -249,6 +268,23 @@
        return 'warning-row'
    } else {
        return 'success-row'
    }
}
// 查看
    // 当前任务详情 1:待执行  2:执行中
    // 历史任务详情 3:已执行  5:执行失败
const isShowCurrentTaskDetails = ref(false)
const currentTaskDetailsId = ref(null)
const deviceJobDetailsShow = ref(false)
const wayLineJodInfoId = ref(null)
const viewDetail = row => {
    if (row.status === 1 || row.status === 2) {
        currentTaskDetailsId.value = row.id
        isShowCurrentTaskDetails.value = true
    }
    if (row.status === 3 || row.status === 5) {
        wayLineJodInfoId.value = row.id
        deviceJobDetailsShow.value = true
    }
}
onMounted(() => {
@@ -331,7 +367,7 @@
}
// 执行中
.distributed {
    color: #FFA768;
    color: #ffa768;
}
// 已执行
.finish {
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: 10px 10px;
}
</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({})