serverRoom/adminSystem/src/views/device/screen-monitor.vue
lvfengfree ed9d1d7325 feat: 屏幕监控大规模优化 - 支持60台设备同时监控
- Agent端优化:
  * 添加质量档位定义 (Low: 320x180@3fps, High: 1280x720@15fps)
  * H.264编码器支持动态质量切换
  * 屏幕流服务支持按需推流和质量控制
  * 添加SignalR信令客户端连接服务器

- 服务器端优化:
  * 添加StreamSignalingHub处理质量控制信令
  * 支持设备注册/注销和监控状态管理
  * 支持教师端监控控制和设备选中

- 前端组件:
  * 创建H264VideoPlayer组件支持H.264和JPEG模式
  * 更新学生屏幕监控页面使用新组件

- 性能提升:
  * 带宽从120Mbps降至6-7Mbps (降低95%)
  * 监控墙模式: 60台100kbps=6Mbps
  * 单机放大模式: 1台1Mbps+59台100kbps=6.9Mbps
  * 无人观看时停止推流节省带宽
2026-01-23 15:37:37 +08:00

231 lines
5.1 KiB
Vue

<template>
<div class="screen-monitor-page">
<ElCard shadow="never">
<template #header>
<div class="card-header">
<span>多屏幕监控 (实时视频流)</span>
<div class="header-actions">
<ElSelect v-model="gridSize" style="width: 120px; margin-right: 10px">
<ElOption :value="2" label="2x2 布局" />
<ElOption :value="3" label="3x3 布局" />
<ElOption :value="4" label="4x4 布局" />
<ElOption :value="5" label="5x5 布局" />
</ElSelect>
<ElButton type="primary" :icon="Refresh" @click="fetchDevices">刷新</ElButton>
</div>
</div>
</template>
<div class="screen-grid" :style="gridStyle">
<div
v-for="device in onlineDevices"
:key="device.uuid"
class="screen-item"
@click="handleScreenClick(device)"
>
<div class="screen-header">
<span class="hostname">{{ device.hostname || device.ipAddress }}</span>
<ElTag type="success" size="small">在线</ElTag>
</div>
<div class="screen-content">
<H264VideoPlayer
:device-uuid="device.uuid"
:width="1280"
:height="720"
:auto-connect="true"
/>
</div>
</div>
<div v-if="onlineDevices.length === 0" class="empty-state">
<el-icon :size="64"><Monitor /></el-icon>
<p>暂无在线设备</p>
</div>
</div>
</ElCard>
<!-- 放大查看弹窗 -->
<ElDialog
v-model="enlargeVisible"
:title="currentDevice?.hostname || currentDevice?.ipAddress"
width="90%"
top="5vh"
>
<div class="enlarge-content">
<H264VideoPlayer
v-if="currentDevice"
:device-uuid="currentDevice.uuid"
:width="1920"
:height="1080"
:auto-connect="true"
/>
</div>
<template #footer>
<ElButton @click="enlargeVisible = false">关闭</ElButton>
<ElButton type="primary" @click="handleRemoteControl">远程控制</ElButton>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh, Monitor } from '@element-plus/icons-vue'
import request from '@/utils/http'
import H264VideoPlayer from '@/components/H264VideoPlayer.vue'
defineOptions({ name: 'ScreenMonitor' })
interface DeviceScreen {
uuid: string
hostname: string
ipAddress: string
}
const onlineDevices = ref<DeviceScreen[]>([])
const gridSize = ref(3)
const enlargeVisible = ref(false)
const currentDevice = ref<DeviceScreen | null>(null)
const gridStyle = computed(() => ({
gridTemplateColumns: `repeat(${gridSize.value}, 1fr)`
}))
const fetchDevices = async () => {
try {
const res = await request.get({ url: '/api/agent/devices' })
// 只显示在线设备
onlineDevices.value = (res?.items || []).filter((d: any) => d.isOnline)
} catch (error) {
console.error('获取设备列表失败:', error)
}
}
const handleScreenClick = (device: DeviceScreen) => {
currentDevice.value = device
enlargeVisible.value = true
}
const handleRemoteControl = () => {
ElMessage.info('远程控制功能开发中')
}
onMounted(() => {
fetchDevices()
})
</script>
<style scoped>
.screen-monitor-page {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 500;
}
.header-actions {
display: flex;
align-items: center;
}
.screen-grid {
display: grid;
gap: 16px;
min-height: 400px;
}
.screen-item {
border: 1px solid #e4e7ed;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
background: #f5f7fa;
}
.screen-item:hover {
border-color: #409eff;
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.2);
}
.screen-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #fff;
border-bottom: 1px solid #e4e7ed;
}
.hostname {
font-size: 13px;
font-weight: 500;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.screen-content {
aspect-ratio: 16 / 9;
display: flex;
align-items: center;
justify-content: center;
background: #1a1a1a;
}
.screen-content img {
width: 100%;
height: 100%;
object-fit: contain;
}
.no-screenshot {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #909399;
}
.no-screenshot p {
margin-top: 8px;
font-size: 12px;
}
.empty-state {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px;
color: #909399;
}
.empty-state p {
margin-top: 16px;
font-size: 14px;
}
.enlarge-content {
display: flex;
justify-content: center;
align-items: center;
background: #1a1a1a;
min-height: 60vh;
}
.enlarge-image {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
</style>