forked from drone/command-center-dashboard

chenyao
2025-04-17 8999b268f85976e6e759fd30df582993a5bd75be
Merge branch 'master' of http://139.196.74.78:10010/r/drone/command-center-dashboard
13 files modified
1 files added
734 ■■■■ changed files
src/api/drc.js 2 ●●●●● patch | view | raw | blame | history
src/api/payload.js 47 ●●●● patch | view | raw | blame | history
src/components/CommonDateTime.vue 12 ●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/ControlPanel/BaseControl.vue 1 ●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/ControlPanel/ControlPanel.vue 52 ●●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/CurrentTaskDetails.vue 19 ●●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/TaskDetailsLeft.vue 117 ●●●● patch | view | raw | blame | history
src/components/ScaleRule.vue 368 ●●●●● patch | view | raw | blame | history
src/hooks/components/DevicePopUpBox.vue 15 ●●●●● patch | view | raw | blame | history
src/hooks/controlDrone/useManualControl.js 55 ●●●● patch | view | raw | blame | history
src/views/Home/HomeLeft/InspectionRaskDetails/InspectionRaskDetails.vue 2 ●●● patch | view | raw | blame | history
src/views/Home/HomeLeft/InspectionRaskDetails/InspectionRaskDetailsDialog.vue 38 ●●●● patch | view | raw | blame | history
src/views/Home/HomeLeft/MachineNestList.vue 5 ●●●●● patch | view | raw | blame | history
src/views/SignMachineNest/MachineRight/InspectionRaskList.vue 1 ●●●● patch | view | raw | blame | history
src/api/drc.js
@@ -9,8 +9,6 @@
  return resp.data
}
// /control/api/v1/workspaces/82f6008b-2068-448c-9094-881e212f31c3/drc/connect
// 进入飞行控制 (建立drc连接&获取云控控制权)
export async function postDrcEnter (body,workspaceId) {
  const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/enter`, body)
src/api/payload.js
@@ -1,12 +1,4 @@
import request from '@/axios'
import { ElMessage } from 'element-plus'
const API_PREFIX = '/drone-device-core/control/api/v1'
// 获取负载控制权
export async function postPayloadAuth(sn, body) {
    return await request.post(`${API_PREFIX}/devices/${sn}/authority/payload`, body)
}
export const PayloadCommandsEnum = {
  CameraModeSwitch: 'camera_mode_switch',
@@ -18,11 +10,36 @@
  GimbalReset: 'gimbal_reset',
  CameraAim: 'camera_aim',
};
const API_PREFIX = '/drone-device-core/control/api/v1'
// 获取负载控制权
export async function postPayloadAuth(sn, body) {
    return await request.post(`${API_PREFIX}/devices/${sn}/authority/payload`, body)
}
// 发送负载名称
export async function postPayloadCommands(sn, body, config = {}) {
    return await request.post(`${API_PREFIX}/devices/${sn}/payload/commands`, body, config)
}
// 获得有效载荷控制
export async function getPayloadControlApi(params) {
  return await request({
    url:`${API_PREFIX}/devices/payload-control/requests`,
    method:'get',
    params
  })
}
// 云台控制api
export async function ptzControlApi(key,params) {
  return await request({
    url:`${API_PREFIX}/devices/payload-control/payload/${key}`,
    method:'get',
    params
  })
}
// 拍照和录像
export async function callPhotoAndVideoCmd(sn, type) {
@@ -33,11 +50,21 @@
}
// 相机参数调整
export async function cameraParamsChangeApi(data) {
  return await request({
    url:`/drone-device-core/payload-control/payload/zoom/level`,
    method:'post',
    data
  })
}
// 切换直播镜头
export const switchLivestream = (data) => {
  const url = `${API_PREFIX}/live/streams/switch`;
  return request({
    url,
    url:`${API_PREFIX}/live/streams/switch`,
    method: 'post',
    data,
  });
src/components/CommonDateTime.vue
@@ -9,12 +9,13 @@
      end-placeholder="结束日期"
      @change="change"
      disabled
    :clearable="false"
    />
    <div class="time-card">
      <div
        class="card-item"
        :class="item === checked ? 'active' : ''"
        v-for="(item, index) in timeList"
        v-for="(item, index) in timeList" :key="index"
        @click="timeClick(item,index)"
      >
        {{ timeListStr[index] }}
@@ -81,12 +82,19 @@
  position: relative;
  display: flex;
  justify-content: space-between;
:deep(.el-date-editor .el-range__close-icon--hidden){
    width: 0 !important;
}
:deep(.el-date-editor .el-range-input){
width: 48% !important;
font-size: 13px !important;
}
  :deep(.el-date-editor) {
    box-shadow: none;
    width: 0;
    flex: 1;
    margin-right: 4px;
    // margin-right: 4px;
    background: rgba(0, 15, 34, 0.5);
    border-radius: 0px 0px 0px 0px;
src/components/CurrentTaskDetails/ControlPanel/BaseControl.vue
@@ -8,6 +8,7 @@
            </div>
            <div
                v-for="item in list3"
                :key="item.name"
                :style="item.style"
                class="operateBtn"
                :title="item.name"
src/components/CurrentTaskDetails/ControlPanel/ControlPanel.vue
@@ -53,12 +53,12 @@
                        <el-icon class="btnIcon">
                            <Top />
                        </el-icon>
                        <div class="btn" @mousedown="onMouseDown(KeyCode.ARROW_UP)" @mouseup="onMouseUp">C</div>
                        <div class="btn" @mousedown="onMouseDown(KeyCode.KEY_C)" @mouseup="onMouseUp">C</div>
                    </div>
                </div>
                <div class="btnGroupT">
                    <div class="btnItem">
                        <div class="btn" @mousedown="onMouseDown(KeyCode.ARROW_DOWN)" @mouseup="onMouseUp">Z</div>
                        <div class="btn" @mousedown="onMouseDown(KeyCode.KEY_Z)" @mouseup="onMouseUp">Z</div>
                        <el-icon class="btnIcon">
                            <Bottom />
                        </el-icon>
@@ -66,7 +66,7 @@
                </div>
            </div>
        </div>
        <!--     指南针-->
        <div class="compass">
            <ControlComPass />
        </div>
@@ -83,7 +83,12 @@
            </div>
            <div class="ptzControlBtnBox">
                <div class="ptzControlBtn b-r">
                    <div v-for="(item, index) in list5" :style="item.style" class="ptzControlItem"></div>
                    <div
                        v-for="(item, index) in list5"
                        :style="item.style"
                        class="ptzControlItem"
                        @mousedown="onMouseDown(item.key)"
                    ></div>
                    <div
                        class="ptzControlItemIcon"
@@ -122,7 +127,7 @@
<script setup>
import ControlComPass from '../ControlComPass/ControlComPass.vue'
import { KeyCode, useManualControl } from '@/hooks/controlDrone/useManualControl'
import { droneController, exitController, postDrc, postDrcExit, returnHome, returnHomeCancel } from '@/api/drc'
import { droneController, exitController, postDrc, returnHome, returnHomeCancel } from '@/api/drc'
import { ElMessage } from 'element-plus'
import { useStore } from 'vuex'
import { UranusMqtt } from '@/mqtt'
@@ -133,23 +138,20 @@
    ArrowUp,
    Bottom,
    CaretRight,
    CaretTop,
    Minus,
    Plus,
    RefreshLeft,
    RefreshRight,
} from '@element-plus/icons-vue'
import controlCenterImg from '@/assets/images/taskManagement/taskIntermediateContent/controlCenter.png'
import BaseControl from '@/components/CurrentTaskDetails/ControlPanel/BaseControl.vue'
import EventBus from '@/event-bus'
import dayjs from 'dayjs'
import { getPayloadControlApi, ptzControlApi } from '@/api/payload'
const deviceOsdInfo = inject('deviceOsdInfo')
const taskDetails = inject('taskDetails')
const dockSn = inject('dockSn')
const droneSn = inject('droneSn')
const store = useStore()
let mqttState = null
@@ -157,7 +159,6 @@
const valueTime = ref('00:00:00')
let timer = null
let totalSeconds = 0
const workspace_id = computed(() => taskDetails?.value?.workspace_id)
const list1 = [
@@ -172,13 +173,13 @@
]
const speed = ref(5)
provide('speed',speed)
provide('speed', speed)
const list5 = [
    { name: '上', style: { top: '-70%' }, imgStyle: { top: '20%', left: '50%' } },
    { name: '右', style: { left: '70%' }, imgStyle: { right: '0', top: '50%' } },
    { name: '下', style: { top: '70%' }, imgStyle: { bottom: '0', left: '50%' } },
    { name: '左', style: { left: '-70%' }, imgStyle: { left: '20%', top: '50%' } },
    { name: '上', key: KeyCode.ARROW_UP, operate: 'up', style: { top: '-70%' } },
    { name: '右', key: KeyCode.ARROW_RIGHT, operate: 'right', style: { left: '70%' } },
    { name: '下', key: KeyCode.ARROW_DOWN, operate: 'down', style: { top: '70%' } },
    { name: '左', key: KeyCode.ARROW_LEFT, operate: 'left', style: { left: '-70%' } },
]
const list4 = [
@@ -193,8 +194,6 @@
        { name: '方向', value: '正北' },
    ],
]
const deviceTopicInfo = ref({
    pubTopic: '',
@@ -247,6 +246,13 @@
        .catch(e => {})
}
// 获得有效载荷控制
function getPayloadControl() {
    getPayloadControlApi({ sn: dockSn.value }).then(res => {
        ElMessage.success('成功获得有效载荷控制')
    })
}
// 手动控制
function control() {
    if (!client_id.value) return ElMessage.error('无人机不在空中,不能进入指挥飞行模式。')
@@ -261,6 +267,7 @@
            deviceTopicInfo.value.pubTopic = data.pub[0]
        }
        ElMessage.success('控制成功')
        getPayloadControl()
        isAutoControl.value = false
    })
}
@@ -309,9 +316,10 @@
}
// useManualControl里面用的参数
const paramsRef = computed(()=>({
    droneSn:droneSn.value,
    speed:speed.value,
const paramsRef = computed(() => ({
    droneSn: droneSn.value,
    dockSn: dockSn.value,
    speed: speed.value,
}))
watch(
@@ -320,7 +328,7 @@
        if (workspace_id.value) {
            await createConnect()
            // 使用控制
            manualControl = useManualControl(mqttState, deviceTopicInfo.value, flightController,paramsRef)
            manualControl = useManualControl(mqttState, deviceTopicInfo.value, flightController, paramsRef)
        }
    }
)
@@ -332,6 +340,7 @@
    EventBus.on('controlPanel-onMouseDown', onMouseDown)
    EventBus.on('controlPanel-timeStart', timeStart)
    EventBus.on('controlPanel-timeStop', timeStop)
    EventBus.on('controlPanel-getPayloadControl', getPayloadControl)
})
onBeforeUnmount(() => {
@@ -341,6 +350,7 @@
    EventBus.off('controlPanel-onMouseDown', onMouseDown)
    EventBus.off('controlPanel-timeStart', timeStart)
    EventBus.off('controlPanel-timeStop', timeStop)
    EventBus.off('controlPanel-getPayloadControl', getPayloadControl)
    destroyConnect()
})
</script>
src/components/CurrentTaskDetails/CurrentTaskDetails.vue
@@ -18,12 +18,12 @@
            <TaskDetailsRight v-if="isAutoControl" />
            <template v-else>
                <TaskDetailsHead />
                <TaskDetailsLeft />
            </template>
            <TaskDetailsLeft />
            <!--    控制面板,里面有方法需要立即执行,不可用v-if        -->
            <ControlPanel v-show="!isAutoControl" />
            <img alt="" :src="amplifyImg" class="amplify" @click="amplify" />
            <img alt="" :src="amplifyImg" class="amplify" @click="isMaxMap = !isMaxMap" />
        </div>
    </el-dialog>
</template>
@@ -46,14 +46,12 @@
import { ElMessage } from 'element-plus'
import EventBus from '@/event-bus'
const isAutoControl = ref(true)
const lineQuality = ref(1) //1流畅,2标清
provide('isAutoControl', isAutoControl)
provide('lineQuality', lineQuality)
const isShow = defineModel('show')
const props = defineProps(['id'])
const currentLiveUrl = ref('')
let taskDetails = ref({})
const deviceOsdInfo = ref({})
provide('taskDetails', taskDetails)
@@ -64,10 +62,12 @@
provide('dockSn', dockSn)
provide('droneSn', droneSn)
const isShow = defineModel('show')
const props = defineProps(['id'])
const currentLiveUrl = ref('')
const isTakeOff = ref(false)
const isMaxMap = ref(false)
const amplify = () => {
    isMaxMap.value = !isMaxMap.value
}
let droneWebSocket  //WS实例
// 机巢直播
const getDeviceLiveUrl = async () => {
@@ -80,8 +80,6 @@
    const res = await liveStart(droneSn.value, lineQuality.value)
    currentLiveUrl.value = res.data.data.rtcs_url
}
const isTakeOff = ref(false)
// 设置当前直播地址
const setCurrentLiveUrl = async () => {
@@ -120,7 +118,6 @@
    }
}
let droneWebSocket
// 创建ws连接
const createWsConnect = () => {
    const workspaceId = taskDetails.value.workspace_id
src/components/CurrentTaskDetails/TaskDetailsLeft.vue
@@ -1,12 +1,18 @@
<template>
    <div class="taskDetailsLeft">
        <div class="title">负载控制</div>
        <div class="singleCol" @click="cameraType('wide', '广角')">广角相机</div>
        <div class="singleCol" @click="cameraType('zoom', '变焦')">变焦相机</div>
        <div class="singleCol" @click="cameraType('ir', '红外')">红外相机</div>
        <div
            class="singleCol"
            v-for="item in list1"
            @click="cameraType(item)"
            :key="item.key"
            :class="{ active: item.key === cameraParams.camera_type }"
        >
            {{ item.name }}相机
        </div>
        <div class="multiCol">
            <div>云台回中</div>
            <div>云台朝下</div>
            <div @click="faceToCenter">云台回中</div>
            <div @click="faceToDown">云台朝下</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" />
@@ -19,17 +25,37 @@
            <div>喊话</div>
            <div>广播</div>
        </div>
        <div class="cameraZoom">
            <el-slider
                v-model="cameraParams.zoom_factor"
                vertical
                :max="cameraParams.camera_type === 'ir' ? 20 : 50"
                @input="sliderChange"
            />
        </div>
    </div>
</template>
<script setup>
import EventBus from '@/event-bus'
import { callPhotoAndVideoCmd, switchLivestream } from '@/api/payload'
import { callPhotoAndVideoCmd, cameraParamsChangeApi } from '@/api/payload'
import { ElMessage } from 'element-plus'
import { throttle } from 'lodash'
const isRecording = ref(false)
const list1 = ref([
    { name: '广角', key: 'wide' },
    { name: '变焦', key: 'zoom' },
    { name: '红外', key: 'ir' },
])
const cameraParams = ref({
    zoom_factor: 0,
    camera_type: 'wide',
})
const droneSn = inject('droneSn')
const dockSn = inject('dockSn')
const lineQuality = inject('lineQuality')
const qualityList = [
    { name: '自适应', id: 0 },
@@ -39,14 +65,41 @@
    { name: '超清', id: 4 },
]
// todo
function cameraType(video_type, name) {
    switchLivestream({
        // video_id: this.videoId,
        video_type,
    }).then(res => {
        ElMessage.success(`切换为${name}相机成功`)
function faceToCenter() {}
function faceToDown() {}
// 倍率调整
function sliderChange(val) {
    if (cameraParams.value.camera_type !== 'ir') {
        cameraParams.value.camera_type = val > 1 ? 'zoom' : 'wide'
    }
    cameraParamsChangeThrottle()
}
// 修改相机参数-节流版
const cameraParamsChangeThrottle = throttle(cameraParamsChange, 500)
// 修改相机参数
function cameraParamsChange() {
    cameraParamsChangeApi({
        sn: dockSn.value,
        ...cameraParams.value,
    })
}
function cameraType(row) {
    cameraParams.value.camera_type = row.key
    if (row.key === 'wide') {
        cameraParams.value.zoom_factor = 0
    }
    if (row.key === 'zoom') {
        cameraParams.value.zoom_factor = 5
    }
    if (row.key === 'ir') {
        cameraParams.value.zoom_factor = 0
    }
    cameraParamsChange()
}
// 画质切换
@@ -100,17 +153,33 @@
    align-items: center;
    gap: 12px;
    .qualityChange{
    .cameraZoom {
        position: absolute;
        padding: 16px 0;
        left: 1600px;
        top: -59px;
        width: 112px;
        height: 490px;
        background: rgba(64, 64, 64, 0.15);
        border-radius: 20px 20px 20px 20px;
        .el-slider {
            height: 100%;
        }
    }
    .qualityChange {
        width: 146px;
        height: 40px;
        background: rgba(74, 72, 72, 0.67);
        border:none!important;
        display:flex;
        justify-content:center;
        border: none !important;
        display: flex;
        justify-content: center;
        box-shadow: 0 0.4rem 7.2rem 0 rgba(0, 0, 0, 0.25);
        border-radius: 0.8rem 0.8rem 0.8rem 0.8rem;
        :deep(){
            .el-select__wrapper{
        :deep() {
            .el-select__wrapper {
                width: 100px;
                height: 100%;
                background: transparent;
@@ -118,13 +187,13 @@
                border: none;
                box-shadow: none;
            }
            .el-select__selected-item{
            .el-select__selected-item {
                font-family: Segoe UI, Segoe UI;
                color: #ffffff;
                font-size: 14px;
            }
        }
    }
    .singleCol {
@@ -137,6 +206,12 @@
        cursor: pointer;
        background: rgba(74, 72, 72, 0.67);
        &.active {
            background: rgba(255, 255, 255, 0.78);
            color: #3d3b3b;
            border: 1px solid rgba(255, 255, 255, 0.99);
        }
        &:hover {
            background: rgba(255, 255, 255, 0.78);
            color: #3d3b3b;
src/components/ScaleRule.vue
New file
@@ -0,0 +1,368 @@
<template>
    <div class="zoom-bar">
        <div class="partial-wayline-zoom-scale">
            <div id="zoom-scale" class="zoom-scale" @click="handleZoneClick">
                <!-- 当前刻度指示器 -->
                <div
                    class="current-scale"
                    :class="{ 'scale-z-index': isDragging }"
                    :style="{ bottom: currentScale + '%' }"
                ></div>
                <div class="current-scale drag" :style="{ bottom: currentScale + '%' }" @mousedown="startDrag"></div>
                <!-- 刻度点 -->
                <div
                    v-for="(point, index) in scalePoints"
                    :key="index"
                    class="point-container"
                    :class="{ optical: point.optical }"
                    :style="{ bottom: point.position + '%' }"
                >
                    <span class="point"></span>
                    <span class="label">{{ point.label }}</span>
                </div>
                <!-- 刻度区域 -->
                <div class="scale-container" @click.stop="handleZoneClick">
                    <div
                        v-for="(area, index) in scaleAreasWithWarning"
                        :key="index"
                        class="scale-area"
                        :class="{ optical: area.optical, warning: area.warning }"
                        :style="{ height: area.height + '%' }"
                    >
                        <div class="scale" v-for="n in area.scales" :key="n"></div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<script setup>
const valueData = defineModel()
const currentScale = ref(0)
const isDragging = ref(false)
const startY = ref(0)
const startScale = ref(0)
const scalePoints = ref([
    { position: 0, label: '2X', optical: false },
    { position: 11.1111, label: '5X', optical: true },
    { position: 44.4444, label: '10X', optical: true },
    { position: 77.7778, label: '20X', optical: true },
    { position: 100, label: '200X', optical: false },
])
const scaleAreas = ref([
    { height: 11.1111, scales: 3, optical: false },
    { height: 33.3333, scales: 5, optical: true },
    { height: 33.3333, scales: 5, optical: true },
    { height: 22.2222, scales: 9, optical: false },
])
const isInWarningZone = computed(() => currentScale.value < 11.1111 || currentScale.value > 77.7778)
const scaleAreasWithWarning = computed(() => {
    // 计算每个区域的起始和结束位置
    let currentPosition = 0
    // 计算当前刻度是否在首尾区域范围内
    const isInDangerZone = isInWarningZone.value
    return scaleAreas.value.map((area, index) => {
        currentPosition = currentPosition + area.height
        // 检查是否是第一个或最后一个区域
        const isFirstArea = index === scaleAreas.value.length - 1
        const isLastArea = index === 0
        return {
            ...area,
            warning: (isFirstArea || isLastArea) && isInDangerZone,
        }
    })
})
const currentXValue = computed(() => {
    // 根据当前百分比计算实际的X值
    if (currentScale.value <= 11.1111) {
        // 2X-5X 区间
        return Number(2 + (currentScale.value / 11.1111) * 3).toFixed(1)
    } else if (currentScale.value <= 44.4444) {
        // 5X-10X 区间
        return Number(5 + ((currentScale.value - 11.1111) / 33.3333) * 5).toFixed(1)
    } else if (currentScale.value <= 77.7778) {
        // 10X-20X 区间
        return Number(10 + ((currentScale.value - 44.4444) / 33.3333) * 10).toFixed(1)
    } else {
        // 20X-200X 区间
        return Number(20 + ((currentScale.value - 77.7778) / 22.2222) * 180).toFixed(1)
    }
})
function startDrag(e) {
    e.preventDefault()
    isDragging.value = true
    startY.value = e.clientY
    startScale.value = currentScale.value
    // 添加全局事件监听
    document.addEventListener('mousemove', onDrag)
    document.addEventListener('mouseup', stopDrag)
}
function onDrag(e) {
    if (!isDragging.value) return
    e.preventDefault()
    // 计算移动距离相对于容器高度的百分比
    const container = document.getElementById('zoom-scale')
    const containerHeight = container.offsetHeight
    const deltaY = e.clientY - startY.value
    const deltaPercent = (deltaY / containerHeight) * 100
    // 更新刻度位置,并限制在0-100之间
    let newScale = startScale.value - deltaPercent
    newScale = Math.max(0, Math.min(100, newScale))
    currentScale.value = newScale
}
function stopDrag() {
    isDragging.value = false
    // 移除全局事件监听
    document.removeEventListener('mousemove', onDrag)
    document.removeEventListener('mouseup', stopDrag)
}
function handleZoneClick(e) {
    // 获取容器信息
    const container = document.getElementById('zoom-scale')
    const rect = container.getBoundingClientRect()
    // 计算点击位置相对于容器底部的百分比
    const clickY = e.clientY - rect.top
    const containerHeight = rect.height
    const percentage = ((containerHeight - clickY) / containerHeight) * 100
    // 限制在0-100之间
    currentScale.value = Math.max(0, Math.min(100, percentage))
}
function calculateScaleFromValue(value) {
    const numValue = Number(value)
    if (numValue <= 5) {
        // 2X-5X 区间
        return ((numValue - 2) / 3) * 11.1111
    } else if (numValue <= 10) {
        // 5X-10X 区间
        return 11.1111 + ((numValue - 5) / 5) * 33.3333
    } else if (numValue <= 20) {
        // 10X-20X 区间
        return 44.4444 + ((numValue - 10) / 10) * 33.3333
    } else {
        // 20X-200X 区间
        return 77.7778 + ((numValue - 20) / 180) * 22.2222
    }
}
watch(valueData, newValue => {
    if (!isDragging.value) {
        currentScale.value = calculateScaleFromValue(newValue)
    }
})
const emit = defineEmits(['input', 'warning-change'])
watch(
    () => currentXValue.value,
    newValue => {
        valueData.value = newValue
        emit('input', newValue)
    },
    { immediate: true }
)
watch(
    () => isInWarningZone.value,
    newValue => {
        emit('warning-change', newValue)
    },
    { immediate: true }
)
</script>
<style lang="scss">
.zoom-bar {
    background: linear-gradient(-90deg, rgba(0, 0, 0, 0.5) 36%, transparent 99%);
    height: 300px;
    padding: 4px 28px 4px 30px;
    pointer-events: auto;
    position: absolute;
    top: 50%;
    right: 0;
    transform: translateY(-50%);
}
.partial-wayline-zoom-scale {
    -webkit-user-select: none;
    user-select: none;
    flex-direction: column;
    height: 100%;
    display: flex;
    position: relative;
    .zoom-scale {
        cursor: pointer;
        flex-direction: column;
        flex: 1;
        margin: 8px;
        display: flex;
        position: relative;
        .scale-container {
            flex-direction: column-reverse;
            width: 4px;
            height: 100%;
            margin-left: 1px;
            display: flex;
            position: relative;
            .scale-area {
                flex-direction: column-reverse;
                display: flex;
                &.optical {
                    background: rgba(216, 216, 216, 0.3);
                }
                &.warning {
                    background: rgba(255, 153, 0, 0.5);
                }
                .scale {
                    border-top: 1px solid rgba(255, 255, 255, 0.5);
                    flex: 1;
                    width: 4px;
                    &:last-child {
                        border-top: none;
                    }
                }
            }
        }
    }
    .current-scale {
        background-color: #fff;
        border-radius: 50%;
        justify-content: center;
        align-items: center;
        width: 16px;
        height: 16px;
        margin-bottom: -8px;
        margin-left: -5px;
        display: flex;
        position: absolute;
        z-index: 10;
        pointer-events: none;
        &.drag {
            z-index: 20;
            background-color: transparent;
            cursor: grab;
            width: 16px;
            height: 16px;
            position: absolute;
            padding: 10px;
            margin: -10px;
            box-sizing: content-box;
            pointer-events: auto;
            &:active {
                cursor: grabbing;
            }
        }
        &.scale-z-index {
            z-index: 25;
        }
    }
    .point-container {
        align-items: flex-end;
        display: flex;
        position: absolute;
        pointer-events: none;
        .point {
            z-index: 5;
            background-color: #fff;
            border-radius: 50%;
            width: 6px;
            height: 6px;
            margin-bottom: -3px;
            position: relative;
        }
        .label {
            color: #fff;
            font-size: 10px;
            line-height: 18px;
            position: absolute;
            top: -6px;
            left: 10px;
            transform: scale(0.8);
        }
        &.optical .point:after {
            content: '';
            background: rgba(216, 216, 216, 0.5);
            border-radius: 50%;
            width: 12px;
            height: 12px;
            position: absolute;
            transform: translate(-3px, -3px);
        }
        &.warning .point:after {
            content: '';
            background: #f90;
            border-radius: 50%;
            width: 6px;
            height: 6px;
            position: absolute;
        }
    }
    .input-number-wrapper {
        width: 70px;
        height: 28px;
        margin: 8px 2px;
        input {
            height: 28px;
        }
    }
}
/* 刻度值显示框样式 */
.scale-value-box {
    position: absolute;
    top: 50%;
    right: 90px;
    transform: translateY(-50%);
    background: rgba(0, 0, 0, 0.5);
    padding: 8px 12px;
    border-radius: 4px;
    color: #fff;
    font-size: 14px;
    line-height: 20px;
    min-width: 60px;
    text-align: center;
    &.warning {
        color: #ff9900;
    }
}
.scale-value {
    font-family: monospace;
    font-weight: 500;
}
</style>
src/hooks/components/DevicePopUpBox.vue
@@ -17,7 +17,7 @@
          <div class="num">{{ dataObj.device_num }}</div>
        </div>
        <div class="info-item">
          任务数:
          任务次数:
          <div class="num">{{ dataObj.jobNum }}</div>
        </div>
      </div>
@@ -55,6 +55,8 @@
  const areaCode = props.data.region_code;
  dataObj.value.region_name = props.data.region_name;
  const res = await getDeviceInfoNum({ areaCode });
  console.log('首页地图弹框',res);
  const resJob = await getTotalJobNum({ areaCode });
  dataObj.value.jobNum = resJob.data.data;
  dataObj.value.device_num = res.data.data.device_num;
@@ -126,7 +128,9 @@
      cursor: pointer;
    }
  }
.content {
padding: 0px 23px 25px 15px;
}
  .info {
    height: 34px;
    font-family: Source Han Sans CN, Source Han Sans CN, serif;
@@ -136,9 +140,8 @@
    line-height: 19px;
    display: flex;
    align-items: center;
    padding-left: 15px;
    gap: 0 20px;
    margin: 18px 0;
    justify-content: space-between;
    .info-item {
      display: flex;
@@ -161,13 +164,13 @@
    color: #ffffff;
    line-height: 23px;
    text-shadow: 0px 0px 12px rgba(68, 105, 255, 0.67);
    padding-left: 15px;
    margin-bottom: 14px;
  }
  .status-list {
    display: flex;
    justify-content: space-evenly;
    justify-content: space-between;
    align-items: center;
    .status-item {
src/hooks/controlDrone/useManualControl.js
@@ -1,7 +1,8 @@
import { DRC_METHOD } from '@/const/drc.js'
import { useMqtt } from '@/hooks/controlDrone/useMqtt'
import { ElMessage } from 'element-plus'
import { postPayloadCommands } from '@/api/payload'
import { postPayloadCommands, ptzControlApi } from '@/api/payload'
import { throttle } from 'lodash'
let myInterval
@@ -39,6 +40,7 @@
    let genPortOne = true //是一代机场
    const mqttHooks = useMqtt(mqttState,deviceTopicInfo)
    let seq = 0
    let throttledPtz = null
    function handlePublish(params) {
        const body = {
@@ -72,63 +74,90 @@
        seq = 0
        switch (keyCode) {
            case 'KeyQ':
                if (activeCodeKey === keyCode) return
                if (activeCodeKey.value === keyCode) return
                let objQ = { yaw: 1024-W_SPEED }
                if (genPortOne) {objQ = {w: -W_SPEED}}
                handlePublish(objQ)
                activeCodeKey.value = keyCode
                break
            case 'KeyW':
                if (activeCodeKey === keyCode) return
                if (activeCodeKey.value === keyCode) return
                let objW = { pitch: 1024+SPEED }
                if (genPortOne) {objW = {x: SPEED}}
                handlePublish(objW)
                activeCodeKey.value = keyCode
                break
            case 'KeyE':
                if (activeCodeKey === keyCode) return
                if (activeCodeKey.value === keyCode) return
                let objE = { yaw: 1024+W_SPEED }
                if (genPortOne) {objE = {w: W_SPEED}}
                handlePublish(objE)
                activeCodeKey.value = keyCode
                break
            case 'KeyA':
                if (activeCodeKey === keyCode) return
                if (activeCodeKey.value === keyCode) return
                let objA = { roll: 1024-SPEED }
                if (genPortOne) {objA = {y: -SPEED}}
                handlePublish(objA)
                activeCodeKey.value = keyCode
                break
            case 'KeyS':
                if (activeCodeKey === keyCode) return
                if (activeCodeKey.value === keyCode) return
                let objS = { pitch: 1024-SPEED }
                if (genPortOne) {objS = {x: -SPEED}}
                handlePublish(objS)
                activeCodeKey.value = keyCode
                break
            case 'KeyD':
                if (activeCodeKey === keyCode) return
                if (activeCodeKey.value === keyCode) return
                let objD = { roll: 1024+SPEED }
                if (genPortOne) {objD = {y: SPEED}}
                handlePublish(objD)
                activeCodeKey.value = keyCode
                break
            case 'ArrowUp':
            case 'KeyZ':
                if (activeCodeKey.value === keyCode) return
                handlePublish({ h: HEIGHT })
                let objZ = { throttle: 1024-HEIGHT }
                if (genPortOne) {objZ = {h: -HEIGHT}}
                handlePublish(objZ)
                activeCodeKey.value = keyCode
                break
            // case 'ArrowDown':
            case 'KeyC':
                if (activeCodeKey.value === keyCode) return
                let objC = { throttle: 1024+HEIGHT }
                if (genPortOne) {objC = {h: HEIGHT}}
                handlePublish(objC)
                activeCodeKey.value = keyCode
                break
            case 'ArrowUp':
                ptzThrottle[0]()
                break
            case 'ArrowDown':
                if (activeCodeKey.value === keyCode) return
                handlePublish({ h: -HEIGHT })
                activeCodeKey.value = keyCode
                ptzThrottle[1]()
                break
            case 'ArrowLeft':
                ptzThrottle[2]()
                break
            case 'ArrowRight':
                ptzThrottle[3]()
                break
            default:
                break
        }
    }
    // 云台控制节流list
    const ptzThrottle = ['up','down','left','right'].map(key => throttle(()=>ptzControl(key), 500))
    // 云台上下左右
    function ptzControl(key) {
        const  {dockSn} =  paramsRef.value
        ptzControlApi(key, { sn: dockSn }).then(res => {
        })
    }
    const handlePayloadTurn = (params) => {
        handleClearInterval()
        if (!this.myInterval) {
src/views/Home/HomeLeft/InspectionRaskDetails/InspectionRaskDetails.vue
@@ -8,7 +8,7 @@
                <div class="name">总任务数</div>
            </div>
            <div class="status">
                <div class="item" v-for="(item, index) in list">
                <div class="item" v-for="(item, index) in list" :key="index">
                    <div>
                        <div :style="{ color: item.color }" class="value">
                            {{ jobStatistics[item.field] }}
src/views/Home/HomeLeft/InspectionRaskDetails/InspectionRaskDetailsDialog.vue
@@ -86,11 +86,9 @@
                                    ? 'distributed'
                                    : scope.row.status === 3
                                    ? 'finish '
                                    : 'fail '
                            "
                        >
                                    : scope.row.status === 5
                                    ? 'fail '
                                    : ' '">
                            {{
                                scope.row.status === 1
                                    ? '待执行'
@@ -98,8 +96,9 @@
                                    ? '执行中'
                                    : scope.row.status === 3
                                    ? '已执行'
                                    : '执行失败'
                                    : scope.row.status === 5
                                    ? '执行失败'
                                    : ''
                            }}
                        </span>
                    </template>
@@ -115,7 +114,7 @@
            </el-table>
        </div>
        <!-- 分页 -->
        <div style="display: flex; justify-content: center;margin:15px 0;">
        <div style="display: flex; justify-content: center; margin: 15px 0">
            <el-pagination
                class="ztzf-pagination"
                v-model:current-page="pageParams.current"
@@ -150,8 +149,8 @@
    { label: '待执行', value: 1 },
    { label: '执行中', value: 2 },
    { label: '已完成', value: 3 },
    { label: '执行失败', value: 4 },
    { label: '执行失败', value: 5 },
]
// 设备页面参数
const devicePageParams = ref({
@@ -216,12 +215,11 @@
}
// 获取任务列表
const getJobList = () => {
// 事件状态:0 =待处理,1=待分拨,2=待处理,3=处理中,4=已完成 5=已完结
    // 事件状态:0 =待处理,1=待分拨,2=待处理,3=处理中,4=已完成 5=已完结
    jobList(taskDetailParams).then(res => {
        if (res.data.code !== 0) return
        taskDetailData.value = res.data.data.records
        total.value = res.data.data.total
    })
}
// 机巢列表数据
@@ -327,20 +325,22 @@
}
// 任务状态颜色
// 待执行
.pending {
    color: #e36913;
    color: #ffe17e;
}
// 执行中
.distributed {
    color: #ffc398;
    color: #FFA768;
}
// 已执行
.finish {
    color: #afd9fb;
    color: #8effac;
}
.cancel {
    color: #11c4ff;
}
// 执行失败
.fail {
    color: #8cfea7;
    color: #ff8e8e;
}
</style>
src/views/Home/HomeLeft/MachineNestList.vue
@@ -177,7 +177,7 @@
                padding-top: 6px;
                margin-top: 10px;
                height: 120px;
                height: 100px;
            }
            img {
                width: 45px;
@@ -218,8 +218,7 @@
            // 作业中
            .atcive {
                color: #ffa768;
                background: linear-gradient(90deg, rgba(12, 45, 92, 1) 0%, #154671 50%, rgba(12, 45, 92, 1) 100%),
                    linear-gradient(90deg, rgba(12, 45, 92, 1) 0%, rgba(12, 45, 92, 1) 50%, rgba(12, 45, 92, 1) 100%);
            }
            .numbering {
                font-size: 12px;
src/views/SignMachineNest/MachineRight/InspectionRaskList.vue
@@ -252,6 +252,7 @@
        color: #fff;
        display: flex;
        justify-content: space-between;
background: linear-gradient( 90deg, rgba(71,157,255,0) 0%, rgba(71, 157, 255, 0.12) 50%, rgba(71,157,255,0) 100%);
        .left {
          .left-t {
            height: 24px;