Files
SmartElderlyCare/src/views/agency/Records.vue
祀梦 1b87097447 refactor(功能模块): 将"时间银行"重命名为"互助中心"并完善相关功能
重构项目中的"时间银行"模块,统一更名为"互助中心",涉及前端路由、组件、文档及多处文本替换。新增互助中心页面功能,包括:
1. 通证兑换服务弹窗
2. 互助任务列表展示
3. 区块链存证交互流程

同时优化护理记录页面,增加筛选、导出功能,完善监管端仪表盘交互
2026-01-13 10:30:21 +08:00

411 lines
18 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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