Răsfoiți Sursa

优化云台控制功能

liujintao 6 zile în urmă
părinte
comite
4d58bf93f0
2 a modificat fișierele cu 184 adăugiri și 204 ștergeri
  1. 12 1
      src/api/ptz.ts
  2. 172 203
      src/views/preview/index.vue

+ 12 - 1
src/api/ptz.ts

@@ -42,7 +42,7 @@ export function getPtzStatus() {
 }
 
 /**
- * Get device channel info (LensNumber)
+ * Get device channel info (LensNumber, ChnNumber)
  */
 export function getDeviceNum() {
   return request<any>({
@@ -50,3 +50,14 @@ export function getDeviceNum() {
     method: 'get'
   })
 }
+
+/**
+ * Switch device channel (Fixed/PTZ camera)
+ */
+export function putDeviceNum(data: { ChnNumber: number }) {
+  return request<any>({
+    url: `/API/V1.0/Video/DeviceNum`,
+    method: 'put',
+    data
+  })
+}

+ 172 - 203
src/views/preview/index.vue

@@ -4,84 +4,91 @@
     class="video-container"
     :class="{ 'is-fullscreen': isFullscreen }"
   >
-    <!-- PTZ 球机控制面板(左侧) -->
-    <div class="ptz-wrapper">
-      <div class="ptz-panel" :class="{ 'is-collapsed': ptzCollapsed }">
-        <div class="ptz-title">PTZ Control</div>
-        <div class="ptz-grid">
-        <div class="ptz-placeholder" />
-        <button
-          class="ptz-btn"
-          title="Up"
-          @mousedown="handlePtzStart(PTZDirection.Up)"
-          @mouseup="handlePtzStop"
-        >
-          <svg viewBox="0 0 24 24" width="22" height="22">
-            <path fill="currentColor" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/>
-          </svg>
-        </button>
-        <div class="ptz-placeholder" />
-
-        <button
-          class="ptz-btn"
-          title="Left"
-          @mousedown="handlePtzStart(PTZDirection.Left)"
-          @mouseup="handlePtzStop"
-        >
-          <svg viewBox="0 0 24 24" width="22" height="22">
-            <path fill="currentColor" d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
-          </svg>
-        </button>
-        <button
-          class="ptz-btn ptz-btn-center"
-          title="Reset"
-          @click="handlePtzCenter"
-        >
-          <svg viewBox="0 0 24 24" width="22" height="22">
-            <path fill="currentColor" d="M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8A5.87 5.87 0 0 1 6 12c0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z"/>
-          </svg>
-        </button>
-        <button
-          class="ptz-btn"
-          title="Right"
-          @mousedown="handlePtzStart(PTZDirection.Right)"
-          @mouseup="handlePtzStop"
-        >
-          <svg viewBox="0 0 24 24" width="22" height="22">
-            <path fill="currentColor" d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
-          </svg>
-        </button>
-
-        <div class="ptz-placeholder" />
-        <button
-          class="ptz-btn"
-          title="Down"
-          @mousedown="handlePtzStart(PTZDirection.Down)"
-          @mouseup="handlePtzStop"
-        >
-          <svg viewBox="0 0 24 24" width="22" height="22">
-            <path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/>
-          </svg>
-        </button>
-        <div class="ptz-placeholder" />
+    <!-- Left sidebar control panel -->
+    <div v-if="showChannelSelector || ptzEnabled" class="control-sidebar">
+      <div class="control-panel" :class="{ 'is-collapsed': ptzCollapsed }">
+        <!-- Channel selector -->
+        <div v-if="showChannelSelector" class="channel-selector">
+          <div class="control-label">Channel</div>
+          <el-select
+            v-model="selectedChn"
+            size="small"
+            @change="handleChnChange"
+          >
+            <el-option
+              v-for="ch in channelOptions"
+              :key="ch.value"
+              :label="ch.label"
+              :value="ch.value"
+            />
+          </el-select>
+        </div>
+
+        <!-- PTZ control -->
+        <template v-if="ptzEnabled">
+          <div v-if="showChannelSelector" class="control-divider" />
+          <div class="control-label">PTZ Control</div>
+          <div class="ptz-grid">
+            <div class="ptz-placeholder" />
+            <button
+              class="ptz-btn"
+              title="Up"
+              @mousedown="handlePtzStart(PTZDirection.Up)"
+              @mouseup="handlePtzStop"
+            >
+              <svg viewBox="0 0 24 24" width="22" height="22">
+                <path fill="currentColor" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/>
+              </svg>
+            </button>
+            <div class="ptz-placeholder" />
+
+            <button
+              class="ptz-btn"
+              title="Left"
+              @mousedown="handlePtzStart(PTZDirection.Left)"
+              @mouseup="handlePtzStop"
+            >
+              <svg viewBox="0 0 24 24" width="22" height="22">
+                <path fill="currentColor" d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
+              </svg>
+            </button>
+            <button
+              class="ptz-btn ptz-btn-center"
+              title="Reset"
+              @click="handlePtzCenter"
+            >
+              <svg viewBox="0 0 24 24" width="22" height="22">
+                <path fill="currentColor" d="M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8A5.87 5.87 0 0 1 6 12c0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z"/>
+              </svg>
+            </button>
+            <button
+              class="ptz-btn"
+              title="Right"
+              @mousedown="handlePtzStart(PTZDirection.Right)"
+              @mouseup="handlePtzStop"
+            >
+              <svg viewBox="0 0 24 24" width="22" height="22">
+                <path fill="currentColor" d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
+              </svg>
+            </button>
+
+            <div class="ptz-placeholder" />
+            <button
+              class="ptz-btn"
+              title="Down"
+              @mousedown="handlePtzStart(PTZDirection.Down)"
+              @mouseup="handlePtzStop"
+            >
+              <svg viewBox="0 0 24 24" width="22" height="22">
+                <path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/>
+              </svg>
+            </button>
+            <div class="ptz-placeholder" />
+          </div>
+        </template>
       </div>
-
-      <!-- 速度控制 -->
-<!--      <div class="ptz-speed">-->
-<!--        <span class="ptz-speed-label">Speed</span>-->
-<!--        <el-slider-->
-<!--          v-model="ptzSpeed"-->
-<!--          :min="1"-->
-<!--          :max="8"-->
-<!--          :step="1"-->
-<!--          :show-tooltip="false"-->
-<!--          size="small"-->
-<!--        />-->
-<!--        <span class="ptz-speed-value">{{ ptzSpeed }}</span>-->
-<!--      </div>-->
-      </div>
-      <!-- 折叠/展开按钮 -->
-      <button class="ptz-toggle" :title="ptzCollapsed ? 'Expand PTZ' : 'Collapse PTZ'" @click="ptzCollapsed = !ptzCollapsed">
+      <!-- Collapse toggle -->
+      <button class="panel-toggle" :title="ptzCollapsed ? 'Expand' : 'Collapse'" @click="ptzCollapsed = !ptzCollapsed">
         <svg viewBox="0 0 24 24" width="16" height="16">
           <path fill="currentColor" :d="ptzCollapsed ? 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z' : 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z'"/>
         </svg>
@@ -93,25 +100,8 @@
       <MyVideo
         ref="videoRef"
         :drag-flag="true"
-        :channel="currentChannel"
         @video-error="handleVideoError"
       />
-      <!-- Channel selector -->
-      <div v-if="channelOptions.length > 1" class="channel-selector">
-        <el-select
-          v-model="currentChannel"
-          size="small"
-          placeholder="Channel"
-          @change="handleChannelChange"
-        >
-          <el-option
-            v-for="ch in channelOptions"
-            :key="ch.value"
-            :label="ch.label"
-            :value="ch.value"
-          />
-        </el-select>
-      </div>
       <button
         class="fullscreen-btn"
         :title="isFullscreen ? 'Exit Fullscreen (F)' : 'Fullscreen (F)'"
@@ -140,8 +130,9 @@
 
 <script setup lang="ts">
 import { ref, onMounted, onUnmounted } from 'vue'
+import { ElMessage } from 'element-plus'
 import MyVideo from '@/components/myVideo.vue'
-import { ptzControl, PTZDirection, getDeviceNum } from '@/api/ptz'
+import { ptzControl, PTZDirection, getDeviceNum, putDeviceNum } from '@/api/ptz'
 
 const videoRef = ref()
 const isFullscreen = ref(false)
@@ -151,32 +142,49 @@ const ptzSpeed = ref(4)
 const isPtzMoving = ref(false)
 const ptzCollapsed = ref(false)
 
-// Channel
-const currentChannel = ref('ch1')
-const channelOptions = ref<{ label: string; value: string }[]>([
-  { label: 'Channel 1', value: 'ch1' }
-])
-
-// Fetch lens count and build channel options
-const fetchChannelCount = async () => {
+// Device state
+const showChannelSelector = ref(false)
+const selectedChn = ref(0) // 0: Fixed, 1: PTZ
+const ptzEnabled = ref(false)
+const channelOptions = [
+  { label: 'Fixed Camera', value: 0 },
+  { label: 'PTZ Camera', value: 1 }
+]
+
+// Fetch device info
+const fetchDeviceInfo = async () => {
   try {
-    // const res = await getDeviceNum()
-    // const lensNumber = res?.data?.LensNumber ?? 1
-    const lensNumber = 2
-    const options = []
-    for (let i = 1; i <= lensNumber; i++) {
-      options.push({ label: `Channel ${i}`, value: `ch${i}` })
-    }
-    channelOptions.value = options
+    const res = await getDeviceNum()
+    // const res = {"data":{"LensNumber":2,"ChnNumber":0}}
+    const lensNumber = res?.data?.LensNumber ?? 1
+    const chnNumber = res?.data?.ChnNumber ?? 0
+
+    // Dual lens: show channel selector
+    showChannelSelector.value = lensNumber >= 2
+
+    // Sync current selection and PTZ state
+    selectedChn.value = chnNumber
+    ptzEnabled.value = chnNumber === 1
   } catch (error) {
-    console.error('Failed to get device channel count:', error)
+    console.error('Failed to get device info:', error)
   }
 }
 
-// Switch channel: refresh video stream
-const handleChannelChange = () => {
-  if (videoRef.value?.refreshWebSocket) {
-    videoRef.value.refreshWebSocket()
+// User switches between Fixed / PTZ camera via dropdown
+const handleChnChange = async (val: number) => {
+  try {
+    await putDeviceNum({ ChnNumber: val })
+    // Re-fetch to get confirmed state from device
+    await fetchDeviceInfo()
+    // Refresh video stream after switch
+    if (videoRef.value?.refreshWebSocket) {
+      videoRef.value.refreshWebSocket()
+    }
+  } catch (error) {
+    console.error('Failed to switch channel:', error)
+    ElMessage.error('Failed to switch channel, please try again')
+    // Revert selection on failure
+    await fetchDeviceInfo()
   }
 }
 
@@ -247,7 +255,7 @@ const handleVideoError = (error: any) => {
 onMounted(() => {
   document.addEventListener('fullscreenchange', handleFullscreenChange)
   document.addEventListener('keydown', handleKeydown)
-  fetchChannelCount()
+  fetchDeviceInfo()
 })
 
 onUnmounted(() => {
@@ -283,12 +291,6 @@ onUnmounted(() => {
 
 .video-wrapper {
   position: relative;
-  /*
-   * 用固定的 calc 尺寸而非 flex:1,
-   * 这样 video.load() 清空内容时容器不会收缩。
-   * 宽度 = 总宽 - PTZ面板(180px) - gap(16px) - 两侧padding(32px) - 留白(20px)
-   * 高度按 16:9 从宽度推算,同时不超过容器高度
-   */
   width: calc(100vw - 208px);
   height: calc((100vw - 208px) / 16 * 9);
   max-height: calc(100vh - 110px);
@@ -300,7 +302,6 @@ onUnmounted(() => {
     height: 100%;
   }
 
-  // 全屏时视频撑满整个屏幕
   .video-container.is-fullscreen & {
     width: 100vw;
     height: 100vh;
@@ -308,11 +309,6 @@ onUnmounted(() => {
     max-height: none;
   }
 
-  :deep(.preview-container) {
-    width: 100%;
-    height: 100%;
-  }
-
   :deep(.video_container) {
     width: 100%;
     height: 100%;
@@ -342,45 +338,6 @@ onUnmounted(() => {
   }
 }
 
-/* Channel selector */
-.channel-selector {
-  position: absolute;
-  bottom: 48px;
-  right: 16px;
-  z-index: 10;
-  opacity: 0;
-  transition: opacity 0.3s ease;
-
-  .video-wrapper:hover & {
-    opacity: 1;
-  }
-
-  :deep(.el-select) {
-    width: 120px;
-  }
-
-  :deep(.el-input__wrapper) {
-    background: rgba(0, 0, 0, 0.6);
-    border: none;
-    box-shadow: none;
-    backdrop-filter: blur(8px);
-    border-radius: 6px;
-  }
-
-  :deep(.el-input__inner) {
-    color: #fff;
-    font-size: 12px;
-
-    &::placeholder {
-      color: rgba(255, 255, 255, 0.6);
-    }
-  }
-
-  :deep(.el-select__caret) {
-    color: #fff;
-  }
-}
-
 .fullscreen-btn {
   position: absolute;
   top: 16px;
@@ -415,22 +372,28 @@ onUnmounted(() => {
   }
 }
 
-/* PTZ 云台控制面板 */
-.ptz-wrapper {
-  position: relative;
+/* Control sidebar */
+.control-sidebar {
   flex-shrink: 0;
   display: flex;
   align-items: center;
+
+  .video-container.is-fullscreen & {
+    position: absolute;
+    left: 20px;
+    top: 50%;
+    transform: translateY(-50%);
+    z-index: 10;
+  }
 }
 
-.ptz-panel {
+.control-panel {
   width: 140px;
   background: #f5f7fa;
   border-radius: 10px;
   padding: 12px;
   display: flex;
   flex-direction: column;
-  align-items: center;
   gap: 10px;
   overflow: hidden;
   transition: width 0.3s ease, padding 0.3s ease, opacity 0.3s ease;
@@ -446,7 +409,9 @@ onUnmounted(() => {
     background: rgba(0, 0, 0, 0.7);
     backdrop-filter: blur(12px);
 
-    .ptz-title { color: #fff; }
+    .control-label { color: #fff; }
+    .control-divider { border-color: rgba(255, 255, 255, 0.15); }
+
     .ptz-btn {
       background: rgba(255, 255, 255, 0.1);
       color: #fff;
@@ -459,11 +424,40 @@ onUnmounted(() => {
       border-color: rgba(64, 158, 255, 0.5) !important;
       &:hover { background: rgba(64, 158, 255, 0.5) !important; }
     }
-    .ptz-speed-label, .ptz-speed-value { color: #fff; }
+
+    :deep(.el-input__wrapper) {
+      background: rgba(255, 255, 255, 0.1);
+      box-shadow: none;
+    }
+    :deep(.el-input__inner) { color: #fff; }
+    :deep(.el-select__caret) { color: #fff; }
+  }
+}
+
+.control-label {
+  font-size: 12px;
+  font-weight: 600;
+  color: #909399;
+  white-space: nowrap;
+}
+
+.control-divider {
+  border: none;
+  border-top: 1px solid #e4e7ed;
+  margin: 2px 0;
+}
+
+.channel-selector {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+
+  :deep(.el-select) {
+    width: 100%;
   }
 }
 
-.ptz-toggle {
+.panel-toggle {
   width: 24px;
   height: 48px;
   border: 1px solid #e4e7ed;
@@ -498,21 +492,6 @@ onUnmounted(() => {
   }
 }
 
-.video-container.is-fullscreen .ptz-wrapper {
-  position: absolute;
-  left: 20px;
-  top: 50%;
-  transform: translateY(-50%);
-  z-index: 10;
-}
-
-.ptz-title {
-  font-size: 14px;
-  font-weight: 600;
-  color: #303133;
-  white-space: nowrap;
-}
-
 .ptz-grid {
   display: grid;
   grid-template-columns: repeat(3, 1fr);
@@ -547,17 +526,6 @@ onUnmounted(() => {
   &:hover { background: #d9ecff !important; }
 }
 
-.ptz-speed {
-  width: 100%;
-  display: flex;
-  align-items: center;
-  gap: 8px;
-
-  .ptz-speed-label { font-size: 12px; color: #909399; white-space: nowrap; }
-  .ptz-speed-value { font-size: 12px; color: #606266; font-weight: 600; min-width: 14px; text-align: center; }
-  :deep(.el-slider) { flex: 1; }
-}
-
 @media (max-width: 768px) {
   .video-container {
     flex-direction: column;
@@ -565,7 +533,8 @@ onUnmounted(() => {
     padding: 0;
     gap: 12px;
   }
-  .ptz-panel { width: 100%; max-width: 200px; order: 2; }
+  .control-sidebar { order: 2; }
+  .control-panel { width: 100%; max-width: 200px; }
   .video-wrapper {
     order: 1;
     width: 100vw;