749 lines
16 KiB
Plaintext
749 lines
16 KiB
Plaintext
<template>
|
||
<view class="elder-management">
|
||
<!-- 顶部搜索和操作区 -->
|
||
<view class="header-section">
|
||
<view class="search-container">
|
||
<input class="search-input" placeholder="搜索老人姓名、房间号..." v-model="searchKeyword" @input="handleSearch" />
|
||
<view class="search-icon">🔍</view>
|
||
</view>
|
||
<view class="header-actions">
|
||
<button class="action-btn primary" @click="addNewElder">
|
||
<text class="btn-icon">➕</text>
|
||
<text class="btn-text">新增老人</text>
|
||
</button>
|
||
<button class="action-btn secondary" @click="exportElders">
|
||
<text class="btn-icon">📊</text>
|
||
<text class="btn-text">导出</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 统计卡片 -->
|
||
<view class="stats-container">
|
||
<view class="stat-card">
|
||
<view class="stat-number">{{ elderStats.total }}</view>
|
||
<view class="stat-label">总入住</view>
|
||
<view class="stat-trend positive">+{{ elderStats.new_this_month }}</view>
|
||
</view>
|
||
<view class="stat-card">
|
||
<view class="stat-number">{{ elderStats.self_care }}</view>
|
||
<view class="stat-label">自理老人</view>
|
||
<view class="stat-percent">{{ getSelfCarePercent() }}%</view>
|
||
</view>
|
||
<view class="stat-card">
|
||
<view class="stat-number">{{ elderStats.assisted_care }}</view>
|
||
<view class="stat-label">半护理</view>
|
||
<view class="stat-percent">{{ getAssistedCarePercent() }}%</view>
|
||
</view>
|
||
<view class="stat-card">
|
||
<view class="stat-number">{{ elderStats.full_care }}</view>
|
||
<view class="stat-label">全护理</view>
|
||
<view class="stat-percent">{{ getFullCarePercent() }}%</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 筛选器 -->
|
||
<view class="filter-container">
|
||
<scroll-view class="filter-scroll" direction="horizontal" :show-scrollbar="false">
|
||
<view class="filter-item" :class="{ active: selectedCareLevel === 'all' }" @click="filterByCareLevel('all')">
|
||
全部
|
||
</view>
|
||
<view class="filter-item" :class="{ active: selectedCareLevel === '1' }" @click="filterByCareLevel('1')">
|
||
一级护理
|
||
</view>
|
||
<view class="filter-item" :class="{ active: selectedCareLevel === '2' }" @click="filterByCareLevel('2')">
|
||
二级护理
|
||
</view>
|
||
<view class="filter-item" :class="{ active: selectedCareLevel === '3' }" @click="filterByCareLevel('3')">
|
||
三级护理
|
||
</view>
|
||
<view class="filter-item" :class="{ active: selectedCareLevel === '4' }" @click="filterByCareLevel('4')">
|
||
特级护理
|
||
</view>
|
||
<view class="filter-item" :class="{ active: selectedHealthStatus === 'stable' }" @click="filterByHealthStatus('stable')">
|
||
健康稳定
|
||
</view>
|
||
<view class="filter-item" :class="{ active: selectedHealthStatus === 'attention' }" @click="filterByHealthStatus('attention')">
|
||
需要关注
|
||
</view>
|
||
<view class="filter-item" :class="{ active: selectedHealthStatus === 'critical' }" @click="filterByHealthStatus('critical')">
|
||
危险状态
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 老人列表 -->
|
||
<view class="elders-list" v-if="filteredElders.length > 0">
|
||
<view class="elder-card" v-for="elder in filteredElders" :key="elder.id">
|
||
<view class="elder-info">
|
||
<view class="elder-avatar">
|
||
<image class="avatar-image" :src="elder.profile_picture" mode="aspectFill"
|
||
@error="handleAvatarError" v-if="elder.profile_picture" />
|
||
<text class="avatar-fallback" v-else>{{ elder.name.charAt(0) }}</text>
|
||
</view>
|
||
<view class="elder-details">
|
||
<view class="elder-name-row">
|
||
<text class="elder-name">{{ elder.name }}</text>
|
||
<view class="elder-status" :class="elder.health_status">
|
||
<text class="status-text">{{ getHealthStatusText(elder.health_status) }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="elder-info-text">{{ elder.age }}岁 · {{ elder.gender === 'male' ? '男' : '女' }}</text>
|
||
<text class="elder-room">{{ elder.room_number }}房 {{ elder.bed_number }}床</text>
|
||
<text class="elder-care-level">{{ getCareLevelText(elder.care_level) }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="elder-actions">
|
||
<button class="action-btn-small" @click="viewElderDetail(elder.id)">
|
||
<text class="btn-text">详情</text>
|
||
</button>
|
||
<button class="action-btn-small edit" @click="editElder(elder.id)">
|
||
<text class="btn-text">编辑</text>
|
||
</button>
|
||
<button class="action-btn-small health" @click="viewHealthRecord(elder.id)">
|
||
<text class="btn-text">健康</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<view class="empty-state" v-else>
|
||
<text class="empty-icon">👥</text>
|
||
<text class="empty-title">暂无老人信息</text>
|
||
<text class="empty-subtitle">点击"新增老人"按钮添加第一位老人</text>
|
||
<button class="empty-action-btn" @click="addNewElder">
|
||
<text class="btn-text">新增老人</text>
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 分页器 -->
|
||
<view class="pagination" v-if="totalPages > 1">
|
||
<button class="page-btn" :disabled="currentPage === 1" @click="goToPage(currentPage - 1)">
|
||
<text class="btn-text">上一页</text>
|
||
</button>
|
||
<view class="page-info">
|
||
<text class="page-text">第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</text>
|
||
</view>
|
||
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">
|
||
<text class="btn-text">下一页</text>
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 加载状态 -->
|
||
<view class="loading-overlay" v-if="isLoading">
|
||
<view class="loading-spinner"></view>
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.elder-management {
|
||
padding: 40rpx;
|
||
background-color: #f8f9fa;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.header-section {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 30rpx;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.search-container {
|
||
flex: 1;
|
||
position: relative;
|
||
}
|
||
|
||
.search-input {
|
||
width: 100%;
|
||
padding: 24rpx 60rpx 24rpx 24rpx;
|
||
background: white;
|
||
border-radius: 24rpx;
|
||
border: 1rpx solid #ddd;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.search-icon {
|
||
position: absolute;
|
||
right: 20rpx;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
font-size: 32rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
padding: 24rpx 32rpx;
|
||
border-radius: 20rpx;
|
||
border: none;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.action-btn.primary {
|
||
background: #007AFF;
|
||
color: white;
|
||
}
|
||
|
||
.action-btn.secondary {
|
||
background: white;
|
||
color: #007AFF;
|
||
border: 1rpx solid #007AFF;
|
||
}
|
||
|
||
.btn-icon {
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.stats-container {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.stat-card {
|
||
flex: 1;
|
||
background: white;
|
||
padding: 40rpx;
|
||
border-radius: 24rpx;
|
||
text-align: center;
|
||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 48rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
display: block;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.stat-trend {
|
||
font-size: 24rpx;
|
||
padding: 6rpx 12rpx;
|
||
border-radius: 12rpx;
|
||
display: inline-block;
|
||
}
|
||
|
||
.stat-trend.positive {
|
||
background: #e8f5e8;
|
||
color: #4caf50;
|
||
}
|
||
|
||
.stat-percent {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
background: #f0f0f0;
|
||
padding: 6rpx 12rpx;
|
||
border-radius: 12rpx;
|
||
display: inline-block;
|
||
}
|
||
|
||
.filter-container {
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.filter-scroll {
|
||
flex-direction: row;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-item {
|
||
display: inline-block;
|
||
padding: 20rpx 30rpx;
|
||
margin-right: 20rpx;
|
||
background: white;
|
||
border-radius: 20rpx;
|
||
border: 1rpx solid #ddd;
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-item.active {
|
||
background: #007AFF;
|
||
color: white;
|
||
border-color: #007AFF;
|
||
}
|
||
|
||
.elders-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24rpx;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.elder-card {
|
||
background: white;
|
||
border-radius: 24rpx;
|
||
padding: 32rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.elder-info {
|
||
display: flex;
|
||
align-items: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.elder-avatar {
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
border-radius: 50rpx;
|
||
margin-right: 24rpx;
|
||
overflow: hidden;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #f0f0f0;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.avatar-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.avatar-fallback {
|
||
font-size: 36rpx;
|
||
color: #666;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.elder-details {
|
||
flex: 1;
|
||
}
|
||
|
||
.elder-name-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.elder-name {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.elder-status {
|
||
padding: 6rpx 12rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.elder-status.stable {
|
||
background: #e8f5e8;
|
||
color: #4caf50;
|
||
}
|
||
|
||
.elder-status.attention {
|
||
background: #fff3e0;
|
||
color: #ff9800;
|
||
}
|
||
|
||
.elder-status.critical {
|
||
background: #ffebee;
|
||
color: #f44336;
|
||
}
|
||
|
||
.elder-info-text {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
display: block;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.elder-room {
|
||
font-size: 26rpx;
|
||
color: #007AFF;
|
||
display: block;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.elder-care-level {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
background: #f0f0f0;
|
||
padding: 4rpx 8rpx;
|
||
border-radius: 8rpx;
|
||
display: inline-block;
|
||
}
|
||
|
||
.elder-actions {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.action-btn-small {
|
||
padding: 16rpx 20rpx;
|
||
border-radius: 16rpx;
|
||
border: none;
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
background: #f0f0f0;
|
||
color: #666;
|
||
}
|
||
|
||
.action-btn-small.edit {
|
||
background: #007AFF;
|
||
color: white;
|
||
}
|
||
|
||
.action-btn-small.health {
|
||
background: #4caf50;
|
||
color: white;
|
||
}
|
||
|
||
.pagination {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 30rpx;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.page-btn {
|
||
padding: 20rpx 30rpx;
|
||
background: white;
|
||
border: 1rpx solid #ddd;
|
||
border-radius: 16rpx;
|
||
font-size: 26rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.page-btn:disabled {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.page-info {
|
||
background: white;
|
||
padding: 20rpx 30rpx;
|
||
border-radius: 16rpx;
|
||
border: 1rpx solid #ddd;
|
||
}
|
||
|
||
.page-text {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 120rpx 40rpx;
|
||
}
|
||
|
||
.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;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.empty-action-btn {
|
||
padding: 30rpx 60rpx;
|
||
background: #007AFF;
|
||
color: white;
|
||
border-radius: 20rpx;
|
||
border: none;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.loading-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(255,255,255,0.8);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
border: 6rpx solid #f0f0f0;
|
||
border-top: 6rpx solid #007AFF;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
margin-top: 20rpx;
|
||
}
|
||
</style>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { getHealthStatusText, getCareLevelText } from '../types.uts'
|
||
import type { ElderInfo, ElderStats } from '../types.uts'
|
||
|
||
// 数据状态
|
||
const elders = ref<ElderInfo[]>([])
|
||
const elderStats = ref<ElderStats>({
|
||
total: 0,
|
||
new_this_month: 0,
|
||
self_care: 0,
|
||
assisted_care: 0,
|
||
full_care: 0
|
||
})
|
||
|
||
// UI状态
|
||
const searchKeyword = ref('')
|
||
const selectedCareLevel = ref('all')
|
||
const selectedHealthStatus = ref('all')
|
||
const currentPage = ref(1)
|
||
const pageSize = ref(20)
|
||
const isLoading = ref(false)
|
||
|
||
// 计算属性
|
||
const filteredElders = computed(() => {
|
||
let filtered = [...elders.value]
|
||
|
||
// 搜索筛选
|
||
if (searchKeyword.value.trim()) {
|
||
const keyword = searchKeyword.value.toLowerCase()
|
||
filtered = filtered.filter(elder =>
|
||
elder.name.toLowerCase().includes(keyword) ||
|
||
(elder.room_number && elder.room_number.includes(keyword)) ||
|
||
(elder.bed_number && elder.bed_number.includes(keyword))
|
||
)
|
||
}
|
||
|
||
// 护理等级筛选
|
||
if (selectedCareLevel.value !== 'all') {
|
||
filtered = filtered.filter(elder => elder.care_level === selectedCareLevel.value)
|
||
}
|
||
|
||
// 健康状态筛选
|
||
if (selectedHealthStatus.value !== 'all') {
|
||
filtered = filtered.filter(elder => elder.health_status === selectedHealthStatus.value)
|
||
}
|
||
|
||
// 分页
|
||
const start = (currentPage.value - 1) * pageSize.value
|
||
const end = start + pageSize.value
|
||
return filtered.slice(start, end)
|
||
})
|
||
|
||
const totalPages = computed(() => {
|
||
const totalFiltered = getTotalFilteredCount()
|
||
return Math.ceil(totalFiltered / pageSize.value)
|
||
})
|
||
|
||
// 辅助函数
|
||
function getTotalFilteredCount(): number {
|
||
let filtered = [...elders.value]
|
||
|
||
if (searchKeyword.value.trim()) {
|
||
const keyword = searchKeyword.value.toLowerCase()
|
||
filtered = filtered.filter(elder =>
|
||
elder.name.toLowerCase().includes(keyword) ||
|
||
(elder.room_number && elder.room_number.includes(keyword)) ||
|
||
(elder.bed_number && elder.bed_number.includes(keyword))
|
||
)
|
||
}
|
||
|
||
if (selectedCareLevel.value !== 'all') {
|
||
filtered = filtered.filter(elder => elder.care_level === selectedCareLevel.value)
|
||
}
|
||
|
||
if (selectedHealthStatus.value !== 'all') {
|
||
filtered = filtered.filter(elder => elder.health_status === selectedHealthStatus.value)
|
||
}
|
||
|
||
return filtered.length
|
||
}
|
||
|
||
function getSelfCarePercent(): number {
|
||
if (elderStats.value.total === 0) return 0
|
||
return Math.round((elderStats.value.self_care / elderStats.value.total) * 100)
|
||
}
|
||
|
||
function getAssistedCarePercent(): number {
|
||
if (elderStats.value.total === 0) return 0
|
||
return Math.round((elderStats.value.assisted_care / elderStats.value.total) * 100)
|
||
}
|
||
|
||
function getFullCarePercent(): number {
|
||
if (elderStats.value.total === 0) return 0
|
||
return Math.round((elderStats.value.full_care / elderStats.value.total) * 100)
|
||
}
|
||
|
||
function handleAvatarError() {
|
||
// 头像加载失败时的处理
|
||
}
|
||
|
||
// 事件处理
|
||
function handleSearch() {
|
||
currentPage.value = 1
|
||
}
|
||
|
||
function filterByCareLevel(level: string) {
|
||
selectedCareLevel.value = level
|
||
selectedHealthStatus.value = 'all'
|
||
currentPage.value = 1
|
||
}
|
||
|
||
function filterByHealthStatus(status: string) {
|
||
selectedHealthStatus.value = status
|
||
selectedCareLevel.value = 'all'
|
||
currentPage.value = 1
|
||
}
|
||
|
||
function goToPage(page: number) {
|
||
if (page >= 1 && page <= totalPages.value) {
|
||
currentPage.value = page
|
||
}
|
||
}
|
||
|
||
function addNewElder() {
|
||
uni.navigateTo({
|
||
url: '/pages/ec/admin/elder-form'
|
||
})
|
||
}
|
||
|
||
function viewElderDetail(elderId: string) {
|
||
uni.navigateTo({
|
||
url: `/pages/ec/admin/elder-detail?elder_id=${elderId}`
|
||
})
|
||
}
|
||
|
||
function editElder(elderId: string) {
|
||
uni.navigateTo({
|
||
url: `/pages/ec/admin/elder-form?elder_id=${elderId}`
|
||
})
|
||
}
|
||
|
||
function viewHealthRecord(elderId: string) {
|
||
uni.navigateTo({
|
||
url: `/pages/ec/admin/health-record?elder_id=${elderId}`
|
||
})
|
||
}
|
||
|
||
async function exportElders() {
|
||
try {
|
||
isLoading.value = true
|
||
const supa = (globalThis as any).supa
|
||
const result = await supa.executeAs('export_elders', {
|
||
filters: {
|
||
care_level: selectedCareLevel.value,
|
||
health_status: selectedHealthStatus.value,
|
||
search_keyword: searchKeyword.value
|
||
}
|
||
})
|
||
|
||
if (result && result.length > 0) {
|
||
uni.showToast({
|
||
title: '导出成功',
|
||
icon: 'success'
|
||
})
|
||
// 这里可以处理导出文件
|
||
}
|
||
} catch (error) {
|
||
console.error('导出失败:', error)
|
||
uni.showToast({
|
||
title: '导出失败',
|
||
icon: 'error'
|
||
})
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 数据加载
|
||
async function loadElders() {
|
||
try {
|
||
isLoading.value = true
|
||
const supa = (globalThis as any).supa
|
||
const result = await supa.executeAs('get_elders_list', {
|
||
page: currentPage.value,
|
||
page_size: pageSize.value
|
||
})
|
||
|
||
if (result && result.length > 0) {
|
||
elders.value = result
|
||
}
|
||
} catch (error) {
|
||
console.error('加载老人列表失败:', error)
|
||
uni.showToast({
|
||
title: '加载失败',
|
||
icon: 'error'
|
||
})
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadElderStats() {
|
||
try {
|
||
const supa = (globalThis as any).supa
|
||
const result = await supa.executeAs('get_elder_stats')
|
||
|
||
if (result && result.length > 0) {
|
||
elderStats.value = result[0]
|
||
}
|
||
} catch (error) {
|
||
console.error('加载统计数据失败:', error)
|
||
}
|
||
}
|
||
|
||
// 初始化
|
||
onMounted(async () => {
|
||
await Promise.all([
|
||
loadElders(),
|
||
loadElderStats()
|
||
])
|
||
})
|
||
</script>
|