feat(agency): 新增应急响应和护理记录功能模块

添加紧急响应中心和护理记录档案两个新路由页面
实现全局状态管理中的告警数据结构和模拟数据
完善机构工作台的机器人调度和任务管理功能
重构仪表盘地图视图为可切换的网格和地图模式
优化告警卡片和区块链存证信息的展示效果
This commit is contained in:
祀梦
2026-01-12 00:42:51 +08:00
parent d22b65f6d3
commit d32985721d
8 changed files with 1530 additions and 128 deletions

View File

@@ -11,16 +11,16 @@
<span class="inline-block w-6">🏠</span> 返回系统首页
</router-link>
<div class="h-px bg-blue-800 my-2 mx-6"></div>
<router-link to="/agency/workspace" class="block px-6 py-3 hover:bg-blue-800 transition-colors border-l-4 border-transparent hover:border-white">
<router-link to="/agency/workspace" class="block px-6 py-3 hover:bg-blue-800 transition-colors border-l-4 border-transparent hover:border-white" active-class="bg-blue-800 border-white">
<span class="inline-block w-6">📊</span> 工作台
</router-link>
<div class="px-6 py-3 text-blue-400 text-xs uppercase tracking-wider mt-4">任务管理</div>
<a href="#" class="block px-6 py-3 hover:bg-blue-800 transition-colors border-l-4 border-transparent hover:border-white opacity-50 cursor-not-allowed">
<router-link to="/agency/emergency" class="block px-6 py-3 hover:bg-blue-800 transition-colors border-l-4 border-transparent hover:border-white" active-class="bg-blue-800 border-white">
<span class="inline-block w-6">🚑</span> 紧急响应
</a>
<a href="#" class="block px-6 py-3 hover:bg-blue-800 transition-colors border-l-4 border-transparent hover:border-white opacity-50 cursor-not-allowed">
</router-link>
<router-link to="/agency/records" class="block px-6 py-3 hover:bg-blue-800 transition-colors border-l-4 border-transparent hover:border-white" active-class="bg-blue-800 border-white">
<span class="inline-block w-6">📝</span> 护理记录
</a>
</router-link>
</nav>
</aside>

View File

@@ -46,6 +46,16 @@ const routes = [
path: 'workspace',
name: 'AgencyWorkspace',
component: () => import('../views/agency/Workspace.vue')
},
{
path: 'emergency',
name: 'AgencyEmergency',
component: () => import('../views/agency/Emergency.vue')
},
{
path: 'records',
name: 'AgencyRecords',
component: () => import('../views/agency/Records.vue')
}
]
},

View File

@@ -4,7 +4,26 @@ import { useNow, useDateFormat } from '@vueuse/core'
export const useGlobalStore = defineStore('global', () => {
// 模拟的全局数据库
const alerts = ref([])
const alerts = ref([
{
id: 1,
level: 'L4',
time: '14:20:15',
type: '跌倒告警',
desc: '青和园社区 A区3号楼 发现疑似老人跌倒',
image: 'https://images.unsplash.com/photo-1516733725897-1aa73b87c8e8?q=80&w=200&auto=format&fit=crop',
read: false
},
{
id: 2,
level: 'L2',
time: '14:15:22',
type: '异常行为',
desc: '长青护理院 2层走廊 监测到长时间静止',
image: 'https://images.unsplash.com/photo-1511884642898-4c92249e20b6?q=80&w=200&auto=format&fit=crop',
read: false
}
])
const auditLogs = ref([
{ hash: '0x8f2a...9b1c', time: '12:30:45', desc: '完成午餐进食' },
{ hash: '0x3d4e...1f9a', time: '08:15:22', desc: '按时服用降压药' },
@@ -46,6 +65,9 @@ export const useGlobalStore = defineStore('global', () => {
time,
type: level === 'L4' ? '跌倒告警' : '异常行为',
desc: message,
image: level === 'L4'
? 'https://images.unsplash.com/photo-1516733725897-1aa73b87c8e8?q=80&w=200&auto=format&fit=crop' // 跌倒模拟/医疗场景
: 'https://images.unsplash.com/photo-1511884642898-4c92249e20b6?q=80&w=200&auto=format&fit=crop', // 监控画面
read: false
})
}

View File

@@ -13,23 +13,107 @@
<!-- Main Vis Area -->
<div class="flex-1 grid grid-cols-3 gap-6 min-h-0">
<!-- Map Area (Center) -->
<div class="col-span-2 bg-slate-800/30 border border-slate-700 rounded-xl relative overflow-hidden group">
<!-- Decoration Grid -->
<div class="absolute inset-0 opacity-20"
style="background-image: radial-gradient(#4f46e5 1px, transparent 1px); background-size: 20px 20px;">
<!-- Left Panel: Interactive Map & Grid -->
<div class="col-span-2 flex flex-col space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<h3 class="text-sm font-bold text-slate-300 uppercase tracking-wider flex items-center">
<span class="w-2 h-2 bg-blue-500 rounded-full mr-2 animate-pulse"></span>
{{ viewMode === 'map' ? '全域态势图 (Shanghai Map)' : '重点社区实时感知' }}
</h3>
<div class="flex bg-slate-800 rounded-lg p-1 border border-slate-700">
<button @click="viewMode = 'map'"
:class="viewMode === 'map' ? 'bg-blue-600 text-white' : 'text-slate-400 hover:text-white'"
class="px-3 py-1 text-[10px] rounded transition-all">地图模式</button>
<button @click="viewMode = 'grid'"
:class="viewMode === 'grid' ? 'bg-blue-600 text-white' : 'text-slate-400 hover:text-white'"
class="px-3 py-1 text-[10px] rounded transition-all">网格监控</button>
</div>
</div>
<div class="flex space-x-2">
<span class="px-2 py-1 rounded bg-blue-500/20 text-blue-400 text-[10px] border border-blue-500/30">
AI 巡检中
</span>
</div>
</div>
<!-- Mock Map Content -->
<div class="absolute inset-0 flex items-center justify-center">
<div class="relative w-3/4 h-3/4 border-2 border-indigo-500/30 rounded-full animate-pulse-slow flex items-center justify-center">
<div class="w-2/3 h-2/3 border border-indigo-400/20 rounded-full"></div>
<!-- Hotspots -->
<div class="absolute top-1/4 right-1/4 w-3 h-3 bg-red-500 rounded-full shadow-[0_0_15px_rgba(239,68,68,0.8)] animate-ping"></div>
<div class="absolute bottom-1/3 left-1/3 w-2 h-2 bg-green-500 rounded-full shadow-[0_0_10px_rgba(34,197,94,0.8)]"></div>
<!-- View Content -->
<div class="flex-1 relative overflow-hidden bg-slate-800/30 border border-slate-700 rounded-xl">
<!-- Mode 1: Shanghai Map -->
<div v-if="viewMode === 'map'" class="absolute inset-0 flex items-center justify-center animate-fade-in">
<!-- Mock SVG Map of Shanghai -->
<svg viewBox="0 0 800 600" class="w-full h-full p-8 text-blue-500/20 fill-current">
<!-- Simplified Shanghai Shape -->
<path d="M400,100 L500,150 L550,250 L520,400 L450,500 L300,550 L200,450 L150,300 L250,150 Z" />
<!-- River Line -->
<path d="M350,100 Q450,300 350,600" fill="none" stroke="currentColor" stroke-width="4" class="text-blue-400/30" />
</svg>
<!-- Interactive Markers -->
<div v-for="community in communities" :key="community.name"
class="absolute cursor-pointer group"
:style="{ left: community.x + '%', top: community.y + '%' }"
@click="selectedCommunity = community">
<div class="relative">
<!-- Outer Pulse -->
<div class="absolute -inset-4 bg-blue-500/20 rounded-full animate-ping group-hover:bg-blue-500/40"></div>
<!-- Marker Dot -->
<div class="relative w-4 h-4 bg-blue-600 border-2 border-white rounded-full shadow-lg shadow-blue-500/50 group-hover:scale-125 transition-transform"></div>
<!-- Label -->
<div class="absolute top-6 left-1/2 -translate-x-1/2 whitespace-nowrap bg-black/80 backdrop-blur px-2 py-1 rounded border border-white/10 text-[10px] text-white pointer-events-none group-hover:opacity-100 transition-opacity">
{{ community.name }}
</div>
</div>
</div>
<!-- Map Tooltip/Detail Popup -->
<div v-if="selectedCommunity" class="absolute bottom-6 left-6 right-6 bg-slate-900/90 backdrop-blur-md border border-blue-500/30 rounded-lg p-4 animate-slide-up flex items-center space-x-4">
<img :src="selectedCommunity.image" crossorigin="anonymous" class="w-32 h-20 object-cover rounded border border-white/10">
<div class="flex-1">
<div class="flex justify-between items-start">
<h4 class="text-white font-bold">{{ selectedCommunity.name }}</h4>
<button @click="selectedCommunity = null" class="text-slate-500 hover:text-white"></button>
</div>
<p class="text-xs text-slate-400 mt-1">{{ selectedCommunity.location }}</p>
<div class="flex space-x-4 mt-2">
<span class="text-[10px] text-blue-400">🤖 部署机器人: {{ selectedCommunity.robots }}</span>
<span class="text-[10px] text-green-400"> 运行状态: 良好</span>
</div>
</div>
<button @click="viewMode = 'grid'" class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-xs rounded-lg transition-colors">
切入监控
</button>
</div>
</div>
<!-- Mode 2: Community Grid -->
<div v-else class="absolute inset-0 p-4 grid grid-cols-2 gap-4 animate-fade-in overflow-y-auto custom-scrollbar">
<div v-for="community in communities" :key="community.name"
class="relative h-48 group rounded-xl overflow-hidden border border-slate-700 bg-slate-800/50 hover:border-blue-500/50 transition-all duration-500">
<!-- Monitoring Image -->
<img :src="community.image" crossorigin="anonymous" class="w-full h-full object-cover opacity-60 group-hover:opacity-80 transition-opacity duration-700 group-hover:scale-105 transform">
<!-- Overlay Info -->
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent p-4 flex flex-col justify-end">
<div class="flex justify-between items-end">
<div>
<div class="text-white font-bold text-sm">{{ community.name }}</div>
<div class="text-xs text-slate-400 flex items-center mt-1">
<span class="w-1.5 h-1.5 rounded-full mr-1.5" :class="community.status === 'normal' ? 'bg-green-500' : 'bg-red-500'"></span>
{{ community.location }}
</div>
</div>
<div class="text-right">
<div class="text-[10px] text-blue-400 font-mono">机器人: {{ community.robots }}</div>
<div class="text-[10px] text-slate-500 font-mono">{{ community.time }}</div>
</div>
</div>
</div>
<!-- Scanline Effect -->
<div class="absolute inset-0 pointer-events-none bg-gradient-to-b from-transparent via-blue-500/5 to-transparent h-1/2 -translate-y-full group-hover:animate-scanline"></div>
</div>
<div class="absolute bottom-4 right-4 bg-black/60 px-3 py-1 rounded text-xs text-indigo-300 border border-indigo-500/30">
Real-time Monitoring Active
</div>
</div>
</div>
@@ -39,15 +123,39 @@
<!-- Recent Alerts -->
<div class="flex-1 bg-slate-800/50 border border-slate-700 rounded-xl p-4 overflow-hidden flex flex-col">
<h3 class="text-sm font-bold text-slate-300 mb-3 uppercase tracking-wider">实时告警流 (Alert Stream)</h3>
<div class="flex-1 overflow-y-auto space-y-2 pr-1 custom-scrollbar">
<div v-for="alert in store.alerts" :key="alert.id" class="bg-slate-700/50 p-3 rounded border-l-2 animate-slide-in" :class="alert.level === 'L4' ? 'border-red-500' : 'border-orange-500'">
<div class="flex-1 overflow-y-auto space-y-3 pr-1 custom-scrollbar">
<div v-for="alert in store.alerts" :key="alert.id"
class="bg-slate-900/50 rounded-lg border border-slate-700/50 overflow-hidden animate-slide-in group hover:border-blue-500/30 transition-colors">
<div class="flex p-3 space-x-3">
<!-- Alert Snapshot -->
<div class="w-20 h-20 rounded bg-slate-800 border border-slate-700 flex-shrink-0 relative overflow-hidden">
<img :src="alert.image" crossorigin="anonymous" class="w-full h-full object-cover">
<div class="absolute inset-0 bg-red-500/10 group-hover:bg-transparent transition-colors"></div>
</div>
<div class="flex-1 min-w-0">
<div class="flex justify-between items-start">
<span class="text-xs font-bold text-white">{{ alert.type }}</span>
<span class="text-[10px] text-slate-400">{{ alert.time }}</span>
<span class="text-xs font-bold" :class="alert.level === 'L4' ? 'text-red-400' : 'text-orange-400'">
{{ alert.type }}
</span>
<span class="text-[10px] text-slate-500">{{ alert.time }}</span>
</div>
<div class="text-xs text-slate-300 mt-1">{{ alert.desc }}</div>
<div class="text-[11px] text-slate-300 mt-1 leading-relaxed">{{ alert.desc }}</div>
<div class="mt-2 flex space-x-2">
<button class="px-2 py-0.5 rounded bg-blue-500/20 text-blue-400 text-[9px] border border-blue-500/30 hover:bg-blue-500/30 transition-colors">
链上验证
</button>
<button class="px-2 py-0.5 rounded bg-slate-700 text-slate-300 text-[9px] border border-slate-600 hover:bg-slate-600 transition-colors">
查看现场
</button>
</div>
</div>
</div>
</div>
<div v-if="store.alerts.length === 0" class="flex flex-col items-center justify-center py-12 space-y-3 opacity-30">
<div class="text-4xl">🛡</div>
<div class="text-xs text-slate-500">全域态势平稳 暂无活跃告警</div>
</div>
<div v-if="store.alerts.length === 0" class="text-center text-xs text-slate-500 py-4">暂无实时告警</div>
</div>
</div>
@@ -72,6 +180,48 @@ import { useGlobalStore } from '../../stores/global'
const store = useGlobalStore()
const viewMode = ref('map') // 'map' or 'grid'
const selectedCommunity = ref(null)
const communities = ref([
{
name: '青和园社区',
location: 'A区 3号楼 102单元',
robots: 12,
status: 'normal',
time: '刚刚',
image: 'https://images.unsplash.com/photo-1545324418-cc1a3fa10c00?q=80&w=400&auto=format&fit=crop',
x: 45, y: 35 // Map coordinates in %
},
{
name: '锦绣华府',
location: '西门 康复广场',
robots: 8,
status: 'normal',
time: '2分钟前',
image: 'https://images.unsplash.com/photo-1512917774080-9991f1c4c750?q=80&w=400&auto=format&fit=crop',
x: 35, y: 55
},
{
name: '长青护理院',
location: '2层 走廊尽头',
robots: 15,
status: 'normal',
time: '刚刚',
image: 'https://images.unsplash.com/photo-1586023492125-27b2c045efd7?q=80&w=400&auto=format&fit=crop',
x: 55, y: 45
},
{
name: '云栖养老中心',
location: '15号 独栋小院',
robots: 6,
status: 'normal',
time: '5分钟前',
image: 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=400&auto=format&fit=crop',
x: 42, y: 65
}
])
const kpis = ref([
{ label: '覆盖社区数', value: '128', trend: '12%', trendUp: true },
{ label: '在线机器人', value: '1,042', trend: '5%', trendUp: true },
@@ -81,6 +231,22 @@ const kpis = ref([
</script>
<style scoped>
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-slide-up {
animation: slideUp 0.4s ease-out;
}
@keyframes scanline {
0% { transform: translateY(-100%); }
100% { transform: translateY(200%); }
}
.animate-scanline {
animation: scanline 3s linear infinite;
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }

View File

@@ -0,0 +1,314 @@
<template>
<div class="space-y-6">
<div class="flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold text-gray-800">应急响应中心</h2>
<p class="text-gray-500 mt-1">实时监控紧急事件快速调度资源并记录区块链证据</p>
</div>
<div class="flex space-x-3">
<div class="bg-red-50 px-4 py-2 rounded-lg border border-red-100 flex items-center space-x-2">
<span class="flex h-3 w-3 relative">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
</span>
<span class="text-red-700 font-bold text-sm">当前活跃告警: {{ activeAlerts.length }}</span>
</div>
</div>
</div>
<!-- Active Emergency Alerts -->
<div class="grid grid-cols-1 gap-6">
<div v-for="alert in activeAlerts" :key="alert.id"
class="bg-white rounded-2xl border-2 border-red-100 shadow-xl overflow-hidden animate-pulse-border">
<div class="bg-red-600 px-6 py-4 flex justify-between items-center text-white">
<div class="flex items-center space-x-4">
<span class="text-2xl"></span>
<div>
<div class="font-black text-lg uppercase tracking-wider">{{ alert.type }} - 紧急响应中</div>
<div class="text-xs opacity-80">触发时间: {{ alert.time }} | 存证哈希: 0x{{ Math.random().toString(16).slice(2, 12) }}...</div>
</div>
</div>
<div class="bg-white/20 px-3 py-1 rounded-full text-xs font-bold">优先级: CRITICAL (L4)</div>
</div>
<div class="p-6 grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Live Feed/Image -->
<div class="lg:col-span-1">
<div class="relative rounded-xl overflow-hidden border-4 border-gray-100 aspect-video shadow-inner">
<img :src="alert.image" crossorigin="anonymous" class="w-full h-full object-cover">
<div class="absolute top-3 left-3 px-2 py-1 bg-red-600 text-white text-[10px] font-bold rounded flex items-center">
<span class="w-1.5 h-1.5 bg-white rounded-full mr-2 animate-pulse"></span>
实时现场画面
</div>
</div>
<div class="mt-4 p-4 bg-gray-50 rounded-xl border border-gray-100">
<h4 class="text-xs font-bold text-gray-500 uppercase mb-2">事件详情</h4>
<p class="text-sm text-gray-800 leading-relaxed">{{ alert.desc }}</p>
</div>
</div>
<!-- Triage & Actions -->
<div class="lg:col-span-1 space-y-4">
<h4 class="text-sm font-bold text-gray-800 flex items-center">
<span class="mr-2">📋</span> 响应处置方案
</h4>
<div class="space-y-3">
<div v-for="(step, index) in protocols" :key="index"
class="flex items-start space-x-3 p-3 rounded-xl border border-gray-100 hover:bg-blue-50 transition-colors cursor-pointer group"
@click="toggleStep(index)">
<div class="mt-0.5 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-colors"
:class="step.done ? 'bg-blue-600 border-blue-600 text-white' : 'border-gray-300 text-transparent'">
<span class="text-[10px]"></span>
</div>
<div class="flex-1">
<div class="text-sm font-bold transition-colors" :class="step.done ? 'text-blue-700 line-through opacity-50' : 'text-gray-700'">{{ step.title }}</div>
<div class="text-[10px] text-gray-400 mt-0.5">{{ step.desc }}</div>
</div>
</div>
</div>
</div>
<!-- Resources & Dispatch -->
<div class="lg:col-span-1 space-y-4">
<h4 class="text-sm font-bold text-gray-800 flex items-center">
<span class="mr-2">🚑</span> 资源调度
</h4>
<div class="bg-gray-900 rounded-xl p-4 text-white space-y-4">
<div class="flex justify-between items-center pb-3 border-b border-gray-800">
<div class="text-xs text-gray-400">就近护士站</div>
<div class="text-xs font-bold text-blue-400">2F 中心站 (距离 35m)</div>
</div>
<div class="flex justify-between items-center pb-3 border-b border-gray-800">
<div class="text-xs text-gray-400">待命护工</div>
<div class="text-xs font-bold text-green-400">3 在线</div>
</div>
<div class="flex justify-between items-center">
<div class="text-xs text-gray-400">急救设备 (AED)</div>
<div class="text-xs font-bold text-orange-400">已就绪</div>
</div>
<button @click="handleEmergencyAction('dispatch')"
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg shadow-lg transition-all active:scale-95 flex items-center justify-center">
一键发起多方协同
</button>
<button @click="resolveAlert(alert.id)"
class="w-full bg-white/10 hover:bg-white/20 text-white text-sm font-bold py-3 rounded-lg transition-all">
标记为已处理 (完成归档)
</button>
</div>
</div>
</div>
</div>
<!-- No Alerts State -->
<div v-if="activeAlerts.length === 0" class="bg-white rounded-2xl border border-dashed border-gray-300 py-20 flex flex-col items-center justify-center opacity-60">
<div class="text-6xl mb-4">🛡</div>
<h3 class="text-xl font-bold text-gray-800">当前无紧急告警</h3>
<p class="text-sm text-gray-500 mt-2">系统运行平稳所有安全指标正常</p>
</div>
</div>
<!-- History & Audit -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center">
<h3 class="font-bold text-gray-800">近期应急响应历史</h3>
<button class="text-blue-600 text-sm font-bold hover:underline">查看完整审计日志</button>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left">
<thead class="bg-gray-50 text-[10px] uppercase font-bold text-gray-400">
<tr>
<th class="px-6 py-3">事件 ID</th>
<th class="px-6 py-3">类型</th>
<th class="px-6 py-3">处理时间</th>
<th class="px-6 py-3">响应时长</th>
<th class="px-6 py-3">存证状态</th>
<th class="px-6 py-3">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
<tr v-for="log in history" :key="log.id" class="text-sm hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 font-mono text-xs">#{{ log.id }}</td>
<td class="px-6 py-4">
<span class="bg-gray-100 text-gray-700 px-2 py-0.5 rounded text-xs">{{ log.type }}</span>
</td>
<td class="px-6 py-4 text-gray-500">{{ log.time }}</td>
<td class="px-6 py-4 text-gray-500">{{ log.duration }}</td>
<td class="px-6 py-4">
<span class="flex items-center text-green-600 font-bold text-xs">
<span class="w-1.5 h-1.5 bg-green-500 rounded-full mr-2"></span>
已固化至区块链
</span>
</td>
<td class="px-6 py-4">
<button @click="viewReport(log)" class="text-blue-600 hover:text-blue-800 font-bold text-xs">查看报告</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Report Modal -->
<div v-if="selectedReport" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-white rounded-3xl w-full max-w-2xl shadow-2xl animate-bounce-in overflow-hidden border border-gray-100">
<div class="px-8 py-6 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<div>
<h3 class="font-black text-gray-900 text-xl tracking-tight">应急响应结案报告</h3>
<p class="text-xs text-gray-400 mt-1 font-mono uppercase tracking-widest">Incident Report #{{ selectedReport.id }}</p>
</div>
<button @click="selectedReport = null" class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-gray-200 transition-colors text-gray-400 text-xl"></button>
</div>
<div class="p-8 space-y-8 overflow-y-auto max-h-[70vh] custom-scrollbar">
<!-- Incident Header -->
<div class="grid grid-cols-2 gap-6 bg-blue-50/30 p-6 rounded-2xl border border-blue-100/50">
<div>
<label class="text-[10px] font-black text-blue-400 uppercase tracking-widest block mb-1">事件类型</label>
<div class="font-bold text-gray-800">{{ selectedReport.type }}</div>
</div>
<div>
<label class="text-[10px] font-black text-blue-400 uppercase tracking-widest block mb-1">响应时长</label>
<div class="font-bold text-gray-800">{{ selectedReport.duration }}</div>
</div>
<div>
<label class="text-[10px] font-black text-blue-400 uppercase tracking-widest block mb-1">发生时间</label>
<div class="font-bold text-gray-800">{{ selectedReport.time }}</div>
</div>
<div>
<label class="text-[10px] font-black text-blue-400 uppercase tracking-widest block mb-1">责任护理员</label>
<div class="font-bold text-gray-800">张护理员</div>
</div>
</div>
<!-- Chronology -->
<div class="space-y-4">
<h4 class="text-sm font-black text-gray-900 flex items-center">
<span class="w-2 h-2 bg-blue-600 rounded-full mr-2"></span>
响应时间轴 (区块链固化记录)
</h4>
<div class="relative pl-6 space-y-6 before:absolute before:left-1 before:top-2 before:bottom-2 before:w-0.5 before:bg-gray-100">
<div class="relative">
<div class="absolute -left-[22px] top-1.5 w-3 h-3 rounded-full bg-red-500 border-2 border-white shadow-sm"></div>
<div class="text-xs font-bold text-gray-800">10:15:00 - 系统触发告警</div>
<div class="text-[10px] text-gray-500 mt-0.5">AI 视觉识别引擎识别到 A区3号楼 疑似跌倒行为</div>
</div>
<div class="relative">
<div class="absolute -left-[22px] top-1.5 w-3 h-3 rounded-full bg-orange-500 border-2 border-white shadow-sm"></div>
<div class="text-xs font-bold text-gray-800">10:15:12 - 护理员响应</div>
<div class="text-[10px] text-gray-500 mt-0.5">护理员 张某 通过移动端确认告警并开启双向语音</div>
</div>
<div class="relative">
<div class="absolute -left-[22px] top-1.5 w-3 h-3 rounded-full bg-blue-500 border-2 border-white shadow-sm"></div>
<div class="text-xs font-bold text-gray-800">10:17:45 - 现场处置完成</div>
<div class="text-[10px] text-gray-500 mt-0.5">确认老人无大碍已协助老人返回房间休息</div>
</div>
</div>
</div>
<!-- Evidence -->
<div class="space-y-4">
<h4 class="text-sm font-black text-gray-900 flex items-center">
<span class="w-2 h-2 bg-blue-600 rounded-full mr-2"></span>
存证哈希与证据
</h4>
<div class="bg-gray-900 rounded-2xl p-6 text-white space-y-4 shadow-2xl">
<div class="flex items-center justify-between">
<div class="text-[10px] font-mono text-blue-400 opacity-80">0x{{ Math.random().toString(16).slice(2, 24) }}...</div>
<span class="text-[10px] bg-blue-600 text-white px-2 py-0.5 rounded uppercase font-bold tracking-tighter">Verified</span>
</div>
<p class="text-[10px] text-gray-400 leading-relaxed">
本报告所有环节均已通过非对称加密算法生成数字签名并实时固化至联盟链证据文件包括现场图像快照 (Hash: a3f1...)语音通话录音摘要 (Hash: 92d4...)
</p>
</div>
</div>
</div>
<div class="p-8 bg-gray-50 flex space-x-4">
<button @click="selectedReport = null" class="flex-1 py-4 text-sm font-bold text-gray-500 hover:bg-gray-200 rounded-2xl transition-all">关闭预览</button>
<button @click="printReport" class="flex-1 py-4 text-sm font-bold bg-blue-600 text-white rounded-2xl shadow-xl shadow-blue-200 hover:bg-blue-700 transition-all active:scale-95 flex items-center justify-center">
<span class="mr-2">🖨</span> 导出加密 PDF
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useGlobalStore } from '../../stores/global'
const store = useGlobalStore()
// Report viewing state
const selectedReport = ref(null)
// Get only L4 alerts for this page
const activeAlerts = computed(() => store.alerts.filter(a => a.level === 'L4'))
const protocols = ref([
{ title: '语音通话核实', desc: '通过现场机器人发起双向语音,确认老人意识状态', done: false },
{ title: '通知就近护理员', desc: '指派 2F 中心站 张护士 立即前往现场', done: false },
{ title: '启动医疗预案', desc: '准备 AED 及 急救包,通知社区签约医生', done: false },
{ title: '家属/紧急联系人', desc: '系统已自动拨打紧急联系人电话,等待反馈', done: false }
])
const history = ref([
{ id: 'ERR-8821', type: '跌倒告警', time: '2024-01-12 10:15', duration: '4m 12s' },
{ id: 'ERR-8815', type: '异常行为', time: '2024-01-11 15:30', duration: '2m 45s' },
{ id: 'ERR-8792', type: '生命体征异常', time: '2024-01-10 09:20', duration: '12m 10s' }
])
const toggleStep = (index) => {
protocols.value[index].done = !protocols.value[index].done
}
const handleEmergencyAction = (type) => {
alert('协同指令已发出!\n1. 护理员手环已震动\n2. 电梯已自动降至 1 层\n3. 监控画面已推送到大厅')
}
const resolveAlert = (id) => {
if (confirm('确认该告警已完全处置?处置记录将作为区块链证据永久保存。')) {
store.resolveAlert(id)
}
}
const viewReport = (report) => {
selectedReport.value = report
}
const printReport = () => {
alert('正在生成加密报告...\n1. 正在提取区块链存证摘要\n2. 正在合成电子签名水印\n3. 文件已准备就绪,即将开始下载。')
}
</script>
<style scoped>
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #e2e8f0;
border-radius: 4px;
}
@keyframes pulse-border {
0%, 100% { border-color: rgb(254 226 226); box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1); }
50% { border-color: rgb(239 68 68 / 0.5); box-shadow: 0 20px 25px -5px rgb(239 68 68 / 0.1); }
}
.animate-pulse-border {
animation: pulse-border 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes bounce-in {
0% { transform: scale(0.95); opacity: 0; }
70% { transform: scale(1.02); opacity: 1; }
100% { transform: scale(1); }
}
.animate-bounce-in {
animation: bounce-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
</style>

View File

@@ -0,0 +1,340 @@
<template>
<div class="space-y-6">
<div class="flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold text-gray-800">护理记录档案</h2>
<p class="text-gray-500 mt-1">基于区块链存证的标准化护理记录确保服务可追溯不可篡改</p>
</div>
<button @click="showAddModal = true" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-bold transition-all shadow-lg shadow-blue-100 flex items-center">
<span class="mr-2">+</span> 新增护理记录
</button>
</div>
<!-- Stats Overview -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div v-for="stat in recordStats" :key="stat.label" class="bg-white p-4 rounded-xl border border-gray-100 shadow-sm">
<div class="text-xs text-gray-400 font-bold uppercase tracking-wider">{{ stat.label }}</div>
<div class="mt-1 flex items-baseline space-x-2">
<div class="text-2xl font-black text-gray-800">{{ stat.value }}</div>
<div class="text-[10px] text-green-500 font-bold">{{ stat.trend }}</div>
</div>
</div>
</div>
<!-- Filters & Search -->
<div class="bg-white p-4 rounded-xl border border-gray-100 shadow-sm flex flex-wrap gap-4 items-center justify-between">
<div class="flex items-center space-x-4">
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs">🔍</span>
<input type="text" placeholder="搜索老人姓名/房间号..." class="pl-9 pr-4 py-2 bg-gray-50 border-none rounded-lg text-sm focus:ring-2 focus:ring-blue-500 w-64">
</div>
<select class="bg-gray-50 border-none rounded-lg text-sm px-4 py-2 focus:ring-2 focus:ring-blue-500">
<option>全部记录类型</option>
<option>生活护理</option>
<option>医疗协助</option>
<option>心理慰藉</option>
<option>日常巡查</option>
</select>
</div>
<div class="flex items-center space-x-2">
<span class="text-xs text-gray-400">数据状态:</span>
<span class="bg-green-50 text-green-600 text-[10px] font-bold px-2 py-1 rounded-full border border-green-100 flex items-center">
<span class="w-1.5 h-1.5 bg-green-500 rounded-full mr-1.5"></span>
链上同步正常
</span>
</div>
</div>
<!-- Records List -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2 space-y-6">
<h3 class="text-lg font-bold text-gray-800 flex items-center px-1">
<span class="mr-3 text-2xl">📅</span> 日常护理日志
</h3>
<div v-for="record in records" :key="record.id"
class="bg-white rounded-3xl border border-gray-100 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all group overflow-hidden">
<div class="flex">
<!-- Side Indicator -->
<div class="w-2.5" :class="getTypeColor(record.type)"></div>
<div class="flex-1 p-6 md:p-8 flex flex-col md:flex-row md:items-center justify-between gap-6">
<div class="flex items-center space-x-6">
<!-- Elder Info -->
<div class="flex items-center space-x-4">
<div class="w-16 h-16 rounded-2xl bg-gray-50 flex items-center justify-center text-3xl shadow-inner border border-gray-100">
{{ record.avatar }}
</div>
<div>
<div class="font-black text-gray-900 text-lg leading-tight">{{ record.elder }}</div>
<div class="text-sm text-gray-400 font-bold mt-0.5">{{ record.room }}</div>
</div>
</div>
<div class="hidden md:block h-12 w-px bg-gray-100"></div>
<!-- Content -->
<div class="flex-1 min-w-[300px]">
<div class="flex items-center space-x-3 mb-2">
<span class="text-xs font-black px-3 py-1 rounded-full uppercase tracking-wider" :class="getTypeBadge(record.type)">
{{ record.type }}
</span>
<span class="text-lg font-black text-gray-800">{{ record.title }}</span>
</div>
<p class="text-sm text-gray-500 leading-relaxed font-medium">{{ record.content }}</p>
</div>
</div>
<!-- Blockchain & Time Info -->
<div class="flex items-center justify-between md:justify-end space-x-6 border-t md:border-t-0 pt-4 md:pt-0">
<div class="text-right">
<div class="text-xs font-black text-gray-300 uppercase tracking-widest mb-1">Entry Time</div>
<div class="text-sm font-bold text-gray-600">{{ record.time }}</div>
</div>
<div class="bg-blue-50/80 px-4 py-2 rounded-xl border border-blue-100/50 hidden xl:block">
<div class="text-[10px] text-blue-400 font-black uppercase tracking-widest mb-0.5">Chain Hash</div>
<div class="text-xs font-mono text-blue-600 font-bold">{{ record.hash }}</div>
</div>
<button class="w-12 h-12 bg-gray-50 hover:bg-blue-600 hover:text-white rounded-2xl transition-all flex items-center justify-center group/btn shadow-sm">
<span class="text-gray-400 group-hover/btn:text-white text-xl transition-colors"></span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Blockchain Audit Logs (From Global Store) -->
<div class="lg:col-span-1 space-y-4">
<h3 class="text-sm font-bold text-gray-800 flex items-center px-1">
<span class="mr-2">🔗</span> 区块链实时存证流
</h3>
<div class="bg-[#0f172a] rounded-3xl p-6 shadow-2xl min-h-[500px] border border-slate-800 relative overflow-hidden group">
<!-- Background Glow -->
<div class="absolute -top-24 -right-24 w-48 h-48 bg-blue-500/10 blur-[100px] rounded-full group-hover:bg-blue-500/20 transition-colors"></div>
<div class="relative z-10 space-y-6">
<div v-for="(log, index) in store.auditLogs" :key="index"
class="relative pl-10 animate-fade-in-right"
:style="{ animationDelay: `${index * 100}ms` }">
<!-- Timeline Connector -->
<div v-if="index !== store.auditLogs.length - 1"
class="absolute left-4 top-8 bottom-0 w-0.5 bg-gradient-to-b from-blue-500/50 to-transparent"></div>
<!-- Animated Dot -->
<div class="absolute left-0 top-1.5 w-8 h-8 flex items-center justify-center">
<div class="w-3 h-3 bg-blue-500 rounded-full shadow-[0_0_15px_rgba(59,130,246,0.8)] relative z-20">
<div class="absolute inset-0 bg-blue-400 rounded-full animate-ping opacity-40"></div>
</div>
</div>
<div class="bg-slate-800/40 backdrop-blur-md border border-slate-700/50 p-4 rounded-2xl hover:border-blue-500/40 transition-all hover:bg-slate-800/60 group/item">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center space-x-2">
<span class="w-1.5 h-1.5 bg-green-500 rounded-full"></span>
<span class="text-[10px] font-mono text-blue-400 font-bold tracking-tighter opacity-70 group-hover/item:opacity-100 transition-opacity">
{{ log.hash }}
</span>
</div>
<span class="text-[9px] text-slate-500 font-black uppercase">{{ log.time }}</span>
</div>
<div class="text-xs text-slate-200 font-medium leading-relaxed">
{{ log.desc }}
</div>
<!-- Verification Tag -->
<div class="mt-3 flex items-center justify-between border-t border-slate-700/50 pt-2">
<div class="flex -space-x-1">
<div v-for="i in 3" :key="i" class="w-4 h-4 rounded-full border border-slate-900 bg-slate-700 flex items-center justify-center text-[8px]">
{{ ['🛡', '🔑', ''][i-1] }}
</div>
</div>
<span class="text-[8px] text-blue-400 font-bold uppercase tracking-widest">Validated Block</span>
</div>
</div>
</div>
</div>
<div v-if="store.auditLogs.length === 0" class="flex flex-col items-center justify-center h-[400px] opacity-20">
<div class="w-16 h-16 border-4 border-blue-500/30 border-t-blue-500 rounded-full animate-spin mb-6"></div>
<p class="text-xs text-white uppercase tracking-[0.3em] font-black">Syncing Chain...</p>
</div>
<div class="mt-8 pt-6 border-t border-slate-800/80">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span class="text-[10px] text-slate-400 font-bold uppercase tracking-widest">Mainnet Live</span>
</div>
<span class="text-[10px] text-slate-600 font-mono italic">v2.0.4-stable</span>
</div>
</div>
</div>
</div>
</div>
<!-- Add Record Modal (Mock) -->
<div v-if="showAddModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div class="bg-white rounded-2xl w-full max-w-lg shadow-2xl animate-bounce-in overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
<h3 class="font-bold text-gray-800">新增护理记录</h3>
<button @click="showAddModal = false" class="text-gray-400 hover:text-gray-600"></button>
</div>
<div class="p-6 space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-1.5">老人姓名</label>
<select class="w-full bg-gray-50 border-none rounded-xl text-sm px-4 py-3">
<option>张大爷 (302)</option>
<option>李奶奶 (105)</option>
<option>王阿姨 (208)</option>
</select>
</div>
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-1.5">记录类型</label>
<select class="w-full bg-gray-50 border-none rounded-xl text-sm px-4 py-3">
<option>生活护理</option>
<option>医疗协助</option>
<option>康复训练</option>
</select>
</div>
</div>
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-1.5">记录标题</label>
<input type="text" placeholder="例如: 完成晨间洗漱" class="w-full bg-gray-50 border-none rounded-xl text-sm px-4 py-3">
</div>
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-1.5">详细内容</label>
<textarea rows="3" placeholder="请详细描述护理过程及观察到的状况..." class="w-full bg-gray-50 border-none rounded-xl text-sm px-4 py-3"></textarea>
</div>
<div class="bg-blue-50 p-3 rounded-xl border border-blue-100">
<div class="flex items-center space-x-2">
<span class="text-sm">🔗</span>
<span class="text-[10px] text-blue-700 font-medium leading-tight">
提交后将自动生成 SHA-256 存证哈希并固化至区块链记录一旦生成将永久可查且不可修改
</span>
</div>
</div>
</div>
<div class="p-6 bg-gray-50 flex space-x-3">
<button @click="showAddModal = false" class="flex-1 py-3 text-sm font-bold text-gray-500 hover:text-gray-700 transition-colors">取消</button>
<button @click="submitRecord" class="flex-1 py-3 text-sm font-bold bg-blue-600 text-white rounded-xl shadow-lg shadow-blue-100 hover:bg-blue-700 transition-all active:scale-95">
提交记录并存证
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useGlobalStore } from '../../stores/global'
const store = useGlobalStore()
const showAddModal = ref(false)
const recordStats = [
{ label: '今日新增记录', value: '42', trend: '+12%' },
{ label: '待审计记录', value: '5', trend: '-20%' },
{ label: '本月服务总时数', value: '1,280h', trend: '+5%' },
{ label: '家属满意度', value: '4.9', trend: '稳定' }
]
const records = ref([
{
id: 1,
elder: '张大爷',
room: '302室',
avatar: '👴',
type: '医疗协助',
title: '按时服用降压药',
content: '观察血压 135/85 mmHg精神状态良好无不适反应。',
time: '今天 08:15',
staff: '王护理员',
hash: '0x8f2a...9b1c'
},
{
id: 2,
elder: '李奶奶',
room: '105室',
avatar: '👵',
type: '生活护理',
title: '完成午餐进食',
content: '进食量 250g摄入水分 150ml自主进食过程顺畅。',
time: '今天 12:30',
staff: '张护理员',
hash: '0x3d4e...1f9a'
},
{
id: 3,
elder: '王阿姨',
room: '208室',
avatar: '👩‍🦳',
type: '康复训练',
title: '室内行走练习',
content: '在护理员辅助下行走 200 米,步态较稳,心率正常。',
time: '昨天 16:45',
staff: '李康复师',
hash: '0x7a8b...2c3d'
},
{
id: 4,
elder: '赵大爷',
room: '401室',
avatar: '👴',
type: '心理慰藉',
title: '情感交流与谈心',
content: '与其交流家乡趣事,老人情绪明显好转,积极参加后续活动。',
time: '昨天 10:20',
staff: '张护理员',
hash: '0x5e4d...3f2b'
}
])
const getTypeColor = (type) => {
switch (type) {
case '医疗协助': return 'bg-red-500'
case '生活护理': return 'bg-green-500'
case '康复训练': return 'bg-blue-500'
case '心理慰藉': return 'bg-purple-500'
default: return 'bg-gray-500'
}
}
const getTypeBadge = (type) => {
switch (type) {
case '医疗协助': return 'bg-red-50 text-red-600'
case '生活护理': return 'bg-green-50 text-green-600'
case '康复训练': return 'bg-blue-50 text-blue-600'
case '心理慰藉': return 'bg-purple-50 text-purple-600'
default: return 'bg-gray-50 text-gray-600'
}
}
const submitRecord = () => {
alert('记录已成功提交!\n1. 区块链存证已完成\n2. 已同步至家属端 APP\n3. 系统已自动计算本次服务积分')
showAddModal.value = false
}
</script>
<style scoped>
@keyframes fade-in-right {
0% { transform: translateX(20px); opacity: 0; }
100% { transform: translateX(0); opacity: 1; }
}
.animate-fade-in-right {
animation: fade-in-right 0.5s ease-out forwards;
}
@keyframes bounce-in {
0% { transform: scale(0.95); opacity: 0; }
70% { transform: scale(1.02); opacity: 1; }
100% { transform: scale(1); }
}
.animate-bounce-in {
animation: bounce-in 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
</style>

View File

@@ -1,116 +1,336 @@
<template>
<div class="space-y-6">
<!-- Dashboard Stats Header -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div v-for="stat in stats" :key="stat.label" class="bg-white p-4 rounded-xl border border-gray-200 shadow-sm flex items-center space-x-4">
<div class="p-3 rounded-lg" :class="stat.bgColor">
<span class="text-xl">{{ stat.icon }}</span>
</div>
<div>
<div class="text-xs text-gray-500">{{ stat.label }}</div>
<div class="text-xl font-bold text-gray-800">{{ stat.value }}</div>
</div>
</div>
</div>
<!-- Active Tasks (Kanban Style) -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- L1/L2 Routine -->
<div class="bg-gray-50 rounded-xl p-4 border border-gray-200">
<!-- Routine Tasks -->
<div class="bg-gray-50 rounded-xl p-4 border border-gray-200 flex flex-col h-[500px]">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-gray-700">常规巡护 (Routine)</h3>
<span class="bg-gray-200 text-gray-600 text-xs px-2 py-1 rounded-full font-bold">12</span>
</div>
<div class="space-y-3">
<div v-for="i in 3" :key="i" class="bg-white p-3 rounded-lg shadow-sm border border-gray-100 cursor-move hover:shadow-md transition-shadow">
<div class="flex justify-between items-start">
<span class="text-sm font-medium text-gray-800">Room 30{{i}} - 晚间巡查</span>
<div class="flex items-center space-x-2">
<span class="w-2 h-2 rounded-full bg-green-500"></span>
<h3 class="font-bold text-gray-700 font-sans">常规巡护任务</h3>
</div>
<div class="mt-2 text-xs text-gray-500 flex justify-between">
<span>Robot-0{{i}}</span>
<span>10m ago</span>
<span class="bg-gray-200 text-gray-600 text-xs px-2 py-1 rounded-full font-bold">{{ routineTasks.length }}</span>
</div>
</div>
</div>
</div>
<!-- L3 Attention Needed -->
<div class="bg-orange-50 rounded-xl p-4 border border-orange-100">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-orange-800">需关注 (Attention)</h3>
<span class="bg-orange-200 text-orange-800 text-xs px-2 py-1 rounded-full font-bold">2</span>
</div>
<div class="space-y-3">
<div class="bg-white p-3 rounded-lg shadow-sm border-l-4 border-orange-400 cursor-pointer hover:shadow-md transition-shadow">
<div class="space-y-3 overflow-y-auto flex-1 pr-1 custom-scrollbar">
<div v-for="task in routineTasks" :key="task.id"
class="bg-white p-3 rounded-lg shadow-sm border border-gray-100 hover:shadow-md transition-all group animate-fade-in">
<div class="flex justify-between items-start">
<span class="text-sm font-bold text-gray-800">Room 402 - 异常静止</span>
<span class="text-xs bg-orange-100 text-orange-600 px-1.5 py-0.5 rounded">L2</span>
<span class="text-sm font-medium text-gray-800">{{ task.room }} - {{ task.type }}</span>
<span class="text-[10px] text-green-600 bg-green-50 px-1.5 py-0.5 rounded">执行中</span>
</div>
<p class="text-xs text-gray-600 mt-2">AI 检测到老人超过 3 小时未移动请确认</p>
<div class="mt-3 flex space-x-2">
<button class="flex-1 bg-orange-100 hover:bg-orange-200 text-orange-700 text-xs py-1.5 rounded transition-colors">呼叫机器人</button>
<button class="flex-1 bg-white border border-gray-200 hover:bg-gray-50 text-gray-600 text-xs py-1.5 rounded transition-colors">人工介入</button>
<div class="mt-2 flex items-center justify-between">
<div class="flex items-center space-x-2">
<div class="w-5 h-5 rounded-full bg-gray-100 flex items-center justify-center text-[10px]">🤖</div>
<span class="text-[10px] text-gray-500">{{ task.robotId }}</span>
</div>
<span class="text-[10px] text-gray-400">{{ task.time }}</span>
</div>
</div>
<div v-if="routineTasks.length === 0" class="flex flex-col items-center justify-center h-full opacity-40 py-10">
<span class="text-3xl mb-2">🍃</span>
<span class="text-xs text-gray-500">暂无常规任务</span>
</div>
</div>
</div>
<!-- L4 Critical -->
<div class="bg-red-50 rounded-xl p-4 border border-red-100">
<!-- Attention Needed (L2/L3) -->
<div class="bg-orange-50 rounded-xl p-4 border border-orange-100 flex flex-col h-[500px]">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-red-800">紧急告警 (Critical)</h3>
<span class="bg-red-200 text-red-800 text-xs px-2 py-1 rounded-full font-bold animate-pulse">1</span>
<div class="flex items-center space-x-2">
<span class="w-2 h-2 rounded-full bg-orange-500"></span>
<h3 class="font-bold text-orange-800">需关注事项</h3>
</div>
<div class="space-y-3">
<div class="bg-white p-3 rounded-lg shadow-lg border-l-4 border-red-600 relative overflow-hidden">
<div class="absolute top-0 right-0 p-1">
<span class="bg-orange-200 text-orange-800 text-xs px-2 py-1 rounded-full font-bold">{{ attentionTasks.length }}</span>
</div>
<div class="space-y-3 overflow-y-auto flex-1 pr-1 custom-scrollbar">
<div v-for="task in attentionTasks" :key="task.id"
class="bg-white p-4 rounded-lg shadow-sm border-l-4 border-orange-400 hover:shadow-md transition-all animate-bounce-in">
<div class="flex justify-between items-start">
<span class="text-sm font-bold text-gray-800">{{ task.desc.split(' ')[0] }} - {{ task.type }}</span>
<span class="text-xs font-bold px-1.5 py-0.5 rounded"
:class="task.level === 'L3' ? 'bg-orange-600 text-white' : 'bg-orange-100 text-orange-600'">
{{ task.level }}
</span>
</div>
<p class="text-xs text-gray-600 mt-2 leading-relaxed">{{ task.desc }}</p>
<!-- Preview Image -->
<div class="mt-3 rounded-lg overflow-hidden border border-gray-100 aspect-video relative">
<img :src="task.image" crossorigin="anonymous" class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-500">
<div class="absolute inset-0 bg-orange-500/5 pointer-events-none"></div>
</div>
<div class="mt-4 flex space-x-2">
<button @click="handleAction(task, 'call')" class="flex-1 bg-orange-100 hover:bg-orange-200 text-orange-700 text-xs py-2 rounded-lg transition-colors font-medium">派遣机器人</button>
<button @click="handleAction(task, 'manual')" class="flex-1 bg-white border border-gray-200 hover:bg-gray-50 text-gray-600 text-xs py-2 rounded-lg transition-colors font-medium">人工核实</button>
</div>
</div>
<div v-if="attentionTasks.length === 0" class="flex flex-col items-center justify-center h-full opacity-40 py-10">
<span class="text-3xl mb-2"></span>
<span class="text-xs text-orange-800/60">状态良好暂无风险</span>
</div>
</div>
</div>
<!-- Critical (L4) -->
<div class="bg-red-50 rounded-xl p-4 border border-red-100 flex flex-col h-[500px]">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center space-x-2">
<span class="w-2 h-2 rounded-full bg-red-500 animate-ping"></span>
<h3 class="font-bold text-red-800">紧急医疗告警</h3>
</div>
<span class="bg-red-200 text-red-800 text-xs px-2 py-1 rounded-full font-bold" :class="{'animate-pulse': criticalTasks.length > 0}">
{{ criticalTasks.length }}
</span>
</div>
<div class="space-y-4 overflow-y-auto flex-1 pr-1 custom-scrollbar">
<div v-for="task in criticalTasks" :key="task.id"
class="bg-white p-4 rounded-xl shadow-xl border-l-4 border-red-600 relative overflow-hidden animate-pulse-fast">
<div class="absolute top-2 right-2">
<span class="flex h-3 w-3 relative">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
</span>
</div>
<div class="flex justify-between items-start pr-4">
<span class="text-sm font-bold text-gray-900">Room 101 - 跌倒告警</span>
<span class="text-xs bg-red-100 text-red-600 px-1.5 py-0.5 rounded font-bold">L4</span>
<div class="flex justify-between items-start pr-6">
<span class="text-sm font-black text-gray-900">{{ task.desc.split(' ')[0] }} - {{ task.type }}</span>
<span class="text-xs bg-red-600 text-white px-2 py-0.5 rounded-full font-bold">紧急处置</span>
</div>
<div class="mt-2 bg-gray-50 p-2 rounded border border-gray-100">
<div class="flex items-center space-x-2 text-xs text-gray-500">
<span>🔗 证据已上链</span>
<span class="font-mono text-gray-400">0x8a...9c</span>
<div class="mt-3 rounded-lg overflow-hidden border-2 border-red-100 aspect-video relative">
<img :src="task.image" crossorigin="anonymous" class="w-full h-full object-cover">
<div class="absolute top-2 left-2 px-2 py-0.5 bg-red-600 text-white text-[10px] rounded animate-pulse">实时画面快照</div>
</div>
<div class="mt-3 bg-gray-50 p-2.5 rounded-lg border border-gray-100">
<div class="flex items-center justify-between text-[10px]">
<div class="flex items-center space-x-1 text-blue-600 font-bold">
<span>🔗 存证哈希</span>
</div>
<span class="font-mono text-gray-400">0x{{ Math.random().toString(16).slice(2, 10) }}...</span>
</div>
</div>
<button class="w-full mt-3 bg-red-600 hover:bg-red-700 text-white text-xs font-bold py-2 rounded shadow-sm transition-colors flex items-center justify-center">
<span class="mr-1"></span> 立即响应 (Sign to Confirm)
<button @click="resolveCritical(task.id)"
class="w-full mt-4 bg-red-600 hover:bg-red-700 text-white text-sm font-bold py-3 rounded-lg shadow-lg shadow-red-200 transition-all flex items-center justify-center active:scale-95">
<span class="mr-2"></span> 立即处理并区块链存证
</button>
</div>
<div v-if="criticalTasks.length === 0" class="flex flex-col items-center justify-center h-full opacity-30 py-10">
<span class="text-4xl mb-2">🛡</span>
<span class="text-xs text-gray-500 font-bold uppercase tracking-widest">环境安全</span>
</div>
</div>
</div>
</div>
<!-- Agency Floor Plan & Fleet Control -->
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div class="lg:col-span-3 bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="flex justify-between items-center mb-6">
<div>
<h3 class="font-bold text-gray-800">机构楼层态势图</h3>
<p class="text-xs text-gray-400 mt-1">2F 康复护理区 - 机器人实时点位</p>
</div>
<div class="flex space-x-2">
<button class="p-2 hover:bg-gray-100 rounded-lg transition-colors">
<span class="text-gray-500 text-sm">层级: 2F</span>
</button>
</div>
</div>
<div class="h-80 bg-slate-50 rounded-2xl relative overflow-hidden border border-slate-100 group">
<!-- Mock Floor Plan SVG -->
<svg viewBox="0 0 800 400" class="w-full h-full opacity-20 text-slate-400 fill-current p-4">
<rect x="50" y="50" width="700" height="300" rx="10" fill="none" stroke="currentColor" stroke-width="2" />
<line x1="250" y1="50" x2="250" y2="350" stroke="currentColor" stroke-width="2" />
<line x1="550" y1="50" x2="550" y2="350" stroke="currentColor" stroke-width="2" />
<line x1="50" y1="200" x2="750" y2="200" stroke="currentColor" stroke-width="2" />
<text x="100" y="100" class="text-20 font-bold opacity-20">A区</text>
<text x="400" y="100" class="text-20 font-bold opacity-20">B区</text>
<text x="650" y="100" class="text-20 font-bold opacity-20">C区</text>
</svg>
<!-- Real-time Robot Indicators -->
<div v-for="robot in robots" :key="robot.id"
class="absolute w-8 h-8 -translate-x-1/2 -translate-y-1/2 transition-all duration-1000 cursor-pointer"
:style="{ left: robot.x + '%', top: robot.y + '%' }">
<div class="relative group/robot">
<!-- Pulse effect for busy robots -->
<div v-if="robot.status !== 'idle'" class="absolute inset-0 rounded-full animate-ping opacity-20"
:class="robot.status === 'alert' ? 'bg-red-500' : 'bg-orange-500'"></div>
<!-- Robot Icon/Dot -->
<div class="relative w-8 h-8 bg-white rounded-full shadow-lg border-2 flex items-center justify-center transition-transform group-hover/robot:scale-125"
:class="robot.status === 'alert' ? 'border-red-500' : (robot.status === 'busy' ? 'border-orange-500' : 'border-green-500')">
<span class="text-sm">{{ robot.status === 'alert' ? '🚨' : '🤖' }}</span>
</div>
<!-- Tooltip -->
<div class="absolute bottom-10 left-1/2 -translate-x-1/2 whitespace-nowrap bg-gray-900 text-white text-[10px] px-2 py-1 rounded opacity-0 group-hover/robot:opacity-100 transition-opacity z-10">
{{ robot.name }} ({{ robot.battery }}%)
</div>
</div>
</div>
</div>
</div>
<!-- Robot Fleet Map (Simplified) -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 class="font-bold text-gray-800 mb-4">机器人部署概览</h3>
<div class="h-64 bg-gray-100 rounded-lg relative overflow-hidden flex items-center justify-center">
<div class="absolute inset-0 grid grid-cols-6 grid-rows-4 gap-1 p-4 opacity-30">
<div v-for="n in 24" :key="n" class="border border-gray-300 rounded"></div>
<!-- Fleet Overview Panel -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 flex flex-col">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-gray-800 text-sm">机器人编队状态</h3>
<span class="text-[10px] bg-green-50 text-green-600 px-2 py-0.5 rounded-full font-bold">全系统在线</span>
</div>
<div class="text-gray-400 text-sm">Interactive Map Placeholder</div>
<!-- Mock Robot Dots -->
<div class="absolute top-1/4 left-1/4 w-4 h-4 bg-green-500 rounded-full border-2 border-white shadow-md" title="Robot-01"></div>
<div class="absolute top-1/2 left-1/2 w-4 h-4 bg-orange-500 rounded-full border-2 border-white shadow-md animate-bounce" title="Robot-05 (Busy)"></div>
<div class="absolute bottom-1/4 right-1/4 w-4 h-4 bg-green-500 rounded-full border-2 border-white shadow-md" title="Robot-03"></div>
<div class="flex-1 space-y-4 overflow-y-auto pr-1 custom-scrollbar">
<div v-for="robot in robots" :key="robot.id" class="flex items-center justify-between p-3 rounded-xl border border-gray-50 hover:bg-gray-50 transition-colors group">
<div class="flex items-center space-x-3">
<div class="w-2 h-2 rounded-full transition-all duration-500"
:class="robot.status === 'idle' ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]' : (robot.status === 'alert' ? 'bg-red-500 animate-pulse shadow-[0_0_8px_rgba(239,68,68,0.5)]' : 'bg-orange-500 shadow-[0_0_8px_rgba(249,115,22,0.5)]')"></div>
<div>
<div class="text-xs font-bold text-gray-700">{{ robot.name }}</div>
<div class="text-[10px] text-gray-400">{{ robot.area }}</div>
</div>
</div>
<div class="text-right">
<div class="text-[10px] font-mono flex items-center justify-end" :class="robot.battery < 20 ? 'text-red-500 font-bold' : 'text-gray-500'">
<span class="mr-1">{{ robot.battery }}%</span>
<span class="text-[8px] opacity-50">{{ robot.battery < 20 ? '🪫' : '🔋' }}</span>
</div>
<div class="text-[9px] font-bold uppercase" :class="robot.status === 'idle' ? 'text-green-600' : (robot.status === 'alert' ? 'text-red-600' : 'text-orange-600')">
{{ robot.status === 'idle' ? '待命' : (robot.status === 'alert' ? '紧急响应' : '任务中') }}
</div>
</div>
</div>
</div>
<button class="mt-6 w-full py-2.5 bg-gray-900 text-white text-xs font-bold rounded-xl hover:bg-gray-800 transition-all active:scale-[0.98] shadow-lg shadow-gray-200">
进入编队管理中心
</button>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useGlobalStore } from '../../stores/global'
const store = useGlobalStore()
// Mock Routine Tasks
const routineTasks = ref([
{ id: 101, room: '205室', type: '午后巡查', robotId: '护工机器人-02', time: '10分钟前' },
{ id: 102, room: '212室', type: '生命体征采集', robotId: '护工机器人-04', time: '5分钟前' },
{ id: 103, room: '301室', type: '服药提醒', robotId: '护工机器人-01', time: '刚刚' },
])
// Computed tasks from store
const attentionTasks = computed(() => store.alerts.filter(a => a.level === 'L2' || a.level === 'L3'))
const criticalTasks = computed(() => store.alerts.filter(a => a.level === 'L4'))
// Dashboard Stats
const stats = computed(() => [
{ label: '在线机器人', value: '18/20', icon: '🤖', bgColor: 'bg-blue-50 text-blue-600' },
{ label: '今日巡护人次', value: '142', icon: '🛡️', bgColor: 'bg-green-50 text-green-600' },
{ label: '活跃待办事项', value: store.alerts.length, icon: '🚨', bgColor: 'bg-red-50 text-red-600' },
{ label: '平均响应时间', value: '1.2s', icon: '⚡', bgColor: 'bg-orange-50 text-orange-600' },
])
// Mock Robots Fleet
const robots = ref([
{ id: 1, name: '护工机器人-01', x: 20, y: 30, status: 'idle', battery: 85, area: 'A区 康复室' },
{ id: 2, name: '护工机器人-02', x: 45, y: 70, status: 'busy', battery: 42, area: 'B区 走廊' },
{ id: 3, name: '护工机器人-03', x: 80, y: 25, status: 'idle', battery: 98, area: 'C区 护士站' },
{ id: 4, name: '护工机器人-04', x: 15, y: 80, status: 'busy', battery: 15, area: 'A区 休息室' },
{ id: 5, name: '护工机器人-05', x: 60, y: 50, status: 'alert', battery: 67, area: 'B区 402室' },
])
// Actions
const resolveCritical = (id) => {
if (confirm('确认执行响应?操作将被记录在区块链审计日志中。')) {
store.resolveAlert(id)
// Find robot assigned to this alert if any and free it
const robot = robots.value.find(r => r.targetId === id)
if (robot) {
robot.status = 'idle'
robot.targetId = null
}
}
}
const handleAction = (task, type) => {
if (type === 'call') {
// Find an idle robot
const idleRobot = robots.value.find(r => r.status === 'idle')
if (idleRobot) {
idleRobot.status = 'busy'
idleRobot.targetId = task.id
alert(`机器人 ${idleRobot.name} 已派遣至 ${task.desc.split(' ')[0]}`)
} else {
alert('当前没有空闲机器人,请手动核实或等待机器人完成任务。')
}
} else {
alert(`人工核实请求已发送,请前往 ${task.desc.split(' ')[0]} 查看。`)
}
}
// Simple robot movement simulation
onMounted(() => {
setInterval(() => {
robots.value.forEach(r => {
if (r.status === 'idle') {
// Random patrol
r.x += (Math.random() - 0.5) * 2
r.y += (Math.random() - 0.5) * 2
} else if (r.status === 'busy' || r.status === 'alert') {
// Move towards center of its area or just wiggle less
r.x += (Math.random() - 0.5) * 0.5
r.y += (Math.random() - 0.5) * 0.5
}
// Boundary checks
r.x = Math.max(10, Math.min(90, r.x))
r.y = Math.max(10, Math.min(90, r.y))
// Battery drain simulation
if (Math.random() > 0.95) {
r.battery = Math.max(0, r.battery - 1)
}
})
}, 2000)
})
</script>
<style scoped>
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #e2e8f0;
border-radius: 4px;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.3s ease-out;
animation: fadeIn 0.4s ease-out;
}
@keyframes bounceIn {
@@ -119,6 +339,14 @@ const criticalTasks = computed(() => store.alerts.filter(a => a.level === 'L4'))
100% { transform: scale(1); }
}
.animate-bounce-in {
animation: bounceIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
animation: bounceIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes pulse-fast {
0%, 100% { transform: scale(1); border-color: rgb(239 68 68 / 0.5); }
50% { transform: scale(1.01); border-color: rgb(239 68 68 / 1); }
}
.animate-pulse-fast {
animation: pulse-fast 1s ease-in-out infinite;
}
</style>

View File

@@ -4,39 +4,213 @@
<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-red-600/80 text-white text-xs font-bold rounded animate-pulse">录制中</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
帧率: {{ fps }} | 延迟: {{ latency }}ms
</div>
<div class="px-2 py-1 bg-blue-600/80 text-white text-xs font-bold rounded">
当前场景: {{ scenes[currentScene].name }}
</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 class="w-full h-full flex items-center justify-center relative overflow-hidden group transition-colors duration-1000"
:class="scenes[currentScene].color">
<!-- Scene Selector -->
<div class="absolute top-16 left-4 z-10 flex flex-col space-y-2">
<button v-for="(scene, key) in scenes" :key="key"
@click="currentScene = key"
class="px-3 py-1.5 text-xs border rounded-sm backdrop-blur-md transition-all duration-300 flex items-center space-x-2"
:class="currentScene === key ? 'bg-green-500 text-black border-green-500 shadow-[0_0_10px_rgba(34,197,94,0.5)]' : 'bg-black/50 text-green-500/50 border-green-500/20 hover:border-green-500/50'">
<span>{{ scene.icon }}</span>
<span>{{ scene.name }}</span>
</button>
</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>
<!-- SVG Filters for Edge Detection Effect -->
<svg class="absolute w-0 h-0">
<filter id="edge-detection">
<feConvolveMatrix kernelMatrix="-1 -1 -1 -1 8 -1 -1 -1 -1" />
</filter>
</svg>
<!-- Grid Background -->
<div class="absolute inset-0 opacity-10"
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: 80px 80px;">
</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>
<!-- Scene Elements (Simplified Props) -->
<div v-for="(el, i) in scenes[currentScene].elements" :key="i"
class="absolute border border-white/10 bg-white/5 rounded-lg backdrop-blur-sm group/el transition-all duration-700 hover:bg-white/10"
:style="{ left: el.x + '%', top: el.y + '%', width: (120 * el.scale) + 'px', height: (80 * el.scale) + 'px', transform: 'perspective(500px) rotateX(15deg)' }">
<div class="absolute inset-0 flex flex-col items-center justify-center font-mono italic">
<span class="text-2xl mb-1 opacity-50 group-hover/el:opacity-100 transition-opacity">{{ el.icon }}</span>
<span class="text-[10px] text-white/40 group-hover/el:text-white/80">{{ el.type }}</span>
</div>
</div>
<!-- Target Tracking Simulation (Multiple Boxes) -->
<div v-for="target in targets" :key="target.id"
class="absolute border border-green-500/30 transition-all duration-1000"
:style="{ left: target.x + '%', top: target.y + '%', width: '120px', height: '180px', transform: 'translate(-50%, -50%)', opacity: target.active ? 0.3 : 0.1 }">
<div class="absolute -top-4 left-0 text-[8px] font-mono text-green-500/50">{{ target.id }}</div>
<div class="absolute top-0 left-0 w-2 h-2 border-t border-l border-green-500"></div>
<div class="absolute top-0 right-0 w-2 h-2 border-t border-r border-green-500"></div>
<div class="absolute bottom-0 left-0 w-2 h-2 border-b border-l border-green-500"></div>
<div class="absolute bottom-0 right-0 w-2 h-2 border-b border-r border-green-500"></div>
</div>
<!-- Radar Scanning UI -->
<div class="absolute top-4 right-4 w-32 h-32 border border-green-500/20 rounded-full overflow-hidden hidden md:block">
<div class="absolute inset-0 border border-green-500/10 rounded-full scale-75"></div>
<div class="absolute inset-0 border border-green-500/10 rounded-full scale-50"></div>
<div class="absolute top-1/2 left-0 w-full h-px bg-green-500/20"></div>
<div class="absolute left-1/2 top-0 w-px h-full bg-green-500/20"></div>
<div class="absolute inset-0 origin-center animate-radar-spin bg-gradient-to-tr from-green-500/20 to-transparent"></div>
<div class="absolute top-1/4 left-1/3 w-1.5 h-1.5 bg-green-400 rounded-full shadow-[0_0_8px_rgba(74,222,128,1)] animate-pulse"></div>
</div>
<!-- System Status Floating Panel -->
<div class="absolute top-4 right-40 hidden lg:flex flex-col space-y-2 bg-black/40 backdrop-blur-md p-3 border border-white/10 rounded font-mono text-[10px]">
<div class="text-white/60 mb-1 border-b border-white/10 pb-1 flex justify-between">
<span>系统负载</span>
<span class="text-green-500">正常</span>
</div>
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
<div class="flex items-center justify-between space-x-2">
<span class="text-white/40">CPU:</span>
<span class="text-white/80">{{ systemStats.cpu }}%</span>
</div>
<div class="flex items-center justify-between space-x-2">
<span class="text-white/40">内存:</span>
<span class="text-white/80">{{ systemStats.mem }}%</span>
</div>
<div class="flex items-center justify-between space-x-2">
<span class="text-white/40">温度:</span>
<span class="text-white/80">{{ systemStats.temp }}°C</span>
</div>
<div class="flex items-center justify-between space-x-2">
<span class="text-white/40">电量:</span>
<span class="text-white/80">{{ systemStats.battery }}%</span>
</div>
</div>
</div>
<!-- Environmental Sensors (Floating) -->
<div class="absolute bottom-4 right-4 space-y-3 font-mono text-[10px] bg-black/40 backdrop-blur-md p-4 border border-white/10 rounded-lg">
<div class="text-white/60 mb-2 border-b border-white/10 pb-1 text-center">多维传感器数据</div>
<div class="space-y-2 min-w-[140px]">
<div class="flex items-center justify-between space-x-3">
<span class="text-green-400/70">环境温度:</span>
<div class="flex items-center space-x-2">
<span class="text-white">24.5°C</span>
<div class="w-12 h-1 bg-gray-800 rounded-full overflow-hidden">
<div class="w-3/5 h-full bg-green-500"></div>
</div>
</div>
</div>
<div class="flex items-center justify-between space-x-3">
<span class="text-blue-400/70">空气湿度:</span>
<div class="flex items-center space-x-2">
<span class="text-white">45%</span>
<div class="w-12 h-1 bg-gray-800 rounded-full overflow-hidden">
<div class="w-2/5 h-full bg-blue-500"></div>
</div>
</div>
</div>
<div class="flex items-center justify-between space-x-3">
<span class="text-yellow-400/70">噪音水平:</span>
<div class="flex items-center space-x-2">
<span class="text-white">-42dB</span>
<div class="w-12 h-1 bg-gray-800 rounded-full overflow-hidden">
<div class="h-full bg-yellow-500" :style="{width: currentLevel === 'L3' ? '90%' : '30%'}"></div>
</div>
</div>
</div>
<div class="flex items-center justify-between space-x-3">
<span class="text-orange-400/70">CO2浓度:</span>
<div class="flex items-center space-x-2">
<span class="text-white">{{ environmentDetails.co2 }}ppm</span>
<div class="w-12 h-1 bg-gray-800 rounded-full overflow-hidden">
<div class="w-4/5 h-full bg-orange-500"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Skeleton / Person Placeholder (SVG Based for better animation) -->
<div class="relative transition-all duration-1000 ease-in-out"
:class="{
'transform rotate-[85deg] translate-y-24 translate-x-4': currentLevel === 'L4',
'translate-y-12': currentLevel === 'L2'
}">
<!-- Virtual Chair for L2 -->
<svg v-if="currentLevel === 'L2'" width="120" height="120" viewBox="0 0 120 120" class="absolute top-24 left-0 opacity-40">
<path d="M30,80 L90,80 M40,80 L40,110 M80,80 L80,110 M30,80 L30,40" stroke="currentColor" stroke-width="2" fill="none" class="text-gray-500" />
</svg>
<svg width="120" height="240" viewBox="0 0 120 240" class="overflow-visible">
<!-- Glow Effect -->
<defs>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<g :class="statusTextClass" filter="url(#glow)">
<!-- Head with Breathing and L2 Tilt -->
<g class="transition-all duration-1000"
:style="{
transform: currentLevel === 'L2' ? 'translate(60px, 40px) rotate(45deg) translate(-60px, -40px)' : '',
animation: currentLevel === 'L1' ? 'breathing 3s ease-in-out infinite' : ''
}">
<circle cx="60" cy="40" r="18" fill="none" stroke="currentColor" stroke-width="2" class="animate-pulse" />
<text x="60" y="42" text-anchor="middle" font-size="8" fill="currentColor" opacity="0.6" font-family="monospace">
{{ confidence }}%
</text>
</g>
<!-- Neck -->
<line x1="60" y1="58" x2="60" y2="65" stroke="currentColor" stroke-width="2" />
<!-- Torso / Spine (Breathing) -->
<path :d="currentLevel === 'L2' ? 'M60,65 Q70,90 60,110' : 'M60,65 Q65,100 60,130'"
fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"
class="transition-all duration-1000"
:style="{ animation: currentLevel === 'L1' ? 'breathing-torso 3s ease-in-out infinite' : '' }" />
<!-- Shoulders & Arms -->
<g class="transition-transform duration-1000"
:style="currentLevel === 'L3' ? 'transform: translateY(-5px)' : (currentLevel === 'L2' ? 'transform: translateY(5px)' : '')">
<line x1="30" y1="75" x2="90" y2="75" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<!-- Left Arm -->
<path :d="currentLevel === 'L3' ? 'M30,75 L15,45' : (currentLevel === 'L2' ? 'M30,75 L25,105 L45,100' : 'M30,75 L20,115')"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="transition-all duration-1000" />
<!-- Right Arm -->
<path :d="currentLevel === 'L3' ? 'M90,75 L105,45' : (currentLevel === 'L2' ? 'M90,75 L95,105 L75,100' : 'M90,75 L100,115')"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="transition-all duration-1000" />
</g>
<!-- Hips & Legs (Sitting for L2) -->
<g class="transition-all duration-1000">
<line :x1="currentLevel === 'L2' ? 45 : 45" y1="130" :x2="currentLevel === 'L2' ? 75 : 75" y2="130" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<!-- Left Leg -->
<path :d="currentLevel === 'L4' ? 'M45,130 L30,170 L50,210' : (currentLevel === 'L2' ? 'M45,110 L25,115 L20,160' : 'M45,130 L40,180 L45,230')"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="transition-all duration-1000" />
<!-- Right Leg -->
<path :d="currentLevel === 'L4' ? 'M75,130 L95,175 L85,215' : (currentLevel === 'L2' ? 'M75,110 L95,115 L100,160' : 'M75,130 L80,180 L75,230')"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="transition-all duration-1000" />
</g>
</g>
</svg>
<!-- 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">
<div class="absolute -inset-10 border border-dashed opacity-40 animate-pulse transition-all duration-500"
:class="statusBorderClass">
<div class="absolute -top-6 left-0 text-[10px] font-mono whitespace-nowrap" :class="statusTextClass">
ID: PERSON_01 [{{ currentLevel }}]
</div>
</div>
@@ -48,9 +222,13 @@
<!-- 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 class="flex items-center space-x-2 text-white/40 mb-2 border-b border-white/5 pb-1">
<span class="animate-pulse"></span>
<span>系统实时审计日志 (SYSTEM AUDIT LOG)</span>
</div>
<div v-for="(log, i) in logs" :key="i" class="mb-1 flex items-start space-x-2">
<span class="text-gray-500 shrink-0">[{{ log.time }}]</span>
<span :class="log.color" class="break-all">{{ log.message }}</span>
</div>
</div>
</div>
@@ -82,19 +260,22 @@
<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>
<div class="w-2 h-2 rounded-full" :class="inferenceStats.yolo.color"></div>
<span class="text-sm text-gray-300">目标检测 (YOLOv8)</span>
<span class="ml-auto text-xs text-green-400">Done</span>
<span class="ml-auto text-xs font-mono text-gray-500">{{ inferenceStats.yolo.time }}ms</span>
<span class="ml-2 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>
<div class="w-2 h-2 rounded-full" :class="inferenceStats.pose.color"></div>
<span class="text-sm text-gray-300">姿态分析 (PosePipe)</span>
<span class="ml-auto text-xs" :class="poseTextColor">{{ poseStatus }}</span>
<span class="ml-auto text-xs font-mono text-gray-500">{{ inferenceStats.pose.time }}ms</span>
<span class="ml-2 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="riskColor"></div>
<div class="w-2 h-2 rounded-full" :class="inferenceStats.risk.color"></div>
<span class="text-sm text-gray-300">风险评估 (RiskEval)</span>
<span class="ml-auto text-xs" :class="riskTextColor">{{ riskScore }}</span>
<span class="ml-auto text-xs font-mono text-gray-500">{{ inferenceStats.risk.time }}ms</span>
<span class="ml-2 text-xs text-green-400">Done</span>
</div>
</div>
</div>
@@ -122,15 +303,15 @@
<!-- 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="text-xs text-gray-400">TrustLink 状态:</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>
<span class="text-xs text-blue-400 font-mono font-bold uppercase">已连接</span>
</div>
<div class="text-xs font-mono text-gray-500">
Last Hash: {{ lastHash }}
上链哈希: {{ lastHash }}
</div>
</div>
</div>
@@ -138,11 +319,13 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useDateFormat, useNow } from '@vueuse/core'
import { useGlobalStore } from '../../stores/global'
import { useBlockchainStore } from '../../stores/blockchain'
const store = useGlobalStore()
const blockchainStore = useBlockchainStore()
// State
const currentLevel = computed(() => store.robotStatus.level)
@@ -151,10 +334,101 @@ const latency = ref(12)
const logs = ref([])
const confidence = ref(98)
// Inference Stats
const inferenceStats = ref({
yolo: { time: 4.2, status: 'Done', color: 'bg-green-500' },
pose: { time: 8.5, status: 'Done', color: 'bg-green-500' },
risk: { time: 2.1, status: 'Done', color: 'bg-green-500' }
})
// Targets for tracking simulation
const targets = ref([
{ id: 'PERSON_01', x: 50, y: 50, active: true },
{ id: 'OBJECT_04', x: 20, y: 30, active: false },
{ id: 'OBJECT_09', x: 80, y: 70, active: false }
])
const currentScene = ref('hospital') // 'hospital', 'office', 'warehouse'
const scenes = {
hospital: {
name: '医院走廊',
color: 'from-blue-900/30 to-slate-900/50',
elements: [
{ type: '智能病床', x: 15, y: 65, scale: 1.2, icon: '🏥' },
{ type: '输液架', x: 82, y: 45, scale: 0.8, icon: '🧪' },
{ type: '消毒机', x: 5, y: 40, scale: 0.7, icon: '🛡️' },
{ type: '导视牌', x: 90, y: 20, scale: 0.6, icon: '🪧' }
]
},
office: {
name: '现代办公室',
color: 'from-emerald-900/20 to-stone-900/40',
elements: [
{ type: '升降桌', x: 75, y: 60, scale: 1.1, icon: '🖥️' },
{ type: '人体工学椅', x: 68, y: 65, scale: 0.9, icon: '💺' },
{ type: '绿植', x: 10, y: 70, scale: 1.0, icon: '🌿' },
{ type: '饮水机', x: 92, y: 50, scale: 0.8, icon: '💧' }
]
},
warehouse: {
name: '自动化仓库',
color: 'from-orange-900/20 to-zinc-900/50',
elements: [
{ type: '重型货架', x: 85, y: 35, scale: 1.4, icon: '📦' },
{ type: '自动分拣机', x: 10, y: 75, scale: 1.3, icon: '🤖' },
{ type: '搬运托盘', x: 40, y: 85, scale: 0.9, icon: '🛒' },
{ type: '监控立柱', x: 5, y: 25, scale: 1.1, icon: '📹' }
]
}
}
// 机器人系统状态
const systemStats = ref({
cpu: 42,
mem: 65,
temp: 38,
battery: 85,
wifi: -45
})
// 环境详细传感器
const environmentDetails = ref({
co2: 420,
oxygen: 20.9,
voc: 0.12,
pressure: 101.3
})
// Mock Data Generators
const lastHash = ref(store.robotStatus.hash || 'Waiting...')
// ... (Computed Styles & Status - No changes needed as they depend on currentLevel)
// Computed Styles & Status
const statusColorClass = computed(() => {
const map = { L1: 'border-green-500', L2: 'border-yellow-500', L3: 'border-orange-500', L4: 'border-red-500' }
return map[currentLevel.value] || 'border-green-500'
})
const statusBgClass = computed(() => {
const map = { L1: 'bg-green-500', L2: 'bg-yellow-500', L3: 'bg-orange-500', L4: 'bg-red-500' }
return map[currentLevel.value] || 'bg-green-500'
})
const statusBorderClass = computed(() => {
const map = { L1: 'border-green-500/50', L2: 'border-yellow-500/50', L3: 'border-orange-500/50', L4: 'border-red-500/50' }
return map[currentLevel.value] || 'border-green-500/50'
})
const statusTextClass = computed(() => {
const map = { L1: 'text-green-500', L2: 'text-yellow-500', L3: 'text-orange-500', L4: 'text-red-500' }
return map[currentLevel.value] || 'text-green-500'
})
const levelBorderClass = computed(() => {
const map = { L1: 'border-green-500/20 bg-green-500/5', L2: 'border-yellow-500/20 bg-yellow-500/5', L3: 'border-orange-500/20 bg-orange-500/5', L4: 'border-red-500/20 bg-red-500/5' }
return map[currentLevel.value] || 'border-green-500/20 bg-green-500/5'
})
const currentStatusText = computed(() => {
const map = { L1: '巡视中 - 环境安全', L2: '观察中 - 行为异常', L3: '警告 - 检测到求救', L4: '紧急 - 跌倒告警' }
return map[currentLevel.value] || '系统就绪'
})
// Methods
const addLog = (msg, level = 'info') => {
@@ -168,48 +442,96 @@ const setLevel = (level) => {
// Update Global Store
if (level === 'L1') {
store.triggerAlert('L1', '系统恢复正常')
addLog('System state normalized.', 'success')
addLog('系统状态已恢复正常,继续执行预定巡护任务。', 'success')
} else if (level === 'L2') {
store.triggerAlert('L2', '检测到长时间静止')
addLog('Anomaly detected: No motion for 180s.', 'warning')
addLog('Initiating approach sequence...', 'info')
addLog('异常警告:检测到目标在当前区域静止超过180秒。', 'warning')
} else if (level === 'L3') {
store.triggerAlert('L3', '检测到呼救声')
addLog('Audio event detected: "Help"', 'warning')
addLog('Risk score escalated to 0.78', 'warning')
addLog('语音事件:实时捕捉到疑似“救命”或“帮帮我”的音频特征。', 'warning')
} else if (level === 'L4') {
store.triggerAlert('L4', '检测到跌倒事件')
addLog('CRITICAL EVENT: FALL DETECTED', 'error')
addLog('Generating privacy-preserving hash...', 'info')
addLog('紧急:视觉算法确认目标发生跌倒行为,立即启动应急响应。', 'error')
// Blockchain Linkage
const txHash = '0x' + Math.random().toString(16).slice(2, 10) + '...' + Math.random().toString(16).slice(2, 6)
blockchainStore.transactions.unshift({
id: txHash,
type: '告警',
from: 'Robot_01',
to: '应急响应中心',
amount: '0.00',
status: '已确认',
time: '刚刚'
})
addLog('正在生成隐私保护哈希存证...', 'info')
setTimeout(() => {
lastHash.value = store.robotStatus.hash
addLog(`Evidence committed to blockchain: ${lastHash.value}`, 'success')
addLog('Emergency contact (120) notified via API', 'error')
addLog(`存证已上链:${lastHash.value}`, 'success')
addLog('已通过 API 通知紧急联系人及医疗中心。', 'error')
}, 800)
}
}
// Loop for background logs
// Simulation Loops
let interval
let confidenceInterval
onMounted(() => {
addLog('System initialized. Model: YOLOv8-Nano loaded.', 'success')
addLog('Privacy Engine: Active (Local Only).', 'success')
addLog('系统初始化完成。视觉模型:YOLOv8-Nano 已加载。', 'success')
addLog('隐私保护引擎:运行中(本地处理模式)。', 'success')
interval = setInterval(() => {
fps.value = Math.floor(Math.random() * 5) + 28
latency.value = Math.floor(Math.random() * 5) + 10
// Randomize system stats
systemStats.value.cpu = 35 + Math.floor(Math.random() * 15)
systemStats.value.mem = 60 + Math.floor(Math.random() * 10)
systemStats.value.temp = 36 + Math.floor(Math.random() * 5)
// Randomize inference times
inferenceStats.value.yolo.time = (Math.random() * 2 + 3).toFixed(1)
inferenceStats.value.pose.time = (Math.random() * 5 + 6).toFixed(1)
inferenceStats.value.risk.time = (Math.random() * 1 + 1.5).toFixed(1)
// Move targets slightly
targets.value.forEach(t => {
t.x += (Math.random() - 0.5) * 2
t.y += (Math.random() - 0.5) * 2
// Keep within bounds
t.x = Math.max(10, Math.min(90, t.x))
t.y = Math.max(10, Math.min(90, t.y))
})
if (currentLevel.value === 'L1' && Math.random() > 0.8) {
addLog('Routine scan complete. No objects found.', 'info')
addLog('常规扫描完成。未发现异常目标。', 'info')
}
}, 2000)
confidenceInterval = setInterval(() => {
const base = currentLevel.value === 'L1' ? 98 : (currentLevel.value === 'L4' ? 99 : 85)
confidence.value = base + Math.floor(Math.random() * 3) - 1
}, 500)
})
onUnmounted(() => {
clearInterval(interval)
clearInterval(confidenceInterval)
})
</script>
<style scoped>
@keyframes breathing {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
@keyframes breathing-torso {
0%, 100% { transform: scaleY(1); }
50% { transform: scaleY(1.02); }
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}