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

648 lines
16 KiB
Plaintext
Raw Permalink 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="caregiver-management-container" direction="vertical">
<!-- Header -->
<view class="header">
<view class="header-content">
<text class="header-title">护工管理</text>
<text class="header-subtitle">管理护理人员信息和工作安排</text>
</view>
<button class="add-btn" @click="addCaregiver">
<text class="btn-text">+ 添加护工</text>
</button>
</view>
<!-- Statistics -->
<view class="stats-section">
<view class="stats-flex">
<view class="stat-card">
<text class="stat-number">{{ stats.total_caregivers }}</text>
<text class="stat-label">总护工数</text>
<text class="stat-trend">在岗人员</text>
</view>
<view class="stat-card">
<text class="stat-number">{{ stats.active_caregivers }}</text>
<text class="stat-label">在线人数</text>
<text class="stat-trend">当前班次</text>
</view>
<view class="stat-card">
<text class="stat-number">{{ stats.on_leave }}</text>
<text class="stat-label">请假人数</text>
<text class="stat-trend">今日</text>
</view>
<view class="stat-card">
<text class="stat-number">{{ stats.workload_avg }}%</text>
<text class="stat-label">平均工作量</text>
<text class="stat-trend">本周</text>
</view>
</view>
</view>
<!-- Filter Section -->
<view class="filter-section">
<view class="search-box">
<input class="search-input" v-model="searchKeyword" placeholder="搜索护工姓名或工号" />
<text class="search-icon">🔍</text>
</view>
<scroll-view class="filter-tabs" direction="horizontal">
<view v-for="filter in filterOptions" :key="filter.value"
class="filter-tab" :class="currentFilter === filter.value ? 'active' : ''"
@tap="setFilter(filter.value)">
<text class="filter-text">{{ filter.label }}</text>
<text v-if="filter.count > 0" class="filter-count">{{ filter.count }}</text>
</view>
</scroll-view>
</view>
<!-- Caregiver List -->
<view class="caregivers-section">
<view v-if="filteredCaregivers.length === 0" class="empty-state">
<text class="empty-icon">👥</text>
<text class="empty-text">暂无护工信息</text>
<text class="empty-subtitle">点击右上角添加新的护工</text>
</view>
<view v-else class="caregivers-list">
<view v-for="caregiver in filteredCaregivers" :key="caregiver.id"
class="caregiver-card" @click="viewCaregiverDetail(caregiver)">
<view class="caregiver-header">
<view class="caregiver-avatar-section">
<image class="caregiver-avatar" :src="caregiver.avatar ?? '/static/default-avatar.png'" mode="aspectFill"></image>
<view class="status-indicator" :class="getStatusClass(caregiver.status)"></view>
</view>
<view class="caregiver-info">
<text class="caregiver-name">{{ caregiver.name }}</text>
<text class="caregiver-id">工号: {{ caregiver.employee_id }}</text>
<text class="caregiver-level">{{ getLevelText(caregiver.care_level) }}</text>
</view>
<view class="caregiver-status">
<text class="status-text" :class="getStatusClass(caregiver.status)">
{{ getStatusText(caregiver.status) }}
</text>
</view>
</view>
<view class="caregiver-details">
<view class="detail-row">
<view class="detail-item">
<text class="detail-label">联系电话:</text>
<text class="detail-value">{{ caregiver.phone ?? '--' }}</text>
</view>
<view class="detail-item">
<text class="detail-label">入职时间:</text>
<text class="detail-value">{{ formatDate(caregiver.hire_date) }}</text>
</view>
</view>
<view class="detail-row">
<view class="detail-item">
<text class="detail-label">负责老人:</text>
<text class="detail-value">{{ caregiver.assigned_elders ?? 0 }} 人</text>
</view>
<view class="detail-item">
<text class="detail-label">本月评分:</text>
<text class="detail-value rating" :class="getRatingClass(caregiver.rating)">
{{ caregiver.rating ?? '--' }}
</text>
</view>
</view>
</view>
<view class="caregiver-actions">
<button class="action-btn primary" @click.stop="editCaregiver(caregiver)">
<text class="btn-text">编辑</text>
</button>
<button class="action-btn secondary" @click.stop="viewSchedule(caregiver)">
<text class="btn-text">排班</text>
</button>
<button class="action-btn" :class="caregiver.status === 'active' ? 'danger' : 'success'"
@click.stop="toggleStatus(caregiver)">
<text class="btn-text">{{ caregiver.status === 'active' ? '停用' : '启用' }}</text>
</button>
</view>
</view>
</view>
</view>
</scroll-view>
</template>
<script lang="uts">
import { CaregiverInfo, CaregiverStats } from '../types.uts'
import supa from '@/components/supadb/aksupainstance.uts'
export default {
data() {
return {
caregivers: [] as CaregiverInfo[],
stats: {
total_caregivers: 0,
active_caregivers: 0,
on_leave: 0,
workload_avg: 0
} as CaregiverStats,
searchKeyword: '',
currentFilter: 'all',
filterOptions: [
{ label: '全部', value: 'all', count: 0 },
{ label: '在岗', value: 'active', count: 0 },
{ label: '请假', value: 'on_leave', count: 0 },
{ label: '离职', value: 'inactive', count: 0 }
]
}
},
computed: {
filteredCaregivers(): CaregiverInfo[] {
let filtered = this.caregivers
// 按关键词搜索
if (this.searchKeyword.trim() !== '') {
const keyword = this.searchKeyword.toLowerCase()
filtered = filtered.filter(caregiver =>
caregiver.name.toLowerCase().includes(keyword) ||
caregiver.employee_id.toLowerCase().includes(keyword)
)
}
// 按状态筛选
if (this.currentFilter !== 'all') {
filtered = filtered.filter(caregiver => caregiver.status === this.currentFilter)
}
return filtered
}
},
onLoad() {
this.loadCaregiverData()
},
onShow() {
this.loadCaregiverData()
},
methods: {
async loadCaregiverData() {
try {
// 获取护工列表ak_users表role为caregiver
const caregiversResult = await supa
.from('ak_users')
.select('*')
.eq('role', 'caregiver')
.executeAs<CaregiverInfo>()
if (caregiversResult.error) throw caregiversResult.error
this.caregivers = caregiversResult.data || []
// 统计数据
const totalRes = await supa.from('ak_users').select('*', { count: 'exact' }).eq('role', 'caregiver').executeAs<CaregiverInfo>()
const activeRes = await supa.from('ak_users').select('*', { count: 'exact' }).eq('role', 'caregiver').eq('status', 'active').executeAs<CaregiverInfo>()
const leaveRes = await supa.from('ak_users').select('*', { count: 'exact' }).eq('role', 'caregiver').eq('status', 'on_leave').executeAs<CaregiverInfo>()
const inactiveRes = await supa.from('ak_users').select('*', { count: 'exact' }).eq('role', 'caregiver').eq('status', 'inactive').executeAs<CaregiverInfo>()
// 平均工作量(假设有 assigned_elders 字段为数字)
let workloadSum = 0
let workloadCount = 0
for (const c of this.caregivers) {
if (typeof c.assigned_elders === 'number') {
workloadSum += c.assigned_elders
workloadCount++
}
}
this.stats = {
total_caregivers: totalRes.count || 0,
active_caregivers: activeRes.count || 0,
on_leave: leaveRes.count || 0,
workload_avg: workloadCount > 0 ? Math.round(workloadSum / workloadCount) : 0
}
this.updateFilterCounts()
} catch (error) {
console.error('加载护工数据失败:', error)
uni.showToast({
title: '加载失败',
icon: 'error'
})
}
},
updateFilterCounts() {
this.filterOptions.forEach(filter => {
if (filter.value === 'all') {
filter.count = this.caregivers.length
} else {
filter.count = this.caregivers.filter(c => c.status === filter.value).length
}
})
},
setFilter(filter: string) {
this.currentFilter = filter
},
addCaregiver() {
uni.navigateTo({
url: '/pages/ec/admin/caregiver-form'
})
},
editCaregiver(caregiver: CaregiverInfo) {
uni.navigateTo({
url: `/pages/ec/admin/caregiver-form?id=${caregiver.id}`
})
},
viewCaregiverDetail(caregiver: CaregiverInfo) {
uni.navigateTo({
url: `/pages/ec/admin/caregiver-detail?id=${caregiver.id}`
})
},
viewSchedule(caregiver: CaregiverInfo) {
uni.navigateTo({
url: `/pages/ec/admin/caregiver-schedule?id=${caregiver.id}`
})
},
async toggleStatus(caregiver: CaregiverInfo) {
const newStatus = caregiver.status === 'active' ? 'inactive' : 'active'
const actionText = newStatus === 'active' ? '启用' : '停用'
uni.showModal({
title: '确认操作',
content: `确定要${actionText}护工 ${caregiver.name} 吗?`,
success: async (res) => {
if (res.confirm) {
try {
const result = await supa.executeAs('update_caregiver_status', {
caregiver_id: caregiver.id,
status: newStatus
})
if (result.success) {
uni.showToast({
title: `${actionText}成功`,
icon: 'success'
})
this.loadCaregiverData()
}
} catch (error) {
console.error(`${actionText}护工失败:`, error)
uni.showToast({
title: `${actionText}失败`,
icon: 'error'
})
}
}
}
})
},
getStatusClass(status: string): string {
const statusMap = {
'active': 'status-active',
'on_leave': 'status-leave',
'inactive': 'status-inactive'
}
return statusMap[status] || 'status-inactive'
},
getStatusText(status: string): string {
const statusMap = {
'active': '在岗',
'on_leave': '请假',
'inactive': '离职'
}
return statusMap[status] || '未知'
},
getLevelText(level: string): string {
const levelMap = {
'junior': '初级护工',
'intermediate': '中级护工',
'senior': '高级护工',
'supervisor': '护工主管'
}
return levelMap[level] || '护工'
},
getRatingClass(rating: number): string {
if (rating >= 4.5) return 'rating-excellent'
if (rating >= 4.0) return 'rating-good'
if (rating >= 3.5) return 'rating-fair'
return 'rating-poor'
},
formatDate(dateString: string): string {
if (dateString === null || dateString === undefined || dateString === '') return '--'
const date = new Date(dateString)
return `${date.getFullYear()}/${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}`
}
}
}
</script>
<style lang="scss">
.caregiver-management-container {
background-color: #f5f5f5;
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40rpx 30rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row;
color: white;
}
.header-content {
flex: 1;
}
.header-title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.header-subtitle {
font-size: 26rpx;
opacity: 0.9;
}
.add-btn {
background: rgba(255,255,255,0.2);
border: 2rpx solid rgba(255,255,255,0.3);
border-radius: 25rpx;
padding: 15rpx 25rpx;
color: white;
}
.btn-text {
font-size: 28rpx;
}
.stats-section {
padding: 30rpx;
}
.stats-flex {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}
.stat-card {
background: white;
padding: 30rpx 20rpx;
border-radius: 15rpx;
text-align: center;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.1);
margin-right: 20rpx;
margin-bottom: 20rpx;
flex: 1 1 40%;
min-width: 260rpx;
max-width: 48%;
}
.stat-number {
font-size: 48rpx;
font-weight: bold;
color: #667eea;
margin-bottom: 10rpx;
}
.stat-label {
font-size: 26rpx;
color: #666;
margin-bottom: 5rpx;
}
.stat-trend {
font-size: 22rpx;
color: #999;
}
.filter-section {
padding: 0 30rpx 20rpx;
}
.search-box {
position: relative;
margin-bottom: 20rpx;
}
.search-input {
width: 100%;
height: 80rpx;
background: white;
border: 2rpx solid #e1e1e1;
border-radius: 40rpx;
padding: 0 60rpx 0 30rpx;
font-size: 28rpx;
}
.search-input:focus {
border-color: #667eea;
}
.search-icon {
position: absolute;
right: 30rpx;
top: 50%;
transform: translateY(-50%);
font-size: 30rpx;
color: #999;
}
.filter-tabs {
white-space: nowrap;
flex-direction: row;
}
.filter-tab {
display: flex;
width:100rpx;
align-items: center;
padding: 20rpx 25rpx;
background: white;
border-radius: 25rpx;
margin-right: 15rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
}
.filter-tab.active {
background: linear-gradient(to bottom right, #667eea, #764ba2);
color: white;
}
.filter-count {
background: #f0f0f0;
color: #666;
padding: 5rpx 10rpx;
border-radius: 12rpx;
font-size: 20rpx;
min-width: 30rpx;
text-align: center;
}
.filter-tab.active .filter-count {
background: rgba(255,255,255,0.3);
color: white;
}
.caregivers-section {
padding: 0 30rpx 30rpx;
}
.empty-state {
text-align: center;
padding: 120rpx 0;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 30rpx;
}
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 15rpx;
}
.empty-subtitle {
font-size: 26rpx;
color: #999;
}
.caregivers-list {
display: flex;
flex-direction: column;
}
.caregiver-card {
background: white;
margin-bottom: 20rpx;
border-radius: 15rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.1);
}
.caregiver-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.caregiver-avatar-section {
position: relative;
margin-right: 20rpx;
}
.caregiver-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50rpx;
border: 4rpx solid #f0f0f0;
}
.status-indicator {
position: absolute;
bottom: 5rpx;
right: 5rpx;
width: 20rpx;
height: 20rpx;
border-radius: 10rpx;
border: 3rpx solid white;
}
.status-indicator.status-active {
background-color: #4CAF50;
}
.status-indicator.status-leave {
background-color: #FF9800;
}
.status-indicator.status-inactive {
background-color: #f44336;
}
.caregiver-info {
flex: 1;
}
.caregiver-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 5rpx;
}
.caregiver-id {
font-size: 24rpx;
color: #666;
margin-bottom: 5rpx;
}
.caregiver-level {
font-size: 24rpx;
color: #667eea;
}
.caregiver-status {
}
.status-text {
padding: 10rpx 15rpx;
border-radius: 20rpx;
font-size: 22rpx;
}
.status-text.status-active {
background: #e8f5e8;
color: #4CAF50;
}
.status-text.status-leave {
background: #fff3e0;
color: #FF9800;
}
.status-text.status-inactive {
background: #ffebee;
color: #f44336;
}
.caregiver-details {
margin-bottom: 20rpx;
}
.detail-row {
display: flex;
margin-bottom: 15rpx;
}
.detail-item {
flex: 1;
display: flex;
align-items: center;
}
.detail-label {
font-size: 24rpx;
color: #666;
min-width: 120rpx;
}
.detail-value {
font-size: 26rpx;
color: #333;
}
.detail-value.rating {
font-weight: bold;
}
.detail-value.rating.rating-excellent {
color: #4CAF50;
}
.detail-value.rating.rating-good {
color: #8BC34A;
}
.detail-value.rating.rating-fair {
color: #FF9800;
}
.detail-value.rating.rating-poor {
color: #f44336;
}
.caregiver-actions {
display: flex;
margin-top: 20rpx;
border-top: 2rpx solid #f0f0f0;
}
.action-btn {
flex: 1;
height: 60rpx;
border-radius: 8rpx;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
border: none;
margin-right: 15rpx;
}
.action-btn:last-child {
margin-right: 0;
}
.action-btn.primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.action-btn.secondary {
background: #f8f9ff;
color: #667eea;
border: 2rpx solid #e1e8ff;
}
.action-btn.success {
background: #e8f5e8;
color: #4CAF50;
border: 2rpx solid #c8e6c8;
}
.action-btn.danger {
background: #ffebee;
color: #f44336;
border: 2rpx solid #ffcdd2;
}
</style>