Kaynağa Gözat

feat(live-stream): enhance preset management and UI improvements

- Added new fields for error handling in the BaseResponse interface.
- Improved the layout of input fields and table components in the live stream view for better readability.
- Streamlined the handling of local presets, including editing, updating, and deleting functionalities.
- Enhanced visual feedback for preset actions and improved user experience with updated styling.
yb 1 gün önce
ebeveyn
işleme
69a63aa4e5
2 değiştirilmiş dosya ile 254 ekleme ve 50 silme
  1. 2 0
      src/types/index.ts
  2. 252 50
      src/views/live-stream/index.vue

+ 2 - 0
src/types/index.ts

@@ -3,6 +3,8 @@ export interface BaseResponse {
   success: boolean
   errCode?: string
   errMessage?: string
+  code?: number
+  msg?: string
 }
 
 // API 响应类型 - 单个数据响应

+ 252 - 50
src/views/live-stream/index.vue

@@ -519,52 +519,45 @@
                 <!-- 预置位列表 -->
                 <el-collapse-item name="preset">
                   <template #title>
-                    <div class="collapse-title-with-action">
-                      <span class="collapse-title">{{ t('预置位') }}</span>
-                      <el-button
-                        type="primary"
-                        link
-                        size="small"
-                        @click.stop="loadPTZPresets"
-                        :loading="presetsLoading"
-                      >
-                        <el-icon>
-                          <Refresh />
-                        </el-icon>
-                      </el-button>
-                    </div>
+                    <span class="collapse-title">{{ t('预置位') }}</span>
                   </template>
-                  <div class="preset-list" v-loading="presetsLoading">
+                  <div class="preset-list">
                     <div
-                      v-for="preset in ptzPresetList"
+                      v-for="preset in localPresetList"
                       :key="preset.id"
-                      :class="['preset-item', { active: activePresetId === preset.id.toString() }]"
+                      :class="['preset-item', { active: activePresetId === preset.id }]"
                     >
-                      <span class="preset-index">{{ preset.id }}</span>
-                      <span class="preset-name">{{ preset.name || `Preset ${preset.id}` }}</span>
+                      <span class="preset-index">{{ preset.pointId }}</span>
+                      <el-input
+                        v-if="editingPresetId === preset.id"
+                        v-model="editingPresetName"
+                        size="small"
+                        class="preset-name-input"
+                        @blur="savePresetName(preset)"
+                        @keyup.enter="savePresetName(preset)"
+                        @keyup.esc="cancelEditPresetName"
+                        autofocus
+                      />
+                      <span v-else class="preset-name" @dblclick="startEditPresetName(preset)">{{ preset.name }}</span>
                       <div class="preset-actions">
                         <el-tooltip :content="t('跳转')" placement="top">
-                          <el-icon class="action-icon" @click="handleGotoPTZPreset(preset)">
+                          <el-icon class="action-icon" @click="handleGotoLocalPreset(preset)">
                             <Position />
                           </el-icon>
                         </el-tooltip>
                         <el-tooltip :content="t('设置')" placement="top">
-                          <el-icon class="action-icon" @click="handleEditPreset(preset)">
+                          <el-icon class="action-icon" @click="handleUpdateLocalPreset(preset)">
                             <Setting />
                           </el-icon>
                         </el-tooltip>
                         <el-tooltip :content="t('删除')" placement="top">
-                          <el-icon class="action-icon delete" @click="handleDeletePreset(preset)">
+                          <el-icon class="action-icon delete" @click="handleDeleteLocalPreset(preset)">
                             <Close />
                           </el-icon>
                         </el-tooltip>
                       </div>
                     </div>
-                    <el-empty
-                      v-if="!presetsLoading && ptzPresetList.length === 0"
-                      :description="t('暂无预置位')"
-                      :image-size="60"
-                    />
+                    <el-empty v-if="localPresetList.length === 0" :description="t('暂无预置位')" :image-size="60" />
                   </div>
                 </el-collapse-item>
 
@@ -737,6 +730,8 @@ interface PTZCapabilities {
 }
 const ptzPresetList = ref<PresetInfo[]>([])
 const activePresetId = ref<string | null>(null)
+const editingPresetId = ref<string | null>(null)
+const editingPresetName = ref('')
 const cameraCapabilities = ref<PTZCapabilities | null>(null)
 const capabilitiesLoading = ref(false)
 
@@ -787,6 +782,20 @@ const draggingPoint = ref<TimelinePoint | null>(null)
 // 计算属性:是否有已激活的点
 const hasActivePoints = computed(() => timelinePoints.value.some((p) => p.active))
 
+// 计算属性:基于 timelinePoints 生成预置位列表(以本地数据为准)
+// presetId 跟打点序号一致,从 1 开始
+const localPresetList = computed(() => {
+  return timelinePoints.value
+    .filter((p) => p.active && p.presetId) // 只显示已激活且有预置位ID的
+    .sort((a, b) => a.id - b.id) // 按序号排序
+    .map((p) => ({
+      id: String(p.id), // presetId = 打点序号
+      name: p.presetName || `Preset ${p.id}`,
+      pointId: p.id, // 关联的时间轴点ID
+      time: p.time
+    }))
+})
+
 // 下拉选项
 const lssOptions = ref<LssNodeDTO[]>([])
 const cameraOptions = ref<CameraInfoDTO[]>([])
@@ -1342,9 +1351,8 @@ async function handleViewCloudflare(row: LiveStreamDTO) {
   activeDrawerTab.value = 'play'
   drawerVisible.value = true
 
-  // 自动加载 PTZ 预置位和能力信息
+  // 自动加载摄像头能力信息
   if (hasCameraConnection()) {
-    loadPTZPresets()
     loadCameraCapabilities()
   }
 
@@ -1590,6 +1598,184 @@ async function handleDeletePreset(preset: PTZPresetInfo) {
   }
 }
 
+// ==================== 本地预置位操作(基于 timelinePoints)====================
+
+// 本地预置位类型
+interface LocalPreset {
+  id: string
+  name: string
+  pointId: number
+  time: number
+}
+
+// 开始编辑预置位名称
+function startEditPresetName(preset: LocalPreset) {
+  editingPresetId.value = preset.id
+  editingPresetName.value = preset.name
+}
+
+// 取消编辑预置位名称
+function cancelEditPresetName() {
+  editingPresetId.value = null
+  editingPresetName.value = ''
+}
+
+// 保存预置位名称
+async function savePresetName(preset: LocalPreset) {
+  const newName = editingPresetName.value.trim()
+  if (!newName || newName === preset.name) {
+    cancelEditPresetName()
+    return
+  }
+
+  const cameraId = currentMediaStream.value?.cameraId
+  if (!cameraId) {
+    cancelEditPresetName()
+    return
+  }
+
+  // 找到对应的时间轴点(通过 pointId)
+  const point = timelinePoints.value.find((p) => p.id === preset.pointId)
+  if (!point) {
+    cancelEditPresetName()
+    return
+  }
+
+  try {
+    const res = await presetSet({
+      cameraId,
+      presetId: point.presetId!, // 使用实际的 presetId
+      presetName: newName
+    })
+
+    if (res.code === 200) {
+      // 更新本地时间轴点的名称
+      point.presetName = newName
+      saveTimelineConfig()
+      ElMessage.success(t('名称已更新'))
+    } else {
+      ElMessage.error(res.errMsg || t('更新失败'))
+    }
+  } catch (error) {
+    console.error('更新预置位名称失败', error)
+    ElMessage.error(t('更新失败'))
+  } finally {
+    cancelEditPresetName()
+  }
+}
+
+// 跳转到本地预置位
+async function handleGotoLocalPreset(preset: LocalPreset) {
+  const cameraId = currentMediaStream.value?.cameraId
+  if (!cameraId) {
+    ElMessage.warning(t('请先配置摄像头连接'))
+    return
+  }
+
+  // 找到对应的时间轴点
+  const point = timelinePoints.value.find((p) => p.id === preset.pointId)
+  if (!point?.presetId) {
+    ElMessage.warning(t('未找到对应的预置位'))
+    return
+  }
+
+  try {
+    activePresetId.value = preset.id
+    const res = await presetGoto({ cameraId, presetId: point.presetId })
+    if (res.code === 200) {
+      ElMessage.success(`${t('已跳转到')}: ${preset.name}`)
+    } else {
+      ElMessage.error(res.errMsg || t('跳转失败'))
+    }
+  } catch (error) {
+    console.error('跳转预置位失败', error)
+    ElMessage.error(t('跳转失败'))
+  }
+}
+
+// 更新本地预置位(将当前摄像头位置保存到该预置位)
+async function handleUpdateLocalPreset(preset: LocalPreset) {
+  const cameraId = currentMediaStream.value?.cameraId
+  if (!cameraId) {
+    ElMessage.warning(t('请先配置摄像头连接'))
+    return
+  }
+
+  // 找到对应的时间轴点
+  const point = timelinePoints.value.find((p) => p.id === preset.pointId)
+  if (!point?.presetId) {
+    ElMessage.warning(t('未找到对应的预置位'))
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(`${t('将当前摄像头位置保存到预置位')} "${preset.name}"?`, t('设置预置位'), {
+      type: 'info'
+    })
+
+    const res = await presetSet({
+      cameraId,
+      presetId: point.presetId,
+      presetName: preset.name
+    })
+
+    if (res.code === 200) {
+      ElMessage.success(`${t('预置位设置成功')}: ${preset.name}`)
+    } else {
+      ElMessage.error(res.errMsg || t('设置失败'))
+    }
+  } catch (error) {
+    if ((error as Error).toString().includes('cancel')) return
+    console.error('设置预置位失败', error)
+    ElMessage.error(t('设置失败'))
+  }
+}
+
+// 删除本地预置位
+async function handleDeleteLocalPreset(preset: LocalPreset) {
+  const cameraId = currentMediaStream.value?.cameraId
+  if (!cameraId) {
+    ElMessage.warning(t('请先配置摄像头连接'))
+    return
+  }
+
+  // 找到对应的时间轴点
+  const point = timelinePoints.value.find((p) => p.id === preset.pointId)
+  if (!point?.presetId) {
+    ElMessage.warning(t('未找到对应的预置位'))
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(`${t('确定删除预置位')} "${preset.name}"?`, t('删除确认'), {
+      type: 'warning'
+    })
+
+    // 删除后端预置位
+    const res = await presetRemove({ cameraId, presetId: point.presetId })
+    if (res.success) {
+      // 找到并删除对应的时间轴点
+      const index = timelinePoints.value.findIndex((p) => p.id === preset.pointId)
+      if (index !== -1) {
+        timelinePoints.value.splice(index, 1)
+        sortAndRenumberPoints()
+        saveTimelineConfig()
+        if (selectedPoint.value?.id === preset.pointId) {
+          selectedPoint.value = null
+        }
+      }
+      ElMessage.success(t('删除成功'))
+    } else {
+      ElMessage.error(res.errMsg || t('删除失败'))
+    }
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('删除预置位失败', error)
+      ElMessage.error(t('删除失败'))
+    }
+  }
+}
+
 // 加载摄像头能力信息 (通过 camera API)
 async function loadCameraCapabilities() {
   const cameraId = currentMediaStream.value?.cameraId
@@ -1682,9 +1868,9 @@ async function addTimelinePoint(time?: number) {
   // 确保时间在有效范围内
   const clampedTime = Math.max(0, Math.min(newTime, timelineDuration.value))
 
-  // 使用时间轴专用的预置位ID范围 (100+)
-  const presetIdNum = 100 + newId
-  const presetName = `Timeline_${newId}`
+  // presetId 跟打点序号一致,从 1 开始
+  const presetIdNum = newId
+  const presetName = `Preset ${newId}`
 
   // 先保存当前摄像头位置到预置位
   try {
@@ -1711,9 +1897,6 @@ async function addTimelinePoint(time?: number) {
       saveTimelineConfig()
       selectPoint(newPoint)
 
-      // 刷新预置位列表
-      loadPTZPresets()
-
       ElMessage.success(`${t('已添加打点')} ${newId}`)
     } else {
       ElMessage.error(res.errMsg || t('添加失败'))
@@ -1724,11 +1907,17 @@ async function addTimelinePoint(time?: number) {
   }
 }
 
-// 按时间排序并重新编号
+// 按时间排序并重新编号(同时更新 presetId 和 presetName)
 function sortAndRenumberPoints() {
   timelinePoints.value.sort((a, b) => a.time - b.time)
   timelinePoints.value.forEach((point, index) => {
-    point.id = index + 1
+    const newId = index + 1
+    point.id = newId
+    // presetId 跟打点序号一致
+    if (point.active) {
+      point.presetId = newId
+      point.presetName = point.presetName?.replace(/\d+$/, String(newId)) || `Preset ${newId}`
+    }
   })
 }
 
@@ -1758,9 +1947,9 @@ async function saveCurrentPoint() {
   }
 
   const point = selectedPoint.value
-  // 使用时间轴专用的预置位ID范围 (100+)
-  const presetIdNum = point.presetId || 100 + point.id
-  const presetName = `Timeline_${point.id}`
+  // presetId 跟打点序号一致
+  const presetIdNum = point.presetId || point.id
+  const presetName = point.presetName || `Preset ${point.id}`
 
   savingPreset.value = true
   try {
@@ -1771,13 +1960,11 @@ async function saveCurrentPoint() {
       presetTime: 5,
       presetTotalTime: timelineDuration.value
     })
-    if (res.success) {
+    if (res.code === 200) {
       point.presetId = presetIdNum
       point.presetName = presetName
       point.active = true
       saveTimelineConfig()
-      // 刷新预置位列表
-      loadPTZPresets()
       ElMessage.success(`${t('已保存')} ${presetName}`)
     } else {
       ElMessage.error(res.errMsg || t('保存失败'))
@@ -1851,10 +2038,6 @@ async function handleContextMenuDelete() {
       if (selectedPoint.value?.id === point.id) {
         selectedPoint.value = null
       }
-      // 刷新预置位列表
-      if (cameraId) {
-        loadPTZPresets()
-      }
       ElMessage.success(t('已删除'))
     }
   } catch (error) {
@@ -2395,9 +2578,9 @@ onMounted(async () => {
 
   .timeline-track {
     position: relative;
-    height: 24px;
+    height: 12px;
     background: #374151;
-    border-radius: 2px;
+    border-radius: 6px;
     margin-bottom: 8px;
     cursor: pointer;
 
@@ -2415,7 +2598,7 @@ onMounted(async () => {
       top: 0;
       left: 0;
       height: 100%;
-      background: linear-gradient(to right, #dc2626, #ef4444);
+      background: linear-gradient(to right, #cfcfcf, #f8f8f8);
       border-radius: 2px 0 0 2px;
       pointer-events: none;
       transition: width 0.3s ease;
@@ -2882,6 +3065,25 @@ onMounted(async () => {
     overflow: hidden;
     text-overflow: ellipsis;
     white-space: nowrap;
+    cursor: pointer;
+    padding: 2px 4px;
+    border-radius: 4px;
+
+    &:hover {
+      background-color: rgba(0, 0, 0, 0.05);
+    }
+  }
+
+  .preset-name-input {
+    flex: 1;
+
+    :deep(.el-input__wrapper) {
+      padding: 0 8px;
+    }
+
+    :deep(.el-input__inner) {
+      font-size: 13px;
+    }
   }
 
   .preset-actions {