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,748 @@
<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>