4.3日报
继续完善服务外包杯,完善前端界面
<template> <div class="home-container"> <header class="app-header"> <img src="@/assets/logo.png" alt="消防AI" class="logo"> <h1>浓烟环境人体识别系统</h1> <div class="status-indicator" :class="connectionStatus"></div> </header> <div class="main-content"> <DualCameraView :visibleImage="visibleImage" :infraredImage="infraredImage" :detections="detections" @frame-click="handleFrameClick" /> <div class="sidebar"> <SmokeDensityMeter :density="smokeDensity" :thresholds="[0.3, 0.6, 0.8]" /> <DetectionResults :detections="detections" :selectedDetection="selectedDetection" @select="selectDetection" /> <AlertPanel :criticalDetections="criticalDetections" v-if="criticalDetections.length > 0" /> </div> </div> <div class="control-panel"> <button @click="connectToRobot" class="control-btn primary"> {{ isConnected ? '已连接机器人' : '连接消防机器人' }} </button> <button @click="toggleRecording" class="control-btn" :class="{active: isRecording}"> {{ isRecording ? '停止记录' : '开始记录' }} </button> <button @click="exportReport" class="control-btn secondary"> 导出救援报告 </button> </div> </div> </template> <script> import DualCameraView from '@/components/DualCameraView.vue' import SmokeDensityMeter from '@/components/SmokeDensityMeter.vue' import DetectionResults from '@/components/DetectionResults.vue' import AlertPanel from '@/components/AlertPanel.vue' export default { components: { DualCameraView, SmokeDensityMeter, DetectionResults, AlertPanel }, data() { return { visibleImage: null, infraredImage: null, detections: [], selectedDetection: null, smokeDensity: 0.45, isConnected: false, isRecording: false, connectionStatus: 'disconnected', socket: null } }, computed: { criticalDetections() { return this.detections.filter(d => d.confidence > 0.7 && d.status === 'critical') } }, mounted() { this.initWebSocket() this.simulateData() }, methods: { initWebSocket() { this.socket = new WebSocket('ws://your-backend-url/ws') this.socket.onopen = () => { this.connectionStatus = 'connected' this.isConnected = true } this.socket.onmessage = (event) => { const data = JSON.parse(event.data) this.processIncomingData(data) } this.socket.onclose = () => { this.connectionStatus = 'disconnected' this.isConnected = false } }, processIncomingData(data) { this.visibleImage = data.visible_image this.infraredImage = data.infrared_image this.detections = data.detections this.smokeDensity = data.smoke_density }, connectToRobot() { if (!this.isConnected) { this.initWebSocket() } else { this.socket.close() } }, toggleRecording() { this.isRecording = !this.isRecording if (this.isRecording) { this.sendCommand('start_recording') } else { this.sendCommand('stop_recording') } }, sendCommand(command) { if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify({ command })) } }, handleFrameClick(position) { // 找到点击位置附近的检测结果 this.selectedDetection = this.detections.find(det => { return this.isPointInRect(position, det.bbox) }) }, isPointInRect(point, rect) { return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height }, selectDetection(detection) { this.selectedDetection = detection }, exportReport() { // 生成并导出PDF报告 console.log('导出救援报告') }, simulateData() { // 演示用模拟数据 setInterval(() => { if (!this.isConnected) { this.detections = this.generateRandomDetections() this.smokeDensity = Math.min(1, Math.max(0, this.smokeDensity + (Math.random() - 0.5) * 0.1)) } }, 1000) }, generateRandomDetections() { const count = Math.floor(Math.random() * 3) + 1 return Array.from({ length: count }, (_, i) => ({ id: Date.now() + i, bbox: { x: Math.random() * 0.8, y: Math.random() * 0.8, width: 0.1 + Math.random() * 0.2, height: 0.1 + Math.random() * 0.3 }, confidence: (0.5 + Math.random() * 0.5).toFixed(2), status: Math.random() > 0.7 ? 'critical' : 'normal', timestamp: new Date().toLocaleTimeString() })) } } } </script> <style scoped> .home-container { display: flex; flex-direction: column; height: 100vh; background-color: #1a1a2e; color: #e6e6e6; } .app-header { display: flex; align-items: center; padding: 1rem; background-color: #16213e; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); } .logo { height: 40px; margin-right: 15px; } .status-indicator { width: 12px; height: 12px; border-radius: 50%; margin-left: auto; } .status-indicator.connected { background-color: #4ade80; box-shadow: 0 0 8px #4ade80; } .status-indicator.disconnected { background-color: #f87171; } .main-content { display: flex; flex: 1; overflow: hidden; } .sidebar { width: 300px; background-color: #0f3460; padding: 1rem; overflow-y: auto; display: flex; flex-direction: column; gap: 1rem; } .control-panel { display: flex; padding: 0.5rem; background-color: #16213e; gap: 0.5rem; } .control-btn { padding: 0.5rem 1rem; border: none; border-radius: 4px; background-color: #374151; color: white; cursor: pointer; transition: all 0.2s; } .control-btn.primary { background-color: #3b82f6; } .control-btn.secondary { background-color: #10b981; } .control-btn.active { background-color: #ef4444; box-shadow: 0 0 0 2px #fca5a5; } .control-btn:hover { opacity: 0.9; transform: translateY(-1px); } </style>