|
@@ -8,16 +8,35 @@
|
|
|
<span class="title">{{ currentMediaStream?.name || t('流媒体播放') }}</span>
|
|
<span class="title">{{ currentMediaStream?.name || t('流媒体播放') }}</span>
|
|
|
<el-tag v-if="playbackInfo.isLive" type="success" size="small">{{ t('直播中') }}</el-tag>
|
|
<el-tag v-if="playbackInfo.isLive" type="success" size="small">{{ t('直播中') }}</el-tag>
|
|
|
<el-tag v-else type="info" size="small">{{ t('离线') }}</el-tag>
|
|
<el-tag v-else type="info" size="small">{{ t('离线') }}</el-tag>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-if="currentMediaStream && currentMediaStream.status === '1'"
|
|
|
|
|
+ type="danger"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ :loading="streamStopping"
|
|
|
|
|
+ @click="$emit('stopStream')"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ t('停止推流') }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="header-right">
|
|
|
|
|
+ <el-segmented
|
|
|
|
|
+ :model-value="playerMode || 'preset'"
|
|
|
|
|
+ :options="modeOptions"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ @change="(v: any) => $emit('update:playerMode', v)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #default="{ item }">
|
|
|
|
|
+ <div class="segmented-item">
|
|
|
|
|
+ <Icon
|
|
|
|
|
+ :icon="item.value === 'preset' ? 'mdi:flag-triangle' : 'mdi:transit-detour'"
|
|
|
|
|
+ width="16"
|
|
|
|
|
+ height="16"
|
|
|
|
|
+ />
|
|
|
|
|
+ <span>{{ item.label }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-segmented>
|
|
|
</div>
|
|
</div>
|
|
|
- <el-button
|
|
|
|
|
- v-if="currentMediaStream && currentMediaStream.status === '1'"
|
|
|
|
|
- type="danger"
|
|
|
|
|
- size="small"
|
|
|
|
|
- :loading="streamStopping"
|
|
|
|
|
- @click="$emit('stopStream')"
|
|
|
|
|
- >
|
|
|
|
|
- {{ t('停止推流') }}
|
|
|
|
|
- </el-button>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
<div class="player-container">
|
|
<div class="player-container">
|
|
|
<div v-if="!playbackInfo.videoId" class="player-placeholder">
|
|
<div v-if="!playbackInfo.videoId" class="player-placeholder">
|
|
@@ -45,8 +64,26 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 时间轴操作条 -->
|
|
|
|
|
- <div class="timeline-container">
|
|
|
|
|
|
|
+ <!-- 轨迹录制时间轴 -->
|
|
|
|
|
+ <WaypointTimeline
|
|
|
|
|
+ v-if="playerMode === 'waypoint'"
|
|
|
|
|
+ :is-recording="isWaypointRecording || false"
|
|
|
|
|
+ :elapsed-time="waypointElapsedTime || 0"
|
|
|
|
|
+ :recorded-waypoints="recordedWaypoints || []"
|
|
|
|
|
+ :total-duration="waypointTotalDuration || 0"
|
|
|
|
|
+ :is-waypoint-playing="isWaypointPlaying || false"
|
|
|
|
|
+ :waypoint-progress="waypointProgress || 0"
|
|
|
|
|
+ :has-recorded-data="hasWaypointData || false"
|
|
|
|
|
+ :format-elapsed-time="formatWaypointTime || (() => '00:00.0')"
|
|
|
|
|
+ @start-recording="$emit('startRecording')"
|
|
|
|
|
+ @stop-recording="$emit('stopRecording')"
|
|
|
|
|
+ @play-preview="$emit('playWaypointPreview')"
|
|
|
|
|
+ @stop-preview="$emit('stopWaypointPreview')"
|
|
|
|
|
+ @open-detail="$emit('openWaypointDetail')"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 预置位时间轴操作条 -->
|
|
|
|
|
+ <div v-else class="timeline-container">
|
|
|
<div class="timeline-header">
|
|
<div class="timeline-header">
|
|
|
<span class="timeline-label">{{ t('巡航时间轴') }}</span>
|
|
<span class="timeline-label">{{ t('巡航时间轴') }}</span>
|
|
|
<el-select
|
|
<el-select
|
|
@@ -234,8 +271,8 @@
|
|
|
</div>
|
|
</div>
|
|
|
</el-collapse-item>
|
|
</el-collapse-item>
|
|
|
|
|
|
|
|
- <!-- 预置位列表 -->
|
|
|
|
|
- <el-collapse-item name="preset">
|
|
|
|
|
|
|
+ <!-- 预置位列表 (仅预置位模式显示) -->
|
|
|
|
|
+ <el-collapse-item v-if="playerMode !== 'waypoint'" name="preset">
|
|
|
<template #title>
|
|
<template #title>
|
|
|
<span class="collapse-title">{{ t('预置位') }}</span>
|
|
<span class="collapse-title">{{ t('预置位') }}</span>
|
|
|
</template>
|
|
</template>
|
|
@@ -281,47 +318,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
</el-collapse-item>
|
|
</el-collapse-item>
|
|
|
|
|
|
|
|
- <!-- 摄像头信息 -->
|
|
|
|
|
- <el-collapse-item name="camera">
|
|
|
|
|
- <template #title>
|
|
|
|
|
- <span class="collapse-title">{{ t('摄像头信息') }}</span>
|
|
|
|
|
- </template>
|
|
|
|
|
- <div class="camera-info-content" v-loading="capabilitiesLoading">
|
|
|
|
|
- <template v-if="cameraCapabilities">
|
|
|
|
|
- <div class="info-item">
|
|
|
|
|
- <span class="info-label">{{ t('最大预置位') }}:</span>
|
|
|
|
|
- <span class="info-value">{{ cameraCapabilities.maxPresetNum || '-' }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="info-item" v-if="cameraCapabilities.controlProtocol">
|
|
|
|
|
- <span class="info-label">{{ t('控制协议') }}:</span>
|
|
|
|
|
- <span class="info-value">{{ cameraCapabilities.controlProtocol.current }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="info-item" v-if="cameraCapabilities.absoluteZoom">
|
|
|
|
|
- <span class="info-label">{{ t('变焦倍数') }}:</span>
|
|
|
|
|
- <span class="info-value">
|
|
|
|
|
- {{ cameraCapabilities.absoluteZoom.min }}x - {{ cameraCapabilities.absoluteZoom.max }}x
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="info-item" v-if="cameraCapabilities.support3DPosition !== undefined">
|
|
|
|
|
- <span class="info-label">{{ t('3D定位') }}:</span>
|
|
|
|
|
- <span class="info-value">
|
|
|
|
|
- {{ cameraCapabilities.support3DPosition ? t('支持') : t('不支持') }}
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="info-item" v-if="cameraCapabilities.supportPtzLimits !== undefined">
|
|
|
|
|
- <span class="info-label">{{ t('PTZ限位') }}:</span>
|
|
|
|
|
- <span class="info-value">
|
|
|
|
|
- {{ cameraCapabilities.supportPtzLimits ? t('支持') : t('不支持') }}
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </template>
|
|
|
|
|
- <el-empty
|
|
|
|
|
- v-else-if="!capabilitiesLoading"
|
|
|
|
|
- :description="currentMediaStream?.cameraId ? t('点击刷新加载') : t('请选择直播流')"
|
|
|
|
|
- :image-size="40"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-collapse-item>
|
|
|
|
|
|
|
+ <!-- 摄像头信息 (暂时隐藏) -->
|
|
|
</el-collapse>
|
|
</el-collapse>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -334,8 +331,9 @@ import { useI18n } from 'vue-i18n'
|
|
|
import { VideoPlay, Position, Delete } from '@element-plus/icons-vue'
|
|
import { VideoPlay, Position, Delete } from '@element-plus/icons-vue'
|
|
|
import { Icon } from '@iconify/vue'
|
|
import { Icon } from '@iconify/vue'
|
|
|
import VideoPlayer from '@/components/VideoPlayer.vue'
|
|
import VideoPlayer from '@/components/VideoPlayer.vue'
|
|
|
-import type { LiveStreamDTO } from '@/types'
|
|
|
|
|
|
|
+import type { LiveStreamDTO, WaypointItem } from '@/types'
|
|
|
import type { PlaybackInfo, TimelinePoint, PTZCapabilities, LocalPreset } from '../types'
|
|
import type { PlaybackInfo, TimelinePoint, PTZCapabilities, LocalPreset } from '../types'
|
|
|
|
|
+import WaypointTimeline from './WaypointTimeline.vue'
|
|
|
|
|
|
|
|
const { t } = useI18n({ useScope: 'global' })
|
|
const { t } = useI18n({ useScope: 'global' })
|
|
|
|
|
|
|
@@ -368,6 +366,16 @@ const props = defineProps<{
|
|
|
editingPresetName: string
|
|
editingPresetName: string
|
|
|
cameraCapabilities: PTZCapabilities | null
|
|
cameraCapabilities: PTZCapabilities | null
|
|
|
capabilitiesLoading: boolean
|
|
capabilitiesLoading: boolean
|
|
|
|
|
+ // Waypoint mode
|
|
|
|
|
+ playerMode?: 'preset' | 'waypoint'
|
|
|
|
|
+ isWaypointRecording?: boolean
|
|
|
|
|
+ waypointElapsedTime?: number
|
|
|
|
|
+ recordedWaypoints?: WaypointItem[]
|
|
|
|
|
+ waypointTotalDuration?: number
|
|
|
|
|
+ isWaypointPlaying?: boolean
|
|
|
|
|
+ waypointProgress?: number
|
|
|
|
|
+ hasWaypointData?: boolean
|
|
|
|
|
+ formatWaypointTime?: (ms: number) => string
|
|
|
}>()
|
|
}>()
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
const emit = defineEmits<{
|
|
@@ -397,8 +405,19 @@ const emit = defineEmits<{
|
|
|
'update:ptzSpeed': [value: number]
|
|
'update:ptzSpeed': [value: number]
|
|
|
'update:activePanels': [value: string[]]
|
|
'update:activePanels': [value: string[]]
|
|
|
'update:editingPresetName': [value: string]
|
|
'update:editingPresetName': [value: string]
|
|
|
|
|
+ 'update:playerMode': [value: 'preset' | 'waypoint']
|
|
|
|
|
+ startRecording: []
|
|
|
|
|
+ stopRecording: []
|
|
|
|
|
+ playWaypointPreview: []
|
|
|
|
|
+ stopWaypointPreview: []
|
|
|
|
|
+ openWaypointDetail: []
|
|
|
}>()
|
|
}>()
|
|
|
|
|
|
|
|
|
|
+const modeOptions = computed(() => [
|
|
|
|
|
+ { label: t('预置位模式'), value: 'preset' },
|
|
|
|
|
+ { label: t('轨迹录制模式'), value: 'waypoint' }
|
|
|
|
|
+])
|
|
|
|
|
+
|
|
|
const timelineDurationModel = computed({
|
|
const timelineDurationModel = computed({
|
|
|
get: () => props.timelineDuration,
|
|
get: () => props.timelineDuration,
|
|
|
set: (v) => emit('update:timelineDuration', v)
|
|
set: (v) => emit('update:timelineDuration', v)
|
|
@@ -470,6 +489,18 @@ const editingPresetNameModel = computed({
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
|
color: #303133;
|
|
color: #303133;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .header-right {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .segmented-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.player-container {
|
|
.player-container {
|
|
@@ -792,11 +823,12 @@ const editingPresetNameModel = computed({
|
|
|
border: none;
|
|
border: none;
|
|
|
|
|
|
|
|
:deep(.el-collapse-item) {
|
|
:deep(.el-collapse-item) {
|
|
|
|
|
+ border: 1px solid #eee;
|
|
|
margin-bottom: 8px;
|
|
margin-bottom: 8px;
|
|
|
background-color: #fff;
|
|
background-color: #fff;
|
|
|
- border-radius: 8px;
|
|
|
|
|
|
|
+ // border-radius: 8px;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
|
|
|
|
+ // box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
|
|
|
|
|
.el-collapse-item__header {
|
|
.el-collapse-item__header {
|
|
|
padding: 0 16px;
|
|
padding: 0 16px;
|
|
@@ -814,7 +846,7 @@ const editingPresetNameModel = computed({
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.el-collapse-item__content {
|
|
.el-collapse-item__content {
|
|
|
- padding: 0;
|
|
|
|
|
|
|
+ padding: 10px;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -831,7 +863,7 @@ const editingPresetNameModel = computed({
|
|
|
display: grid;
|
|
display: grid;
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
gap: 6px;
|
|
gap: 6px;
|
|
|
- margin: 12px;
|
|
|
|
|
|
|
+ // margin: 12px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.ptz-btn {
|
|
.ptz-btn {
|
|
@@ -875,7 +907,8 @@ const editingPresetNameModel = computed({
|
|
|
display: flex;
|
|
display: flex;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
gap: 12px;
|
|
gap: 12px;
|
|
|
- margin-bottom: 12px;
|
|
|
|
|
|
|
+ margin-top: 10px;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
|
|
|
.el-button {
|
|
.el-button {
|
|
|
background-color: #f5f7fa;
|
|
background-color: #f5f7fa;
|