W2: 视频墙多路播放+HLS回放 全部对接网关真实数据
This commit is contained in:
@@ -3,25 +3,17 @@
|
||||
<div class="monitor-header">
|
||||
<h2>历史回放</h2>
|
||||
<div class="monitor-controls">
|
||||
<el-button type="primary" size="small">刷新</el-button>
|
||||
<el-button type="primary" size="small" @click="loadCameras">刷新</el-button>
|
||||
<el-button size="small" plain>全屏显示</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日期时间选择 -->
|
||||
<div class="date-filter">
|
||||
<h3>时间段选择</h3>
|
||||
<div class="date-filter-controls">
|
||||
<el-date-picker
|
||||
v-model="dateTimeRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期时间"
|
||||
end-placeholder="结束日期时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 400px;"
|
||||
></el-date-picker>
|
||||
<el-date-picker v-model="dateTimeRange" type="daterange" range-separator="至"
|
||||
start-placeholder="开始日期时间" end-placeholder="结束日期时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" style="width: 400px;" />
|
||||
<el-button type="primary" size="small" @click="searchRecordings">查询</el-button>
|
||||
<el-button size="small" plain @click="resetDateTimeRange">重置</el-button>
|
||||
</div>
|
||||
@@ -33,9 +25,7 @@
|
||||
<div class="camera-info">
|
||||
<div class="camera-name">{{ camera.name }}</div>
|
||||
<div class="camera-location">{{ camera.location }}</div>
|
||||
<div class="camera-status" :class="camera.status">
|
||||
{{ camera.status === 'online' ? '在线' : '离线' }}
|
||||
</div>
|
||||
<div class="camera-status" :class="camera.status">{{ camera.status === 'online' ? '在线' : '离线' }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -43,29 +33,12 @@
|
||||
<div class="main-view">
|
||||
<h3>历史回放画面</h3>
|
||||
<div class="video-container" v-if="selectedCamera">
|
||||
<div class="video-placeholder">
|
||||
<video v-if="playbackUrl" :src="playbackUrl" autoplay controls style="width:100%;height:400px;background:#000" />
|
||||
<div v-else class="video-placeholder">
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
<p>正在加载 {{ selectedCamera.name }} 的历史画面...</p>
|
||||
<p>{{ playbackLoading ? '正在加载...' : '' }} {{ selectedCamera.name }} 的历史画面</p>
|
||||
<p v-if="dateTimeRange && dateTimeRange.length === 2">{{ formatDateTime(dateTimeRange[0]) }} - {{ formatDateTime(dateTimeRange[1]) }}</p>
|
||||
</div>
|
||||
<!-- 播放进度条 -->
|
||||
<div class="video-progress">
|
||||
<el-slider v-model="playProgress" :min="0" :max="100" show-tooltip="always" />
|
||||
<div class="time-display">{{ formatProgressTime() }} / {{ totalDuration }}</div>
|
||||
</div>
|
||||
<div class="video-controls">
|
||||
<el-button size="small" icon="el-icon-video-play" @click="togglePlay">{{ isPlaying ? '暂停' : '播放' }}</el-button>
|
||||
<el-button size="small" icon="el-icon-refresh-right" @click="stepForward">前进10秒</el-button>
|
||||
<el-button size="small" icon="el-icon-refresh-left" @click="stepBackward">后退10秒</el-button>
|
||||
<el-button size="small" icon="el-icon-camera" @click="screenshot">截图</el-button>
|
||||
<el-select v-model="playSpeed" placeholder="播放速度" size="small">
|
||||
<el-option label="0.5x" :value="0.5"></el-option>
|
||||
<el-option label="1.0x" :value="1"></el-option>
|
||||
<el-option label="1.5x" :value="1.5"></el-option>
|
||||
<el-option label="2.0x" :value="2"></el-option>
|
||||
</el-select>
|
||||
<el-button size="small" icon="el-icon-full-screen" @click="toggleFullScreen">全屏</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="no-selection" v-else>
|
||||
<el-empty description="请从左侧选择摄像头查看历史画面" />
|
||||
@@ -79,8 +52,6 @@
|
||||
<el-descriptions-item label="在线设备">{{ onlineCount }}</el-descriptions-item>
|
||||
<el-descriptions-item label="离线设备">{{ offlineCount }}</el-descriptions-item>
|
||||
<el-descriptions-item label="当前时间">{{ currentTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="回放状态" v-if="selectedCamera">{{ isPlaying ? '播放中' : '已暂停' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="选择时间" v-if="dateTimeRange && dateTimeRange.length === 2">{{ formatDate(dateTimeRange[0]) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,438 +61,70 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { VideoPlay } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { fetchCameras, gwGet, type Camera } from '@/api/gateway'
|
||||
|
||||
interface Camera {
|
||||
id: string
|
||||
name: string
|
||||
location: string
|
||||
status: 'online' | 'offline'
|
||||
streamUrl?: string
|
||||
}
|
||||
|
||||
// 摄像头数据
|
||||
const cameras = ref<Camera[]>([
|
||||
{
|
||||
id: 'camera1',
|
||||
name: '仓库入口',
|
||||
location: '仓库正门',
|
||||
status: 'online'
|
||||
},
|
||||
{
|
||||
id: 'camera2',
|
||||
name: '货架区域A',
|
||||
location: '仓库东区',
|
||||
status: 'online'
|
||||
},
|
||||
{
|
||||
id: 'camera3',
|
||||
name: '货架区域B',
|
||||
location: '仓库西区',
|
||||
status: 'online'
|
||||
},
|
||||
{
|
||||
id: 'camera4',
|
||||
name: '装卸平台',
|
||||
location: '仓库后门',
|
||||
status: 'offline'
|
||||
},
|
||||
{
|
||||
id: 'camera5',
|
||||
name: '办公区域',
|
||||
location: '办公楼',
|
||||
status: 'online'
|
||||
}
|
||||
])
|
||||
|
||||
// 当前选中的摄像头
|
||||
const cameras = ref<Camera[]>([])
|
||||
const selectedCamera = ref<Camera | null>(null)
|
||||
|
||||
// 日期时间范围
|
||||
const dateTimeRange = ref<string[]>([])
|
||||
const playbackUrl = ref('')
|
||||
const playbackLoading = ref(false)
|
||||
const currentTime = ref('')
|
||||
|
||||
// 播放进度
|
||||
const playProgress = ref(0)
|
||||
const isPlaying = ref(false)
|
||||
const playSpeed = ref(1)
|
||||
|
||||
// 总时长(模拟值)
|
||||
const totalDuration = ref('00:30:00')
|
||||
|
||||
// 计算在线和离线设备数量
|
||||
const onlineCount = computed(() => cameras.value.filter(c => c.status === 'online').length)
|
||||
const offlineCount = computed(() => cameras.value.filter(c => c.status === 'offline').length)
|
||||
|
||||
// 当前时间
|
||||
const currentTime = ref('')
|
||||
const loadCameras = async () => { try { cameras.value = await fetchCameras('Owl:main') } catch {} }
|
||||
|
||||
// 选择摄像头
|
||||
const selectCamera = (camera: Camera) => {
|
||||
selectedCamera.value = camera
|
||||
playProgress.value = 0
|
||||
isPlaying.value = false
|
||||
|
||||
// 设置默认日期时间范围(前一天到今天)
|
||||
if (!dateTimeRange.value || dateTimeRange.value.length === 0) {
|
||||
const yesterday = new Date()
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
const today = new Date()
|
||||
|
||||
dateTimeRange.value = [
|
||||
yesterday.toISOString().slice(0, 19).replace('T', ' '),
|
||||
today.toISOString().slice(0, 19).replace('T', ' ')
|
||||
]
|
||||
}
|
||||
|
||||
ElMessage.success(`已选择 ${camera.name}`)
|
||||
}
|
||||
|
||||
// 搜索录像记录
|
||||
const searchRecordings = () => {
|
||||
if (!selectedCamera.value) {
|
||||
ElMessage.warning('请先选择摄像头')
|
||||
return
|
||||
}
|
||||
|
||||
if (!dateTimeRange.value || dateTimeRange.value.length !== 2) {
|
||||
ElMessage.warning('请选择有效的日期时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success(`正在查询 ${selectedCamera.value.name} 的历史记录`)
|
||||
// 重置播放状态
|
||||
playProgress.value = 0
|
||||
isPlaying.value = false
|
||||
}
|
||||
|
||||
// 重置日期时间范围
|
||||
const resetDateTimeRange = () => {
|
||||
dateTimeRange.value = []
|
||||
}
|
||||
|
||||
// 切换播放/暂停
|
||||
const togglePlay = () => {
|
||||
isPlaying.value = !isPlaying.value
|
||||
ElMessage.info(isPlaying.value ? '开始播放' : '暂停播放')
|
||||
|
||||
// 模拟播放进度更新
|
||||
if (isPlaying.value) {
|
||||
simulatePlayback()
|
||||
if (!dateTimeRange.value.length) {
|
||||
const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1)
|
||||
dateTimeRange.value = [formatISOLocal(yesterday), formatISOLocal(new Date())]
|
||||
}
|
||||
}
|
||||
|
||||
// 快退10秒
|
||||
const stepBackward = () => {
|
||||
playProgress.value = Math.max(0, playProgress.value - 5)
|
||||
ElMessage.info('后退10秒')
|
||||
const searchRecordings = async () => {
|
||||
if (!selectedCamera.value) return ElMessage.warning('请先选择摄像头')
|
||||
if (!dateTimeRange.value || dateTimeRange.value.length !== 2) return ElMessage.warning('请选择时间段')
|
||||
playbackLoading.value = true; playbackUrl.value = ''
|
||||
try {
|
||||
const cam = selectedCamera.value
|
||||
const start = new Date(dateTimeRange.value[0]).toISOString()
|
||||
const end = new Date(dateTimeRange.value[1]).toISOString()
|
||||
const data = await gwGet(`/api/gateway/streams/${cam.adapterCode}/${cam.sourceId}/playback?start=${start}&end=${end}`)
|
||||
playbackUrl.value = data.hls || ''
|
||||
if (!playbackUrl.value) ElMessage.warning('未获取到回放流地址')
|
||||
} catch { ElMessage.error('获取回放流失败') }
|
||||
finally { playbackLoading.value = false }
|
||||
}
|
||||
|
||||
// 快进10秒
|
||||
const stepForward = () => {
|
||||
playProgress.value = Math.min(100, playProgress.value + 5)
|
||||
ElMessage.info('前进10秒')
|
||||
}
|
||||
const resetDateTimeRange = () => { dateTimeRange.value = []; playbackUrl.value = '' }
|
||||
const formatISOLocal = (d: Date) => d.toISOString().slice(0, 19).replace('T', ' ')
|
||||
const formatDateTime = (s: string) => s
|
||||
|
||||
// 截图
|
||||
const screenshot = () => {
|
||||
if (!selectedCamera.value) {
|
||||
ElMessage.warning('请先选择摄像头')
|
||||
return
|
||||
}
|
||||
ElMessage.success('截图成功')
|
||||
}
|
||||
|
||||
// 切换全屏
|
||||
const toggleFullScreen = () => {
|
||||
ElMessage.info('全屏显示')
|
||||
// 实际项目中需要实现全屏逻辑
|
||||
}
|
||||
|
||||
// 模拟播放进度
|
||||
let playbackInterval: number | null = null
|
||||
const simulatePlayback = () => {
|
||||
if (playbackInterval) {
|
||||
clearInterval(playbackInterval)
|
||||
}
|
||||
|
||||
playbackInterval = window.setInterval(() => {
|
||||
if (isPlaying.value && playProgress.value < 100) {
|
||||
playProgress.value += 0.1 * playSpeed.value
|
||||
|
||||
// 播放结束
|
||||
if (playProgress.value >= 100) {
|
||||
playProgress.value = 100
|
||||
isPlaying.value = false
|
||||
if (playbackInterval) {
|
||||
clearInterval(playbackInterval)
|
||||
}
|
||||
ElMessage.info('播放结束')
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateTimeStr: string) => {
|
||||
const date = new Date(dateTimeStr)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateTimeStr: string) => {
|
||||
const date = new Date(dateTimeStr)
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
// 格式化进度时间
|
||||
const formatProgressTime = () => {
|
||||
const [hours, minutes, seconds] = totalDuration.value.split(':')
|
||||
const totalSeconds = parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds)
|
||||
const currentSeconds = Math.floor(totalSeconds * (playProgress.value / 100))
|
||||
|
||||
const h = Math.floor(currentSeconds / 3600).toString().padStart(2, '0')
|
||||
const m = Math.floor((currentSeconds % 3600) / 60).toString().padStart(2, '0')
|
||||
const s = (currentSeconds % 60).toString().padStart(2, '0')
|
||||
|
||||
return `${h}:${m}:${s}`
|
||||
}
|
||||
|
||||
// 更新时间
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
setInterval(updateTime, 1000)
|
||||
|
||||
// 设置默认日期时间范围
|
||||
const yesterday = new Date()
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
const today = new Date()
|
||||
|
||||
dateTimeRange.value = [
|
||||
yesterday.toISOString().slice(0, 19).replace('T', ' '),
|
||||
today.toISOString().slice(0, 19).replace('T', ' ')
|
||||
]
|
||||
})
|
||||
const updateTime = () => { currentTime.value = new Date().toLocaleString('zh-CN') }
|
||||
onMounted(() => { loadCameras(); updateTime(); setInterval(updateTime, 1000) })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.history-playback {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.monitor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.monitor-header h2 {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.monitor-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.date-filter {
|
||||
margin-bottom: 20px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.date-filter h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.date-filter-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.camera-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.camera-list h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.camera-item {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.camera-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.camera-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.camera-name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.camera-location {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.camera-status {
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.camera-status.online {
|
||||
background: rgba(103, 194, 58, 0.2);
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.camera-status.offline {
|
||||
background: rgba(245, 108, 108, 0.2);
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.main-view {
|
||||
flex: 1;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-view h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
flex: 1;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.video-placeholder .el-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.video-placeholder p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.video-progress {
|
||||
padding: 10px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.time-display {
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.video-controls {
|
||||
padding: 10px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.no-selection {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.system-info {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.system-info h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-descriptions {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.el-descriptions__label {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.el-descriptions__content {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.el-card.is-hover-shadow:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.el-slider__runway {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.el-slider__bar {
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.el-slider__button-wrapper {
|
||||
border-color: #409eff;
|
||||
}
|
||||
.history-playback { height: 100%; display: flex; flex-direction: column; padding: 20px; }
|
||||
.monitor-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.monitor-header h2 { margin: 0; color: #fff; font-size: 20px; font-weight: 500; }
|
||||
.monitor-controls { display: flex; gap: 10px; }
|
||||
.date-filter h3 { color: #fff; margin-bottom: 10px; }
|
||||
.date-filter-controls { display: flex; gap: 10px; align-items: center; margin-bottom: 20px; }
|
||||
.camera-list { margin-bottom: 20px; }
|
||||
.camera-list h3 { color: #fff; }
|
||||
.camera-item { background: rgba(255,255,255,0.05); cursor: pointer; }
|
||||
.camera-name { font-size: 14px; color: #fff; }
|
||||
.camera-location { font-size: 12px; color: #999; }
|
||||
.camera-status.online { color: #67c23a; }
|
||||
.camera-status.offline { color: #f56c6c; }
|
||||
.main-view { flex: 1; }
|
||||
.main-view h3 { color: #fff; margin-bottom: 10px; }
|
||||
.video-container { background: #000; border-radius: 8px; overflow: hidden; }
|
||||
.video-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 400px; color: #fff; }
|
||||
.no-selection { flex: 1; display: flex; align-items: center; justify-content: center; }
|
||||
.system-info { margin-top: 20px; }
|
||||
.system-info h3 { color: #fff; margin-bottom: 10px; }
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user