|
|
@@ -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 {
|