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

838 lines
17 KiB
Plaintext
Raw 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>
<scroll-view class="care-records-container">
<!-- Header with Filter -->
<view class="records-header">
<text class="header-title">护理记录</text>
<text class="header-subtitle">{{ elderInfo.name }}的详细护理历史</text>
<view class="filter-bar">
<picker class="filter-picker" mode="selector" :value="selectedTimeFilter" :range="timeFilterLabels" @change="onTimeFilterChange">
<view class="picker-item">
<text class="picker-text">{{ timeFilterLabels[selectedTimeFilter] }}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
<picker class="filter-picker" mode="selector" :value="selectedTypeFilter" :range="typeFilterLabels" @change="onTypeFilterChange">
<view class="picker-item">
<text class="picker-text">{{ typeFilterLabels[selectedTypeFilter] }}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
</view>
<!-- Records Summary -->
<view class="summary-cards">
<view class="summary-card">
<text class="summary-number">{{ recordsSummary.total }}</text>
<text class="summary-label">总记录数</text>
</view>
<view class="summary-card">
<text class="summary-number">{{ recordsSummary.today }}</text>
<text class="summary-label">今日记录</text>
</view>
<view class="summary-card">
<text class="summary-number">{{ recordsSummary.this_week }}</text>
<text class="summary-label">本周记录</text>
</view>
</view>
<!-- Records Timeline -->
<view v-if="filteredRecords.length > 0" class="records-timeline">
<view v-for="(dateGroup, dateKey) in groupedRecords" :key="dateKey" class="date-group">
<view class="date-header">
<text class="date-text">{{ formatDateHeader(dateKey) }}</text>
<text class="date-count">{{ dateGroup.length }}条记录</text>
</view>
<view v-for="record in dateGroup" :key="record.id" class="record-item" :class="getRecordTypeClass(record.type)">
<view class="record-timeline-marker" :class="getRecordTypeClass(record.type)"></view>
<view class="record-content-wrapper">
<view class="record-header">
<text class="record-type">{{ getCareRecordTypeText(record.type) }}</text>
<text class="record-time">{{ formatTime(record.created_at) }}</text>
</view>
<view class="record-content">
<text class="record-notes">{{ record.notes }}</text>
<view class="record-details" v-if="record.details">
<view class="detail-item" v-for="(value, key) in parseRecordDetails(record.details)" :key="key">
<text class="detail-label">{{ getDetailLabel(key) }}:</text>
<text class="detail-value">{{ value }}</text>
</view>
</view>
</view>
<view class="record-footer">
<text class="record-caregiver">护理员:{{ record.caregiver_name }}</text>
<view class="record-actions">
<button class="action-btn" @tap="viewRecordDetail(record)" v-if="record.type === 'vital_signs'">
<text class="btn-text">查看详情</text>
</button>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- Empty State -->
<view v-else class="empty-records">
<text class="empty-icon">📋</text>
<text class="empty-title">暂无护理记录</text>
<text class="empty-subtitle">选择不同的筛选条件查看记录</text>
</view>
<!-- Record Detail Modal -->
<view class="record-modal" v-if="showRecordDetail" @tap="hideRecordDetail">
<view class="modal-content" @tap.stop>
<view class="modal-header">
<text class="modal-title">{{ getCareRecordTypeText(selectedRecord.type) }}</text>
<button class="close-btn" @tap="hideRecordDetail">
<text class="close-icon">×</text>
</button>
</view>
<view class="modal-body">
<view class="detail-section">
<text class="section-title">基本信息</text>
<view class="detail-row">
<text class="detail-label">记录时间:</text>
<text class="detail-value">{{ formatDateTime(selectedRecord.created_at) }}</text>
</view>
<view class="detail-row">
<text class="detail-label">护理员:</text>
<text class="detail-value">{{ selectedRecord.caregiver_name }}</text>
</view>
</view>
<view class="detail-section" v-if="selectedRecord.type === 'vital_signs'">
<text class="section-title">生命体征</text>
<view class="vitals-grid" v-if="selectedRecord.vital_signs">
<view class="vital-item" v-for="vital in selectedRecord.vital_signs" :key="vital.type">
<text class="vital-label">{{ getVitalLabel(vital.type) }}</text>
<text class="vital-value">{{ vital.value }}{{ getVitalUnit(vital.type) }}</text>
<text class="vital-status" :class="vital.status">{{ getStatusText(vital.status) }}</text>
</view>
</view>
</view>
<view class="detail-section" v-if="selectedRecord.notes">
<text class="section-title">护理记录</text>
<text class="notes-content">{{ selectedRecord.notes }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</template>
<style scoped>
.care-records-container {
min-height: 100vh;
background-color: #f8f9fa;
}
.records-header {
background: white;
padding: 40rpx;
border-bottom: 1rpx solid #eee;
}
.header-title {
font-size: 44rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.header-subtitle {
font-size: 28rpx;
color: #666;
display: block;
margin-bottom: 30rpx;
}
.filter-bar {
display: flex;
gap: 20rpx;
}
.filter-picker {
flex: 1;
}
.picker-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
border: 1rpx solid #ddd;
}
.picker-text {
font-size: 28rpx;
color: #333;
}
.picker-arrow {
font-size: 24rpx;
color: #666;
}
.summary-cards {
display: flex;
padding: 40rpx;
gap: 20rpx;
}
.summary-card {
flex: 1;
background: white;
padding: 30rpx;
border-radius: 16rpx;
text-align: center;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.summary-number {
font-size: 48rpx;
font-weight: bold;
color: #007AFF;
display: block;
margin-bottom: 8rpx;
}
.summary-label {
font-size: 26rpx;
color: #666;
display: block;
}
.records-timeline {
padding: 0 40rpx 40rpx;
}
.date-group {
margin-bottom: 40rpx;
}
.date-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
padding: 20rpx;
background: white;
border-radius: 12rpx;
}
.date-text {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.date-count {
font-size: 24rpx;
color: #666;
background: #f0f0f0;
padding: 6rpx 12rpx;
border-radius: 12rpx;
}
.record-item {
display: flex;
margin-bottom: 20rpx;
position: relative;
}
.record-timeline-marker {
width: 24rpx;
height: 24rpx;
border-radius: 12rpx;
margin-right: 20rpx;
margin-top: 20rpx;
flex-shrink: 0;
}
.record-timeline-marker.vital_signs {
background: #ff6b6b;
}
.record-timeline-marker.medication {
background: #4ecdc4;
}
.record-timeline-marker.nursing {
background: #45b7d1;
}
.record-timeline-marker.meal {
background: #f9ca24;
}
.record-timeline-marker.activity {
background: #6c5ce7;
}
.record-timeline-marker.incident {
background: #fd79a8;
}
.record-content-wrapper {
flex: 1;
background: white;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.record-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.record-type {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.record-time {
font-size: 26rpx;
color: #666;
}
.record-content {
margin-bottom: 16rpx;
}
.record-notes {
font-size: 28rpx;
color: #333;
line-height: 1.6;
display: block;
margin-bottom: 12rpx;
}
.record-details {
background: #f8f9fa;
padding: 16rpx;
border-radius: 8rpx;
}
.detail-item {
display: flex;
margin-bottom: 8rpx;
}
.detail-item:last-child {
margin-bottom: 0;
}
.detail-label {
width: 160rpx;
font-size: 26rpx;
color: #666;
flex-shrink: 0;
}
.detail-value {
font-size: 26rpx;
color: #333;
flex: 1;
}
.record-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 16rpx;
border-top: 1rpx solid #f0f0f0;
}
.record-caregiver {
font-size: 24rpx;
color: #666;
}
.record-actions {
display: flex;
gap: 12rpx;
}
.action-btn {
padding: 12rpx 20rpx;
background: #007AFF;
color: white;
border-radius: 16rpx;
border: none;
font-size: 24rpx;
}
.empty-records {
padding: 120rpx 40rpx;
text-align: center;
}
.empty-icon {
font-size: 120rpx;
display: block;
margin-bottom: 30rpx;
opacity: 0.3;
}
.empty-title {
font-size: 36rpx;
color: #333;
display: block;
margin-bottom: 12rpx;
font-weight: 600;
}
.empty-subtitle {
font-size: 28rpx;
color: #666;
display: block;
}
.record-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;
border-radius: 24rpx;
width: 90%;
max-width: 800rpx;
max-height: 80%;
overflow: hidden;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #eee;
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.close-btn {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
background: #f0f0f0;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.close-icon {
font-size: 40rpx;
color: #666;
}
.modal-body {
padding: 40rpx;
max-height: 600rpx;
overflow-y: auto;
}
.detail-section {
margin-bottom: 40rpx;
}
.detail-section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 20rpx;
}
.detail-row {
display: flex;
margin-bottom: 16rpx;
}
.detail-row:last-child {
margin-bottom: 0;
}
.vitals-grid {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.vital-item {
flex: 1;
min-width: 200rpx;
background: #f8f9fa;
padding: 24rpx;
border-radius: 12rpx;
text-align: center;
}
.vital-label {
font-size: 26rpx;
color: #666;
display: block;
margin-bottom: 8rpx;
}
.vital-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.vital-status {
font-size: 24rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
display: inline-block;
}
.vital-status.normal {
background: #e8f5e8;
color: #4caf50;
}
.vital-status.warning {
background: #fff3e0;
color: #ff9800;
}
.vital-status.danger {
background: #ffebee;
color: #f44336;
}
.notes-content {
font-size: 28rpx;
color: #333;
line-height: 1.6;
background: #f8f9fa;
padding: 20rpx;
border-radius: 12rpx;
}
</style>
<script setup lang="uts">
import { ref, computed, onMounted } from 'vue'
import { formatDate, formatTime, formatDateTime, getStatusText } from '../types.uts'
import type { ElderInfo, CareRecord, HealthVitals } from '../types.uts'
// 数据状态
const elderInfo = ref<ElderInfo>({
id: '',
name: '',
age: 0,
gender: 'male',
room_number: '',
bed_number: '',
admission_date: '',
health_status: 'stable',
care_level: 1,
emergency_contact: '',
profile_picture: '',
family_contact: ''
})
const allRecords = ref<CareRecord[]>([])
const recordsSummary = ref({
total: 0,
today: 0,
this_week: 0
})
const selectedTimeFilter = ref(0)
const selectedTypeFilter = ref(0)
const showRecordDetail = ref(false)
const selectedRecord = ref<CareRecord>({
id: '',
elder_id: '',
type: 'nursing',
notes: '',
created_at: '',
caregiver_id: '',
caregiver_name: '',
details: null,
vital_signs: null
})
// 筛选选项
const timeFilters = [
{ label: '全部', value: 'all' },
{ label: '今天', value: 'today' },
{ label: '本周', value: 'week' },
{ label: '本月', value: 'month' }
]
const typeFilters = [
{ label: '全部类型', value: 'all' },
{ label: '生命体征', value: 'vital_signs' },
{ label: '用药记录', value: 'medication' },
{ label: '护理服务', value: 'nursing' },
{ label: '用餐记录', value: 'meal' },
{ label: '活动记录', value: 'activity' },
{ label: '事件记录', value: 'incident' }
]
const timeFilterLabels = computed(() => timeFilters.map(f => f.label))
const typeFilterLabels = computed(() => typeFilters.map(f => f.label))
// 筛选记录
const filteredRecords = computed(() => {
let records = [...allRecords.value]
// 时间筛选
const timeFilter = timeFilters[selectedTimeFilter.value]
if (timeFilter.value !== 'all') {
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
records = records.filter(record => {
const recordDate = new Date(record.created_at)
switch (timeFilter.value) {
case 'today':
return recordDate >= today
case 'week':
const weekStart = new Date(today)
weekStart.setDate(today.getDate() - today.getDay())
return recordDate >= weekStart
case 'month':
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1)
return recordDate >= monthStart
default:
return true
}
})
}
// 类型筛选
const typeFilter = typeFilters[selectedTypeFilter.value]
if (typeFilter.value !== 'all') {
records = records.filter(record => record.type === typeFilter.value)
}
return records
})
// 按日期分组记录
const groupedRecords = computed(() => {
const groups: Record<string, CareRecord[]> = {}
filteredRecords.value.forEach(record => {
const date = new Date(record.created_at).toISOString().split('T')[0]
if (!groups[date]) {
groups[date] = []
}
groups[date].push(record)
})
// 按日期排序
const sortedGroups: Record<string, CareRecord[]> = {}
Object.keys(groups)
.sort((a, b) => new Date(b).getTime() - new Date(a).getTime())
.forEach(date => {
// 每组内按时间排序
sortedGroups[date] = groups[date].sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
)
})
return sortedGroups
})
// 辅助函数
function getCareRecordTypeText(type: string): string {
const typeMap = {
'vital_signs': '生命体征',
'medication': '用药记录',
'nursing': '护理服务',
'meal': '用餐记录',
'activity': '活动记录',
'incident': '事件记录'
}
return typeMap[type] || type
}
function getRecordTypeClass(type: string): string {
return 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 formatDateHeader(dateStr: string): string {
const date = new Date(dateStr)
const today = new Date()
const yesterday = new Date(today)
yesterday.setDate(today.getDate() - 1)
if (date.toDateString() === today.toDateString()) {
return '今天'
} else if (date.toDateString() === yesterday.toDateString()) {
return '昨天'
} else {
return formatDate(dateStr)
}
}
function parseRecordDetails(details: string | null): Record<string, string> {
if (!details) return {}
try {
return JSON.parse(details)
} catch {
return {}
}
}
function getDetailLabel(key: string): string {
const labels = {
'duration': '持续时间',
'dosage': '剂量',
'location': '位置',
'temperature': '温度',
'amount': '数量',
'notes': '备注'
}
return labels[key] || key
}
// 事件处理
function onTimeFilterChange(e: any) {
selectedTimeFilter.value = e.detail.value
}
function onTypeFilterChange(e: any) {
selectedTypeFilter.value = e.detail.value
}
function viewRecordDetail(record: CareRecord) {
selectedRecord.value = record
showRecordDetail.value = true
}
function hideRecordDetail() {
showRecordDetail.value = false
}
// 数据加载
async function loadElderInfo() {
try {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const elderId = currentPage.$route.query?.elder_id as string
if (!elderId) {
uni.showToast({
title: '缺少老人ID',
icon: 'error'
})
return
}
const supa = (globalThis as any).supa
const result = await supa.executeAs('get_elder_info', {
elder_id: elderId
})
if (result && result.length > 0) {
elderInfo.value = result[0]
}
} catch (error) {
console.error('加载老人信息失败:', error)
}
}
async function loadCareRecords() {
try {
const supa = (globalThis as any).supa
const result = await supa.executeAs('get_elder_care_records', {
elder_id: elderInfo.value.id,
limit: 100
})
if (result && result.length > 0) {
allRecords.value = result
}
} catch (error) {
console.error('加载护理记录失败:', error)
}
}
async function loadRecordsSummary() {
try {
const supa = (globalThis as any).supa
const result = await supa.executeAs('get_care_records_summary', {
elder_id: elderInfo.value.id
})
if (result && result.length > 0) {
recordsSummary.value = result[0]
}
} catch (error) {
console.error('加载记录统计失败:', error)
}
}
// 初始化
onMounted(async () => {
await loadElderInfo()
if (elderInfo.value.id) {
await Promise.all([
loadCareRecords(),
loadRecordsSummary()
])
}
})
</script>