754 lines
18 KiB
Plaintext
754 lines
18 KiB
Plaintext
<template>
|
|
<scroll-view class="elder-details-container">
|
|
<!-- Header with Elder Basic Info -->
|
|
<view class="elder-header">
|
|
<view class="elder-avatar-section">
|
|
<image class="elder-avatar" :src="elderInfo.avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
|
|
<view class="elder-status-badge" :class="getStatusClass(elderInfo.health_status)">
|
|
<text class="status-text">{{ getHealthStatusText(elderInfo.health_status) }}</text>
|
|
</view>
|
|
</view>
|
|
<view class="elder-basic-info">
|
|
<text class="elder-name">{{ elderInfo.name }}</text>
|
|
<text class="elder-details">{{ elderInfo.age }}岁 | {{ elderInfo.gender === 'male' ? '男' : '女' }}</text>
|
|
<text class="elder-room">房间: {{ elderInfo.room_number }}</text>
|
|
<text class="care-level">护理等级: {{ getCareLevelText(elderInfo.care_level) }}</text>
|
|
</view>
|
|
<view class="action-buttons">
|
|
<button class="btn-edit" @click="editElder">编辑</button>
|
|
<button class="btn-record" @click="recordVitals">记录体征</button>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Quick Vital Signs -->
|
|
<view class="section-card">
|
|
<view class="section-header">
|
|
<text class="section-title">生命体征</text>
|
|
<text class="last-update">最后更新: {{ formatTime(latestVitals.recorded_at) }}</text>
|
|
</view>
|
|
<view class="vitals-grid">
|
|
<view class="vital-item">
|
|
<text class="vital-label">血压</text>
|
|
<text class="vital-value" :class="getVitalStatusClass('blood_pressure', latestVitals.blood_pressure)">
|
|
{{ latestVitals.blood_pressure || '--' }}
|
|
</text>
|
|
</view>
|
|
<view class="vital-item">
|
|
<text class="vital-label">心率</text>
|
|
<text class="vital-value" :class="getVitalStatusClass('heart_rate', latestVitals.heart_rate)">
|
|
{{ latestVitals.heart_rate ? latestVitals.heart_rate + ' bpm' : '--' }}
|
|
</text>
|
|
</view>
|
|
<view class="vital-item">
|
|
<text class="vital-label">体温</text>
|
|
<text class="vital-value" :class="getVitalStatusClass('temperature', latestVitals.temperature)">
|
|
{{ latestVitals.temperature ? latestVitals.temperature + '°C' : '--' }}
|
|
</text>
|
|
</view>
|
|
<view class="vital-item">
|
|
<text class="vital-label">血氧</text>
|
|
<text class="vital-value" :class="getVitalStatusClass('oxygen_saturation', latestVitals.oxygen_saturation)">
|
|
{{ latestVitals.oxygen_saturation ? latestVitals.oxygen_saturation + '%' : '--' }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Today's Tasks -->
|
|
<view class="section-card">
|
|
<view class="section-header">
|
|
<text class="section-title">今日护理任务</text>
|
|
<text class="task-count">{{ todayTasks.length }} 项任务</text>
|
|
</view>
|
|
<view v-if="todayTasks.length === 0" class="empty-state">
|
|
<text class="empty-text">今日暂无护理任务</text>
|
|
</view>
|
|
<view v-else class="tasks-list">
|
|
<view v-for="task in todayTasks" :key="task.id" class="task-item" @click="completeTask(task)">
|
|
<view class="task-info">
|
|
<text class="task-title">{{ task.title }}</text>
|
|
<text class="task-time">{{ formatTime(task.scheduled_time) }}</text>
|
|
</view>
|
|
<view class="task-status" :class="getTaskStatusClass(task.status)">
|
|
<text class="status-text">{{ getTaskStatusText(task.status) }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Health Records -->
|
|
<view class="section-card">
|
|
<view class="section-header">
|
|
<text class="section-title">健康记录</text>
|
|
<text class="view-all" @click="viewAllRecords">查看全部</text>
|
|
</view>
|
|
<view v-if="healthRecords.length === 0" class="empty-state">
|
|
<text class="empty-text">暂无健康记录</text>
|
|
</view>
|
|
<view v-else class="records-list">
|
|
<view v-for="record in healthRecords.slice(0, 5)" :key="record.id" class="record-item">
|
|
<view class="record-info">
|
|
<text class="record-type">{{ getRecordTypeText(record.record_type) }}</text>
|
|
<text class="record-content">{{ record.content }}</text>
|
|
<text class="record-time">{{ formatTime(record.recorded_at) }}</text>
|
|
</view>
|
|
<view class="record-priority" :class="getPriorityClass(record.priority)">
|
|
<text class="priority-text">{{ getPriorityText(record.priority) }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Medications -->
|
|
<view class="section-card">
|
|
<view class="section-header">
|
|
<text class="section-title">用药信息</text>
|
|
<text class="medication-count">{{ medications.length }} 种药物</text>
|
|
</view>
|
|
<view v-if="medications.length === 0" class="empty-state">
|
|
<text class="empty-text">暂无用药记录</text>
|
|
</view>
|
|
<view v-else class="medications-list">
|
|
<view v-for="medication in medications" :key="medication.id" class="medication-item">
|
|
<view class="medication-info">
|
|
<text class="medication-name">{{ medication.medication_name }}</text>
|
|
<text class="medication-dosage">{{ medication.dosage }} | {{ medication.frequency }}</text>
|
|
<text class="medication-time">下次用药: {{ formatTime(medication.next_dose_time) }}</text>
|
|
</view>
|
|
<view class="medication-status" :class="getMedicationStatusClass(medication.status)">
|
|
<text class="status-text">{{ getMedicationStatusText(medication.status) }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Emergency Contact -->
|
|
<view class="section-card">
|
|
<view class="section-header">
|
|
<text class="section-title">紧急联系人</text>
|
|
</view>
|
|
<view class="contact-info">
|
|
<view class="contact-item">
|
|
<text class="contact-label">姓名:</text>
|
|
<text class="contact-value">{{ elderInfo.emergency_contact_name || '--' }}</text>
|
|
</view>
|
|
<view class="contact-item">
|
|
<text class="contact-label">关系:</text>
|
|
<text class="contact-value">{{ elderInfo.emergency_contact_relationship || '--' }}</text>
|
|
</view>
|
|
<view class="contact-item">
|
|
<text class="contact-label">电话:</text>
|
|
<text class="contact-value phone-number" @click="callEmergencyContact">
|
|
{{ elderInfo.emergency_contact_phone || '--' }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Quick Actions -->
|
|
<view class="quick-actions">
|
|
<button class="action-btn emergency" @click="reportEmergency">
|
|
<text class="btn-icon">🚨</text>
|
|
<text class="btn-text">紧急报告</text>
|
|
</button>
|
|
<button class="action-btn medication" @click="recordMedication">
|
|
<text class="btn-icon">💊</text>
|
|
<text class="btn-text">用药记录</text>
|
|
</button>
|
|
<button class="action-btn activity" @click="recordActivity">
|
|
<text class="btn-icon">🏃</text>
|
|
<text class="btn-text">活动记录</text>
|
|
</button>
|
|
</view>
|
|
</scroll-view>
|
|
</template>
|
|
|
|
<script lang="uts">
|
|
import { ElderInfo, HealthRecord, Medication, CareTask, VitalSigns } from '../types.uts'
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
elderId: '',
|
|
elderInfo: {} as ElderInfo,
|
|
latestVitals: {} as VitalSigns,
|
|
todayTasks: [] as CareTask[],
|
|
healthRecords: [] as HealthRecord[],
|
|
medications: [] as Medication[]
|
|
}
|
|
},
|
|
onLoad(options) {
|
|
if (options.id) {
|
|
this.elderId = options.id
|
|
this.loadElderDetails()
|
|
}
|
|
},
|
|
methods: {
|
|
async loadElderDetails() {
|
|
try {
|
|
await Promise.all([
|
|
this.loadElderInfo(),
|
|
this.loadVitalSigns(),
|
|
this.loadTodayTasks(),
|
|
this.loadHealthRecords(),
|
|
this.loadMedications()
|
|
])
|
|
} catch (error) {
|
|
console.error('加载老人详情失败:', error)
|
|
uni.showToast({
|
|
title: '加载失败',
|
|
icon: 'error'
|
|
})
|
|
}
|
|
},
|
|
|
|
async loadElderInfo() {
|
|
const result = await supa.executeAs('elder_by_id', {
|
|
elder_id: this.elderId
|
|
})
|
|
|
|
if (result.success && result.data.length > 0) {
|
|
this.elderInfo = result.data[0] as ElderInfo
|
|
}
|
|
},
|
|
|
|
async loadVitalSigns() {
|
|
const result = await supa.executeAs('latest_vital_signs', {
|
|
elder_id: this.elderId
|
|
})
|
|
|
|
if (result.success && result.data.length > 0) {
|
|
this.latestVitals = result.data[0] as VitalSigns
|
|
}
|
|
},
|
|
|
|
async loadTodayTasks() {
|
|
const today = new Date().toISOString().split('T')[0]
|
|
const result = await supa.executeAs('elder_tasks_by_date', {
|
|
elder_id: this.elderId,
|
|
date: today
|
|
})
|
|
|
|
if (result.success) {
|
|
this.todayTasks = result.data as CareTask[]
|
|
}
|
|
},
|
|
|
|
async loadHealthRecords() {
|
|
const result = await supa.executeAs('elder_health_records', {
|
|
elder_id: this.elderId,
|
|
limit: 10
|
|
})
|
|
|
|
if (result.success) {
|
|
this.healthRecords = result.data as HealthRecord[]
|
|
}
|
|
},
|
|
|
|
async loadMedications() {
|
|
const result = await supa.executeAs('elder_medications', {
|
|
elder_id: this.elderId
|
|
})
|
|
|
|
if (result.success) {
|
|
this.medications = result.data as Medication[]
|
|
}
|
|
},
|
|
|
|
getStatusClass(status: string): string {
|
|
const statusMap = {
|
|
'good': 'status-good',
|
|
'fair': 'status-fair',
|
|
'poor': 'status-poor',
|
|
'critical': 'status-critical'
|
|
}
|
|
return statusMap[status] || 'status-good'
|
|
},
|
|
|
|
getHealthStatusText(status: string): string {
|
|
const statusMap = {
|
|
'good': '良好',
|
|
'fair': '一般',
|
|
'poor': '较差',
|
|
'critical': '危重'
|
|
}
|
|
return statusMap[status] || '未知'
|
|
},
|
|
|
|
getCareLevelText(level: string): string {
|
|
const levelMap = {
|
|
'level1': '一级护理',
|
|
'level2': '二级护理',
|
|
'level3': '三级护理',
|
|
'special': '特级护理'
|
|
}
|
|
return levelMap[level] || '未设置'
|
|
},
|
|
|
|
getVitalStatusClass(type: string, value: string): string {
|
|
// 根据生命体征类型和值判断状态
|
|
if (!value) return 'vital-normal'
|
|
|
|
// 这里可以根据具体的医学标准来判断
|
|
return 'vital-normal'
|
|
},
|
|
|
|
getTaskStatusClass(status: string): string {
|
|
const statusMap = {
|
|
'pending': 'task-pending',
|
|
'in_progress': 'task-progress',
|
|
'completed': 'task-completed',
|
|
'overdue': 'task-overdue'
|
|
}
|
|
return statusMap[status] || 'task-pending'
|
|
},
|
|
|
|
getTaskStatusText(status: string): string {
|
|
const statusMap = {
|
|
'pending': '待执行',
|
|
'in_progress': '进行中',
|
|
'completed': '已完成',
|
|
'overdue': '已逾期'
|
|
}
|
|
return statusMap[status] || '未知'
|
|
},
|
|
|
|
getRecordTypeText(type: string): string {
|
|
const typeMap = {
|
|
'vital_signs': '生命体征',
|
|
'medication': '用药记录',
|
|
'activity': '活动记录',
|
|
'incident': '事件记录',
|
|
'observation': '观察记录'
|
|
}
|
|
return typeMap[type] || type
|
|
},
|
|
|
|
getPriorityClass(priority: string): string {
|
|
const priorityMap = {
|
|
'low': 'priority-low',
|
|
'normal': 'priority-normal',
|
|
'high': 'priority-high',
|
|
'urgent': 'priority-urgent'
|
|
}
|
|
return priorityMap[priority] || 'priority-normal'
|
|
},
|
|
|
|
getPriorityText(priority: string): string {
|
|
const priorityMap = {
|
|
'low': '低',
|
|
'normal': '普通',
|
|
'high': '高',
|
|
'urgent': '紧急'
|
|
}
|
|
return priorityMap[priority] || '普通'
|
|
},
|
|
|
|
getMedicationStatusClass(status: string): string {
|
|
const statusMap = {
|
|
'active': 'med-active',
|
|
'paused': 'med-paused',
|
|
'completed': 'med-completed'
|
|
}
|
|
return statusMap[status] || 'med-active'
|
|
},
|
|
|
|
getMedicationStatusText(status: string): string {
|
|
const statusMap = {
|
|
'active': '正在服用',
|
|
'paused': '暂停',
|
|
'completed': '已完成'
|
|
}
|
|
return statusMap[status] || '未知'
|
|
},
|
|
|
|
formatTime(timestamp: string): string {
|
|
if (!timestamp) return '--'
|
|
|
|
const date = new Date(timestamp)
|
|
const now = new Date()
|
|
const diff = now.getTime() - date.getTime()
|
|
const minutes = Math.floor(diff / (1000 * 60))
|
|
const hours = Math.floor(minutes / 60)
|
|
const days = Math.floor(hours / 24)
|
|
|
|
if (days > 0) {
|
|
return `${days}天前`
|
|
} else if (hours > 0) {
|
|
return `${hours}小时前`
|
|
} else if (minutes > 0) {
|
|
return `${minutes}分钟前`
|
|
} else {
|
|
return '刚刚'
|
|
}
|
|
},
|
|
|
|
editElder() {
|
|
uni.navigateTo({
|
|
url: `/pages/ec/admin/elder-form?id=${this.elderId}`
|
|
})
|
|
},
|
|
|
|
recordVitals() {
|
|
uni.navigateTo({
|
|
url: `/pages/ec/health/vital-signs-form?elder_id=${this.elderId}`
|
|
})
|
|
},
|
|
|
|
async completeTask(task: CareTask) {
|
|
try {
|
|
const result = await supa.executeAs('update_task_status', {
|
|
task_id: task.id,
|
|
status: 'completed',
|
|
completed_at: new Date().toISOString()
|
|
})
|
|
|
|
if (result.success) {
|
|
uni.showToast({
|
|
title: '任务已完成',
|
|
icon: 'success'
|
|
})
|
|
this.loadTodayTasks()
|
|
}
|
|
} catch (error) {
|
|
console.error('完成任务失败:', error)
|
|
uni.showToast({
|
|
title: '操作失败',
|
|
icon: 'error'
|
|
})
|
|
}
|
|
},
|
|
|
|
viewAllRecords() {
|
|
uni.navigateTo({
|
|
url: `/pages/ec/health/records?elder_id=${this.elderId}`
|
|
})
|
|
},
|
|
|
|
callEmergencyContact() {
|
|
if (this.elderInfo.emergency_contact_phone) {
|
|
uni.makePhoneCall({
|
|
phoneNumber: this.elderInfo.emergency_contact_phone
|
|
})
|
|
}
|
|
},
|
|
|
|
reportEmergency() {
|
|
uni.navigateTo({
|
|
url: `/pages/ec/incident/report-form?elder_id=${this.elderId}&type=emergency`
|
|
})
|
|
},
|
|
|
|
recordMedication() {
|
|
uni.navigateTo({
|
|
url: `/pages/ec/medication/record-form?elder_id=${this.elderId}`
|
|
})
|
|
},
|
|
|
|
recordActivity() {
|
|
uni.navigateTo({
|
|
url: `/pages/ec/activity/record-form?elder_id=${this.elderId}`
|
|
})
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.elder-details-container {
|
|
background-color: #f5f5f5;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.elder-header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
padding: 40rpx 30rpx;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.elder-avatar-section {
|
|
position: relative;
|
|
|
|
.elder-avatar {
|
|
width: 120rpx;
|
|
height: 120rpx;
|
|
border-radius: 60rpx;
|
|
border: 4rpx solid rgba(255,255,255,0.3);
|
|
}
|
|
|
|
.elder-status-badge {
|
|
position: absolute;
|
|
bottom: 0;
|
|
right: 0;
|
|
padding: 5rpx 10rpx;
|
|
border-radius: 20rpx;
|
|
font-size: 20rpx;
|
|
|
|
&.status-good {
|
|
background-color: #4CAF50;
|
|
}
|
|
|
|
&.status-fair {
|
|
background-color: #FF9800;
|
|
}
|
|
|
|
&.status-poor {
|
|
background-color: #f44336;
|
|
}
|
|
|
|
&.status-critical {
|
|
background-color: #9C27B0;
|
|
}
|
|
}
|
|
}
|
|
|
|
.elder-basic-info {
|
|
flex: 1;
|
|
|
|
.elder-name {
|
|
font-size: 36rpx;
|
|
font-weight: bold;
|
|
margin-bottom: 10rpx;
|
|
}
|
|
|
|
.elder-details, .elder-room, .care-level {
|
|
font-size: 26rpx;
|
|
opacity: 0.9;
|
|
margin-bottom: 5rpx;
|
|
}
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10rpx;
|
|
|
|
button {
|
|
padding: 15rpx 20rpx;
|
|
border-radius: 10rpx;
|
|
font-size: 24rpx;
|
|
background-color: rgba(255,255,255,0.2);
|
|
color: white;
|
|
border: 2rpx solid rgba(255,255,255,0.3);
|
|
}
|
|
}
|
|
|
|
.section-card {
|
|
background: white;
|
|
margin: 20rpx 30rpx;
|
|
border-radius: 20rpx;
|
|
padding: 30rpx;
|
|
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 30rpx;
|
|
padding-bottom: 20rpx;
|
|
border-bottom: 2rpx solid #f0f0f0;
|
|
|
|
.section-title {
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
|
|
.last-update, .task-count, .medication-count {
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
}
|
|
|
|
.view-all {
|
|
font-size: 26rpx;
|
|
color: #667eea;
|
|
}
|
|
}
|
|
|
|
.vitals-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.vital-item {
|
|
background-color: #f8f9ff;
|
|
padding: 20rpx;
|
|
border-radius: 15rpx;
|
|
text-align: center;
|
|
|
|
.vital-label {
|
|
font-size: 24rpx;
|
|
color: #666;
|
|
margin-bottom: 10rpx;
|
|
}
|
|
|
|
.vital-value {
|
|
font-size: 28rpx;
|
|
font-weight: bold;
|
|
|
|
&.vital-normal {
|
|
color: #4CAF50;
|
|
}
|
|
|
|
&.vital-warning {
|
|
color: #FF9800;
|
|
}
|
|
|
|
&.vital-danger {
|
|
color: #f44336;
|
|
}
|
|
}
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60rpx 0;
|
|
|
|
.empty-text {
|
|
font-size: 28rpx;
|
|
color: #999;
|
|
}
|
|
}
|
|
|
|
.tasks-list, .records-list, .medications-list {
|
|
.task-item, .record-item, .medication-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 20rpx 0;
|
|
border-bottom: 2rpx solid #f0f0f0;
|
|
|
|
&:last-child {
|
|
border-bottom: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
.task-info, .record-info, .medication-info {
|
|
flex: 1;
|
|
|
|
.task-title, .record-type, .medication-name {
|
|
font-size: 28rpx;
|
|
color: #333;
|
|
margin-bottom: 5rpx;
|
|
}
|
|
|
|
.task-time, .record-content, .medication-dosage {
|
|
font-size: 24rpx;
|
|
color: #666;
|
|
margin-bottom: 5rpx;
|
|
}
|
|
|
|
.record-time, .medication-time {
|
|
font-size: 22rpx;
|
|
color: #999;
|
|
}
|
|
}
|
|
|
|
.task-status, .record-priority, .medication-status {
|
|
padding: 10rpx 15rpx;
|
|
border-radius: 20rpx;
|
|
font-size: 22rpx;
|
|
|
|
&.task-pending, &.priority-low, &.med-paused {
|
|
background-color: #e3f2fd;
|
|
color: #1976d2;
|
|
}
|
|
|
|
&.task-progress, &.priority-normal, &.med-active {
|
|
background-color: #e8f5e8;
|
|
color: #388e3c;
|
|
}
|
|
|
|
&.task-completed, &.med-completed {
|
|
background-color: #f3e5f5;
|
|
color: #7b1fa2;
|
|
}
|
|
|
|
&.task-overdue, &.priority-urgent {
|
|
background-color: #ffebee;
|
|
color: #d32f2f;
|
|
}
|
|
|
|
&.priority-high {
|
|
background-color: #fff3e0;
|
|
color: #f57c00;
|
|
}
|
|
}
|
|
|
|
.contact-info {
|
|
.contact-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 15rpx 0;
|
|
border-bottom: 2rpx solid #f0f0f0;
|
|
|
|
&:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.contact-label {
|
|
width: 120rpx;
|
|
font-size: 26rpx;
|
|
color: #666;
|
|
}
|
|
|
|
.contact-value {
|
|
flex: 1;
|
|
font-size: 28rpx;
|
|
color: #333;
|
|
|
|
&.phone-number {
|
|
color: #667eea;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.quick-actions {
|
|
padding: 30rpx;
|
|
display: flex;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.action-btn {
|
|
flex: 1;
|
|
height: 120rpx;
|
|
border-radius: 15rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10rpx;
|
|
border: none;
|
|
|
|
&.emergency {
|
|
background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%);
|
|
color: white;
|
|
}
|
|
|
|
&.medication {
|
|
background: linear-gradient(135deg, #4ecdc4 0%, #26a69a 100%);
|
|
color: white;
|
|
}
|
|
|
|
&.activity {
|
|
background: linear-gradient(135deg, #45b7d1 0%, #2196f3 100%);
|
|
color: white;
|
|
}
|
|
|
|
.btn-icon {
|
|
font-size: 36rpx;
|
|
}
|
|
|
|
.btn-text {
|
|
font-size: 24rpx;
|
|
}
|
|
}
|
|
</style>
|