233 lines
9.9 KiB
Vue
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>
|