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

845 lines
19 KiB
Plaintext

<!-- 养老管理系统 - 管理员仪表板 (简化版) -->
<template>
<view class="admin-dashboard">
<view class="header">
<text class="title">养老管理系统</text>
<text class="welcome">管理员,{{ currentTime }}</text>
</view>
<!-- 数据概览卡片 -->
<view class="overview-section">
<view class="overview-card" v-for="(card, idx) in overviewCards" :key="idx" :class="{ 'is-last': idx === overviewCards.length - 1 }" @click="navTo(card.navurl)" >
<view class="card-icon">{{ card.icon }}</view>
<view class="card-content">
<text class="card-number">{{ card.number }}</text>
<text class="card-label">{{ card.label }}</text>
</view>
<view v-if="card.trend !== undefined" class="card-trend" :class="card.trend >= 0 ? 'positive' : 'negative'">
<text class="trend-text">{{ card.trend >= 0 ? '+' : '' }}{{ card.trend }}</text>
</view>
<view v-if="card.status !== undefined" class="card-status">
<text class="status-text">{{ card.status }}</text>
</view>
<view v-if="card.alert !== undefined && card.alert > 0" class="card-alert">
<text class="alert-text">需要处理</text>
</view>
</view>
</view>
<!-- 快速操作区 -->
<view class="actions-section">
<text class="section-title">快速操作</text>
<view class="actions-grid">
<view class="action-card" @click="navigateToElderManagement">
<view class="action-icon">👴</view>
<text class="action-title">老人管理</text>
<text class="action-desc">档案、健康、护理</text>
</view>
<view class="action-card" @click="navigateToCaregiverManagement">
<view class="action-icon">👩‍⚕️</view>
<text class="action-title">员工管理</text>
<text class="action-desc">排班、绩效、培训</text>
</view>
<view class="action-card" @click="navigateToHealthMonitoring">
<view class="action-icon">💊</view>
<text class="action-title">健康监测</text>
<text class="action-desc">体征、用药、预警</text>
</view>
<view class="action-card is-last" @click="navigateToServiceRecords">
<view class="action-icon">📋</view>
<text class="action-title">服务记录</text>
<text class="action-desc">护理、餐饮、活动</text>
</view>
</view>
</view>
<!-- 紧急提醒列表 -->
<view class="alerts-section">
<view class="section-header">
<text class="section-title">紧急提醒</text>
<text class="section-more" @click="navigateToAlerts">查看全部</text>
</view>
<view class="alerts-list">
<view v-for="alert in urgentAlerts" :key="alert.id" class="alert-item" :class="alert.severity"
@click="handleAlert(alert)">
<view class="alert-icon">
<text class="icon-text">{{ getAlertIconDisplay(alert.severity ?? '') }}</text>
</view>
<view class="alert-content">
<text class="alert-title">{{ alert.title ?? '' }}</text>
<text class="alert-elder">{{ alert.elder_name ?? '未知' }}</text>
<text class="alert-time">{{ formatDateTimeDisplay(alert.created_at ?? '') }}</text>
</view>
<view class="alert-actions">
<button class="alert-btn" @click.stop="acknowledgeAlert(alert)">处理</button>
</view>
</view>
</view>
</view>
<!-- 今日护理任务 -->
<view class="tasks-section">
<view class="section-header">
<text class="section-title">今日护理任务</text>
<text class="section-more" @click="navigateToTasks">查看全部</text>
</view>
<view class="tasks-list">
<view v-for="task in todayTasks" :key="task.id" class="task-item" :class="task.status"
@click="viewTaskDetail(task)">
<view class="task-info">
<text class="task-title">{{ task.task_name }}</text> <text
class="task-elder">{{ task.elder_name ?? '未知' }}</text>
<text class="task-time">{{ formatTimeDisplay(task.scheduled_time ?? '') }}</text>
</view>
<view class="task-status">
<view class="status-badge" :class="task.status">
<text class="badge-text">{{ getTaskStatusTextDisplay(task.status ?? '') }}</text>
</view>
</view>
<view class="task-caregiver">
<text class="caregiver-name">{{ task.caregiver_name ?? '未分配' }}</text>
</view>
</view>
</view>
</view>
<!-- 最近活动记录 -->
<view class="activities-section">
<view class="section-header">
<text class="section-title">最近活动</text>
<text class="section-more" @click="navigateToActivities">查看全部</text>
</view>
<view class="activities-list">
<view v-for="activity in recentActivities" :key="activity.id" class="activity-item">
<view class="activity-avatar">
<text class="avatar-text">{{ activity.elder_name?.charAt(0)??'--' }}</text>
</view>
<view class="activity-content">
<text class="activity-title">{{ activity.description ?? '' }}</text>
<text
class="activity-meta">{{ (activity.elder_name ?? '未知') + ' · ' + formatDateTimeDisplay(activity.created_at ?? '') }}</text>
</view>
<view class="activity-type">
<text class="type-tag"
:class="activity.record_type">{{ getRecordTypeTextDisplay(activity.record_type ?? '') }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import type { Elder, HealthAlert, CareTask, CareRecord, DashboardStats } from '../types.uts'
import { formatDateTime as formatDateTimeUtil, formatTime as formatTimeUtil, getAlertIcon as getAlertIconUtil, getTaskStatusText as getTaskStatusTextUtil, getRecordTypeText as getRecordTypeTextUtil } from '../types.uts' // 将函数作为方法暴露给模板
function formatDateTimeDisplay(dateTime : string | null) : string {
if (dateTime == null) return ''
return formatDateTimeUtil(dateTime)
}
function formatTimeDisplay(time : string | null) : string {
if (time == null) return ''
return formatTimeUtil(time)
}
function getAlertIconDisplay(severity : string) : string {
if (severity == null) return '❓'
return getAlertIconUtil(severity)
}
function getTaskStatusTextDisplay(status : string) : string {
if (status == null) return '未知'
return getTaskStatusTextUtil(status)
}
function getRecordTypeTextDisplay(type : string | null) : string {
return getRecordTypeTextUtil(type)
}
// 响应式数据
const currentTime = ref<string>('')
const stats = ref<DashboardStats>({
total_elders: 0,
total_caregivers: 0,
on_duty_caregivers: 0,
occupancy_rate: 0,
available_beds: 0,
urgent_alerts: 0,
elders_trend: 0
})
// 数据列表
const urgentAlerts = ref<Array<HealthAlert>>([])
const todayTasks = ref<Array<CareTask>>([])
const recentActivities = ref<Array<CareRecord>>([])
// 更新当前时间
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 = () : UTSJSONObject => {
const today = new Date()
const startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate())
const endDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1)
return {
start: startDate.toISOString(),
end: endDate.toISOString()
} as UTSJSONObject
}
// 加载统计数据
const loadStatistics = async () => {
try {
// 加载老人总数
const eldersResult = await supa
.from('ec_elders')
.select('*', { count: 'exact' })
.eq('status', 'active')
.executeAs<Elder>()
if (eldersResult.error === null) {
stats.value.total_elders = eldersResult.total ?? 0
}
// 加载护理员总数
const caregiversResult = await supa
.from('ak_users')
.select('*', { count: 'exact' })
.eq('role', 'caregiver')
.eq('status', 'active')
.executeAs<any>()
if (caregiversResult.error === null) {
stats.value.total_caregivers = caregiversResult.total ?? 0
stats.value.on_duty_caregivers = Math.floor((caregiversResult.total ?? 0) * 0.7) // 假设70%在班
}
// 计算入住率
const facilityResult = await supa
.from('ec_facilities')
.select('capacity, current_occupancy', {})
.single()
.executeAs<UTSJSONObject>()
if (facilityResult.error === null && facilityResult.data !== null) {
let facilityData = facilityResult.data
// 先判断是否为数组
if (Array.isArray(facilityData) && facilityData.length > 0) {
facilityData = facilityData[0]
}
let capacity = 0
let occupancy = 0
if (facilityData && typeof facilityData.get === 'function') {
capacity = facilityData.get('capacity') as number ?? 0
occupancy = facilityData.get('current_occupancy') as number ?? 0
} else if (facilityData) {
capacity = (facilityData['capacity'] as number) ?? 0
occupancy = (facilityData['current_occupancy'] as number) ?? 0
}
if (capacity > 0) {
stats.value.occupancy_rate = Math.round((occupancy / capacity) * 100)
stats.value.available_beds = capacity - occupancy
}
}
// 加载紧急提醒数量
const alertsResult = await supa
.from('ec_health_alerts')
.select('*', { count: 'exact' })
.in('severity', ['high', 'critical'])
.eq('status', 'active')
.executeAs<HealthAlert>()
if (alertsResult.error === null) {
stats.value.urgent_alerts = alertsResult.total ?? 0
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
}
// 加载紧急提醒列表
const loadUrgentAlerts = async () => {
try {
const result = await supa
.from('ec_health_alerts')
.select('id, title, severity, elder_id, created_at, status, ec_elders!ec_health_alerts_elder_id_fkey(name)', {})
.in('severity', ['high', 'critical'])
.eq('status', 'active')
.order('created_at', { ascending: false })
.limit(5)
.executeAs<Array<HealthAlert>>()
if (result.error === null && result.data !== null) {
urgentAlerts.value = result.data as Array<HealthAlert>
}
} catch (error) {
console.error('加载紧急提醒失败:', error)
}
}
// 加载今日任务列表
const loadTodayTasks = async () => {
try {
const todayRange = getTodayRange()
const start = todayRange.get('start') as string
const end = todayRange.get('end') as string
const result = await supa
.from('ec_care_tasks')
.select(`
id,
task_name,
elder_name,
scheduled_time,
status,
priority,
caregiver_name
`, {})
.gte('scheduled_time', start).lt('scheduled_time', end)
.order('scheduled_time', { ascending: true })
.limit(8)
.executeAs<Array<CareTask>>()
if (result.error === null && result.data !== null) {
todayTasks.value = result.data as Array<CareTask>
}
} catch (error) {
console.error('加载今日任务失败:', error)
}
}
// 加载最近活动记录
const loadRecentActivities = async () => {
try {
const threeDaysAgo = new Date()
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3)
const result = await supa
.from('ec_care_records')
.select('id, description, ec_care_records_elder_id_fkey(name) , record_type, created_at', {})
.gte('created_at', threeDaysAgo.toISOString())
.order('created_at', { ascending: false })
.limit(5)
.executeAs<Array<CareRecord>>()
if (result.error === null && result.data !== null) {
recentActivities.value = result.data as Array<CareRecord>
}
} catch (error) {
console.error('加载最近活动失败:', error)
}
}
// 处理提醒
const handleAlert = (alert : HealthAlert) => {
uni.navigateTo({
url: `/pages/ec/alerts/detail?id=${alert.id}`
})
}
const acknowledgeAlert = async (alert : HealthAlert) => {
try {
await supa
.from('ec_health_alerts')
.update({ status: 'acknowledged' })
.eq('id', alert.id)
.executeAs<any>()
// 重新加载数据
loadUrgentAlerts()
loadStatistics()
} catch (error) {
console.error('处理提醒失败:', error)
}
}
const viewTaskDetail = (task : CareTask) => {
uni.navigateTo({
url: `/pages/ec/tasks/detail?id=${task.id}`
})
}
// 导航函数
const navigateToElderManagement = () => {
uni.navigateTo({
url: '/pages/ec/admin/elder-management'
})
}
const navigateToCaregiverManagement = () => {
uni.navigateTo({
url: '/pages/ec/admin/caregiver-management'
})
}
const navigateToHealthMonitoring = () => {
uni.navigateTo({
url: '/pages/ec/admin/health-monitoring'
})
}
const navigateToServiceRecords = () => {
uni.navigateTo({
url: '/pages/ec/admin/service-records'
})
}
const navigateToAlerts = () => {
uni.navigateTo({
url: '/pages/ec/health/ecalert'
})
}
const navigateToTasks = () => {
uni.navigateTo({
url: '/pages/ec/tasks/list'
})
}
const navigateToActivities = () => {
uni.navigateTo({
url: '/pages/ec/activity/management'
})
}
// 数据概览卡片数据
const overviewCards = computed(() => [
{
icon: '👥',
number: stats.value.total_elders,
label: '入住老人',
trend: stats.value.elders_trend,
navurl: '/pages/ec/admin/elder-management'
},
{
icon: '👨‍⚕️',
number: stats.value.total_caregivers,
label: '护理人员',
status: `${stats.value.on_duty_caregivers} 在班`,
navurl: '/pages/ec/admin/caregiver-management'
},
{
icon: '🏥',
number: stats.value.occupancy_rate + '%',
label: '入住率',
status: `${stats.value.available_beds} 空床`,
navurl: '/pages/ec/admin/health-monitoring'
},
{
icon: '⚡',
number: stats.value.urgent_alerts,
label: '紧急提醒',
alert: stats.value.urgent_alerts,
navurl: '/pages/ec/health/ecalert-history'
}
])
// 生命周期
onMounted(() => {
updateCurrentTime()
loadStatistics()
loadUrgentAlerts()
loadTodayTasks()
loadRecentActivities()
// 定时更新时间
setInterval(() => {
updateCurrentTime()
}, 60000) // 每分钟更新一次
})
function navTo(url: string | undefined) {
if (url) {
uni.navigateTo({ url })
}
}
</script>
<style scoped>
.admin-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;
}
.overview-section {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 30px;
}
.overview-card {
flex: 1 1 160px;
min-width: 140px;
max-width: 220px;
background-color: #fff;
border-radius: 8px;
padding: 16px 10px;
margin-right: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
}
.overview-card.is-last {
margin-right: 0;
}
.card-icon {
font-size: 32px;
margin-right: 15px;
}
.card-content {
flex: 1;
display: flex;
flex-direction: column;
}
.card-number {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.card-label {
font-size: 14px;
color: #666;
}
.card-trend {
display: flex;
align-items: center;
}
.positive {
color: #52c41a;
}
.negative {
color: #ff4d4f;
}
.card-status {
display: flex;
align-items: center;
}
.status-text {
font-size: 12px;
color: #1890ff;
background-color: #e6f7ff;
padding: 4px 8px;
border-radius: 4px;
}
.card-alert {
display: flex;
align-items: center;
}
.alert-text {
font-size: 12px;
color: #ff4d4f;
background-color: #fff2f0;
padding: 4px 8px;
border-radius: 4px;
}
.actions-section {
margin-bottom: 30px;
}
.section-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
}
.actions-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 12px;
}
.action-card {
flex: 1 1 140px;
min-width: 110px;
max-width: 180px;
background-color: #fff;
border-radius: 8px;
padding: 14px 8px;
margin-right: 0;
margin-bottom: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
cursor: pointer;
}
.action-card.is-last {
margin-right: 0;
}
.action-icon {
font-size: 32px;
margin-bottom: 10px;
}
.action-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.action-desc {
font-size: 12px;
color: #666;
}
.alerts-section,
.tasks-section,
.activities-section {
background-color: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.section-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-more {
font-size: 14px;
color: #1890ff;
}
.alert-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f0f0f0;
}
.alert-item.is-last {
border-bottom: none;
}
.alert-icon {
margin-right: 15px;
}
.icon-text {
font-size: 24px;
}
.alert-content {
flex: 1;
display: flex;
flex-direction: column;
}
.alert-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.alert-elder {
font-size: 14px;
color: #666;
margin-bottom: 3px;
}
.alert-time {
font-size: 12px;
color: #999;
}
.alert-btn {
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
}
.task-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f0f0f0;
}
.task-item.is-last {
border-bottom: none;
}
.task-info {
flex: 1;
display: flex;
flex-direction: column;
}
.task-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.task-elder {
font-size: 14px;
color: #666;
margin-bottom: 3px;
}
.task-time {
font-size: 12px;
color: #999;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-badge-pending {
background-color: #fff7e6;
color: #d48806;
}
.status-badge-in_progress {
background-color: #e6f7ff;
color: #1890ff;
}
.status-badge-completed {
background-color: #f6ffed;
color: #52c41a;
}
.activity-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f0f0f0;
}
.activity-item.is-last {
border-bottom: none;
}
.activity-avatar {
width: 40px;
height: 40px;
border-radius: 20px;
background-color: #1890ff;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
}
.avatar-text {
color: #fff;
font-size: 16px;
font-weight: bold;
}
.activity-content {
flex: 1;
display: flex;
flex-direction: column;
}
.activity-title {
font-size: 14px;
color: #333;
margin-bottom: 5px;
}
.activity-meta {
font-size: 12px;
color: #999;
}
.type-tag {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
background-color: #f0f0f0;
color: #666;
}
@media (max-width: 600px) {
.overview-section {
gap: 8px;
}
.overview-card {
flex: 1 1 120px;
min-width: 100px;
max-width: 160px;
padding: 10px 4px;
margin-right: 6px;
margin-bottom: 8px;
}
.actions-grid {
gap: 6px;
}
.action-card {
flex: 1 1 90px;
min-width: 80px;
max-width: 120px;
padding: 8px 2px;
}
}
</style>