From b80dbbcb1340a5d02b2137efc9ff0e5d84c50647 Mon Sep 17 00:00:00 2001
From: 罗广辉 <guanghui.luo@foxmail.com>
Date: Thu, 17 Apr 2025 17:12:39 +0800
Subject: [PATCH] feat: 控制台相机控制

---
 src/hooks/controlDrone/useManualControl.js                      |   55 +++-
 src/api/drc.js                                                  |    2 
 src/components/CurrentTaskDetails/ControlPanel/ControlPanel.vue |   52 ++-
 src/components/CurrentTaskDetails/ControlPanel/BaseControl.vue  |    1 
 src/api/payload.js                                              |   47 +++
 src/components/ScaleRule.vue                                    |  368 ++++++++++++++++++++++++++++++
 src/components/CurrentTaskDetails/TaskDetailsLeft.vue           |  117 ++++++++-
 src/components/CurrentTaskDetails/CurrentTaskDetails.vue        |   19 
 8 files changed, 583 insertions(+), 78 deletions(-)

diff --git a/src/api/drc.js b/src/api/drc.js
index fba2361..674f8c7 100644
--- a/src/api/drc.js
+++ b/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)
diff --git a/src/api/payload.js b/src/api/payload.js
index ed91d52..61e91ea 100644
--- a/src/api/payload.js
+++ b/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,
   });
diff --git a/src/components/CurrentTaskDetails/ControlPanel/BaseControl.vue b/src/components/CurrentTaskDetails/ControlPanel/BaseControl.vue
index aacaf7d..fd914f6 100644
--- a/src/components/CurrentTaskDetails/ControlPanel/BaseControl.vue
+++ b/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"
diff --git a/src/components/CurrentTaskDetails/ControlPanel/ControlPanel.vue b/src/components/CurrentTaskDetails/ControlPanel/ControlPanel.vue
index f4ba147..d78a55a 100644
--- a/src/components/CurrentTaskDetails/ControlPanel/ControlPanel.vue
+++ b/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>
diff --git a/src/components/CurrentTaskDetails/CurrentTaskDetails.vue b/src/components/CurrentTaskDetails/CurrentTaskDetails.vue
index 649f760..8d51880 100644
--- a/src/components/CurrentTaskDetails/CurrentTaskDetails.vue
+++ b/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
diff --git a/src/components/CurrentTaskDetails/TaskDetailsLeft.vue b/src/components/CurrentTaskDetails/TaskDetailsLeft.vue
index ce9f85d..812bd86 100644
--- a/src/components/CurrentTaskDetails/TaskDetailsLeft.vue
+++ b/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;
diff --git a/src/components/ScaleRule.vue b/src/components/ScaleRule.vue
new file mode 100644
index 0000000..bb86e81
--- /dev/null
+++ b/src/components/ScaleRule.vue
@@ -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>
diff --git a/src/hooks/controlDrone/useManualControl.js b/src/hooks/controlDrone/useManualControl.js
index a2e0b97..4dc12ff 100644
--- a/src/hooks/controlDrone/useManualControl.js
+++ b/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) {

--
Gitblit v1.9.3