重构项目中的"时间银行"模块,统一更名为"互助中心",涉及前端路由、组件、文档及多处文本替换。新增互助中心页面功能,包括: 1. 通证兑换服务弹窗 2. 互助任务列表展示 3. 区块链存证交互流程 同时优化护理记录页面,增加筛选、导出功能,完善监管端仪表盘交互
411 lines
18 KiB
Vue
411 lines
18 KiB
Vue
<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" v-model="searchQuery" 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 transition-all">
|
||
</div>
|
||
<select v-model="filterType" class="bg-gray-50 border-none rounded-lg text-sm px-4 py-2 focus:ring-2 focus:ring-blue-500 transition-all cursor-pointer">
|
||
<option value="all">全部记录类型</option>
|
||
<option value="生活护理">生活护理</option>
|
||
<option value="医疗协助">医疗协助</option>
|
||
<option value="心理慰藉">心理慰藉</option>
|
||
<option value="日常巡查">日常巡查</option>
|
||
</select>
|
||
<button @click="exportRecords" class="px-4 py-2 bg-gray-50 hover:bg-gray-100 text-gray-600 rounded-lg text-sm font-bold transition-colors border border-gray-100 flex items-center">
|
||
<span class="mr-2">📥</span> 导出数据
|
||
</button>
|
||
</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> 日常护理日志
|
||
<span class="ml-3 text-xs bg-gray-100 text-gray-500 px-2 py-1 rounded-full">{{ filteredRecords.length }} 条记录</span>
|
||
</h3>
|
||
<TransitionGroup name="list" tag="div" class="space-y-6">
|
||
<div v-for="record in filteredRecords" :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>
|
||
</TransitionGroup>
|
||
</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 v-model="newRecord.elder" class="w-full bg-gray-50 border-none rounded-xl text-sm px-4 py-3">
|
||
<option value="张大爷 (302室)">张大爷 (302室)</option>
|
||
<option value="李奶奶 (105室)">李奶奶 (105室)</option>
|
||
<option value="王阿姨 (208室)">王阿姨 (208室)</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-bold text-gray-400 uppercase mb-1.5">记录类型</label>
|
||
<select v-model="newRecord.type" 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" v-model="newRecord.title" 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 v-model="newRecord.content" 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, computed } from 'vue'
|
||
import { useGlobalStore } from '../../stores/global'
|
||
|
||
const store = useGlobalStore()
|
||
const showAddModal = ref(false)
|
||
const searchQuery = ref('')
|
||
const filterType = ref('all')
|
||
|
||
const recordStats = ref([
|
||
{ label: '今日记录总数', value: '42', trend: '+12%', trendUp: true },
|
||
{ label: '异常护理项', value: '3', trend: '-1', trendUp: false },
|
||
{ label: '区块链确认', value: '100%', trend: '稳定', trendUp: true },
|
||
{ label: '服务满意度', value: '4.9', trend: '+0.1', trendUp: true },
|
||
])
|
||
|
||
const records = ref([
|
||
{
|
||
id: 1,
|
||
elder: '王建国',
|
||
room: '201室',
|
||
avatar: '👴',
|
||
type: '生活护理',
|
||
title: '完成晨间助浴与更衣',
|
||
content: '老人精神状态良好,配合度高。助浴过程中检查皮肤无异常,已更换干净衣物。',
|
||
time: '08:30',
|
||
hash: '0x7a8...9b2'
|
||
},
|
||
{
|
||
id: 2,
|
||
elder: '李秀兰',
|
||
room: '203室',
|
||
avatar: '👵',
|
||
type: '医疗协助',
|
||
title: '协助服用降压药',
|
||
content: '血压测量结果 135/85,遵医嘱协助服用降压药一粒。观察15分钟无不良反应。',
|
||
time: '09:15',
|
||
hash: '0x3c4...1f9'
|
||
},
|
||
{
|
||
id: 3,
|
||
elder: '张德福',
|
||
room: '205室',
|
||
avatar: '<27>',
|
||
type: '日常巡查',
|
||
title: '房间环境安全检查',
|
||
content: '地面干燥无积水,呼叫器功能正常,通风状况良好。',
|
||
time: '10:00',
|
||
hash: '0x8d2...4e6'
|
||
},
|
||
{
|
||
id: 4,
|
||
elder: '赵淑芬',
|
||
room: '202室',
|
||
avatar: '<27>',
|
||
type: '心理慰藉',
|
||
title: '情绪疏导与陪伴',
|
||
content: '老人因想念子女情绪低落,陪同聊天30分钟,情绪明显好转。',
|
||
time: '11:20',
|
||
hash: '0x5b1...8a3'
|
||
},
|
||
])
|
||
|
||
const filteredRecords = computed(() => {
|
||
return records.value.filter(record => {
|
||
const matchQuery = record.elder.includes(searchQuery.value) || record.room.includes(searchQuery.value)
|
||
const matchType = filterType.value === 'all' || record.type === filterType.value
|
||
return matchQuery && matchType
|
||
})
|
||
})
|
||
|
||
const exportRecords = () => {
|
||
const csvContent = "data:text/csv;charset=utf-8,"
|
||
+ "ID,老人,房间,类型,标题,时间,Hash\n"
|
||
+ filteredRecords.value.map(e => `${e.id},${e.elder},${e.room},${e.type},${e.title},${e.time},${e.hash}`).join("\n");
|
||
|
||
const encodedUri = encodeURI(csvContent);
|
||
const link = document.createElement("a");
|
||
link.setAttribute("href", encodedUri);
|
||
link.setAttribute("download", "nursing_records_export.csv");
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
alert(`成功导出 ${filteredRecords.value.length} 条护理记录!`)
|
||
}
|
||
|
||
const newRecord = ref({
|
||
elder: '',
|
||
type: '生活护理',
|
||
title: '',
|
||
content: ''
|
||
})
|
||
|
||
const submitRecord = () => {
|
||
// Simulate Blockchain Transaction
|
||
const newId = records.value.length + 1
|
||
const mockHash = '0x' + Math.random().toString(16).slice(2, 10) + '...' + Math.random().toString(16).slice(2, 6)
|
||
|
||
records.value.unshift({
|
||
id: newId,
|
||
elder: newRecord.value.elder || '未知老人',
|
||
room: '待定',
|
||
avatar: '👤',
|
||
type: newRecord.value.type,
|
||
title: newRecord.value.title || '新提交护理记录',
|
||
content: newRecord.value.content,
|
||
time: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}),
|
||
hash: mockHash
|
||
})
|
||
|
||
showAddModal.value = false
|
||
newRecord.value = { elder: '', type: '生活护理', title: '', content: '' }
|
||
|
||
// Show success toast (simulated)
|
||
alert(`记录已上链存证!\nHash: ${mockHash}`)
|
||
}
|
||
|
||
const getTypeColor = (type) => {
|
||
const colors = {
|
||
'生活护理': 'bg-blue-500',
|
||
'医疗协助': 'bg-red-500',
|
||
'心理慰藉': 'bg-purple-500',
|
||
'日常巡查': 'bg-green-500'
|
||
}
|
||
return colors[type] || 'bg-gray-500'
|
||
}
|
||
|
||
const getTypeBadge = (type) => {
|
||
const badges = {
|
||
'生活护理': 'bg-blue-100 text-blue-600',
|
||
'医疗协助': 'bg-red-100 text-red-600',
|
||
'心理慰藉': 'bg-purple-100 text-purple-600',
|
||
'日常巡查': 'bg-green-100 text-green-600'
|
||
}
|
||
return badges[type] || 'bg-gray-100 text-gray-600'
|
||
}
|
||
</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);
|
||
}
|
||
|
||
.list-move,
|
||
.list-enter-active,
|
||
.list-leave-active {
|
||
transition: all 0.5s ease;
|
||
}
|
||
|
||
.list-enter-from,
|
||
.list-leave-to {
|
||
opacity: 0;
|
||
transform: translateX(30px);
|
||
}
|
||
|
||
.list-leave-active {
|
||
position: absolute;
|
||
}
|
||
</style>
|