BodyBalanceEvaluation/vue_websocket_example.vue

421 lines
8.3 KiB
Vue
Raw Normal View History

2025-07-29 18:28:40 +08:00
<template>
<div class="websocket-example">
<div class="header">
<h2>Vue WebSocket连接示例</h2>
<div class="status" :class="connectionStatus">
{{ statusText }}
</div>
</div>
<div class="controls">
<button
@click="connectWebSocket"
:disabled="isConnected"
class="btn btn-primary"
>
连接WebSocket
</button>
<button
@click="disconnectWebSocket"
:disabled="!isConnected"
class="btn btn-secondary"
>
断开连接
</button>
<button
@click="startRtsp"
:disabled="!isConnected || isRtspRunning"
class="btn btn-success"
>
启动RTSP
</button>
<button
@click="stopRtsp"
:disabled="!isConnected || !isRtspRunning"
class="btn btn-danger"
>
停止RTSP
</button>
</div>
<div class="video-container">
<h3>RTSP视频流</h3>
<div v-if="rtspImageSrc" class="video-wrapper">
<img :src="rtspImageSrc" alt="RTSP视频流" class="rtsp-image" />
<div class="frame-info">已接收帧数: {{ frameCount }}</div>
</div>
<div v-else class="no-video">
暂无视频流
</div>
</div>
<div class="log-container">
<h3>连接日志</h3>
<div class="log-content" ref="logContainer">
<div v-for="(log, index) in logs" :key="index" class="log-item">
<span class="log-time">[{{ log.time }}]</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { io } from 'socket.io-client'
// 响应式数据
const socket = ref(null)
const isConnected = ref(false)
const isRtspRunning = ref(false)
const rtspImageSrc = ref('')
const frameCount = ref(0)
const logs = ref([])
const logContainer = ref(null)
// 后端服务器配置
const BACKEND_URL = 'http://localhost:5000' // 根据实际情况修改
// 如果是远程服务器,使用: 'http://192.168.1.173:5000'
// 计算属性
const connectionStatus = computed(() => {
if (isConnected.value) return 'connected'
return 'disconnected'
})
const statusText = computed(() => {
if (isConnected.value) {
return isRtspRunning.value ? '已连接 - RTSP运行中' : '已连接'
}
return '未连接'
})
// 日志记录函数
const addLog = (message) => {
const now = new Date()
const time = now.toLocaleTimeString()
logs.value.push({ time, message })
// 限制日志数量
if (logs.value.length > 100) {
logs.value.shift()
}
// 自动滚动到底部
nextTick(() => {
if (logContainer.value) {
logContainer.value.scrollTop = logContainer.value.scrollHeight
}
})
}
// WebSocket连接函数
const connectWebSocket = () => {
try {
addLog(`正在连接到 ${BACKEND_URL}`)
// 创建Socket.IO连接
socket.value = io(BACKEND_URL, {
transports: ['websocket', 'polling'],
timeout: 10000,
forceNew: true
})
// 连接成功事件
socket.value.on('connect', () => {
isConnected.value = true
addLog(`WebSocket连接成功Socket ID: ${socket.value.id}`)
})
// 连接失败事件
socket.value.on('connect_error', (error) => {
isConnected.value = false
addLog(`连接失败: ${error.message}`)
})
// 断开连接事件
socket.value.on('disconnect', (reason) => {
isConnected.value = false
isRtspRunning.value = false
rtspImageSrc.value = ''
addLog(`连接断开: ${reason}`)
})
// 监听RTSP状态事件
socket.value.on('rtsp_status', (data) => {
addLog(`RTSP状态: ${JSON.stringify(data)}`)
if (data.status === 'started') {
isRtspRunning.value = true
frameCount.value = 0
} else if (data.status === 'stopped') {
isRtspRunning.value = false
rtspImageSrc.value = ''
} else if (data.status === 'already_running') {
addLog('RTSP已在运行中')
}
})
// 监听RTSP帧数据
socket.value.on('rtsp_frame', (data) => {
if (data.image) {
frameCount.value++
rtspImageSrc.value = 'data:image/jpeg;base64,' + data.image
// 每30帧记录一次
if (frameCount.value % 30 === 0) {
addLog(`已接收 ${frameCount.value}`)
}
}
})
} catch (error) {
addLog(`连接异常: ${error.message}`)
}
}
// 断开WebSocket连接
const disconnectWebSocket = () => {
if (socket.value) {
socket.value.disconnect()
socket.value = null
isConnected.value = false
isRtspRunning.value = false
rtspImageSrc.value = ''
addLog('主动断开连接')
}
}
// 启动RTSP
const startRtsp = () => {
if (socket.value && isConnected.value) {
addLog('发送start_rtsp事件')
socket.value.emit('start_rtsp')
} else {
addLog('WebSocket未连接无法启动RTSP')
}
}
// 停止RTSP
const stopRtsp = () => {
if (socket.value && isConnected.value) {
addLog('发送stop_rtsp事件')
socket.value.emit('stop_rtsp')
} else {
addLog('WebSocket未连接无法停止RTSP')
}
}
// 生命周期钩子
onMounted(() => {
addLog('组件已挂载可以开始连接WebSocket')
addLog(`后端地址: ${BACKEND_URL}`)
addLog('请确保后端服务已启动')
})
onUnmounted(() => {
// 组件卸载时清理连接
if (socket.value) {
socket.value.disconnect()
}
})
</script>
<style scoped>
.websocket-example {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #e9ecef;
}
.header h2 {
margin: 0;
color: #333;
}
.status {
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
font-size: 14px;
}
.status.connected {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.disconnected {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background-color: #545b62;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover:not(:disabled) {
background-color: #1e7e34;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover:not(:disabled) {
background-color: #c82333;
}
.video-container {
margin-bottom: 20px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.video-container h3 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
}
.video-wrapper {
text-align: center;
position: relative;
}
.rtsp-image {
max-width: 100%;
height: auto;
border: 2px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.frame-info {
margin-top: 10px;
font-size: 14px;
color: #666;
font-weight: 500;
}
.no-video {
text-align: center;
padding: 40px;
color: #6c757d;
font-style: italic;
background-color: #e9ecef;
border-radius: 8px;
}
.log-container {
margin-top: 20px;
}
.log-container h3 {
margin-bottom: 10px;
color: #333;
}
.log-content {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
height: 200px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
}
.log-item {
margin-bottom: 5px;
word-wrap: break-word;
}
.log-time {
color: #6c757d;
font-weight: bold;
}
.log-message {
margin-left: 8px;
color: #333;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.controls {
flex-direction: column;
}
.btn {
width: 100%;
}
}
</style>