Files
akmon/pages/ec/elder/dashboard.uvue
2026-01-20 08:04:15 +08:00

929 lines
21 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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>
<view class="elder-dashboard">
<!-- 头部欢迎区域 -->
<view class="header-section">
<view class="welcome-card">
<text class="welcome-text">{{ greeting }}{{ elderInfo.name }}</text>
<text class="weather-info">今天天气:{{ weatherInfo.description }}</text>
<text class="date-info">{{ formatDate(new Date()) }}</text>
</view>
</view>
<!-- 健康状态卡片 -->
<view class="health-section">
<view class="section-title">
<text class="title-text">我的健康</text>
<button class="view-all-btn" @tap="goToHealthDetails">
<text class="btn-text">查看详情</text>
</button>
</view>
<view class="health-cards">
<view class="health-card" v-for="vital in vitals" :key="vital.type">
<text class="health-icon">{{ getVitalIcon(vital.type) }}</text>
<text class="health-label">{{ getVitalLabel(vital.type) }}</text>
<text class="health-value">{{ vital.value }}{{ getVitalUnit(vital.type) }}</text>
<text class="health-status" :class="vital.status">{{ getStatusText(vital.status) }}</text>
</view>
</view>
</view>
<!-- 今日活动 -->
<view class="activity-section">
<view class="section-title">
<text class="title-text">今日活动</text>
<text class="activity-count">{{ todayActivities.length }}项</text>
</view>
<view class="activity-list" v-if="todayActivities.length > 0">
<view class="activity-item" v-for="activity in todayActivities" :key="activity.id">
<view class="activity-time">
<text class="time-text">{{ formatTime(activity.scheduled_time) }}</text>
</view>
<view class="activity-info">
<text class="activity-name">{{ activity.title }}</text>
<text class="activity-location">{{ activity.location }}</text>
</view>
<view class="activity-status" :class="activity.status">
<text class="status-text">{{ getActivityStatusText(activity.status) }}</text>
</view>
</view>
</view>
<view class="empty-state" v-else>
<text class="empty-text">今天没有安排活动</text>
</view>
</view>
<!-- 用药提醒 -->
<view class="medication-section">
<view class="section-title">
<text class="title-text">用药提醒</text>
<view class="medication-alert" v-if="upcomingMedications.length > 0">
<text class="alert-text">{{ upcomingMedications.length }}个即将到期</text>
</view>
</view>
<view class="medication-list" v-if="todayMedications.length > 0">
<view class="medication-item" v-for="medication in todayMedications" :key="medication.id">
<view class="medication-time">
<text class="time-text">{{ formatTime(medication.scheduled_time) }}</text>
</view>
<view class="medication-info">
<text class="medication-name">{{ medication.medication_name }}</text>
<text class="medication-dosage">{{ medication.dosage }}</text>
</view>
<view class="medication-actions">
<button class="action-btn taken" @tap="markMedicationTaken(medication.id)" v-if="medication.status !== 'taken'">
<text class="btn-text">已服用</text>
</button>
<view class="taken-indicator" v-else>
<text class="taken-text">✓ 已服用</text>
</view>
</view>
</view>
</view>
<view class="empty-state" v-else>
<text class="empty-text">今天没有用药安排</text>
</view>
</view>
<!-- 护理服务 -->
<view class="care-section">
<view class="section-title">
<text class="title-text">护理服务</text>
</view>
<view class="care-summary">
<view class="care-card" @tap="goToCareRecords">
<text class="care-icon">👩‍⚕️</text>
<text class="care-label">护理员</text>
<text class="care-value">{{ caregiverInfo.name }}</text>
<text class="care-status">在线</text>
</view>
<view class="care-card" @tap="goToServiceRequests">
<text class="care-icon">🔔</text>
<text class="care-label">服务请求</text>
<text class="care-value">{{ pendingRequests }}</text>
<text class="care-status">待处理</text>
</view>
</view>
</view>
<!-- 快捷操作 -->
<view class="quick-actions">
<view class="section-title">
<text class="title-text">快捷操作</text>
</view>
<view class="action-grid">
<view class="action-item" @tap="callEmergency">
<text class="action-icon">🚨</text>
<text class="action-label">紧急呼叫</text>
</view>
<view class="action-item" @tap="callNurse">
<text class="action-icon">🔔</text>
<text class="action-label">呼叫护理员</text>
</view>
<view class="action-item" @tap="viewMenu">
<text class="action-icon">🍽️</text>
<text class="action-label">今日菜单</text>
</view>
<view class="action-item" @tap="contactFamily">
<text class="action-icon">📞</text>
<text class="action-label">联系家人</text>
</view>
</view>
</view>
<!-- 紧急呼叫悬浮按钮 -->
<view class="emergency-fab" @tap="showEmergencyOptions">
<text class="fab-icon">🚨</text>
</view>
<!-- 紧急呼叫选项弹窗 -->
<view class="emergency-modal" v-if="showEmergencyModal" @tap="hideEmergencyOptions">
<view class="modal-content" @tap.stop>
<text class="modal-title">紧急呼叫</text>
<view class="emergency-options">
<button class="emergency-btn medical" @tap="callMedicalEmergency">
<text class="btn-text">医疗急救</text>
</button>
<button class="emergency-btn nurse" @tap="callNurseEmergency">
<text class="btn-text">护理员</text>
</button>
<button class="emergency-btn family" @tap="callFamilyEmergency">
<text class="btn-text">联系家人</text>
</button>
</view>
<button class="cancel-btn" @tap="hideEmergencyOptions">
<text class="btn-text">取消</text>
</button>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, computed, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import { state, getCurrentUserId } from '@/utils/store.uts'
import { formatDate, formatTime, getStatusText, getActivityStatusText } from '../types.uts'
import type { Elder, VitalSign, Activity, Medication, CaregiverInfo } from '../types.uts'
// 数据状态
const elderInfo = ref<Elder>({
id: '',
name: '',
age: 0,
gender: 'male',
room_number: '',
bed_number: '',
admission_date: '',
health_status: 'stable',
care_level: 1,
emergency_contact: '',
profile_picture: '',
family_contact: '',
status: 'normal',
})
const vitals = ref<VitalSign[]>([])
const todayActivities = ref<Activity[]>([])
const todayMedications = ref<Medication[]>([])
const caregiverInfo = ref<CaregiverInfo>({
id: '',
employee_id:'',
name: '',
phone: '',
department: '',
specialization: '',
shift: 'day'
})
const weatherInfo = ref({
description: '晴朗',
temperature: 22,
humidity: 65
})
const pendingRequests = ref(0)
const showEmergencyModal = ref(false)
// 计算属性
const greeting = computed(() => {
const hour = new Date().getHours()
if (hour < 12) return '早上好'
if (hour < 18) return '下午好'
return '晚上好'
})
const upcomingMedications = computed(() => {
const now = new Date()
const oneHour = 60 * 60 * 1000
return todayMedications.value.filter(med => {
const scheduledTime = new Date(med.scheduled_time)
return scheduledTime.getTime() - now.getTime() <= oneHour && scheduledTime.getTime() > now.getTime()
})
})
// 辅助函数
function getVitalIcon(type: string): string {
const icons = {
'heart_rate': '❤️',
'blood_pressure': '🩸',
'temperature': '🌡️',
'blood_sugar': '🍯',
'oxygen_saturation': '🫁'
}
return icons[type] || '📊'
}
function getVitalLabel(type: string): string {
const labels = {
'heart_rate': '心率',
'blood_pressure': '血压',
'temperature': '体温',
'blood_sugar': '血糖',
'oxygen_saturation': '血氧'
}
return labels[type] || type
}
function getVitalUnit(type: string): string {
const units = {
'heart_rate': 'bpm',
'blood_pressure': 'mmHg',
'temperature': '°C',
'blood_sugar': 'mmol/L',
'oxygen_saturation': '%'
}
return units[type] || ''
}
// 事件处理
function goToHealthDetails() {
uni.navigateTo({
url: '/pages/ec/elder/health-details'
})
}
function goToCareRecords() {
uni.navigateTo({
url: '/pages/ec/elder/care-records'
})
}
function goToServiceRequests() {
uni.navigateTo({
url: '/pages/ec/elder/service-requests'
})
}
async function markMedicationTaken(medicationId: string) {
try {
const { error } = await supa
.from('ec_medications')
.update({ status: 'taken', updated_at: new Date().toISOString() })
.eq('id', medicationId)
.execute()
if (!error) {
const index = todayMedications.value.findIndex(med => med.id === medicationId)
if (index !== -1) {
todayMedications.value[index].status = 'taken'
}
uni.showToast({
title: '已标记为已服用',
icon: 'success'
})
} else {
throw error
}
} catch (error) {
console.error('标记用药失败:', error)
uni.showToast({
title: '操作失败',
icon: 'error'
})
}
}
function showEmergencyOptions() {
showEmergencyModal.value = true
}
function hideEmergencyOptions() {
showEmergencyModal.value = false
}
async function callEmergency() {
try {
console.log(elderInfo.value.id)
await supa
.from('ec_service_requests')
.insert({
elder_id: elderInfo.value.id,
type: 'emergency_call',
priority:'normal',
description: '老人主动发起紧急呼叫',
created_at: new Date().toISOString()
})
.execute()
uni.showToast({
title: '紧急呼叫已发送',
icon: 'success'
})
} catch (error) {
console.error('紧急呼叫失败:', error)
uni.showToast({
title: '呼叫失败',
icon: 'error'
})
}
}
async function callMedicalEmergency() {
hideEmergencyOptions()
try {
await supa
.from('ec_service_requests')
.insert({
elder_id: elderInfo.value.id,
type: 'medical',
description: '医疗急救',
created_at: new Date().toISOString()
})
.execute()
uni.showToast({
title: '医疗急救已呼叫',
icon: 'success'
})
} catch (error) {
console.error('医疗急救呼叫失败:', error)
}
}
async function callNurse() {
hideEmergencyOptions()
try {
await supa
.from('ec_service_requests')
.insert({
elder_id: elderInfo.value.id,
type: 'nurse_call',
priority: 'high',
description: '老人呼叫护理员',
status: 'pending',
created_at: new Date().toISOString()
})
.execute()
uni.showToast({
title: '护理员呼叫已发送',
icon: 'success'
})
} catch (error) {
console.error('呼叫护理员失败:', error)
}
}
function viewMenu() {
uni.navigateTo({
url: '/pages/ec/elder/daily-menu'
})
}
async function contactFamily() {
if (elderInfo.value.family_contact) {
uni.makePhoneCall({
phoneNumber: elderInfo.value.family_contact
})
} else {
uni.showToast({
title: '未设置家属联系方式',
icon: 'none'
})
}
}
// 数据加载
async function loadElderInfo() {
try {
const user = await supa.auth.getUser()
if (user?.data?.user?.id) {
const { data, error } = await supa
.from('ec_elders')
.select('*')
.or(`id.eq.${user.data.user.id},user_id.eq.${user.data.user.id}`)
.limit(1)
.single()
.executeAs<Elder>()
if (!error && data) {
elderInfo.value = data
}
}
} catch (error) {
console.error('加载老人信息失败:', error)
}
}
async function loadVitalSign() {
try {
const { data, error } = await supa
.from('ec_vital_signs')
.select('*')
.eq('elder_id', elderInfo.value.id)
.order('measured_at', { ascending: false })
.limit(3)
.executeAs<VitalSign[]>()
if (!error && data) {
vitals.value = data
}
} catch (error) {
console.error('加载健康数据失败:', error)
}
}
async function loadTodayActivities() {
try {
// 第一步先查询老人报名的活动ID列表
const participationRes = await supa.from('ec_activity_participations')
.select('activity_id')
.eq('elder_id', elderInfo.value.id)
.eq('participation_status', 'registered')
.executeAs<UTSJSONObject[]>()
if (participationRes.error != null) {
console.error('加载活动参与情况失败:', participationRes.error)
return
}
const activityIds = participationRes.data?.map((item):string => item['activity_id'] as string) ?? []
if (activityIds.length == 0) {
todayActivities.value = []
return
}
// 第二步根据ID列表查询活动详情
const today = new Date().toISOString().split('T')[0]
const { data, error } = await supa
.from('ec_activities')
.select('*')
.in('id', activityIds)
.gte('start_time', `${today} 00:00:00`)
.lte('start_time', `${today} 23:59:59`)
.order('start_time', { ascending: true })
.executeAs<Activity[]>()
if (!error && data) {
todayActivities.value = data
}
} catch (error) {
console.error('加载今日活动失败:', error)
}
}
async function loadTodayMedications() {
try {
const today = new Date().toISOString().split('T')[0]
const { data, error } = await supa
.from('ec_medications')
.select('*')
.eq('elder_id', elderInfo.value.id)
.or(`start_date.lte.${today},start_date.is.null`)
.or(`end_date.gte.${today},end_date.is.null`)
.order('medication_name', { ascending: true })
.executeAs<Medication[]>()
if (!error && data) {
todayMedications.value = data
}
} catch (error) {
console.error('加载用药信息失败:', error)
}
}
async function loadCaregiverInfo() {
try {
const { data, error } = await supa
.from('ec_caregivers')
.select('*')
.eq('id', elderInfo.value.caregiver_id)
.limit(1)
.single()
.executeAs<CaregiverInfo>()
if (!error && data) {
caregiverInfo.value = data
}
} catch (error) {
console.error('加载护理员信息失败:', error)
}
}
async function loadPendingRequests() {
try {
const { data, error } = await supa
.from('ec_service_requests')
.select('id')
.eq('elder_id', elderInfo.value.id)
.eq('status', 'pending')
.executeAs<any[]>()
if (!error && data) {
pendingRequests.value = data.length
}
} catch (error) {
console.error('加载待处理请求数失败:', error)
}
}
// // 初始化
// onMounted(async () => {
// await loadElderInfo()
// if (elderInfo.value.id) {
// await Promise.all([
// loadVitalSign(),
// loadTodayActivities(),
// loadTodayMedications(),
// loadCaregiverInfo(),
// loadPendingRequests()
// ])
// }
// })
onLoad(async(options: OnLoadOptions) => {
elderInfo.value.id = options['id'] ?? getCurrentUserId()
if (elderInfo.value.id !='' ) {
await Promise.all([
loadVitalSign(),
loadTodayActivities(),
loadTodayMedications(),
loadCaregiverInfo(),
loadPendingRequests()
])
}
})
</script>
<style scoped>
.elder-dashboard {
padding: 40rpx;
background-color: #f8f9fa;
min-height: 100vh;
}
.header-section {
margin-bottom: 40rpx;
}
.welcome-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40rpx;
border-radius: 24rpx;
color: white;
}
.welcome-text {
font-size: 48rpx;
font-weight: bold;
display: block;
margin-bottom: 12rpx;
}
.weather-info {
font-size: 32rpx;
display: block;
margin-bottom: 8rpx;
opacity: 0.9;
}
.date-info {
font-size: 28rpx;
display: block;
opacity: 0.8;
}
.section-title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.title-text {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.view-all-btn {
background: #007AFF;
color: white;
padding: 16rpx 24rpx;
border-radius: 20rpx;
border: none;
font-size: 24rpx;
}
.activity-count {
background: #ff6b6b;
color: white;
padding: 8rpx 16rpx;
border-radius: 16rpx;
font-size: 24rpx;
}
.health-section {
margin-bottom: 40rpx;
}
.health-cards {
display: flex;
flex-direction: row;
margin-bottom: 20rpx;
}
.health-card {
flex: 1;
min-width: 200rpx;
background: white;
padding: 30rpx;
border-radius: 16rpx;
text-align: center;
margin-right: 20rpx;
}
.health-card.is-last {
margin-right: 0;
}
.health-icon {
font-size: 48rpx;
display: block;
margin-bottom: 12rpx;
}
.health-label {
font-size: 28rpx;
color: #666;
display: block;
margin-bottom: 8rpx;
}
.health-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 12rpx;
}
.health-status {
font-size: 24rpx;
padding: 6rpx 12rpx;
border-radius: 12rpx;
display: inline-block;
}
.health-status.is-normal {
background: #e8f5e8;
color: #4caf50;
}
.health-status.is-warning {
background: #fff3e0;
color: #ff9800;
}
.health-status.is-danger {
background: #ffebee;
color: #f44336;
}
.activity-section,
.medication-section,
.care-section {
margin-bottom: 40rpx;
}
.activity-list,
.medication-list {
background: white;
border-radius: 16rpx;
overflow: hidden;
}
.activity-item,
.medication-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.activity-item.is-last,
.medication-item.is-last {
border-bottom: none;
}
.activity-time,
.medication-time {
width: 140rpx;
flex-shrink: 0;
}
.time-text {
font-size: 28rpx;
color: #007AFF;
font-weight: 600;
}
.activity-info,
.medication-info {
flex: 1;
margin-left: 20rpx;
}
.activity-name,
.medication-name {
font-size: 32rpx;
color: #333;
display: block;
margin-bottom: 4rpx;
}
.activity-location,
.medication-dosage {
font-size: 26rpx;
color: #666;
display: block;
}
.activity-status,
.medication-actions {
flex-shrink: 0;
}
.activity-status {
padding: 8rpx 16rpx;
border-radius: 12rpx;
font-size: 24rpx;
}
.activity-status.is-pending {
background: #fff3e0;
color: #ff9800;
}
.activity-status.is-completed {
background: #e8f5e8;
color: #4caf50;
}
.medication-alert {
background: #ff6b6b;
color: white;
padding: 8rpx 16rpx;
border-radius: 16rpx;
font-size: 24rpx;
}
.action-btn {
padding: 12rpx 20rpx;
border-radius: 16rpx;
border: none;
font-size: 24rpx;
}
.action-btn.is-taken {
background: #4caf50;
color: white;
}
.taken-indicator {
padding: 12rpx 20rpx;
}
.taken-text {
color: #4caf50;
font-size: 24rpx;
}
.care-summary {
display: flex;
flex-direction: row;
margin-bottom: 20rpx;
}
.care-card {
flex: 1;
background: white;
padding: 30rpx;
border-radius: 16rpx;
text-align: center;
margin-right: 20rpx;
}
.care-card.is-last {
margin-right: 0;
}
.care-icon {
font-size: 48rpx;
display: block;
margin-bottom: 12rpx;
}
.care-label {
font-size: 28rpx;
color: #666;
display: block;
margin-bottom: 8rpx;
}
.care-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 12rpx;
}
.care-status {
font-size: 24rpx;
color: #4caf50;
background: #e8f5e8;
padding: 6rpx 12rpx;
border-radius: 12rpx;
display: inline-block;
}
.quick-actions {
margin-bottom: 40rpx;
}
.action-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.action-item {
flex: 1;
min-width: 150rpx;
background: white;
padding: 30rpx;
border-radius: 16rpx;
text-align: center;
margin-right: 20rpx;
margin-bottom: 20rpx;
}
.action-item.is-last {
margin-right: 0;
}
.action-icon {
font-size: 48rpx;
display: block;
margin-bottom: 12rpx;
}
.action-label {
font-size: 28rpx;
color: #333;
display: block;
}
.empty-state {
background: white;
padding: 60rpx;
border-radius: 16rpx;
text-align: center;
}
.empty-text {
font-size: 30rpx;
color: #999;
}
.emergency-fab {
position: fixed;
right: 40rpx;
bottom: 40rpx;
width: 120rpx;
height: 120rpx;
background: #ff4444;
border-radius: 60rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.fab-icon {
font-size: 48rpx;
color: white;
}
.emergency-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 40rpx;
border-radius: 24rpx;
width: 600rpx;
max-width: 90%;
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.emergency-options {
margin-bottom: 30rpx;
}
.emergency-btn {
width: 100%;
padding: 30rpx;
border-radius: 16rpx;
border: none;
margin-bottom: 20rpx;
font-size: 32rpx;
font-weight: 600;
}
.emergency-btn.is-medical {
background: #ff4444;
color: white;
}
.emergency-btn.is-nurse {
background: #007AFF;
color: white;
}
.emergency-btn.is-family {
background: #4caf50;
color: white;
}
.cancel-btn {
width: 100%;
padding: 24rpx;
border-radius: 16rpx;
border: 1rpx solid #ddd;
background: white;
color: #666;
font-size: 30rpx;
}
</style>