189 lines
7.7 KiB
Vue
189 lines
7.7 KiB
Vue
<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>
|