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

@@ -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>