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

830 lines
19 KiB
Plaintext
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>
<view class="family-dashboard">
<view class="header">
<text class="title">家属关怀</text>
<text class="welcome">{{ familyName }}{{ currentTime }}</text>
</view>
<!-- 老人状态卡片 -->
<view class="elder-status-card">
<view class="elder-profile">
<view class="elder-avatar">
<image class="avatar-image" :src="elderInfo.profile_picture ?? ''" mode="aspectFill"
v-if="elderInfo.profile_picture !== null" />
<text class="avatar-fallback" v-else>{{ elderInfo.name.charAt(0) }}</text>
</view>
<view class="elder-basic">
<text class="elder-name">{{ elderInfo.name }}</text>
<text class="elder-info">{{ elderInfo.age }}岁 · {{ elderInfo.gender === 'male' ? '男' : '女' }}</text>
<text class="elder-room">{{ elderInfo.room_number }}{{ elderInfo.bed_number }}</text>
</view>
</view>
<view class="elder-health">
<view class="health-status" :class="getHealthStatusClass(elderInfo.health_status)">
<text class="status-text">{{ getHealthStatusText(elderInfo.health_status) }}</text>
</view>
<text class="last-update">最后更新:{{ formatDateTime(elderInfo.updated_at) }}</text>
</view>
</view>
<!-- 今日护理概览 -->
<view class="today-care-section">
<view class="section-header">
<text class="section-title">今日护理</text>
<text class="section-more" @click="viewAllCareRecords">查看详情</text>
</view>
<view class="care-timeline">
<view v-for="record in todayCareRecords" :key="record.id" class="timeline-item">
<view class="timeline-dot" :class="record.record_type"></view>
<view class="timeline-content">
<text class="care-title">{{ record.description }}</text>
<text class="care-time">{{ formatTime(record.created_at) }}</text>
</view>
<view class="care-type">
<text class="type-text">{{ getRecordTypeText(record.record_type) }}</text>
</view>
</view>
</view>
</view>
<!-- 健康监测 -->
<view class="health-monitoring-section">
<view class="section-header">
<text class="section-title">健康监测</text>
<text class="section-more" @click="viewHealthDetails">查看详情</text>
</view>
<view class="health-metrics">
<view class="metric-item" v-for="vital in recentVitals" :key="vital.id">
<view class="metric-icon">{{ getVitalIcon(vital.vital_type) }}</view>
<view class="metric-info">
<text class="metric-name">{{ getVitalName(vital.vital_type) }}</text>
<text class="metric-value">{{ getVitalValue(vital) }}</text>
<text class="metric-time">{{ formatDateTime(vital.measured_at) }}</text>
</view>
<view class="metric-status" :class="vital.is_abnormal ? 'abnormal' : 'normal'">
<text class="status-text">{{ vital.is_abnormal ? '异常' : '正常' }}</text>
</view>
</view>
</view>
</view>
<!-- 探访安排 -->
<view class="visit-section">
<view class="section-header">
<text class="section-title">探访安排</text>
<text class="section-more" @click="scheduleVisit">预约探访</text>
</view>
<view class="visit-info" v-if="nextVisit !== null">
<view class="visit-item">
<view class="visit-icon"></view>
<view class="visit-details">
<text class="visit-title">下次探访</text>
<text class="visit-time">{{ formatDateTime(nextVisit.visit_time) }}</text>
<text class="visit-note">{{ nextVisit.notes ?? '常规探访' }}</text>
</view>
<button class="visit-btn" @click="modifyVisit">修改</button>
</view>
</view>
<view class="no-visit" v-else>
<text class="no-visit-text">暂无探访安排</text>
<button class="schedule-btn" @click="scheduleVisit">预约探访</button>
</view>
</view>
<!-- 服务费用 */
<view class="billing-section">
<view class="section-header">
<text class="section-title">本月费用</text>
<text class="section-more" @click="viewBillingDetails">查看账单</text>
</view>
<view class="billing-summary">
<view class="billing-item">
<text class="billing-label">护理费</text>
<text class="billing-amount">¥{{ monthlyBilling.care_fee }}</text>
</view>
<view class="billing-item">
<text class="billing-label">餐费</text>
<text class="billing-amount">¥{{ monthlyBilling.meal_fee }}</text>
</view>
<view class="billing-item">
<text class="billing-label">其他费用</text>
<text class="billing-amount">¥{{ monthlyBilling.other_fee }}</text>
</view>
<view class="billing-total">
<text class="total-label">本月总计</text>
<text class="total-amount">¥{{ monthlyBilling.total_fee }}</text>
</view>
</view>
</view>
<!-- 快速操作 -->
<view class="quick-actions">
<view class="action-item" @click="contactCaregiver">
<view class="action-icon"></view>
<text class="action-text">联系护理员</text>
</view>
<view class="action-item" @click="viewPhotos">
<view class="action-icon"></view>
<text class="action-text">生活照片</text>
</view>
<view class="action-item" @click="feedbackSuggestion">
<view class="action-icon"></view>
<text class="action-text">意见反馈</text>
</view>
<view class="action-item" @click="emergencyContact">
<view class="action-icon"></view>
<text class="action-text">紧急联系</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import type { Elder, CareRecord, VitalSign } from '../types.uts'
import { formatDateTime, formatTime, getHealthStatusText, getHealthStatusClass, getRecordTypeText } from '../types.uts'
// 响应式数据
const familyName = ref<string>('家属')
const currentTime = ref<string>('')
// 老人信息
const elderInfo = ref<Elder>({
id: '',
name: '',
age: 0,
gender: '',
room_number: '',
bed_number: '',
health_status: '',
care_level: '',
admission_date: '',
profile_picture: null,
emergency_contact: '',
emergency_phone: '',
status: '',
created_at: '',
updated_at: ''
})
// 数据列表
const todayCareRecords = ref<Array<CareRecord>>([])
const recentVitals = ref<Array<VitalSign>>([])
// 探访信息
const nextVisit = ref<any>(null)
// 费用信息
const monthlyBilling = ref({
care_fee: 0,
meal_fee: 0,
other_fee: 0,
total_fee: 0
})
// 更新当前时间
const updateCurrentTime = () => {
const now = new Date()
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
currentTime.value = `今天 ${hours}:${minutes}`
}
// 获取今天的时间范围
const getTodayRange = () => {
const today = new Date()
const start = new Date(today.getFullYear(), today.getMonth(), today.getDate())
const end = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1)
return {
start: start.toISOString(),
end: end.toISOString()
}
}
// 加载老人基本信息
const loadElderInfo = async () => {
try {
const elderId = 'elder-001' // 替换为实际的老人ID可以从页面参数获取
const result = await supa
.from('ec_elders')
.select(`
id,
name,
age,
gender,
room_number,
bed_number,
health_status,
care_level,
admission_date,
profile_picture,
emergency_contact,
emergency_phone,
status,
created_at,
updated_at
`)
.eq('id', elderId)
.single()
.executeAs<Elder>()
if (result.error === null && result.data !== null) {
elderInfo.value = result.data
}
} catch (error) {
console.error('加载老人信息失败:', error)
}
}
// 加载今日护理记录
const loadTodayCareRecords = async () => {
try {
const { start, end } = getTodayRange()
const elderId = elderInfo.value.id
const result = await supa
.from('ec_care_records')
.select('id, description, record_type, created_at')
.eq('elder_id', elderId)
.gte('created_at', start)
.lt('created_at', end)
.order('created_at', { ascending: false })
.limit(5)
.executeAs<Array<CareRecord>>()
if (result.error === null && result.data !== null) {
todayCareRecords.value = result.data
}
} catch (error) {
console.error('加载护理记录失败:', error)
}
}
// 加载最近生命体征
const loadRecentVitals = async () => {
try {
const elderId = elderInfo.value.id
const result = await supa
.from('ec_vital_signs')
.select(`
id,
elder_id,
elder_name,
vital_type,
systolic_pressure,
diastolic_pressure,
heart_rate,
temperature,
oxygen_saturation,
glucose_level,
measured_at,
is_abnormal
`)
.eq('elder_id', elderId)
.order('measured_at', { ascending: false })
.limit(4)
.executeAs<Array<VitalSign>>()
if (result.error === null && result.data !== null) {
recentVitals.value = result.data
}
} catch (error) {
console.error('加载生命体征失败:', error)
}
}
// 加载下次探访安排
const loadNextVisit = async () => {
try {
const elderId = elderInfo.value.id
const now = new Date().toISOString()
const result = await supa
.from('ec_visits')
.select('id, visit_time, notes')
.eq('elder_id', elderId)
.gte('visit_time', now)
.order('visit_time', { ascending: true })
.limit(1)
.executeAs<Array<any>>()
if (result.error === null && result.data !== null && result.data.length > 0) {
nextVisit.value = result.data[0]
}
} catch (error) {
console.error('加载探访安排失败:', error)
}
}
// 加载本月费用
const loadMonthlyBilling = async () => {
try {
// 模拟费用数据,实际应该从数据库加载
monthlyBilling.value = {
care_fee: 3500,
meal_fee: 1200,
other_fee: 300,
total_fee: 5000
}
} catch (error) {
console.error('加载费用信息失败:', error)
}
}
// 获取生命体征图标
const getVitalIcon = (vitalType: string): string => {
switch (vitalType) {
case 'blood_pressure': return ''
case 'heart_rate': return ''
case 'temperature': return ''
case 'oxygen': return ''
case 'glucose': return ''
default: return ''
}
}
// 获取生命体征名称
const getVitalName = (vitalType: string): string => {
switch (vitalType) {
case 'blood_pressure': return '血压'
case 'heart_rate': return '心率'
case 'temperature': return '体温'
case 'oxygen': return '血氧'
case 'glucose': return '血糖'
default: return vitalType
}
}
// 获取生命体征值
const getVitalValue = (vital: VitalSign): string => {
switch (vital.vital_type) {
case 'blood_pressure':
return `${vital.systolic_pressure ?? 0}/${vital.diastolic_pressure ?? 0} mmHg`
case 'heart_rate':
return `${vital.heart_rate ?? 0} 次/分`
case 'temperature':
return `${vital.temperature ?? 0}°C`
case 'oxygen':
return `${vital.oxygen_saturation ?? 0}%`
case 'glucose':
return `${vital.glucose_level ?? 0} mmol/L`
default:
return '未知'
}
}
// 导航和操作函数
const viewAllCareRecords = () => {
uni.navigateTo({
url: `/pages/ec/family/care-records?elderId=${elderInfo.value.id}`
})
}
const viewHealthDetails = () => {
uni.navigateTo({
url: `/pages/ec/family/health-monitoring?elderId=${elderInfo.value.id}`
})
}
const scheduleVisit = () => {
uni.navigateTo({
url: `/pages/ec/family/schedule-visit?elderId=${elderInfo.value.id}`
})
}
const modifyVisit = () => {
if (nextVisit.value !== null) {
uni.navigateTo({
url: `/pages/ec/family/modify-visit?visitId=${nextVisit.value.id}`
})
}
}
const viewBillingDetails = () => {
uni.navigateTo({
url: `/pages/ec/family/billing?elderId=${elderInfo.value.id}`
})
}
// 快速操作
const contactCaregiver = () => {
uni.makePhoneCall({
phoneNumber: '13800138000'
})
}
const viewPhotos = () => {
uni.navigateTo({
url: `/pages/ec/family/photos?elderId=${elderInfo.value.id}`
})
}
const feedbackSuggestion = () => {
uni.navigateTo({
url: '/pages/ec/family/feedback'
})
}
const emergencyContact = () => {
uni.showActionSheet({
itemList: ['联系护理员', '联系医生', '联系管理员', '拨打急救电话'],
success: (res) => {
switch (res.tapIndex) {
case 0:
uni.makePhoneCall({ phoneNumber: '13800138000' })
break
case 1:
uni.makePhoneCall({ phoneNumber: '13800138001' })
break
case 2:
uni.makePhoneCall({ phoneNumber: '13800138002' })
break
case 3:
uni.makePhoneCall({ phoneNumber: '120' })
break
}
}
})
}
// 生命周期
onMounted(() => {
updateCurrentTime()
loadElderInfo().then(() => {
loadTodayCareRecords()
loadRecentVitals()
loadNextVisit()
})
loadMonthlyBilling()
// 定时更新时间
setInterval(() => {
updateCurrentTime()
}, 60000)
})
</script>
<style scoped>
/* uts-android 兼容性重构:
1. 移除所有嵌套选择器、伪类(如 :last-child全部 class 扁平化。
2. 所有间距用 margin-right/margin-bottom 控制,禁止 gap、flex-wrap、嵌套。
3. 所有布局 display: flex禁止 grid、gap、伪类。
4. 组件间距、分隔线全部用 border/margin 控制。
*/
.family-dashboard {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f5f5f5;
padding: 20px;
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: bold;
color: #333;
}
.welcome {
font-size: 14px;
color: #666;
}
.elder-status-card {
background-color: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.elder-profile {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 15px;
}
.elder-avatar {
width: 80px;
height: 80px;
border-radius: 40px;
margin-right: 20px;
overflow: hidden;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-image {
width: 100%;
height: 100%;
}
.avatar-fallback {
font-size: 32px;
color: #666;
font-weight: bold;
}
.elder-basic {
flex: 1;
display: flex;
flex-direction: column;
}
.elder-name {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.elder-info {
font-size: 16px;
color: #666;
margin-bottom: 5px;
}
.elder-room {
font-size: 16px;
color: #1890ff;
}
.elder-health {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.health-status {
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
}
.health-excellent {
background-color: #f6ffed;
color: #52c41a;
}
.health-good {
background-color: #e6f7ff;
color: #1890ff;
}
.health-fair {
background-color: #fff7e6;
color: #d48806;
}
.health-poor {
background-color: #fff2f0;
color: #ff4d4f;
}
.last-update {
font-size: 12px;
color: #999;
}
.today-care-section,
.health-monitoring-section,
.visit-section,
.billing-section {
background-color: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
}
.section-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.section-more {
font-size: 14px;
color: #1890ff;
}
.timeline-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.timeline-item.is-last {
border-bottom: none;
}
.timeline-dot {
width: 12px;
height: 12px;
border-radius: 6px;
margin-right: 15px;
background-color: #1890ff;
}
.timeline-dot.medication {
background-color: #52c41a;
}
.timeline-dot.hygiene {
background-color: #722ed1;
}
.timeline-dot.nutrition {
background-color: #fa8c16;
}
.timeline-content {
flex: 1;
display: flex;
flex-direction: column;
}
.care-title {
font-size: 14px;
color: #333;
margin-bottom: 5px;
}
.care-time {
font-size: 12px;
color: #999;
}
.care-type {
padding: 4px 8px;
border-radius: 4px;
background-color: #f0f0f0;
}
.type-text {
font-size: 12px;
color: #666;
}
.metric-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.metric-item.is-last {
border-bottom: none;
}
.metric-icon {
font-size: 24px;
margin-right: 15px;
}
.metric-info {
flex: 1;
display: flex;
flex-direction: column;
}
.metric-name {
font-size: 14px;
color: #333;
margin-bottom: 3px;
}
.metric-value {
font-size: 16px;
font-weight: bold;
color: #1890ff;
margin-bottom: 3px;
}
.metric-time {
font-size: 12px;
color: #999;
}
.metric-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.metric-status.normal {
background-color: #f6ffed;
color: #52c41a;
}
.metric-status.abnormal {
background-color: #fff2f0;
color: #ff4d4f;
}
.visit-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 15px;
background-color: #f0f9ff;
border-radius: 8px;
}
.visit-icon {
font-size: 24px;
margin-right: 15px;
}
.visit-details {
flex: 1;
display: flex;
flex-direction: column;
}
.visit-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.visit-time {
font-size: 14px;
color: #1890ff;
margin-bottom: 3px;
}
.visit-note {
font-size: 12px;
color: #666;
}
.visit-btn {
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
}
.no-visit {
text-align: center;
padding: 30px;
}
.no-visit-text {
font-size: 14px;
color: #666;
margin-bottom: 15px;
}
.schedule-btn {
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 6px;
padding: 10px 20px;
font-size: 14px;
}
.billing-summary {
display: flex;
flex-direction: column;
}
.billing-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
}
.billing-item.is-last {
border-bottom: none;
}
.billing-label {
font-size: 14px;
color: #666;
}
.billing-amount {
font-size: 16px;
color: #333;
font-weight: bold;
}
.billing-total {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 15px 0 10px;
border-top: 2px solid #f0f0f0;
margin-top: 10px;
}
.total-label {
font-size: 16px;
color: #333;
font-weight: bold;
}
.total-amount {
font-size: 20px;
color: #ff4d4f;
font-weight: bold;
}
.quick-actions {
display: flex;
flex-direction: row;
background-color: #fff;
border-radius: 8px;
padding: 15px;
}
.action-item {
width: 50%;
display: flex;
flex-direction: column;
align-items: center;
padding: 15px 10px;
}
.action-icon {
font-size: 32px;
margin-bottom: 8px;
}
.action-text {
font-size: 14px;
color: #666;
}
</style>