- 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 * 无人观看时停止推流节省带宽
231 lines
5.1 KiB
Vue
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>
|