Files
SmartElderlyCare/src/views/robot/Monitor.vue
2026-01-11 22:51:06 +08:00

233 lines
9.9 KiB
Vue

<template>
<div class="h-full grid grid-cols-12 gap-0">
<!-- Left: Vision Stream & AI Detection -->
<div class="col-span-8 bg-black relative border-r border-gray-800">
<!-- Live Stream Overlay -->
<div class="absolute top-4 left-4 z-10 flex space-x-2">
<div class="px-2 py-1 bg-red-600/80 text-white text-xs font-bold rounded animate-pulse">REC</div>
<div class="px-2 py-1 bg-gray-800/80 text-green-400 text-xs font-mono border border-green-500/30">
FPS: {{ fps }} | LATENCY: {{ latency }}ms
</div>
</div>
<!-- Main Visual Area (Simulated Pose Estimation) -->
<div class="w-full h-full flex items-center justify-center relative overflow-hidden group">
<!-- Grid Background -->
<div class="absolute inset-0 opacity-20"
style="background-image: linear-gradient(rgba(0, 255, 0, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 255, 0, 0.1) 1px, transparent 1px); background-size: 40px 40px;">
</div>
<!-- Skeleton / Person Placeholder -->
<div class="relative transition-all duration-500" :class="{'transform rotate-90 translate-y-20': currentLevel === 'L4'}">
<!-- Head -->
<div class="w-16 h-16 border-2 rounded-full flex items-center justify-center relative z-10"
:class="statusColorClass">
<span class="text-xs font-mono text-white opacity-50">{{ confidence }}%</span>
</div>
<!-- Body -->
<div class="w-1 h-32 mx-auto relative z-0" :class="statusBgClass"></div>
<!-- Arms -->
<div class="absolute top-20 left-1/2 -translate-x-1/2 w-40 h-1" :class="statusBgClass"></div>
<!-- Legs -->
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 w-32 h-1 flex justify-between">
<div class="w-1 h-32 transform -rotate-12 origin-top" :class="statusBgClass"></div>
<div class="w-1 h-32 transform rotate-12 origin-top" :class="statusBgClass"></div>
</div>
<!-- Bounding Box -->
<div class="absolute -inset-8 border border-dashed opacity-50 animate-pulse" :class="statusBorderClass">
<div class="absolute -top-6 left-0 text-xs font-mono" :class="statusTextClass">
ID: PERSON_01 [{{ currentLevel }}]
</div>
</div>
</div>
<!-- Privacy Mask Effect (Scanline) -->
<div class="absolute inset-0 pointer-events-none bg-gradient-to-b from-transparent via-green-500/10 to-transparent h-8 w-full animate-scan"></div>
</div>
<!-- Bottom Console Log -->
<div class="absolute bottom-0 left-0 w-full h-48 bg-black/90 border-t border-gray-800 p-4 font-mono text-xs overflow-y-auto custom-scrollbar">
<div v-for="(log, i) in logs" :key="i" class="mb-1">
<span class="text-gray-500">[{{ log.time }}]</span>
<span :class="log.color" class="ml-2">{{ log.message }}</span>
</div>
</div>
</div>
<!-- Right: AI Logic & Control -->
<div class="col-span-4 bg-gray-900 flex flex-col">
<!-- Header -->
<div class="p-4 border-b border-gray-800 bg-gray-800/50">
<h2 class="text-white font-bold flex items-center">
<span class="mr-2 text-xl">🧠</span> 决策引擎 (Brain)
</h2>
</div>
<!-- Status Panel -->
<div class="p-6 flex-1 space-y-8">
<!-- Current Level Indicator -->
<div class="text-center p-6 rounded-2xl border-2 transition-all duration-300"
:class="levelBorderClass">
<div class="text-sm text-gray-400 uppercase tracking-widest mb-2">当前风险等级</div>
<div class="text-6xl font-black transition-all duration-300" :class="statusTextClass">
{{ currentLevel }}
</div>
<div class="mt-4 text-lg font-medium text-white">{{ currentStatusText }}</div>
</div>
<!-- AI Logic Tree Visualization -->
<div class="space-y-4">
<h3 class="text-xs text-gray-500 uppercase font-bold tracking-wider">推理逻辑链 (Inference Chain)</h3>
<div class="flex flex-col space-y-2">
<div class="flex items-center space-x-3 p-3 rounded bg-gray-800/50 border border-gray-700">
<div class="w-2 h-2 rounded-full bg-green-500"></div>
<span class="text-sm text-gray-300">目标检测 (YOLOv8)</span>
<span class="ml-auto text-xs text-green-400">Done</span>
</div>
<div class="flex items-center space-x-3 p-3 rounded bg-gray-800/50 border border-gray-700">
<div class="w-2 h-2 rounded-full" :class="poseColor"></div>
<span class="text-sm text-gray-300">姿态分析 (PosePipe)</span>
<span class="ml-auto text-xs" :class="poseTextColor">{{ poseStatus }}</span>
</div>
<div class="flex items-center space-x-3 p-3 rounded bg-gray-800/50 border border-gray-700">
<div class="w-2 h-2 rounded-full" :class="riskColor"></div>
<span class="text-sm text-gray-300">风险评估 (RiskEval)</span>
<span class="ml-auto text-xs" :class="riskTextColor">{{ riskScore }}</span>
</div>
</div>
</div>
<!-- Action Panel (Demo Controls) -->
<div class="pt-6 border-t border-gray-800">
<h3 class="text-xs text-gray-500 uppercase font-bold tracking-wider mb-4">模拟触发 (Simulate Trigger)</h3>
<div class="grid grid-cols-2 gap-3">
<button @click="setLevel('L1')" class="px-4 py-3 rounded bg-gray-800 hover:bg-gray-700 text-green-400 text-xs font-bold border border-gray-700 transition-colors">
L1 正常巡护
</button>
<button @click="setLevel('L2')" class="px-4 py-3 rounded bg-gray-800 hover:bg-gray-700 text-yellow-400 text-xs font-bold border border-gray-700 transition-colors">
L2 异常静止
</button>
<button @click="setLevel('L3')" class="px-4 py-3 rounded bg-gray-800 hover:bg-gray-700 text-orange-400 text-xs font-bold border border-gray-700 transition-colors">
L3 呼救检测
</button>
<button @click="setLevel('L4')" class="px-4 py-3 rounded bg-red-900/50 hover:bg-red-800 text-red-400 text-xs font-bold border border-red-800 transition-colors animate-pulse">
L4 跌倒告警
</button>
</div>
</div>
</div>
<!-- Blockchain Status Footer -->
<div class="p-4 bg-gray-800 border-t border-gray-700 flex justify-between items-center">
<div class="flex items-center space-x-2">
<span class="text-xs text-gray-400">TrustLink Status:</span>
<span class="flex h-2 w-2 relative">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-blue-500"></span>
</span>
<span class="text-xs text-blue-400 font-mono">Connected</span>
</div>
<div class="text-xs font-mono text-gray-500">
Last Hash: {{ lastHash }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useDateFormat, useNow } from '@vueuse/core'
import { useGlobalStore } from '../../stores/global'
const store = useGlobalStore()
// State
const currentLevel = computed(() => store.robotStatus.level)
const fps = ref(30)
const latency = ref(12)
const logs = ref([])
const confidence = ref(98)
// Mock Data Generators
const lastHash = ref(store.robotStatus.hash || 'Waiting...')
// ... (Computed Styles & Status - No changes needed as they depend on currentLevel)
// Methods
const addLog = (msg, level = 'info') => {
const time = useDateFormat(useNow(), 'HH:mm:ss.SSS').value
const colors = { info: 'text-gray-400', warning: 'text-yellow-400', error: 'text-red-400', success: 'text-green-400' }
logs.value.unshift({ time, message: msg, color: colors[level] })
if (logs.value.length > 50) logs.value.pop()
}
const setLevel = (level) => {
// Update Global Store
if (level === 'L1') {
store.triggerAlert('L1', '系统恢复正常')
addLog('System state normalized.', 'success')
} else if (level === 'L2') {
store.triggerAlert('L2', '检测到长时间静止')
addLog('Anomaly detected: No motion for 180s.', 'warning')
addLog('Initiating approach sequence...', 'info')
} else if (level === 'L3') {
store.triggerAlert('L3', '检测到呼救声')
addLog('Audio event detected: "Help"', 'warning')
addLog('Risk score escalated to 0.78', 'warning')
} else if (level === 'L4') {
store.triggerAlert('L4', '检测到跌倒事件')
addLog('CRITICAL EVENT: FALL DETECTED', 'error')
addLog('Generating privacy-preserving hash...', 'info')
setTimeout(() => {
lastHash.value = store.robotStatus.hash
addLog(`Evidence committed to blockchain: ${lastHash.value}`, 'success')
addLog('Emergency contact (120) notified via API', 'error')
}, 800)
}
}
// Loop for background logs
let interval
onMounted(() => {
addLog('System initialized. Model: YOLOv8-Nano loaded.', 'success')
addLog('Privacy Engine: Active (Local Only).', 'success')
interval = setInterval(() => {
fps.value = Math.floor(Math.random() * 5) + 28
latency.value = Math.floor(Math.random() * 5) + 10
if (currentLevel.value === 'L1' && Math.random() > 0.8) {
addLog('Routine scan complete. No objects found.', 'info')
}
}, 2000)
})
onUnmounted(() => {
clearInterval(interval)
})
</script>
<style scoped>
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #111;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #333;
}
@keyframes scan {
0% { top: 0%; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { top: 100%; opacity: 0; }
}
.animate-scan {
animation: scan 3s linear infinite;
}
</style>