Files
akmon/pages/ec/admin/elder-management_new.uvue
2026-01-20 08:04:15 +08:00

749 lines
16 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>
<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>