Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,928 @@
<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>