W1: 实时视频页对接网关 真实摄像机+WS-FLV播放+云台控制
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<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>
|
||||
@@ -16,14 +16,12 @@
|
||||
<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>
|
||||
@@ -48,15 +46,30 @@
|
||||
<div class="main-view">
|
||||
<h3>主监控画面</h3>
|
||||
<div class="video-container" v-if="selectedCamera">
|
||||
<div class="video-placeholder">
|
||||
<el-icon><VideoCamera /></el-icon>
|
||||
<p>正在加载 {{ selectedCamera.name }} 的实时画面...</p>
|
||||
<!-- 视频画面 -->
|
||||
<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="video-controls">
|
||||
<el-button size="small" icon="el-icon-video-play">播放</el-button>
|
||||
<el-button size="small" icon="el-icon-video-pause">暂停</el-button>
|
||||
<el-button size="small" icon="el-icon-refresh">刷新</el-button>
|
||||
<el-button size="small" icon="el-icon-full-screen">全屏</el-button>
|
||||
<!-- 云台控制面板 -->
|
||||
<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>
|
||||
@@ -79,299 +92,96 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { VideoCamera } from '@element-plus/icons-vue'
|
||||
import { fetchCameras, gwGet, gwPost, 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 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)
|
||||
|
||||
// 统计数据 - 设备总数(直接使用cameras.length)
|
||||
// 达标总数 - 基于在线设备模拟为在线数量减1
|
||||
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 currentTime = ref('')
|
||||
// ── 加载摄像机列表 ──
|
||||
const loadCameras = async () => {
|
||||
try {
|
||||
cameras.value = await fetchCameras('Owl:main')
|
||||
} catch (e) {
|
||||
console.error('获取摄像机列表失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择摄像头
|
||||
const selectCamera = (camera: Camera) => {
|
||||
// ── 选中摄像机 → 取流 ──
|
||||
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 updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString('zh-CN')
|
||||
// ── 云台控制 ──
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
.video-placeholder .el-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.video-controls {
|
||||
padding: 10px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 统计卡片样式 */
|
||||
.stats-cards {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #67c23a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
@media (max-width: 768px) {
|
||||
.stats-cards {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user