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