Files
SecMPS/warehouse/src/view/video/Live.vue

189 lines
7.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="live-monitor">
<div class="monitor-header">
<h2>实时监控画面</h2>
<div class="monitor-controls">
<el-button type="primary" size="small" @click="loadCameras">刷新画面</el-button>
<el-button size="small" plain>全屏显示</el-button>
</div>
</div>
<!-- 统计卡片区域 -->
<div class="stats-cards">
<el-card class="stat-card">
<div class="stat-item">
<div class="stat-value">{{ cameras.length }}</div>
<div class="stat-label">设备总数</div>
</div>
</el-card>
<el-card class="stat-card">
<div class="stat-item">
<div class="stat-value">{{ qualifiedCount }}</div>
<div class="stat-label">达标总数</div>
</div>
</el-card>
<el-card class="stat-card">
<div class="stat-item">
<div class="stat-value">{{ qualifiedRate }}</div>
<div class="stat-label">达标率</div>
</div>
</el-card>
</div>
<div class="camera-list">
<h3>摄像头列表</h3>
<el-card class="camera-item" v-for="camera in cameras" :key="camera.id" @click="selectCamera(camera)">
<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>
</el-card>
</div>
<div class="main-view">
<h3>主监控画面</h3>
<div class="video-container" v-if="selectedCamera">
<!-- 视频画面 -->
<div class="video-area">
<video v-if="streamUrl" :src="streamUrl" autoplay muted controls style="width:100%;height:100%"></video>
<div v-else class="video-placeholder">
<el-icon><VideoCamera /></el-icon>
<p>{{ streamLoading ? '正在加载' : '' }} {{ selectedCamera.name }} 的实时画面...</p>
</div>
</div>
<!-- 云台控制面板 -->
<div class="ptz-panel" v-if="selectedCamera.hasPtz">
<h4>云台控制</h4>
<div class="ptz-pad">
<el-button circle @mousedown="ptzGo('up')" @mouseup="ptzStop" @mouseleave="ptzStop"><el-icon><ArrowUp /></el-icon></el-button>
<div class="ptz-row">
<el-button circle @mousedown="ptzGo('left')" @mouseup="ptzStop"><el-icon><ArrowLeft /></el-icon></el-button>
<el-button circle type="danger" size="small" @click="ptzGo('stop')"><el-icon><Close /></el-icon></el-button>
<el-button circle @mousedown="ptzGo('right')" @mouseup="ptzStop"><el-icon><ArrowRight /></el-icon></el-button>
</div>
<el-button circle @mousedown="ptzGo('down')" @mouseup="ptzStop"><el-icon><ArrowDown /></el-icon></el-button>
<div class="ptz-row" style="margin-top:8px">
<el-button circle size="small" @mousedown="ptzGo('zoom_in')" @mouseup="ptzStop">+</el-button>
<el-button circle size="small" @mousedown="ptzGo('zoom_out')" @mouseup="ptzStop"></el-button>
</div>
</div>
</div>
</div>
<div class="no-selection" v-else>
<el-empty description="请从左侧选择摄像头查看实时画面" />
</div>
</div>
<div class="system-info">
<h3>系统信息</h3>
<el-descriptions border>
<el-descriptions-item label="设备总数">{{ cameras.length }}</el-descriptions-item>
<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>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { VideoCamera } from '@element-plus/icons-vue'
import { fetchCameras, gwGet, gwPost } from '@/api/gateway'
import type { Camera } from '@/api/gateway'
const cameras = ref<Camera[]>([])
const selectedCamera = ref<Camera | null>(null)
const streamUrl = ref('')
const streamLoading = ref(false)
const currentTime = ref('')
const onlineCount = computed(() => cameras.value.filter(c => c.status === 'online').length)
const offlineCount = computed(() => cameras.value.filter(c => c.status === 'offline').length)
const qualifiedCount = computed(() => Math.max(0, onlineCount.value - 1))
const qualifiedRate = computed(() => {
if (cameras.value.length === 0) return '0%'
return `${((qualifiedCount.value / cameras.value.length) * 100).toFixed(1)}%`
})
// ── 加载摄像机列表 ──
const loadCameras = async () => {
try {
cameras.value = await fetchCameras('Owl:main')
} catch (e) {
console.error('获取摄像机列表失败:', e)
}
}
// ── 选中摄像机 → 取流 ──
const selectCamera = async (camera: Camera) => {
selectedCamera.value = camera
if (camera.status !== 'online') return
streamLoading.value = true
streamUrl.value = ''
try {
const data = await gwGet(`/api/gateway/streams/${camera.adapterCode}/${camera.sourceId}/live`)
streamUrl.value = data.wsFlv || data.httpFlv || data.hls || ''
} catch (e) {
console.error('取流失败:', e)
} finally {
streamLoading.value = false
}
}
// ── 云台控制 ──
const ptzSend = (direction: string, speed = 0.5) => {
const cam = selectedCamera.value
if (!cam) return
gwPost(`/api/gateway/streams/${cam.adapterCode}/${cam.sourceId}/ptz`, {
direction,
action: direction === 'stop' ? 'stop' : 'continuous',
speed
})
}
const ptzGo = (d: string) => ptzSend(d)
const ptzStop = () => ptzSend('stop')
const updateTime = () => { currentTime.value = new Date().toLocaleString('zh-CN') }
onMounted(() => {
loadCameras()
updateTime()
setInterval(updateTime, 1000)
})
</script>
<style scoped>
.live-monitor { 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; }
.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); 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; }
.stats-cards { display: flex; gap: 16px; margin-bottom: 20px; }
.stat-card { flex: 1; background: rgba(255,255,255,0.05); }
.stat-value { font-size: 24px; color: #409eff; font-weight: bold; }
.stat-label { font-size: 12px; color: #999; }
.main-view { flex: 1; }
.main-view h3 { color: #fff; margin-bottom: 10px; }
.video-container { display: flex; gap: 12px; }
.video-area { flex: 1; min-height: 400px; background: #000; display: flex; align-items: center; justify-content: center; }
.video-placeholder { color: #fff; text-align: center; }
.video-placeholder .el-icon { font-size: 48px; margin-bottom: 8px; }
.ptz-panel { width: 120px; padding: 10px; background: rgba(255,255,255,0.05); border-radius: 8px; }
.ptz-panel h4 { color: #fff; text-align: center; margin: 0 0 8px 0; font-size: 13px; }
.ptz-pad { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.ptz-row { display: flex; gap: 4px; }
.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>