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,829 @@
<!-- 养老管理系统 - 家属仪表板 (简化版) -->
<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>