yb hai 2 días
pai
achega
e0629829b6
Modificáronse 1 ficheiros con 186 adicións e 55 borrados
  1. 186 55
      src/views/live-stream/index.vue

+ 186 - 55
src/views/live-stream/index.vue

@@ -266,6 +266,7 @@
                     v-model="timelineDuration"
                     size="small"
                     style="width: 100px"
+                    :disabled="isTimelinePlaying"
                     @change="handleDurationChange"
                   >
                     <el-option :value="60" :label="t('1分钟')" />
@@ -273,7 +274,16 @@
                     <el-option :value="300" :label="t('5分钟')" />
                     <el-option :value="600" :label="t('10分钟')" />
                   </el-select>
-                  <el-button size="small" @click="addTimelinePoint()">+ {{ t('添加点') }}</el-button>
+                  <el-button size="small" :disabled="isTimelinePlaying" @click="addTimelinePoint()">
+                    + {{ t('添加点') }}
+                  </el-button>
+                  <el-switch
+                    v-model="isLoopEnabled"
+                    :disabled="isTimelinePlaying"
+                    size="small"
+                    :active-text="t('循环')"
+                    style="margin-left: 8px"
+                  />
                   <el-button
                     size="small"
                     type="primary"
@@ -292,7 +302,7 @@
                 </div>
 
                 <!-- 时间轴轨道 -->
-                <div class="timeline-track" ref="timelineTrackRef" @click="handleTimelineClick">
+                <div :class="['timeline-track', { 'is-playing': isTimelinePlaying }]" ref="timelineTrackRef">
                   <!-- 时间轴进度指示器 -->
                   <div class="timeline-progress" :style="{ width: `${timelineProgress}%` }"></div>
                   <!-- 关键点 -->
@@ -303,14 +313,14 @@
                       'timeline-point',
                       {
                         active: point.active,
-                        selected: selectedPoint?.id === point.id,
+                        selected: !isTimelinePlaying && selectedPoint?.id === point.id,
                         dragging: draggingPoint?.id === point.id
                       }
                     ]"
                     :style="{ left: `${(point.time / timelineDuration) * 100}%` }"
-                    @click.stop="selectPoint(point)"
-                    @mousedown.stop="startDragPoint($event, point)"
-                    @contextmenu.prevent="handlePointContextMenu($event, point)"
+                    @click.stop="!isTimelinePlaying && selectPoint(point)"
+                    @mousedown.stop="!isTimelinePlaying && startDragPoint($event, point)"
+                    @contextmenu.prevent="!isTimelinePlaying && handlePointContextMenu($event, point)"
                   >
                     <span class="point-label">{{ point.id }}</span>
                     <div class="point-tooltip">
@@ -327,16 +337,47 @@
                   </span>
                 </div>
 
-                <!-- 选中点的操作面板 -->
-                <div v-if="selectedPoint" class="timeline-point-panel">
+                <!-- 选中点的操作面板(巡航中隐藏) -->
+                <div v-if="selectedPoint && !isTimelinePlaying" class="timeline-point-panel">
                   <span class="panel-label">
                     {{ t('当前选中') }}: {{ selectedPoint.presetName || `Point ${selectedPoint.id}` }}
                   </span>
+                  <!-- 关联已有预置位 -->
+                  <el-select
+                    v-model="linkPresetId"
+                    size="small"
+                    :placeholder="t('关联预置位')"
+                    style="width: 140px"
+                    clearable
+                    @change="handleLinkPreset"
+                  >
+                    <el-option
+                      v-for="preset in ptzPresetList"
+                      :key="preset.id"
+                      :value="preset.id"
+                      :label="preset.name || `Preset ${preset.id}`"
+                    />
+                  </el-select>
                   <el-button size="small" type="primary" :loading="savingPreset" @click="saveCurrentPoint">
                     {{ selectedPoint.active ? t('更新位置') : t('保存位置') }}
                   </el-button>
                   <el-button size="small" type="danger" @click="deleteSelectedPoint">{{ t('删除') }}</el-button>
                 </div>
+                <!-- 自动映射按钮(巡航中隐藏) -->
+                <div v-if="timelinePoints.length > 0 && !isTimelinePlaying" class="timeline-auto-link">
+                  <el-button
+                    size="small"
+                    type="success"
+                    @click="autoLinkPresets"
+                    :disabled="ptzPresetList.length === 0"
+                  >
+                    <el-icon>
+                      <Link />
+                    </el-icon>
+                    {{ t('自动映射') }}
+                  </el-button>
+                  <span class="auto-link-hint">{{ t('将点1-N映射到Preset 1-N') }}</span>
+                </div>
               </div>
 
               <!-- 底部播放控制 -->
@@ -627,7 +668,8 @@ import {
   ZoomIn,
   ZoomOut,
   Close,
-  Position
+  Position,
+  Link
 } from '@element-plus/icons-vue'
 import { Icon } from '@iconify/vue'
 import { useI18n } from 'vue-i18n'
@@ -732,6 +774,8 @@ const selectedPoint = ref<TimelinePoint | null>(null)
 const isTimelinePlaying = ref(false)
 const timelineProgress = ref(0)
 const savingPreset = ref(false)
+const linkPresetId = ref<string | null>(null) // 关联预置位选择
+const isLoopEnabled = ref(false) // 是否循环巡航
 let timelinePlayAbort: AbortController | null = null
 
 // 拖拽状态
@@ -1547,22 +1591,6 @@ function formatTimelineTime(seconds: number): string {
   return `${mins}:${secs.toString().padStart(2, '0')}`
 }
 
-// 点击时间轴添加点
-function handleTimelineClick(e: MouseEvent) {
-  // 如果正在拖拽,不添加点
-  if (draggingPoint.value) return
-
-  const track = timelineTrackRef.value
-  if (!track) return
-
-  const rect = track.getBoundingClientRect()
-  const x = e.clientX - rect.left
-  const percent = x / rect.width
-  const time = Math.round(percent * timelineDuration.value)
-
-  addTimelinePoint(time)
-}
-
 // 开始拖拽点
 function startDragPoint(e: MouseEvent, point: TimelinePoint) {
   // 只响应鼠标左键
@@ -1701,6 +1729,55 @@ function deleteSelectedPoint() {
   }
 }
 
+// 关联已有预置位
+function handleLinkPreset(presetId: string | null) {
+  if (!selectedPoint.value || !presetId) {
+    linkPresetId.value = null
+    return
+  }
+
+  const preset = ptzPresetList.value.find((p) => p.id === presetId)
+  if (preset) {
+    selectedPoint.value.presetId = parseInt(preset.id)
+    selectedPoint.value.presetName = preset.name || `Preset ${preset.id}`
+    selectedPoint.value.active = true
+    saveTimelineConfig()
+    ElMessage.success(`${t('已关联')}: ${selectedPoint.value.presetName}`)
+    linkPresetId.value = null
+  }
+}
+
+// 自动映射:点1-N 对应 Preset 1-N
+function autoLinkPresets() {
+  if (ptzPresetList.value.length === 0) {
+    ElMessage.warning(t('暂无可用预置位'))
+    return
+  }
+
+  // 按ID排序预置位列表(取前几个数字小的)
+  const sortedPresets = [...ptzPresetList.value]
+    .filter((p) => !isNaN(parseInt(p.id)))
+    .sort((a, b) => parseInt(a.id) - parseInt(b.id))
+
+  let linkedCount = 0
+  timelinePoints.value.forEach((point, index) => {
+    if (index < sortedPresets.length) {
+      const preset = sortedPresets[index]
+      point.presetId = parseInt(preset.id)
+      point.presetName = preset.name || `Preset ${preset.id}`
+      point.active = true
+      linkedCount++
+    }
+  })
+
+  if (linkedCount > 0) {
+    saveTimelineConfig()
+    ElMessage.success(`${t('已映射')} ${linkedCount} ${t('个点位')}`)
+  } else {
+    ElMessage.warning(t('没有可映射的点位'))
+  }
+}
+
 // 右键菜单删除点
 function handlePointContextMenu(e: MouseEvent, point: TimelinePoint) {
   ElMessageBox.confirm(`${t('确定删除')} "${point.presetName || `Point ${point.id}`}"?`, t('删除确认'), {
@@ -1732,6 +1809,10 @@ function handleDurationChange() {
   saveTimelineConfig()
 }
 
+// 进度条动画帧ID
+let progressAnimationId: number | null = null
+let loopStartTime = 0
+
 // 播放巡航
 async function playTimeline() {
   const activePoints = timelinePoints.value.filter((p) => p.active).sort((a, b) => a.time - b.time)
@@ -1748,46 +1829,61 @@ async function playTimeline() {
 
   isTimelinePlaying.value = true
   timelineProgress.value = 0
+  selectedPoint.value = null // 清除选中状态
   timelinePlayAbort = new AbortController()
 
-  const startTime = Date.now()
   const totalDuration = timelineDuration.value * 1000 // 转为毫秒
 
-  try {
-    // 循环播放各个点
-    for (let i = 0; i < activePoints.length; i++) {
-      if (timelinePlayAbort?.signal.aborted) break
-
-      const point = activePoints[i]
-      const nextPoint = activePoints[i + 1]
-      const waitTime = (point.time / timelineDuration.value) * totalDuration - (Date.now() - startTime)
+  // 启动进度条动画(持续更新)
+  function updateProgress() {
+    if (!isTimelinePlaying.value) return
+    const elapsed = Date.now() - loopStartTime
+    const progress = Math.min((elapsed / totalDuration) * 100, 100)
+    timelineProgress.value = progress
+    progressAnimationId = requestAnimationFrame(updateProgress)
+  }
 
-      // 等待到达该点的时间
-      if (waitTime > 0) {
-        await sleep(waitTime, timelinePlayAbort.signal)
+  try {
+    // 循环播放(do-while 支持循环模式)
+    do {
+      loopStartTime = Date.now()
+      timelineProgress.value = 0
+
+      // 启动/重启进度条动画
+      if (progressAnimationId) {
+        cancelAnimationFrame(progressAnimationId)
       }
+      progressAnimationId = requestAnimationFrame(updateProgress)
 
-      if (timelinePlayAbort?.signal.aborted) break
+      // 播放各个点
+      for (let i = 0; i < activePoints.length; i++) {
+        if (timelinePlayAbort?.signal.aborted) break
 
-      // 跳转到该预置位
-      if (point.presetId) {
-        await presetGoto({ cameraId, presetId: point.presetId })
-        selectedPoint.value = point
-      }
+        const point = activePoints[i]
+        const targetTime = (point.time / timelineDuration.value) * totalDuration
+        const waitTime = targetTime - (Date.now() - loopStartTime)
 
-      // 更新进度
-      timelineProgress.value = (point.time / timelineDuration.value) * 100
+        // 等待到达该点的时间
+        if (waitTime > 0) {
+          await sleep(waitTime, timelinePlayAbort.signal)
+        }
+
+        if (timelinePlayAbort?.signal.aborted) break
 
-      // 计算在这个点停留的时间
-      if (nextPoint) {
-        const stayTime = ((nextPoint.time - point.time) / timelineDuration.value) * totalDuration
-        if (stayTime > 0) {
-          await sleep(stayTime, timelinePlayAbort.signal)
+        // 跳转到该预置位
+        if (point.presetId) {
+          await presetGoto({ cameraId, presetId: point.presetId })
         }
       }
-    }
 
-    // 播放完成
+      // 等待剩余时间
+      const remainingTime = totalDuration - (Date.now() - loopStartTime)
+      if (remainingTime > 0 && !timelinePlayAbort?.signal.aborted) {
+        await sleep(remainingTime, timelinePlayAbort.signal)
+      }
+    } while (isLoopEnabled.value && !timelinePlayAbort?.signal.aborted)
+
+    // 播放完成(非循环模式或被停止)
     if (!timelinePlayAbort?.signal.aborted) {
       timelineProgress.value = 100
       await sleep(500)
@@ -1799,6 +1895,11 @@ async function playTimeline() {
       ElMessage.error(t('巡航播放失败'))
     }
   } finally {
+    // 停止进度条动画
+    if (progressAnimationId) {
+      cancelAnimationFrame(progressAnimationId)
+      progressAnimationId = null
+    }
     isTimelinePlaying.value = false
     timelineProgress.value = 0
     timelinePlayAbort = null
@@ -1807,6 +1908,11 @@ async function playTimeline() {
 
 // 停止巡航
 function stopTimeline() {
+  // 停止进度条动画
+  if (progressAnimationId) {
+    cancelAnimationFrame(progressAnimationId)
+    progressAnimationId = null
+  }
   if (timelinePlayAbort) {
     timelinePlayAbort.abort()
     timelinePlayAbort = null
@@ -2216,9 +2322,17 @@ onMounted(async () => {
     height: 32px;
     background: linear-gradient(to right, #374151, #374151);
     border-radius: 4px;
-    cursor: crosshair;
     margin-bottom: 8px;
 
+    &.is-playing {
+      cursor: not-allowed;
+
+      .timeline-point {
+        cursor: not-allowed;
+        pointer-events: none;
+      }
+    }
+
     .timeline-progress {
       position: absolute;
       top: 0;
@@ -2344,11 +2458,28 @@ onMounted(async () => {
     padding: 10px 12px;
     background: #2a2a2a;
     border-radius: 4px;
+    flex-wrap: wrap;
 
     .panel-label {
       color: #d1d5db;
       font-size: 12px;
       flex: 1;
+      min-width: 100%;
+    }
+  }
+
+  .timeline-auto-link {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-top: 8px;
+    padding: 8px 12px;
+    background: #1f1f1f;
+    border-radius: 4px;
+
+    .auto-link-hint {
+      color: #6b7280;
+      font-size: 11px;
     }
   }
 }
@@ -2387,8 +2518,8 @@ onMounted(async () => {
       }
 
       .el-collapse-item__content {
-        padding: 16px;
-        padding-bottom: 12px;
+        padding: 0;
+        // padding-bottom: 12px;
       }
     }