Initial commit of akmon project
This commit is contained in:
889
pages/ec/activity/management.uvue
Normal file
889
pages/ec/activity/management.uvue
Normal file
@@ -0,0 +1,889 @@
|
||||
<!-- 养老管理系统 - 活动管理 -->
|
||||
<template>
|
||||
<view class="activity-management">
|
||||
<view class="header">
|
||||
<text class="title">活动管理</text>
|
||||
<button class="add-btn" @click="showAddActivity">安排活动</button>
|
||||
</view>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<view class="filter-section">
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">类型:</text>
|
||||
<picker-view class="picker" :value="selectedTypeIndex" @change="onTypeChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(type, index) in typeOptions" :key="index" class="picker-item">
|
||||
{{ type.label }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">状态:</text>
|
||||
<picker-view class="picker" :value="selectedStatusIndex" @change="onStatusChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(status, index) in statusOptions" :key="index" class="picker-item">
|
||||
{{ status.label }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">日期:</text>
|
||||
<input class="date-picker" :value="selectedDate" readonly @click="showDatePicker = true" placeholder="选择日期" />
|
||||
|
||||
</view>
|
||||
<l-popup v-if="showDatePicker" v-model="showDatePicker" position="center" :closeable="true" @click-close="showDatePicker = false">
|
||||
<l-date-time-picker
|
||||
v-model="tempDate"
|
||||
title="选择日期"
|
||||
mode="年月日"
|
||||
:start="'1920-01-01'"
|
||||
:end="new Date().toISOString().split('T')[0]"
|
||||
confirm-btn="确认"
|
||||
cancel-btn="取消"
|
||||
@confirm="onDateConfirm"
|
||||
@cancel="showDatePicker = false"
|
||||
/>
|
||||
</l-popup>
|
||||
<button class="search-btn" @click="searchActivities">搜索</button>
|
||||
</view>
|
||||
|
||||
<!-- 活动列表 -->
|
||||
<view class="activities-list">
|
||||
<view v-for="activity in activities" :key="activity.id" class="activity-item" @click="viewActivityDetail(activity)">
|
||||
<view class="activity-header">
|
||||
<text class="activity-name">{{ activity.activity_name }}</text>
|
||||
<view class="status-badge" :class="getStatusClass(activity.status)">
|
||||
<text class="status-text">{{ getStatusText(activity.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="activity-info">
|
||||
<text class="activity-type">{{ getTypeText(activity.activity_type) }}</text>
|
||||
<text class="activity-location">地点: {{ activity.location ?? '未设置' }}</text>
|
||||
<text class="activity-instructor">指导员: {{ activity.instructor ?? '未分配' }}</text>
|
||||
</view>
|
||||
<view class="activity-time">
|
||||
<text class="time-text">开始: {{ formatDateTime(activity.start_time) }}</text>
|
||||
<text class="time-text">结束: {{ formatDateTime(activity.end_time) }}</text>
|
||||
</view>
|
||||
<view class="activity-participants">
|
||||
<text class="participants-text">最大参与人数: {{ activity.max_participants ?? '不限' }}</text>
|
||||
<text class="participants-count">当前参与: {{ getParticipantsCount(activity.id) }} 人</text>
|
||||
</view>
|
||||
<view class="activity-actions">
|
||||
<button class="action-btn edit-btn" @click.stop="editActivity(activity)">编辑</button>
|
||||
<button class="action-btn participants-btn" @click.stop="manageParticipants(activity)">参与管理</button>
|
||||
<button class="action-btn cancel-btn" v-if="activity.status === 'scheduled'" @click.stop="cancelActivity(activity)">取消</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加/编辑活动弹窗 -->
|
||||
<view v-if="showActivityModal" class="modal-overlay" @click="closeActivityModal">
|
||||
<view class="modal-content" @click.stop="">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ isEditMode ? '编辑活动' : '安排活动' }}</text>
|
||||
<button class="close-btn" @click="closeActivityModal">×</button>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-group">
|
||||
<text class="form-label">活动名称:</text>
|
||||
<input class="form-input" v-model="formData.activity_name" placeholder="请输入活动名称" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">活动类型:</text>
|
||||
<picker-view class="form-picker" :value="formData.typeIndex" @change="onFormTypeChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(type, index) in activityTypes" :key="index" class="picker-item">
|
||||
{{ type.label }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">活动描述:</text>
|
||||
<textarea class="form-textarea" v-model="formData.description" placeholder="请输入活动描述"></textarea>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">活动地点:</text>
|
||||
<input class="form-input" v-model="formData.location" placeholder="请输入活动地点" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">开始时间:</text>
|
||||
<input class="form-input" :value="formData.start_time" readonly @click="showStartTimePicker = true" placeholder="选择开始时间" />
|
||||
<l-date-time-picker
|
||||
v-if="showStartTimePicker"
|
||||
v-model="tempStartTime"
|
||||
title="选择开始时间"
|
||||
mode="年月日 时分"
|
||||
confirm-btn="确认"
|
||||
cancel-btn="取消"
|
||||
@confirm="onStartTimeConfirm"
|
||||
@cancel="showStartTimePicker = false"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">结束时间:</text>
|
||||
<input class="form-input" :value="formData.end_time" readonly @click="showEndTimePicker = true" placeholder="选择结束时间" />
|
||||
<l-date-time-picker
|
||||
v-if="showEndTimePicker"
|
||||
v-model="tempEndTime"
|
||||
title="选择结束时间"
|
||||
mode="年月日 时分"
|
||||
confirm-btn="确认"
|
||||
cancel-btn="取消"
|
||||
@confirm="onEndTimeConfirm"
|
||||
@cancel="showEndTimePicker = false"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">最大参与人数:</text>
|
||||
<input class="form-input" v-model="formData.max_participants" type="number" placeholder="不限制请留空" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">指导员:</text>
|
||||
<input class="form-input" v-model="formData.instructor" placeholder="请输入指导员姓名" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">参与要求:</text>
|
||||
<textarea class="form-textarea" v-model="formData.requirements" placeholder="请输入参与要求"></textarea>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">所需物品:</text>
|
||||
<textarea class="form-textarea" v-model="formData.materials_needed" placeholder="请输入所需物品"></textarea>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="cancel-btn-modal" @click="closeActivityModal">取消</button>
|
||||
<button class="save-btn" @click="saveActivity">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { Activity } from '../types.uts'
|
||||
import { formatDateTime, getStatusClass, formatDate } from '../types.uts'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
|
||||
// 响应式数据
|
||||
const activities = ref<Activity[]>([])
|
||||
const participantsCountMap = ref<Map<string, number>>(new Map())
|
||||
|
||||
// 筛选相关
|
||||
const selectedTypeIndex = ref([0])
|
||||
const selectedStatusIndex = ref([0])
|
||||
const selectedDate = ref('')
|
||||
const showDatePicker = ref(false)
|
||||
const tempDate = ref('')
|
||||
|
||||
const typeOptions = [
|
||||
{ value: 'all', label: '全部类型' },
|
||||
{ value: 'recreation', label: '娱乐活动' },
|
||||
{ value: 'therapy', label: '治疗活动' },
|
||||
{ value: 'education', label: '教育活动' },
|
||||
{ value: 'social', label: '社交活动' },
|
||||
{ value: 'exercise', label: '运动活动' }
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'scheduled', label: '已安排' },
|
||||
{ value: 'in_progress', label: '进行中' },
|
||||
{ value: 'completed', label: '已完成' },
|
||||
{ value: 'cancelled', label: '已取消' }
|
||||
]
|
||||
|
||||
const activityTypes = [
|
||||
{ value: 'recreation', label: '娱乐活动' },
|
||||
{ value: 'therapy', label: '治疗活动' },
|
||||
{ value: 'education', label: '教育活动' },
|
||||
{ value: 'social', label: '社交活动' },
|
||||
{ value: 'exercise', label: '运动活动' }
|
||||
]
|
||||
|
||||
// 弹窗相关
|
||||
const showActivityModal = ref(false)
|
||||
const isEditMode = ref(false)
|
||||
const currentActivityId = ref<string | null>(null)
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
activity_name: '',
|
||||
typeIndex: [0],
|
||||
description: '',
|
||||
location: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
max_participants: '',
|
||||
instructor: '',
|
||||
requirements: '',
|
||||
materials_needed: '' })
|
||||
|
||||
// 时间选择器相关
|
||||
const showStartTimePicker = ref(false)
|
||||
const showEndTimePicker = ref(false)
|
||||
const tempStartTime = ref('')
|
||||
const tempEndTime = ref('')
|
||||
|
||||
// 加载数据
|
||||
async function loadData(): Promise<void> {
|
||||
try {
|
||||
await Promise.all([
|
||||
loadActivities(),
|
||||
loadParticipantsCounts()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
uni.showToast({
|
||||
title: '加载数据失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
} // 页面加载
|
||||
onLoad(() => {
|
||||
const today = new Date()
|
||||
selectedDate.value = formatDate(today.toISOString())
|
||||
loadData()
|
||||
})
|
||||
|
||||
// 加载活动列表
|
||||
async function loadActivities(): Promise<void> {
|
||||
// 构建查询条件
|
||||
const filters: any[] = []
|
||||
if (selectedTypeIndex.value[0] > 0) {
|
||||
const selectedType = typeOptions[selectedTypeIndex.value[0]]
|
||||
filters.push({ key: 'activity_type', value: selectedType.value })
|
||||
}
|
||||
if (selectedStatusIndex.value[0] > 0) {
|
||||
const selectedStatus = statusOptions[selectedStatusIndex.value[0]]
|
||||
filters.push({ key: 'status', value: selectedStatus.value })
|
||||
}
|
||||
if (selectedDate.value !== '') {
|
||||
filters.push({ key: 'start_time', date: selectedDate.value })
|
||||
}
|
||||
|
||||
let query = supa.from('ec_activities').select('*')
|
||||
for (let i = 0; i < filters.length; i++) {
|
||||
const f = filters[i]
|
||||
if (f.key === 'start_time' && f.date) {
|
||||
// 日期筛选,start_time 字段只保留当天
|
||||
query = query.gte('start_time', `${f.date} 00:00:00`).lte('start_time', `${f.date} 23:59:59`)
|
||||
} else {
|
||||
query = query.eq(f.key, f.value)
|
||||
}
|
||||
}
|
||||
query = query.order('start_time', { ascending: false })
|
||||
const result = await query.executeAs<Activity>()
|
||||
activities.value = result.data
|
||||
}
|
||||
// 加载参与人数统计
|
||||
async function loadParticipantsCounts(): Promise<void> {
|
||||
const result = await supa
|
||||
.from('ec_activity_participations')
|
||||
.select('activity_id')
|
||||
.eq('participation_status', 'registered')
|
||||
.executeAs<ActivityParticipation>()
|
||||
|
||||
const countMap = new Map<string, number>()
|
||||
if (result && Array.isArray(result)) {
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const id = result[i].activity_id
|
||||
if (id) {
|
||||
countMap.set(id, (countMap.get(id) ?? 0) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
participantsCountMap.value = countMap
|
||||
}
|
||||
|
||||
// 获取参与人数
|
||||
function getParticipantsCount(activityId: string): number {
|
||||
return participantsCountMap.value.get(activityId) ?? 0
|
||||
}
|
||||
|
||||
// 获取类型文本
|
||||
function getTypeText(type: string | null): string {
|
||||
if (type === null) return '其他'
|
||||
const typeMap: Record<string, string> = {
|
||||
'recreation': '娱乐活动',
|
||||
'therapy': '治疗活动',
|
||||
'education': '教育活动',
|
||||
'social': '社交活动',
|
||||
'exercise': '运动活动'
|
||||
}
|
||||
return typeMap[type] ?? type
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
'scheduled': '已安排',
|
||||
'in_progress': '进行中',
|
||||
'completed': '已完成',
|
||||
'cancelled': '已取消'
|
||||
}
|
||||
return statusMap[status] ?? status
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr: string): string {
|
||||
if (dateStr === '') return ''
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 筛选事件
|
||||
function onTypeChange(e: any): void {
|
||||
selectedTypeIndex.value = e.detail.value
|
||||
}
|
||||
|
||||
function onStatusChange(e: any): void {
|
||||
selectedStatusIndex.value = e.detail.value
|
||||
}
|
||||
|
||||
function onDateChange(date: string): void {
|
||||
selectedDate.value = date
|
||||
}
|
||||
|
||||
function onDateConfirm(date: string): void {
|
||||
selectedDate.value = date
|
||||
showDatePicker.value = false
|
||||
}
|
||||
|
||||
// 搜索活动
|
||||
function searchActivities(): void {
|
||||
loadActivities()
|
||||
}
|
||||
|
||||
// 查看活动详情
|
||||
function viewActivityDetail(activity: Activity): void {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/activity/detail?id=${activity.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑活动
|
||||
function editActivity(activity: Activity): void {
|
||||
isEditMode.value = true
|
||||
currentActivityId.value = activity.id
|
||||
|
||||
// 填充表单数据
|
||||
const typeIndex = activityTypes.findIndex(type => type.value === activity.activity_type)
|
||||
const startDateTime = activity.start_time ? new Date(activity.start_time) : new Date()
|
||||
const endDateTime = activity.end_time ? new Date(activity.end_time) : new Date()
|
||||
|
||||
formData.value = {
|
||||
activity_name: activity.activity_name,
|
||||
typeIndex: [typeIndex > 0 ? typeIndex : 0],
|
||||
description: activity.description ?? '',
|
||||
location: activity.location ?? '',
|
||||
start_time: startDateTime.getHours().toString().padStart(2, '0') + ':' + startDateTime.getMinutes().toString().padStart(2, '0'),
|
||||
end_time: endDateTime.getHours().toString().padStart(2, '0') + ':' + endDateTime.getMinutes().toString().padStart(2, '0'),
|
||||
max_participants: activity.max_participants?.toString() ?? '',
|
||||
instructor: activity.instructor ?? '',
|
||||
requirements: activity.requirements ?? '',
|
||||
materials_needed: activity.materials_needed ?? ''
|
||||
}
|
||||
|
||||
showActivityModal.value = true
|
||||
}
|
||||
|
||||
// 管理参与者
|
||||
function manageParticipants(activity: Activity): void {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/activity/participants?id=${activity.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 取消活动
|
||||
async function cancelActivity(activity: Activity): Promise<void> {
|
||||
uni.showModal({
|
||||
title: '确认取消',
|
||||
content: '确定要取消这个活动吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await supa
|
||||
.from('ec_activities')
|
||||
.update({ status: 'cancelled', updated_at: new Date().toISOString() })
|
||||
.eq('id', activity.id)
|
||||
.executeAs<Activity>()
|
||||
|
||||
uni.showToast({
|
||||
title: '取消成功',
|
||||
icon: 'success'
|
||||
})
|
||||
loadActivities()
|
||||
} catch (error) {
|
||||
console.error('取消活动失败:', error)
|
||||
uni.showToast({
|
||||
title: '取消失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 显示添加活动弹窗
|
||||
function showAddActivity(): void {
|
||||
isEditMode.value = false
|
||||
currentActivityId.value = null
|
||||
|
||||
// 重置表单
|
||||
const now = new Date()
|
||||
const today = formatDate(now.toISOString())
|
||||
const currentTime = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0')
|
||||
|
||||
formData.value = {
|
||||
activity_name: '',
|
||||
typeIndex: [0],
|
||||
description: '',
|
||||
location: '',
|
||||
start_time: currentTime,
|
||||
end_time: currentTime,
|
||||
max_participants: '',
|
||||
instructor: '',
|
||||
requirements: '',
|
||||
materials_needed: ''
|
||||
}
|
||||
|
||||
showActivityModal.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeActivityModal(): void {
|
||||
showActivityModal.value = false
|
||||
}
|
||||
|
||||
// 表单事件
|
||||
function onFormTypeChange(e: any): void {
|
||||
formData.value.typeIndex = e.detail.value
|
||||
}
|
||||
|
||||
function onStartTimeConfirm(val: string): void {
|
||||
formData.value.start_time = val
|
||||
showStartTimePicker.value = false
|
||||
}
|
||||
function onEndTimeConfirm(val: string): void {
|
||||
formData.value.end_time = val
|
||||
showEndTimePicker.value = false
|
||||
}
|
||||
|
||||
// 保存活动
|
||||
async function saveActivity(): Promise<void> {
|
||||
// 验证表单
|
||||
if (formData.value.activity_name.trim() === '') {
|
||||
uni.showToast({
|
||||
title: '请输入活动名称',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const selectedType = activityTypes[formData.value.typeIndex[0]]
|
||||
const startDateTime = `${new Date().toISOString().split('T')[0]} ${formData.value.start_time}:00`
|
||||
const endDateTime = `${new Date().toISOString().split('T')[0]} ${formData.value.end_time}:00`
|
||||
|
||||
if (isEditMode.value && currentActivityId.value !== null) {
|
||||
// 更新活动(链式写法,移除SQL字符串,类型安全)
|
||||
await supa
|
||||
.from('ec_activities')
|
||||
.update({
|
||||
activity_name: formData.value.activity_name,
|
||||
activity_type: selectedType.value,
|
||||
description: formData.value.description,
|
||||
location: formData.value.location,
|
||||
start_time: startDateTime,
|
||||
end_time: endDateTime,
|
||||
max_participants: formData.value.max_participants ? Number(formData.value.max_participants) : null,
|
||||
instructor: formData.value.instructor,
|
||||
requirements: formData.value.requirements,
|
||||
materials_needed: formData.value.materials_needed,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', currentActivityId.value)
|
||||
.execute()
|
||||
} else {
|
||||
// 新增活动
|
||||
// 先查 facility_id
|
||||
const facilities = await supa.from('ec_facilities').select('id').limit(1).executeAs<UTSJSONObject>()
|
||||
const facilityId = facilities && facilities.length > 0 ? facilities[0].id : ''
|
||||
await supa
|
||||
.from('ec_activities')
|
||||
.insert([{
|
||||
facility_id: facilityId,
|
||||
activity_name: formData.value.activity_name,
|
||||
activity_type: selectedType.value,
|
||||
description: formData.value.description,
|
||||
location: formData.value.location,
|
||||
start_time: startDateTime,
|
||||
end_time: endDateTime,
|
||||
max_participants: formData.value.max_participants ? Number(formData.value.max_participants) : null,
|
||||
instructor: formData.value.instructor,
|
||||
requirements: formData.value.requirements,
|
||||
materials_needed: formData.value.materials_needed,
|
||||
status: 'scheduled'
|
||||
}])
|
||||
.execute()
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
closeActivityModal()
|
||||
loadActivities()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.activity-management {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.picker {
|
||||
width: 120px;
|
||||
height: 40px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.picker-item {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.activities-list {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.activity-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.activity-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-scheduled {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
|
||||
.status-progress {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
.activity-info, .activity-time, .activity-participants {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.activity-type, .activity-location, .activity-instructor,
|
||||
.time-text, .participants-text, .participants-count {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.activity-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.participants-btn {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 80%;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-picker {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.datetime-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.date-input, .time-input {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.time-input {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.cancel-btn-modal, .save-btn {
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.cancel-btn-modal {
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 小屏幕适配 */
|
||||
@media (max-width: 768px) {
|
||||
.activity-management {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
margin-right: 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.picker {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.datetime-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.date-input, .time-input {
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
0
pages/ec/admin/all-service-records.uvue
Normal file
0
pages/ec/admin/all-service-records.uvue
Normal file
303
pages/ec/admin/care-records.uvue
Normal file
303
pages/ec/admin/care-records.uvue
Normal file
@@ -0,0 +1,303 @@
|
||||
<!-- 服务记录页面 - uts-android 兼容版 -->
|
||||
<template>
|
||||
<view class="service-records">
|
||||
<view class="header">
|
||||
<text class="header-title">服务记录</text>
|
||||
<button class="refresh-btn" @click="refreshData">
|
||||
<text class="refresh-text">🔄 刷新</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="filters-section">
|
||||
<view class="filter-row">
|
||||
<view class="filter-group">
|
||||
<text class="filter-label">老人</text>
|
||||
<button class="picker-btn" @click="showElderActionSheet">
|
||||
<text class="picker-text">{{ selectedElder?.name ?? '全部' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="filter-group">
|
||||
<text class="filter-label">服务类型</text>
|
||||
<button class="picker-btn" @click="showTypeActionSheet">
|
||||
<text class="picker-text">{{ selectedType?.label ?? '全部' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="filter-group">
|
||||
<text class="filter-label">时间范围</text>
|
||||
<button class="picker-btn" @click="showTimeRangeActionSheet">
|
||||
<text class="picker-text">{{ selectedTimeRange?.label ?? '近7天' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view class="records-list" direction="vertical" :style="{ height: '500px' }">
|
||||
<view v-for="record in filteredRecords" :key="record.id" class="record-item" @click="viewDetail(record)">
|
||||
<view class="record-header">
|
||||
<text class="elder-name">{{ record.elder_name ?? '未知' }}</text>
|
||||
<text class="service-type">{{ record.service_type ?? '未知类型' }}</text>
|
||||
<text class="record-time">{{ formatDateTime(record.created_at ?? '') }}</text>
|
||||
</view>
|
||||
<view class="record-content">
|
||||
<text class="caregiver">护理员: {{ record.caregiver_name ?? '未分配' }}</text>
|
||||
<text class="notes" v-if="record.notes">备注: {{ record.notes }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="filteredRecords.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无服务记录</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { formatDateTime as formatDateTimeUtil } from '../types.uts'
|
||||
|
||||
type ServiceRecord = {
|
||||
id: string
|
||||
task_id: string | null
|
||||
elder_id: string
|
||||
caregiver_id: string
|
||||
elder_name?: string
|
||||
caregiver_name?: string
|
||||
start_time: string | null
|
||||
end_time: string | null
|
||||
actual_duration: number | null
|
||||
care_content: string | null
|
||||
elder_condition: string | null
|
||||
issues_notes: string | null
|
||||
photo_urls: string[] | null
|
||||
status: string
|
||||
rating: number | null
|
||||
supervisor_notes: string | null
|
||||
created_at: string
|
||||
}
|
||||
type Elder = { id: string, name: string }
|
||||
type FilterOption = { value: string, label: string }
|
||||
|
||||
const records = ref<ServiceRecord[]>([])
|
||||
const elders = ref<Elder[]>([])
|
||||
const selectedElderIndex = ref<number>(-1)
|
||||
const selectedTypeIndex = ref<number>(-1)
|
||||
const selectedTimeRangeIndex = ref<number>(1)
|
||||
|
||||
const typeOptions = ref<FilterOption[]>([
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'nursing', label: '护理' },
|
||||
{ value: 'meal', label: '餐饮' },
|
||||
{ value: 'activity', label: '活动' },
|
||||
{ value: 'cleaning', label: '清洁' }
|
||||
])
|
||||
const timeRangeOptions = ref<FilterOption[]>([
|
||||
{ value: '3days', label: '近3天' },
|
||||
{ value: '7days', label: '近7天' },
|
||||
{ value: '30days', label: '近30天' }
|
||||
])
|
||||
|
||||
const elderOptions = computed<Elder[]>(() => [ { id: 'all', name: '全部' }, ...elders.value ])
|
||||
const selectedElder = computed(() => elderOptions.value[selectedElderIndex.value] ?? elderOptions.value[0])
|
||||
const selectedType = computed(() => typeOptions.value[selectedTypeIndex.value] ?? typeOptions.value[0])
|
||||
const selectedTimeRange = computed(() => timeRangeOptions.value[selectedTimeRangeIndex.value] ?? timeRangeOptions.value[1])
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
let list = records.value
|
||||
if (selectedElder.value.id !== 'all') {
|
||||
list = list.filter(r => r.elder_id === selectedElder.value.id)
|
||||
}
|
||||
if (selectedType.value.value !== 'all') {
|
||||
list = list.filter(r => r.service_type === selectedType.value.value)
|
||||
}
|
||||
// 时间范围
|
||||
const now = new Date()
|
||||
let startDate = new Date()
|
||||
if (selectedTimeRange.value.value === '3days') startDate.setDate(now.getDate() - 3)
|
||||
else if (selectedTimeRange.value.value === '7days') startDate.setDate(now.getDate() - 7)
|
||||
else if (selectedTimeRange.value.value === '30days') startDate.setDate(now.getDate() - 30)
|
||||
list = list.filter(r => r.created_at >= startDate.toISOString())
|
||||
return list
|
||||
})
|
||||
|
||||
const formatDateTime = (dt: string) => formatDateTimeUtil(dt)
|
||||
|
||||
const refreshData = () => { loadRecords(); loadElders(); }
|
||||
|
||||
const loadRecords = async () => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_care_records')
|
||||
.select('id, elder_id, ec_care_records_elder_id_fkey(name), record_type, ec_care_records_caregiver_id_fkey(username), created_at,issues_notes, supervisor_notes', {})
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(100)
|
||||
.executeAs<ServiceRecord[]>()
|
||||
if (result.error == null && result.data != null) {
|
||||
records.value = result.data
|
||||
}
|
||||
} catch (e) { console.error('加载服务记录失败', e) }
|
||||
}
|
||||
const loadElders = async () => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_elders')
|
||||
.select('id, name', {})
|
||||
.eq('status', 'active')
|
||||
.order('name', { ascending: true })
|
||||
.executeAs<Elder[]>()
|
||||
if (result.error == null && result.data != null) {
|
||||
elders.value = result.data
|
||||
}
|
||||
} catch (e) { console.error('加载老人列表失败', e) }
|
||||
}
|
||||
const showElderActionSheet = () => {
|
||||
const options = elderOptions.value.map(e => e.name)
|
||||
uni.showActionSheet({
|
||||
itemList: options,
|
||||
success: (res:any) => { selectedElderIndex.value = res.tapIndex }
|
||||
})
|
||||
}
|
||||
const showTypeActionSheet = () => {
|
||||
const options = typeOptions.value.map(t => t.label)
|
||||
uni.showActionSheet({
|
||||
itemList: options,
|
||||
success: (res:any) => { selectedTypeIndex.value = res.tapIndex }
|
||||
})
|
||||
}
|
||||
const showTimeRangeActionSheet = () => {
|
||||
const options = timeRangeOptions.value.map(t => t.label)
|
||||
uni.showActionSheet({
|
||||
itemList: options,
|
||||
success: (res:any) => { selectedTimeRangeIndex.value = res.tapIndex }
|
||||
})
|
||||
}
|
||||
const viewDetail = (record: ServiceRecord) => {
|
||||
uni.navigateTo({ url: `/pages/ec/admin/service-record-detail?id=${record.id}` })
|
||||
}
|
||||
onMounted(() => { refreshData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* uts-android 兼容性重构:
|
||||
1. 移除所有嵌套选择器、伪类(如 :last-child),全部 class 扁平化。
|
||||
2. 所有间距用 margin-right/margin-bottom 控制,禁止 gap、flex-wrap、嵌套。
|
||||
3. 所有布局 display: flex,禁止 grid、gap、伪类。
|
||||
4. 组件间距、分隔线全部用 border/margin 控制。
|
||||
*/
|
||||
.service-records {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.refresh-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #52c41a;
|
||||
background-color: #52c41a;
|
||||
color: white;
|
||||
}
|
||||
.filters-section {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.filter-group {
|
||||
flex: 1;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.filter-group.is-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
.picker-btn {
|
||||
width: 180rpx;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.picker-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: #f9f9f9;
|
||||
display: block;
|
||||
}
|
||||
.records-list {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
min-height: 300px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.record-item {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.record-item.is-last {
|
||||
border-bottom: none;
|
||||
}
|
||||
.record-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.elder-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.service-type {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.record-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.record-content {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.caregiver {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.notes {
|
||||
color: #faad14;
|
||||
}
|
||||
.empty-state {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
647
pages/ec/admin/caregiver-management.uvue
Normal file
647
pages/ec/admin/caregiver-management.uvue
Normal 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>
|
||||
845
pages/ec/admin/dashboard.uvue
Normal file
845
pages/ec/admin/dashboard.uvue
Normal file
@@ -0,0 +1,845 @@
|
||||
<!-- 养老管理系统 - 管理员仪表板 (简化版) -->
|
||||
<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>
|
||||
837
pages/ec/admin/elder-form.uvue
Normal file
837
pages/ec/admin/elder-form.uvue
Normal file
@@ -0,0 +1,837 @@
|
||||
<template> <scroll-view class="elder-form-container">
|
||||
<!-- Header -->
|
||||
<view class="form-header">
|
||||
<text class="header-title">{{ isEdit ? '编辑老人信息' : '添加新老人' }}</text>
|
||||
<text class="header-subtitle">请填写完整的老人基本信息和健康状况</text>
|
||||
</view>
|
||||
|
||||
<!-- 使用 form 标签进行统一数据收集 -->
|
||||
<form @submit="onFormSubmit">
|
||||
<!-- 基本信息 -->
|
||||
<view class="form-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">基本信息</text>
|
||||
<text class="required-tip">* 必填项</text>
|
||||
</view>
|
||||
|
||||
<!-- 头像上传 -->
|
||||
<view class="avatar-upload-section">
|
||||
<view class="avatar-container" @click="uploadAvatar">
|
||||
<image v-if="avatar" class="avatar-preview" :src="avatar" mode="aspectFill"></image>
|
||||
<view v-else class="avatar-placeholder">
|
||||
<text class="upload-icon">📷</text>
|
||||
<text class="upload-text">上传头像</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 头像数据隐藏字段 -->
|
||||
<input name="avatar" type="text" :value="avatar" style="display: none;" />
|
||||
</view>
|
||||
|
||||
<view class="form-row">
|
||||
<view class="form-item required">
|
||||
<text class="form-label">姓名</text>
|
||||
<input class="form-input" name="name" :value="name" placeholder="请输入老人姓名" />
|
||||
</view>
|
||||
<view class="form-item required">
|
||||
<text class="form-label">性别</text>
|
||||
<!-- 选择器改为 actionSheet -->
|
||||
<view class="form-input" @click="chooseGender">
|
||||
<text class="picker-text">{{ genderOptions[gender === 'male' ? 0 : 1] }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
<!-- 性别数据隐藏字段 -->
|
||||
<input name="gender" type="text" :value="gender" style="display: none;" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-row">
|
||||
<view class="form-item required">
|
||||
<text class="form-label">出生日期</text>
|
||||
<!-- 出生日期选择,改用 lime-date-time-picker -->
|
||||
<view class="form-item required">
|
||||
<text class="form-label">出生日期</text>
|
||||
<view class="form-input" @click="showBirthDatePicker = true">
|
||||
<text class="picker-text">{{ birth_date !== '' ? birth_date : '选择日期' }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
<input name="birth_date" type="text" :value="birth_date" style="display: none;" />
|
||||
<l-date-time-picker v-if="showBirthDatePicker" v-model="birth_date" title="选择出生日期"
|
||||
mode="年月日" :start="'1920-01-01'" :end="new Date().toISOString().split('T')[0]"
|
||||
confirm-btn="确认" cancel-btn="取消" @confirm="onBirthDateConfirm"
|
||||
@cancel="showBirthDatePicker = false" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">身份证号</text>
|
||||
<view class="id-card-input-row">
|
||||
<input class="form-input" name="id_card" :value="id_card" placeholder="请输入身份证号码" />
|
||||
<button class="id-card-photo-btn" type="button" @click="uploadIdCardPhoto">
|
||||
<text class="photo-icon">📷</text>
|
||||
<text class="photo-text">上传照片</text>
|
||||
</button>
|
||||
</view>
|
||||
<view v-if="id_card_photo_url" class="id-card-photo-preview">
|
||||
<image :src="id_card_photo_url" mode="aspectFill" class="id-card-photo-img" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-row">
|
||||
<view class="form-item">
|
||||
<text class="form-label">联系电话</text>
|
||||
<input class="form-input" name="phone" :value="phone" placeholder="请输入联系电话" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">房间号</text>
|
||||
<input class="form-input" name="room_number" :value="room_number" placeholder="请输入房间号" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">联系地址</text>
|
||||
<textarea class="form-textarea" name="address" :value="address" placeholder="请输入详细地址" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康状况 -->
|
||||
<view class="form-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">健康状况</text>
|
||||
</view>
|
||||
|
||||
<view class="form-row">
|
||||
<view class="form-item">
|
||||
<text class="form-label">健康状态</text>
|
||||
<!-- 选择器改为 actionSheet -->
|
||||
<view class="form-input" @click="chooseHealthStatus">
|
||||
<text
|
||||
class="picker-text">{{ healthStatusOptions[health_status === 'good' ? 0 : health_status === 'fair' ? 1 : health_status === 'poor' ? 2 : 3] }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
<!-- 健康状态数据隐藏字段 -->
|
||||
<input name="health_status" type="text" :value="health_status" style="display: none;" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">护理等级</text>
|
||||
<!-- 选择器改为 actionSheet -->
|
||||
<view class="form-input" @click="chooseCareLevel">
|
||||
<text
|
||||
class="picker-text">{{ careLevelOptions[care_level === 'level1' ? 0 : care_level === 'level2' ? 1 : care_level === 'level3' ? 2 : 3] }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
<!-- 护理等级数据隐藏字段 -->
|
||||
<input name="care_level" type="text" :value="care_level" style="display: none;" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">疾病史</text>
|
||||
<textarea class="form-textarea" name="medical_history" :value="medical_history"
|
||||
placeholder="请输入主要疾病史和治疗情况" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">过敏史</text>
|
||||
<textarea class="form-textarea" name="allergies" :value="allergies" placeholder="请输入过敏史,如无请填写'无'" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">特殊需求</text>
|
||||
<textarea class="form-textarea" name="special_needs" :value="special_needs"
|
||||
placeholder="请输入特殊护理需求" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 紧急联系人 -->
|
||||
<view class="form-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">紧急联系人</text>
|
||||
</view>
|
||||
|
||||
<view class="form-row">
|
||||
<view class="form-item">
|
||||
<text class="form-label">联系人姓名</text>
|
||||
<input class="form-input" name="emergency_contact_name" :value="emergency_contact_name"
|
||||
placeholder="请输入联系人姓名" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">与老人关系</text>
|
||||
<input class="form-input" name="emergency_contact_relationship"
|
||||
:value="emergency_contact_relationship" placeholder="如:子女、配偶等" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-row">
|
||||
<view class="form-item">
|
||||
<text class="form-label">联系电话</text>
|
||||
<input class="form-input" name="emergency_contact_phone" :value="emergency_contact_phone"
|
||||
placeholder="请输入联系电话" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="form-actions">
|
||||
<button class="btn btn-cancel" type="button" @click="goBack">取消</button>
|
||||
<button class="btn btn-submit" form-type="submit" :disabled="isSubmitting">
|
||||
{{ isSubmitting ? '保存中...' : (isEdit ? '更新信息' : '添加老人') }}
|
||||
</button>
|
||||
</view>
|
||||
</form>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import type { Elder } from '../types.uts'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isEdit: false,
|
||||
elderId: '',
|
||||
isSubmitting: false,
|
||||
// 所有表单变量一维展开
|
||||
name: '',
|
||||
id_card: '',
|
||||
gender: 'male',
|
||||
birth_date: '',
|
||||
avatar: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
health_status: 'good',
|
||||
care_level: 'level1',
|
||||
medical_history: '',
|
||||
allergies: '',
|
||||
special_needs: '',
|
||||
emergency_contact_name: '',
|
||||
emergency_contact_relationship: '',
|
||||
emergency_contact_phone: '',
|
||||
room_number: '',
|
||||
id_card_photo_url: '', // 身份证照片url
|
||||
// 选项
|
||||
genderOptions: ['男', '女'],
|
||||
healthStatusOptions: ['良好', '一般', '较差', '危重'],
|
||||
careLevelOptions: ['一级护理', '二级护理', '三级护理', '特级护理'],
|
||||
showBirthDatePicker: false // 控制出生日期选择器显示
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options['id'] !== null && options['id'] !== undefined) {
|
||||
this.isEdit = true
|
||||
this.elderId = options['id'] as string
|
||||
this.loadElderInfo(this.elderId)
|
||||
} else {
|
||||
this.isEdit = false
|
||||
this.elderId = ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 性别选择
|
||||
chooseGender() {
|
||||
uni.showActionSheet({
|
||||
itemList: this.genderOptions,
|
||||
success: (res : any) => {
|
||||
if (res.tapIndex !== null) {
|
||||
this.gender = res.tapIndex === 0 ? 'male' : 'female'
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
// 出生日期选择
|
||||
chooseBirthDate() {
|
||||
uni.showActionSheet({
|
||||
itemList: this.getDateOptions(),
|
||||
success: (res : any) => {
|
||||
if (res.tapIndex !== null) {
|
||||
const selectedDate = this.getDateOptions()[res.tapIndex]
|
||||
this.birth_date = selectedDate
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
// 健康状态选择
|
||||
chooseHealthStatus() {
|
||||
uni.showActionSheet({
|
||||
itemList: this.healthStatusOptions,
|
||||
success: (res : any) => {
|
||||
if (res.tapIndex !== null) {
|
||||
const statusMap = ['good', 'fair', 'poor', 'critical']
|
||||
this.health_status = statusMap[res.tapIndex]
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
// 护理等级选择
|
||||
chooseCareLevel() {
|
||||
uni.showActionSheet({
|
||||
itemList: this.careLevelOptions,
|
||||
success: (res : any) => {
|
||||
if (res.tapIndex !== null) {
|
||||
const levelMap = ['level1', 'level2', 'level3', 'special']
|
||||
this.care_level = levelMap[res.tapIndex]
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
// 统一的表单提交处理
|
||||
onFormSubmit(e : UniFormSubmitEvent) {
|
||||
// 从表单数据中更新 formData
|
||||
const formValues = e.detail.value
|
||||
|
||||
// 更新所有表单字段
|
||||
this.name = formValues.getString('name') ?? ''
|
||||
this.id_card = formValues.getString('id_card') ?? ''
|
||||
this.phone = formValues.getString('phone') ?? ''
|
||||
this.room_number = formValues.getString('room_number') ?? ''
|
||||
this.address = formValues.getString('address') ?? ''
|
||||
this.medical_history = formValues.getString('medical_history') ?? ''
|
||||
this.allergies = formValues.getString('allergies') ?? ''
|
||||
this.special_needs = formValues.getString('special_needs') ?? ''
|
||||
this.emergency_contact_name = formValues.getString('emergency_contact_name') ?? ''
|
||||
this.emergency_contact_relationship = formValues.getString('emergency_contact_relationship') ?? ''
|
||||
this.emergency_contact_phone = formValues.getString('emergency_contact_phone') ?? ''
|
||||
|
||||
// 对于 picker 组件,数据已经通过事件处理更新到 formData 了
|
||||
// avatar, gender, birth_date, health_status, care_level 无需从表单中获取
|
||||
|
||||
// 执行表单提交逻辑
|
||||
this.submitForm()
|
||||
},
|
||||
async loadElderInfo(id : string) {
|
||||
try {
|
||||
const result = await supa.from('ec_elders')
|
||||
.select('*', {})
|
||||
.eq('id', id)
|
||||
.executeAs<Elder>()
|
||||
// UTS/uni-app-x: result 结构兼容性处理
|
||||
if (result !== null && typeof result === 'object' && result.data !== null && result.data instanceof Array && result.data.length > 0) {
|
||||
const elder = result.data[0] as Elder
|
||||
this.formData = { ...elder }
|
||||
this.updateFormIndexes()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '未找到老人信息',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载老人信息失败:', error)
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
updateFormIndexes() {
|
||||
// 更新选择器索引
|
||||
this.genderIndex = this.formData.gender === 'male' ? 0 : 1
|
||||
|
||||
const healthStatusMap = {
|
||||
'good': 0,
|
||||
'fair': 1,
|
||||
'poor': 2,
|
||||
'critical': 3
|
||||
}
|
||||
this.healthStatusIndex = healthStatusMap[this.formData.health_status] ?? 0
|
||||
|
||||
const careLevelMap = {
|
||||
'level1': 0,
|
||||
'level2': 1,
|
||||
'level3': 2,
|
||||
'special': 3
|
||||
}
|
||||
this.careLevelIndex = careLevelMap[this.formData.care_level] ?? 0
|
||||
},
|
||||
onGenderChange(e : UniPickerChangeEvent) {
|
||||
this.genderIndex = e.detail.value as number
|
||||
this.formData.gender = (e.detail.value as number) === 0 ? 'male' : 'female'
|
||||
},
|
||||
|
||||
onBirthDateChange(e : UniPickerChangeEvent) {
|
||||
this.formData.birth_date = e.detail.value as string
|
||||
},
|
||||
|
||||
onHealthStatusChange(e : UniPickerChangeEvent) {
|
||||
this.healthStatusIndex = e.detail.value as number
|
||||
const statusMap = ['good', 'fair', 'poor', 'critical']
|
||||
this.formData.health_status = statusMap[e.detail.value as number]
|
||||
},
|
||||
|
||||
onCareLevelChange(e : UniPickerChangeEvent) {
|
||||
this.careLevelIndex = e.detail.value as number
|
||||
const levelMap = ['level1', 'level2', 'level3', 'special']
|
||||
this.formData.care_level = levelMap[e.detail.value as number]
|
||||
},
|
||||
|
||||
uploadAvatar() {
|
||||
// 上传头像逻辑
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
// 这里应该上传到服务器并获取URL
|
||||
// 暂时使用本地路径
|
||||
this.formData.avatar = res.tempFilePaths[0]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
uploadIdCardPhoto() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
const filePath = res.tempFilePaths[0]
|
||||
// 上传到 storage
|
||||
const cloudPath = 'elder_idcard/' + Date.now() + '_' + Math.floor(Math.random() * 10000) + '.jpg'
|
||||
uniCloud.uploadFile({
|
||||
filePath: filePath,
|
||||
cloudPath: cloudPath,
|
||||
success: (uploadRes) => {
|
||||
if (uploadRes.fileID) {
|
||||
this.id_card_photo_url = uploadRes.fileID
|
||||
uni.showToast({ title: '上传成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '上传失败', icon: 'error' })
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('身份证上传失败:', err)
|
||||
uni.showToast({ title: '上传失败', icon: 'error' })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async submitForm() {
|
||||
if (!this.validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isSubmitting = true
|
||||
|
||||
try {
|
||||
if (this.isEdit) {
|
||||
await this.updateElder()
|
||||
} else {
|
||||
await this.createElder()
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: this.isEdit ? '更新成功' : '添加成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
this.goBack()
|
||||
}, 1500)
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
this.isSubmitting = false
|
||||
}
|
||||
},
|
||||
|
||||
async createElder() {
|
||||
const result = await supa.executeAs('create_elder', {
|
||||
...this.formData,
|
||||
age: this.calculateAge(this.formData.birth_date),
|
||||
admission_date: new Date().toISOString().split('T')[0],
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error ?? '添加失败')
|
||||
}
|
||||
},
|
||||
|
||||
async updateElder() {
|
||||
const result = await supa.executeAs('update_elder', {
|
||||
elder_id: this.elderId,
|
||||
...this.formData,
|
||||
age: this.calculateAge(this.formData.birth_date)
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error ?? '更新失败')
|
||||
}
|
||||
},
|
||||
|
||||
validateForm() {
|
||||
if (!this.formData.name.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入姓名',
|
||||
icon: 'error'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!this.formData.birth_date) {
|
||||
uni.showToast({
|
||||
title: '请选择出生日期',
|
||||
icon: 'error'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
|
||||
calculateAge(birthDate : string) : number {
|
||||
const today = new Date()
|
||||
const birth = new Date(birthDate)
|
||||
let age = today.getFullYear() - birth.getFullYear()
|
||||
const monthDiff = today.getMonth() - birth.getMonth()
|
||||
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
|
||||
age--
|
||||
}
|
||||
|
||||
return age
|
||||
},
|
||||
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
|
||||
scanIdCardPhoto() {
|
||||
// 调用拍照识别身份证的接口
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['original'],
|
||||
sourceType: ['camera'],
|
||||
success: (res) => {
|
||||
const imagePath = res.tempFilePaths[0]
|
||||
|
||||
// 这里调用身份证识别的云函数或API
|
||||
// 假设有一个云函数叫做 'recognizeIdCard'
|
||||
uni.cloud.callFunction({
|
||||
name: 'recognizeIdCard',
|
||||
data: {
|
||||
image: imagePath
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.result && res.result.code === 200) {
|
||||
// 假设返回的结果中有 name, id_card, gender, birth_date 字段
|
||||
const { name, id_card, gender, birth_date } = res.result.data
|
||||
|
||||
// 更新表单数据
|
||||
this.name = name
|
||||
this.id_card = id_card
|
||||
this.gender = gender === '男' ? 'male' : 'female'
|
||||
this.birth_date = birth_date
|
||||
|
||||
uni.showToast({
|
||||
title: '识别成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '识别失败,请重试',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('调用云函数失败:', err)
|
||||
uni.showToast({
|
||||
title: '识别失败,请重试',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
uploadIdCardPhoto() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
const filePath = res.tempFilePaths[0]
|
||||
// 上传到 storage
|
||||
const cloudPath = 'elder_idcard/' + Date.now() + '_' + Math.floor(Math.random() * 10000) + '.jpg'
|
||||
uniCloud.uploadFile({
|
||||
filePath: filePath,
|
||||
cloudPath: cloudPath,
|
||||
success: (uploadRes) => {
|
||||
if (uploadRes.fileID) {
|
||||
this.id_card_photo_url = uploadRes.fileID
|
||||
uni.showToast({ title: '上传成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '上传失败', icon: 'error' })
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('身份证上传失败:', err)
|
||||
uni.showToast({ title: '上传失败', icon: 'error' })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.elder-form-container {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40rpx 30rpx 30rpx;
|
||||
color: white;
|
||||
|
||||
.header-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 26rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: white;
|
||||
margin: 20rpx 30rpx;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.required-tip {
|
||||
font-size: 24rpx;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-upload-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
overflow: hidden;
|
||||
border: 4rpx solid #e1e1e1;
|
||||
position: relative;
|
||||
|
||||
.avatar-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f8f8f8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.upload-icon {
|
||||
font-size: 30rpx;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
flex: 1;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border: 2rpx solid #e1e1e1;
|
||||
border-radius: 10rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #fff;
|
||||
|
||||
&:focus {
|
||||
border-color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 120rpx;
|
||||
border: 2rpx solid #e1e1e1;
|
||||
border-radius: 10rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #fff;
|
||||
|
||||
&:focus {
|
||||
border-color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
.form-picker {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
border: 2rpx solid #e1e1e1;
|
||||
border-radius: 10rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.picker-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 0 20rpx;
|
||||
|
||||
.picker-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
border: 2rpx solid #e1e1e1;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.btn-last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.id-card-input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 2rpx solid #e1e1e1;
|
||||
border-radius: 10rpx;
|
||||
background-color: #fff;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.id-card-input-row .form-input {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border: none;
|
||||
padding: 0 10rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.id-card-photo-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100rpx;
|
||||
height: 80rpx;
|
||||
background-color: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.photo-icon {
|
||||
margin-right: 5rpx;
|
||||
}
|
||||
|
||||
.id-card-photo-preview {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.id-card-photo-img {
|
||||
width: 100%;
|
||||
max-width: 300rpx;
|
||||
height: auto;
|
||||
border-radius: 10rpx;
|
||||
border: 2rpx solid #e1e1e1;
|
||||
}
|
||||
</style>
|
||||
734
pages/ec/admin/elder-management.uvue
Normal file
734
pages/ec/admin/elder-management.uvue
Normal file
@@ -0,0 +1,734 @@
|
||||
<!-- 养老管理系统 - 老人管理页面 (简化版) -->
|
||||
<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">{{ totalElders }}</view>
|
||||
<view class="stat-label">总入住</view>
|
||||
<view class="stat-trend positive">+{{ newEldersThisMonth }}</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-number">{{ selfCareElders }}</view>
|
||||
<view class="stat-label">自理老人</view>
|
||||
<view class="stat-percent">{{ selfCarePercent }}%</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-number">{{ assistedCareElders }}</view>
|
||||
<view class="stat-label">半护理</view>
|
||||
<view class="stat-percent">{{ assistedCarePercent }}%</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-number">{{ fullCareElders }}</view>
|
||||
<view class="stat-label">全护理</view>
|
||||
<view class="stat-percent">{{ fullCarePercent }}%</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<view class="filter-container">
|
||||
<scroll-view class="filter-scroll" direction="horizontal" :show-scrollbar="false">
|
||||
<view class="filter-item" :class="{ 'is-active': selectedCareLevel == 'all' }" @click="filterByCareLevel('all')">
|
||||
全部
|
||||
</view>
|
||||
<view class="filter-item" :class="{ 'is-active': selectedCareLevel == 'self_care' }" @click="filterByCareLevel('self_care')">
|
||||
自理
|
||||
</view>
|
||||
<view class="filter-item" :class="{ 'is-active': selectedCareLevel == 'assisted' }" @click="filterByCareLevel('assisted')">
|
||||
半护理
|
||||
</view>
|
||||
<view class="filter-item" :class="{ 'is-active': selectedCareLevel == 'full_care' }" @click="filterByCareLevel('full_care')">
|
||||
全护理
|
||||
</view>
|
||||
<view class="filter-item" :class="{ 'is-active': selectedCareLevel == 'dementia' }" @click="filterByCareLevel('dementia')">
|
||||
失智护理
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 老人列表 -->
|
||||
<view class="elders-section">
|
||||
<scroll-view class="elders-list" direction="vertical" :refresher-enabled="true"
|
||||
:refresher-triggered="isRefreshing" @refresherrefresh="refreshElders">
|
||||
<view class="elder-card" v-for="elder in filteredElders" :key="elder.id" @click="viewElderDetail(elder)">
|
||||
<view class="elder-header">
|
||||
<view class="elder-avatar">
|
||||
<image class="avatar-image" :src="elder.profile_picture ?? ''" mode="aspectFill"
|
||||
@error="handleAvatarError" v-if="elder.profile_picture !== null" />
|
||||
<text class="avatar-fallback" v-else>{{ elder.name.charAt(0) }}</text>
|
||||
</view>
|
||||
<view class="elder-basic">
|
||||
<text class="elder-name">{{ elder.name }}</text>
|
||||
<text class="elder-info">{{ elder.age ?? 0 }}岁 · {{ elder.gender == 'male' ? '男' : '女' }}</text>
|
||||
<text class="elder-room">{{ (elder.room_number ?? '') + (elder.bed_number ?? '') }}</text>
|
||||
</view>
|
||||
<view class="elder-status">
|
||||
<view class="health-status" :class="getHealthStatusClass(elder.health_status)">
|
||||
<text class="status-text">{{ getHealthStatusText(elder.health_status) }}</text>
|
||||
</view>
|
||||
<view class="care-level" :class="getCareLevelClass(elder.care_level)">
|
||||
<text class="level-text">{{ getCareLevelText(elder.care_level) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="elder-details">
|
||||
<view class="detail-row">
|
||||
<view class="detail-item">
|
||||
<text class="detail-icon"></text>
|
||||
<text class="detail-text">入住:{{ formatDate(elder.admission_date) }}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-icon">⚕️</text>
|
||||
<text class="detail-text">护理员:待分配</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<view class="detail-item">
|
||||
<text class="detail-icon"></text>
|
||||
<text class="detail-text">联系人:{{ elder.emergency_contact ?? '无' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="elder-actions">
|
||||
<button class="action-btn small" @click.stop="viewHealthRecord(elder)">
|
||||
<text class="btn-text">健康记录</text>
|
||||
</button>
|
||||
<button class="action-btn small" @click.stop="viewCareRecord(elder)">
|
||||
<text class="btn-text">护理记录</text>
|
||||
</button>
|
||||
<button class="action-btn small primary" @click.stop="editElder(elder)">
|
||||
<text class="btn-text">编辑</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="filteredElders.length == 0 && !isLoading">
|
||||
<text class="empty-icon"></text>
|
||||
<text class="empty-title">暂无老人信息</text>
|
||||
<text class="empty-description">{{ getEmptyStateText() }}</text>
|
||||
<button class="empty-action" @click="addNewElder">添加第一位老人</button>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-state" v-if="isLoading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 浮动操作按钮 -->
|
||||
<view class="fab-container">
|
||||
<view class="fab" @click="quickActions">
|
||||
<text class="fab-icon">⚡</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
import type { Elder } from '../types.uts'
|
||||
import { formatDate, getCareLevelText, getHealthStatusText } from '../types.uts'
|
||||
|
||||
// 响应式数据
|
||||
const elders = ref<Array<Elder>>([])
|
||||
const filteredElders = ref<Array<Elder>>([])
|
||||
const searchKeyword = ref<string>('')
|
||||
const selectedCareLevel = ref<string>('all')
|
||||
const isLoading = ref<boolean>(false)
|
||||
const isRefreshing = ref<boolean>(false)
|
||||
|
||||
// 统计数据
|
||||
const totalElders = ref<number>(0)
|
||||
const selfCareElders = ref<number>(0)
|
||||
const assistedCareElders = ref<number>(0)
|
||||
const fullCareElders = ref<number>(0)
|
||||
const newEldersThisMonth = ref<number>(0)
|
||||
|
||||
// 计算百分比
|
||||
const selfCarePercent = computed(() => {
|
||||
return totalElders.value > 0 ? Math.round((selfCareElders.value / totalElders.value) * 100) : 0
|
||||
})
|
||||
|
||||
const assistedCarePercent = computed(() => {
|
||||
return totalElders.value > 0 ? Math.round((assistedCareElders.value / totalElders.value) * 100) : 0
|
||||
})
|
||||
|
||||
const fullCarePercent = computed(() => {
|
||||
return totalElders.value > 0 ? Math.round((fullCareElders.value / totalElders.value) * 100) : 0
|
||||
})
|
||||
|
||||
// 加载老人数据
|
||||
const loadElders = async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
const result = await supa
|
||||
.from('ec_elders')
|
||||
.select(`
|
||||
id,
|
||||
name,
|
||||
age,
|
||||
gender,
|
||||
room_number,
|
||||
bed_number,
|
||||
health_status,
|
||||
care_level,
|
||||
profile_picture,
|
||||
emergency_contact,
|
||||
emergency_phone,
|
||||
admission_date,
|
||||
status
|
||||
`)
|
||||
.eq('status', 'active')
|
||||
.order('created_at', { ascending: false })
|
||||
.executeAs<Array<Elder>>()
|
||||
|
||||
if (result.error == null && result.data !== null) {
|
||||
elders.value = result.data
|
||||
applyFilters()
|
||||
updateStatistics()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载老人数据失败:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计数据
|
||||
const updateStatistics = () => {
|
||||
totalElders.value = elders.value.length
|
||||
|
||||
selfCareElders.value = elders.value.filter(elder => elder.care_level == 'self_care').length
|
||||
assistedCareElders.value = elders.value.filter(elder => elder.care_level == 'assisted').length
|
||||
fullCareElders.value = elders.value.filter(elder => elder.care_level == 'full_care').length
|
||||
|
||||
// 计算本月新增老人数
|
||||
const thisMonth = new Date()
|
||||
thisMonth.setDate(1)
|
||||
thisMonth.setHours(0, 0, 0, 0)
|
||||
|
||||
newEldersThisMonth.value = elders.value.filter(elder => {
|
||||
const admissionDate = elder.admission_date
|
||||
if (admissionDate !== '') {
|
||||
const admission = new Date(admissionDate)
|
||||
return admission >= thisMonth
|
||||
}
|
||||
return false
|
||||
}).length
|
||||
}
|
||||
|
||||
// 应用筛选
|
||||
const applyFilters = () => {
|
||||
let filtered = elders.value
|
||||
|
||||
// 护理等级筛选
|
||||
if (selectedCareLevel.value !== 'all') {
|
||||
filtered = filtered.filter(elder => elder.care_level == selectedCareLevel.value)
|
||||
}
|
||||
|
||||
// 搜索关键词筛选
|
||||
if (searchKeyword.value !== '') {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
filtered = filtered.filter(elder => {
|
||||
const name = elder.name.toLowerCase()
|
||||
const roomNumber = elder.room_number ?? ''
|
||||
return name.includes(keyword) || roomNumber.includes(keyword)
|
||||
})
|
||||
}
|
||||
|
||||
filteredElders.value = filtered
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
applyFilters()
|
||||
}
|
||||
|
||||
// 护理等级筛选
|
||||
const filterByCareLevel = (level: string) => {
|
||||
selectedCareLevel.value = level
|
||||
applyFilters()
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshElders = async () => {
|
||||
isRefreshing.value = true
|
||||
await loadElders()
|
||||
isRefreshing.value = false
|
||||
}
|
||||
|
||||
// 查看老人详情
|
||||
const viewElderDetail = (elder: Elder) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/elders/detail?id=${elder.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 查看健康记录
|
||||
const viewHealthRecord = (elder: Elder) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/health/records?elderId=${elder.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 查看护理记录
|
||||
const viewCareRecord = (elder: Elder) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/care/records?elderId=${elder.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑老人信息
|
||||
const editElder = (elder: Elder) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/elders/edit?id=${elder.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 新增老人
|
||||
const addNewElder = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ec/admin/elder-form'
|
||||
})
|
||||
}
|
||||
|
||||
// 导出数据
|
||||
const exportElders = () => {
|
||||
uni.showToast({
|
||||
title: '导出功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 快速操作
|
||||
const quickActions = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ['批量操作', '数据同步', '生成报表'],
|
||||
success: (res) => {
|
||||
console.log('选择了第' + (res.tapIndex + 1) + '个操作')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 头像错误处理
|
||||
const handleAvatarError = () => {
|
||||
// 头像加载失败时的处理
|
||||
}
|
||||
|
||||
// 获取空状态文本
|
||||
const getEmptyStateText = (): string => {
|
||||
if (searchKeyword.value !== '') {
|
||||
return '没有找到匹配的老人信息'
|
||||
}
|
||||
if (selectedCareLevel.value !== 'all') {
|
||||
return `没有${getCareLevelText(selectedCareLevel.value)}的老人`
|
||||
}
|
||||
return '还没有老人入住,点击下方按钮添加第一位老人'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadElders()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.elder-management {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 头部区域 */
|
||||
.header-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
flex: 1;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
padding: 0 40px 0 15px;
|
||||
font-size: 14px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
margin-left: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #fff;
|
||||
color: #666;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-right: 15px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card.is-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
font-size: 12px;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.stat-percent {
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 筛选器 */
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-scroll {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
margin-right: 10px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-item.is-active {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
/* 老人列表 */
|
||||
.elders-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.elders-list {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.elder-card {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.elder-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.elder-avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 30px;
|
||||
margin-right: 15px;
|
||||
overflow: hidden;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-fallback {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.elder-basic {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.elder-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.elder-info {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.elder-room {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.elder-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.health-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.health-excellent {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.health-good {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.health-fair {
|
||||
background-color: #fff7e6;
|
||||
color: #d48806;
|
||||
}
|
||||
|
||||
.health-poor {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.care-level {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.care-self {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.care-assisted {
|
||||
background-color: #fff7e6;
|
||||
color: #d48806;
|
||||
}
|
||||
|
||||
.care-full {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
/* 详细信息 */
|
||||
.elder-details {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.elder-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.empty-action {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 浮动按钮 */
|
||||
.fab-container {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
.fab {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 28px;
|
||||
background-color: #1890ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
}
|
||||
</style>
|
||||
748
pages/ec/admin/elder-management_new.uvue
Normal file
748
pages/ec/admin/elder-management_new.uvue
Normal 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>
|
||||
1253
pages/ec/admin/health-monitoring.uvue
Normal file
1253
pages/ec/admin/health-monitoring.uvue
Normal file
File diff suppressed because it is too large
Load Diff
337
pages/ec/admin/service-records.uvue
Normal file
337
pages/ec/admin/service-records.uvue
Normal file
@@ -0,0 +1,337 @@
|
||||
|
||||
<template>
|
||||
<view class="all-service-records">
|
||||
<view class="header">
|
||||
<text class="header-title">全部服务记录</text>
|
||||
<button class="refresh-btn" @click="refreshData">
|
||||
<text class="refresh-text">🔄 刷新</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="filters-section">
|
||||
<view class="filter-row">
|
||||
<view class="filter-group">
|
||||
<text class="filter-label">老人</text>
|
||||
<button class="picker-btn" @click="showElderActionSheet">
|
||||
<text class="picker-text">{{ selectedElder?.name ?? '全部' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="filter-group">
|
||||
<text class="filter-label">服务类型</text>
|
||||
<button class="picker-btn" @click="showTypeActionSheet">
|
||||
<text class="picker-text">{{ selectedType?.label ?? '全部' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="filter-group">
|
||||
<text class="filter-label">时间范围</text>
|
||||
<button class="picker-btn" @click="showTimeRangeActionSheet">
|
||||
<text class="picker-text">{{ selectedTimeRange?.label ?? '近7天' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view class="records-list" direction="vertical" :style="{ height: '500px' }">
|
||||
<view v-for="record in filteredRecords" :key="record.id" class="record-item" @click="viewDetail(record)">
|
||||
<view class="record-header">
|
||||
<text class="elder-name">{{ record.elder_name ?? '未知' }}</text>
|
||||
<text class="service-type">{{ serviceTypeLabel(record.service_type) }}</text>
|
||||
<text class="record-time">{{ formatDateTime(record.created_at ?? '') }}</text>
|
||||
</view>
|
||||
<view class="record-content">
|
||||
<text v-if="record.caregiver_name">护理员: {{ record.caregiver_name }}</text>
|
||||
<text v-if="record.doctor_name">医生: {{ record.doctor_name }}</text>
|
||||
<text v-if="record.meal_type">餐次: {{ record.meal_type }}</text>
|
||||
<text v-if="record.activity_name">活动: {{ record.activity_name }}</text>
|
||||
<text class="notes" v-if="record.notes">备注: {{ record.notes }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="filteredRecords.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无服务记录</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { formatDateTime as formatDateTimeUtil } from '../types.uts'
|
||||
|
||||
type AggregatedServiceRecord = {
|
||||
id: string
|
||||
service_type: 'nursing' | 'medical' | 'meal' | 'activity'
|
||||
elder_id: string
|
||||
elder_name?: string
|
||||
caregiver_name?: string
|
||||
doctor_name?: string
|
||||
meal_type?: string
|
||||
activity_name?: string
|
||||
created_at: string
|
||||
notes?: string
|
||||
}
|
||||
type Elder = { id: string, name: string }
|
||||
type FilterOption = { value: string, label: string }
|
||||
|
||||
const records = ref<AggregatedServiceRecord[]>([])
|
||||
const elders = ref<Elder[]>([])
|
||||
const selectedElderIndex = ref<number>(-1)
|
||||
const selectedTypeIndex = ref<number>(-1)
|
||||
const selectedTimeRangeIndex = ref<number>(1)
|
||||
|
||||
const typeOptions = ref<FilterOption[]>([
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'nursing', label: '护理' },
|
||||
{ value: 'medical', label: '医疗' },
|
||||
{ value: 'meal', label: '餐饮' },
|
||||
{ value: 'activity', label: '活动' }
|
||||
])
|
||||
const timeRangeOptions = ref<FilterOption[]>([
|
||||
{ value: '3days', label: '近3天' },
|
||||
{ value: '7days', label: '近7天' },
|
||||
{ value: '30days', label: '近30天' }
|
||||
])
|
||||
|
||||
const elderOptions = computed<Elder[]>(() => [ { id: 'all', name: '全部' }, ...elders.value ])
|
||||
const selectedElder = computed(() => elderOptions.value[selectedElderIndex.value] ?? elderOptions.value[0])
|
||||
const selectedType = computed(() => typeOptions.value[selectedTypeIndex.value] ?? typeOptions.value[0])
|
||||
const selectedTimeRange = computed(() => timeRangeOptions.value[selectedTimeRangeIndex.value] ?? timeRangeOptions.value[1])
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
let list = records.value
|
||||
if (selectedElder.value.id !== 'all') {
|
||||
list = list.filter(r => r.elder_id === selectedElder.value.id)
|
||||
}
|
||||
if (selectedType.value.value !== 'all') {
|
||||
list = list.filter(r => r.service_type === selectedType.value.value)
|
||||
}
|
||||
// 时间范围
|
||||
const now = new Date()
|
||||
let startDate = new Date()
|
||||
if (selectedTimeRange.value.value === '3days') startDate.setDate(now.getDate() - 3)
|
||||
else if (selectedTimeRange.value.value === '7days') startDate.setDate(now.getDate() - 7)
|
||||
else if (selectedTimeRange.value.value === '30days') startDate.setDate(now.getDate() - 30)
|
||||
list = list.filter(r => r.created_at >= startDate.toISOString())
|
||||
return list
|
||||
})
|
||||
|
||||
const formatDateTime = (dt: string) => formatDateTimeUtil(dt)
|
||||
const serviceTypeLabel = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
nursing: '护理',
|
||||
medical: '医疗',
|
||||
meal: '餐饮',
|
||||
activity: '活动'
|
||||
}
|
||||
return map[type] ?? type
|
||||
}
|
||||
|
||||
const refreshData = () => { loadRecords(); loadElders(); }
|
||||
|
||||
const loadRecords = async () => {
|
||||
try {
|
||||
// 聚合查询:分别查四个表,合并后排序
|
||||
const [nursing, medical, meal, activity] = await Promise.all([
|
||||
supa.from('ec_care_records').select('id, elder_id, ec_care_records_elder_id_fkey(name), ec_care_records_caregiver_id_fkey(username), created_at, issues_notes', {}).order('created_at', { ascending: false }).limit(50).executeAs<any[]>(),
|
||||
supa.from('ec_medical_records').select('id, elder_id, ec_medical_records_elder_id_fkey(name), doctor_id, created_at, diagnosis, notes', {}).order('created_at', { ascending: false }).limit(50).executeAs<any[]>(),
|
||||
supa.from('ec_meal_records').select('id, elder_id, ec_meal_records_elder_id_fkey(name), meal_type, created_at, notes', {}).order('created_at', { ascending: false }).limit(50).executeAs<any[]>(),
|
||||
supa.from('ec_activity_participations').select('id, elder_id, ec_activity_participations_elder_id_fkey(name), activity_id, created_at, behavior_notes', {}).order('created_at', { ascending: false }).limit(50).executeAs<any[]>()
|
||||
])
|
||||
const nList = (nursing.data ?? []).map(r => ({
|
||||
id: r.id,
|
||||
service_type: 'nursing',
|
||||
elder_id: r.elder_id,
|
||||
elder_name: r.ec_care_records_elder_id_fkey?.name,
|
||||
caregiver_name: r.ec_care_records_caregiver_id_fkey?.username,
|
||||
created_at: r.created_at,
|
||||
notes: r.issues_notes
|
||||
}))
|
||||
const mList = (medical.data ?? []).map(r => ({
|
||||
id: r.id,
|
||||
service_type: 'medical',
|
||||
elder_id: r.elder_id,
|
||||
elder_name: r.ec_medical_records_elder_id_fkey?.name,
|
||||
doctor_name: r.doctor_id, // 可进一步 join doctor name
|
||||
created_at: r.created_at,
|
||||
notes: r.diagnosis || r.notes
|
||||
}))
|
||||
const mealList = (meal.data ?? []).map(r => ({
|
||||
id: r.id,
|
||||
service_type: 'meal',
|
||||
elder_id: r.elder_id,
|
||||
elder_name: r.ec_meal_records_elder_id_fkey?.name,
|
||||
meal_type: r.meal_type,
|
||||
created_at: r.created_at,
|
||||
notes: r.notes
|
||||
}))
|
||||
const aList = (activity.data ?? []).map(r => ({
|
||||
id: r.id,
|
||||
service_type: 'activity',
|
||||
elder_id: r.elder_id,
|
||||
elder_name: r.ec_activity_participations_elder_id_fkey?.name,
|
||||
activity_name: r.activity_id, // 可进一步 join activity name
|
||||
created_at: r.created_at,
|
||||
notes: r.behavior_notes
|
||||
}))
|
||||
// 合并并按时间排序
|
||||
const all = [...nList, ...mList, ...mealList, ...aList].sort((a, b) => b.created_at.localeCompare(a.created_at))
|
||||
records.value = all
|
||||
} catch (e) { console.error('加载服务记录失败', e) }
|
||||
}
|
||||
const loadElders = async () => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_elders')
|
||||
.select('id, name', {})
|
||||
.eq('status', 'active')
|
||||
.order('name', { ascending: true })
|
||||
.executeAs<Elder[]>()
|
||||
if (result.error == null && result.data != null) {
|
||||
elders.value = result.data
|
||||
}
|
||||
} catch (e) { console.error('加载老人列表失败', e) }
|
||||
}
|
||||
const showElderActionSheet = () => {
|
||||
const options = elderOptions.value.map(e => e.name)
|
||||
uni.showActionSheet({
|
||||
itemList: options,
|
||||
success: (res:any) => { selectedElderIndex.value = res.tapIndex }
|
||||
})
|
||||
}
|
||||
const showTypeActionSheet = () => {
|
||||
const options = typeOptions.value.map(t => t.label)
|
||||
uni.showActionSheet({
|
||||
itemList: options,
|
||||
success: (res:any) => { selectedTypeIndex.value = res.tapIndex }
|
||||
})
|
||||
}
|
||||
const showTimeRangeActionSheet = () => {
|
||||
const options = timeRangeOptions.value.map(t => t.label)
|
||||
uni.showActionSheet({
|
||||
itemList: options,
|
||||
success: (res:any) => { selectedTimeRangeIndex.value = res.tapIndex }
|
||||
})
|
||||
}
|
||||
const viewDetail = (record: AggregatedServiceRecord) => {
|
||||
// 可根据 service_type 跳转不同详情页
|
||||
uni.navigateTo({ url: `/pages/ec/admin/service-record-detail?id=${record.id}&type=${record.service_type}` })
|
||||
}
|
||||
onMounted(() => { refreshData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.all-service-records {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.refresh-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #52c41a;
|
||||
background-color: #52c41a;
|
||||
color: white;
|
||||
}
|
||||
.filters-section {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.filter-group {
|
||||
flex: 1;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.filter-group.is-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
.picker-btn {
|
||||
width: 180rpx;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.picker-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: #f9f9f9;
|
||||
display: block;
|
||||
}
|
||||
.records-list {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
min-height: 300px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.record-item {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.record-item.is-last {
|
||||
border-bottom: none;
|
||||
}
|
||||
.record-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.elder-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.service-type {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.record-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.record-content {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.notes {
|
||||
color: #faad14;
|
||||
}
|
||||
.empty-state {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
1238
pages/ec/analytics/dashboard.uvue
Normal file
1238
pages/ec/analytics/dashboard.uvue
Normal file
File diff suppressed because it is too large
Load Diff
855
pages/ec/caregiver/dashboard.uvue
Normal file
855
pages/ec/caregiver/dashboard.uvue
Normal file
@@ -0,0 +1,855 @@
|
||||
<!-- 养老管理系统 - 护理员仪表板 (简化版) -->
|
||||
<template>
|
||||
<view class="caregiver-dashboard">
|
||||
<view class="header">
|
||||
<text class="title">护理工作台</text>
|
||||
<text class="welcome">{{ caregiverName }},{{ currentTime }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 今日工作概览 -->
|
||||
<view class="today-overview">
|
||||
<view class="overview-card">
|
||||
<view class="card-icon">📋</view>
|
||||
<view class="card-content">
|
||||
<text class="card-number">{{ todayStats.total_tasks }}</text>
|
||||
<text class="card-label">今日任务</text>
|
||||
<view class="card-status">
|
||||
<text class="status-text">{{ todayStats.completed_tasks }}/{{ todayStats.total_tasks }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<view class="overview-card">
|
||||
<view class="card-icon">👥</view>
|
||||
<view class="card-content">
|
||||
<text class="card-number">{{ todayStats.assigned_elders }}</text>
|
||||
<text class="card-label">负责老人</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="overview-card">
|
||||
<view class="card-icon">⚠️</view>
|
||||
<view class="card-content">
|
||||
<text class="card-number">{{ todayStats.urgent_tasks }}</text>
|
||||
<text class="card-label">紧急任务</text>
|
||||
</view>
|
||||
<view class="card-alert" v-if="todayStats.urgent_tasks > 0">
|
||||
<text class="alert-text">需处理</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待处理任务 -->
|
||||
<view class="pending-tasks-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">待处理任务</text>
|
||||
<text class="section-more" @click="viewAllTasks">查看全部</text>
|
||||
</view>
|
||||
<view class="tasks-list">
|
||||
<view v-for="task in pendingTasks" :key="task.id" class="task-item" :class="getTaskPriorityClass(task.priority)" @click="startTask(task)">
|
||||
<view class="task-main">
|
||||
<view class="task-info">
|
||||
<text class="task-name">{{ task.task_name }}</text>
|
||||
<text class="task-elder">{{ task.elder_name }}</text>
|
||||
<text class="task-time">{{ formatTime(task.scheduled_time) }}</text>
|
||||
</view>
|
||||
<view class="task-priority">
|
||||
<view class="priority-badge" :class="task.priority">
|
||||
<text class="priority-text">{{ getPriorityText(task.priority) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="task-actions">
|
||||
<button class="task-btn start" @click.stop="startTask(task)">开始</button>
|
||||
<button class="task-btn detail" @click.stop="viewTaskDetail(task)">详情</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 负责的老人 -->
|
||||
<view class="assigned-elders-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">我负责的老人</text>
|
||||
<text class="section-more" @click="viewAllElders">查看全部</text>
|
||||
</view>
|
||||
<view class="elders-list">
|
||||
<view v-for="elder in assignedElders" :key="elder.id" class="elder-item" @click="viewElderDetail(elder)">
|
||||
<view class="elder-avatar">
|
||||
<image class="avatar-image" :src="elder.profile_picture ?? ''" mode="aspectFill"
|
||||
v-if="elder.profile_picture !== null" />
|
||||
<text class="avatar-fallback" v-else>{{ elder.name.charAt(0) }}</text>
|
||||
</view>
|
||||
<view class="elder-info">
|
||||
<text class="elder-name">{{ elder.name }}</text>
|
||||
<text class="elder-room">{{ elder.room_number }}{{ elder.bed_number }}</text>
|
||||
<text class="elder-care-level">{{ getCareLevelText(elder.care_level) }}</text>
|
||||
</view>
|
||||
<view class="elder-status">
|
||||
<view class="health-indicator" :class="getHealthStatusClass(elder.health_status)">
|
||||
<text class="health-text">{{ getHealthStatusText(elder.health_status) }}</text>
|
||||
</view>
|
||||
<view class="alert-count" v-if="getElderAlertCount(elder.id) > 0">
|
||||
<text class="alert-number">{{ getElderAlertCount(elder.id) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近完成的任务 -->
|
||||
<view class="completed-tasks-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最近完成</text>
|
||||
<text class="section-more" @click="viewCompletedTasks">查看更多</text>
|
||||
</view>
|
||||
<view class="completed-list">
|
||||
<view v-for="task in completedTasks" :key="task.id" class="completed-item">
|
||||
<view class="completed-icon">✅</view>
|
||||
<view class="completed-info">
|
||||
<text class="completed-name">{{ task.task_name }}</text>
|
||||
<text class="completed-elder">{{ task.elder_name }}</text>
|
||||
<text class="completed-time">{{ formatDateTime(task.scheduled_time) }}</text>
|
||||
</view>
|
||||
<view class="completed-status">
|
||||
<text class="status-text">已完成</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<view class="quick-actions">
|
||||
<view class="action-item" @click="quickReport">
|
||||
<view class="action-icon">📝</view>
|
||||
<text class="action-text">快速记录</text>
|
||||
</view>
|
||||
<view class="action-item" @click="emergencyCall">
|
||||
<view class="action-icon">🚨</view>
|
||||
<text class="action-text">紧急呼叫</text>
|
||||
</view>
|
||||
<view class="action-item" @click="healthCheck">
|
||||
<view class="action-icon">💊</view>
|
||||
<text class="action-text">健康检查</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import type { Elder, CareTask } from '../types.uts'
|
||||
import { formatDateTime, formatTime,getPriorityText, getCareLevelText, getHealthStatusText, getHealthStatusClass, getTaskPriorityClass } from '../types.uts'
|
||||
import { state, getCurrentUserId } from '@/utils/store.uts'
|
||||
import type { UserProfile, UserStats } from '@/pages/user/types.uts'
|
||||
|
||||
const profile =ref<UserProfile>(state.userProfile)
|
||||
// 响应式数据
|
||||
const caregiverName = ref<string>('护理员')
|
||||
const currentTime = ref<string>('')
|
||||
|
||||
// 今日统计
|
||||
const todayStats = ref({
|
||||
total_tasks: 0,
|
||||
completed_tasks: 0,
|
||||
assigned_elders: 0,
|
||||
urgent_tasks: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const pendingTasks = ref<Array<CareTask>>([])
|
||||
const assignedElders = ref<Array<Elder>>([])
|
||||
const completedTasks = ref<Array<CareTask>>([])
|
||||
const elderAlerts = ref<Map<string, number>>(new Map())
|
||||
|
||||
// 更新当前时间
|
||||
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 = () => {
|
||||
const today = new Date()
|
||||
const start = new Date(today.getFullYear(), today.getMonth(), today.getDate())
|
||||
const end = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1)
|
||||
return {
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载今日统计
|
||||
const loadTodayStats = async () => {
|
||||
try {
|
||||
const { start, end } = getTodayRange()
|
||||
const currentUserId = profile.value.id
|
||||
// 加载今日任务统计
|
||||
const tasksResult = await supa
|
||||
.from('ec_care_tasks')
|
||||
.select('*', { count: 'exact' })
|
||||
.eq('assigned_to', currentUserId)
|
||||
.gte('scheduled_time', start)
|
||||
.lt('scheduled_time', end)
|
||||
.executeAs<Array<CareTask>>()
|
||||
if (tasksResult.error === null) {
|
||||
todayStats.value.total_tasks = tasksResult.count ?? 0
|
||||
// 计算已完成任务数
|
||||
const completedCount = tasksResult.data?.filter(task => task.status === 'completed').length ?? 0
|
||||
todayStats.value.completed_tasks = completedCount
|
||||
// 计算紧急任务数
|
||||
const urgentCount = tasksResult.data?.filter(task => task.priority === 'urgent' && task.status !== 'completed').length ?? 0
|
||||
todayStats.value.urgent_tasks = urgentCount
|
||||
}
|
||||
// 通过任务表统计负责老人数量
|
||||
const eldersResult = await supa
|
||||
.from('ec_care_tasks')
|
||||
.select('elder_id')
|
||||
.eq('assigned_to', currentUserId)
|
||||
.executeAs<Array<{ elder_id: string }>>()
|
||||
if (eldersResult.error === null && eldersResult.data !== null) {
|
||||
const uniqueElderIds = Array.from(new Set(eldersResult.data.map(e => e.elder_id)))
|
||||
todayStats.value.assigned_elders = uniqueElderIds.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载待处理任务
|
||||
const loadPendingTasks = async () => {
|
||||
try {
|
||||
const currentUserId = profile.value.id
|
||||
|
||||
const result = await supa
|
||||
.from('ec_care_tasks')
|
||||
.select(`
|
||||
id,
|
||||
task_name,
|
||||
elder_name,
|
||||
scheduled_time,
|
||||
status,
|
||||
priority
|
||||
`)
|
||||
.eq('assigned_to', currentUserId)
|
||||
.eq('status', 'pending')
|
||||
.order('scheduled_time', { ascending: true })
|
||||
.limit(5)
|
||||
.executeAs<Array<CareTask>>()
|
||||
|
||||
if (result.error === null && result.data !== null) {
|
||||
pendingTasks.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载待处理任务失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载负责的老人
|
||||
const loadAssignedElders = async () => {
|
||||
try {
|
||||
const currentUserId = profile.value.id
|
||||
// 先查找当前护理员负责的所有老人ID
|
||||
const taskResult = await supa
|
||||
.from('ec_care_tasks')
|
||||
.select('elder_id')
|
||||
.eq('assigned_to', currentUserId)
|
||||
.executeAs<Array<{ elder_id: string }>>()
|
||||
|
||||
if (taskResult.error === null && taskResult.data !== null) {
|
||||
const uniqueElderIds = Array.from(new Set(taskResult.data.map(e => e.elder_id)))
|
||||
if (uniqueElderIds.length === 0) {
|
||||
assignedElders.value = []
|
||||
return
|
||||
}
|
||||
// 查询老人信息
|
||||
const eldersResult = await supa
|
||||
.from('ec_elders')
|
||||
.select(`
|
||||
id,
|
||||
name,
|
||||
room_number,
|
||||
bed_number,
|
||||
health_status,
|
||||
care_level,
|
||||
profile_picture
|
||||
`)
|
||||
.in('id', uniqueElderIds)
|
||||
.eq('status', 'active')
|
||||
.limit(6)
|
||||
.executeAs<Array<Elder>>()
|
||||
|
||||
if (eldersResult.error === null && eldersResult.data !== null) {
|
||||
assignedElders.value = eldersResult.data
|
||||
// 加载每个老人的告警数量
|
||||
loadElderAlerts()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载负责老人失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最近完成的任务
|
||||
const loadCompletedTasks = async () => {
|
||||
try {
|
||||
const currentUserId = profile.value.id
|
||||
|
||||
const result = await supa
|
||||
.from('ec_care_tasks')
|
||||
.select(`
|
||||
id,
|
||||
task_name,
|
||||
elder_name,
|
||||
scheduled_time,
|
||||
status
|
||||
`)
|
||||
.eq('assigned_to', currentUserId)
|
||||
.eq('status', 'completed')
|
||||
.order('updated_at', { ascending: false })
|
||||
.limit(3)
|
||||
.executeAs<Array<CareTask>>()
|
||||
|
||||
if (result.error === null && result.data !== null) {
|
||||
completedTasks.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载完成任务失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载老人告警数量
|
||||
const loadElderAlerts = async () => {
|
||||
try {
|
||||
for (let i: Int = 0; i < assignedElders.value.length; i++) {
|
||||
const elder = assignedElders.value[i]
|
||||
const alertsResult = await supa
|
||||
.from('ec_health_alerts')
|
||||
.select('*', { count: 'exact' })
|
||||
.eq('elder_id', elder.id)
|
||||
.eq('status', 'active')
|
||||
.executeAs<Array<any>>()
|
||||
|
||||
if (alertsResult.error === null) {
|
||||
elderAlerts.value.set(elder.id, alertsResult.count ?? 0)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载告警数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取老人告警数量
|
||||
const getElderAlertCount = (elderId: string): number => {
|
||||
return elderAlerts.value.get(elderId) ?? 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 开始任务
|
||||
const startTask = async (task: CareTask) => {
|
||||
try {
|
||||
await supa
|
||||
.from('ec_care_tasks')
|
||||
.update({
|
||||
status: 'in_progress',
|
||||
start_time: new Date().toISOString()
|
||||
})
|
||||
.eq('id', task.id)
|
||||
.executeAs<any>()
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/tasks/execute?id=${task.id}`
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('开始任务失败:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 查看任务详情
|
||||
const viewTaskDetail = (task: CareTask) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/tasks/detail?id=${task.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 查看老人详情
|
||||
const viewElderDetail = (elder: Elder) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/elders/detail?id=${elder.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 导航函数
|
||||
const viewAllTasks = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ec/tasks/my-tasks'
|
||||
})
|
||||
}
|
||||
|
||||
const viewAllElders = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ec/caregiver/my-elders'
|
||||
})
|
||||
}
|
||||
|
||||
const viewCompletedTasks = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ec/tasks/completed'
|
||||
})
|
||||
}
|
||||
|
||||
// 快速操作
|
||||
const quickReport = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ec/reports/quick-add'
|
||||
})
|
||||
}
|
||||
|
||||
const emergencyCall = () => {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: '120'
|
||||
})
|
||||
}
|
||||
|
||||
const healthCheck = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ec/health/quick-check'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
onLoad(async(options: OnLoadOptions) => {
|
||||
profile.value.id = options['id'] ?? getCurrentUserId()
|
||||
|
||||
if (profile.value.id !='' ) {
|
||||
loadTodayStats()
|
||||
loadPendingTasks()
|
||||
loadAssignedElders()
|
||||
loadCompletedTasks()
|
||||
|
||||
// 定时更新时间
|
||||
setInterval(() => {
|
||||
updateCurrentTime()
|
||||
}, 60000)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.caregiver-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;
|
||||
}
|
||||
|
||||
/* 今日概览 */
|
||||
.today-overview {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-right: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.overview-card:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-number {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.card-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.card-alert {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.alert-text {
|
||||
font-size: 12px;
|
||||
color: #ff4d4f;
|
||||
background-color: #fff2f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 通用列表样式 */
|
||||
.pending-tasks-section,
|
||||
.assigned-elders-section,
|
||||
.completed-tasks-section {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
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-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 任务列表 */
|
||||
.task-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
border-left: 3px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.task-item.priority-urgent {
|
||||
border-left-color: #ff4d4f;
|
||||
background-color: #fff2f0;
|
||||
}
|
||||
|
||||
.task-item.priority-high {
|
||||
border-left-color: #fa8c16;
|
||||
background-color: #fff7e6;
|
||||
}
|
||||
|
||||
.task-item.priority-normal {
|
||||
border-left-color: #1890ff;
|
||||
background-color: #f0f9ff;
|
||||
}
|
||||
|
||||
.task-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.task-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.task-elder {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.task-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.priority-badge {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.priority-badge.urgent {
|
||||
background-color: #ff4d4f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.priority-badge.high {
|
||||
background-color: #fa8c16;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.priority-badge.normal {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.task-btn {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.task-btn.start {
|
||||
background-color: #52c41a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-btn.detail {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 老人列表 */
|
||||
.elder-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.elder-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.elder-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
margin-right: 12px;
|
||||
overflow: hidden;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-fallback {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.elder-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.elder-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.elder-room {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.elder-care-level {
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.elder-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.health-indicator {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.health-excellent {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.health-good {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.health-fair {
|
||||
background-color: #fff7e6;
|
||||
color: #d48806;
|
||||
}
|
||||
|
||||
.health-poor {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.alert-count {
|
||||
background-color: #ff4d4f;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.alert-number {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* 完成列表 */
|
||||
.completed-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.completed-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.completed-icon {
|
||||
margin-right: 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.completed-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.completed-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.completed-elder {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.completed-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.completed-status .status-text {
|
||||
font-size: 12px;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
/* 快速操作 */
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
753
pages/ec/caregiver/elder-details.uvue
Normal file
753
pages/ec/caregiver/elder-details.uvue
Normal file
@@ -0,0 +1,753 @@
|
||||
<template>
|
||||
<scroll-view class="elder-details-container">
|
||||
<!-- Header with Elder Basic Info -->
|
||||
<view class="elder-header">
|
||||
<view class="elder-avatar-section">
|
||||
<image class="elder-avatar" :src="elderInfo.avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
|
||||
<view class="elder-status-badge" :class="getStatusClass(elderInfo.health_status)">
|
||||
<text class="status-text">{{ getHealthStatusText(elderInfo.health_status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="elder-basic-info">
|
||||
<text class="elder-name">{{ elderInfo.name }}</text>
|
||||
<text class="elder-details">{{ elderInfo.age }}岁 | {{ elderInfo.gender === 'male' ? '男' : '女' }}</text>
|
||||
<text class="elder-room">房间: {{ elderInfo.room_number }}</text>
|
||||
<text class="care-level">护理等级: {{ getCareLevelText(elderInfo.care_level) }}</text>
|
||||
</view>
|
||||
<view class="action-buttons">
|
||||
<button class="btn-edit" @click="editElder">编辑</button>
|
||||
<button class="btn-record" @click="recordVitals">记录体征</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Quick Vital Signs -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">生命体征</text>
|
||||
<text class="last-update">最后更新: {{ formatTime(latestVitals.recorded_at) }}</text>
|
||||
</view>
|
||||
<view class="vitals-grid">
|
||||
<view class="vital-item">
|
||||
<text class="vital-label">血压</text>
|
||||
<text class="vital-value" :class="getVitalStatusClass('blood_pressure', latestVitals.blood_pressure)">
|
||||
{{ latestVitals.blood_pressure || '--' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="vital-item">
|
||||
<text class="vital-label">心率</text>
|
||||
<text class="vital-value" :class="getVitalStatusClass('heart_rate', latestVitals.heart_rate)">
|
||||
{{ latestVitals.heart_rate ? latestVitals.heart_rate + ' bpm' : '--' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="vital-item">
|
||||
<text class="vital-label">体温</text>
|
||||
<text class="vital-value" :class="getVitalStatusClass('temperature', latestVitals.temperature)">
|
||||
{{ latestVitals.temperature ? latestVitals.temperature + '°C' : '--' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="vital-item">
|
||||
<text class="vital-label">血氧</text>
|
||||
<text class="vital-value" :class="getVitalStatusClass('oxygen_saturation', latestVitals.oxygen_saturation)">
|
||||
{{ latestVitals.oxygen_saturation ? latestVitals.oxygen_saturation + '%' : '--' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Today's Tasks -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">今日护理任务</text>
|
||||
<text class="task-count">{{ todayTasks.length }} 项任务</text>
|
||||
</view>
|
||||
<view v-if="todayTasks.length === 0" class="empty-state">
|
||||
<text class="empty-text">今日暂无护理任务</text>
|
||||
</view>
|
||||
<view v-else class="tasks-list">
|
||||
<view v-for="task in todayTasks" :key="task.id" class="task-item" @click="completeTask(task)">
|
||||
<view class="task-info">
|
||||
<text class="task-title">{{ task.title }}</text>
|
||||
<text class="task-time">{{ formatTime(task.scheduled_time) }}</text>
|
||||
</view>
|
||||
<view class="task-status" :class="getTaskStatusClass(task.status)">
|
||||
<text class="status-text">{{ getTaskStatusText(task.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Health Records -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">健康记录</text>
|
||||
<text class="view-all" @click="viewAllRecords">查看全部</text>
|
||||
</view>
|
||||
<view v-if="healthRecords.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无健康记录</text>
|
||||
</view>
|
||||
<view v-else class="records-list">
|
||||
<view v-for="record in healthRecords.slice(0, 5)" :key="record.id" class="record-item">
|
||||
<view class="record-info">
|
||||
<text class="record-type">{{ getRecordTypeText(record.record_type) }}</text>
|
||||
<text class="record-content">{{ record.content }}</text>
|
||||
<text class="record-time">{{ formatTime(record.recorded_at) }}</text>
|
||||
</view>
|
||||
<view class="record-priority" :class="getPriorityClass(record.priority)">
|
||||
<text class="priority-text">{{ getPriorityText(record.priority) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Medications -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">用药信息</text>
|
||||
<text class="medication-count">{{ medications.length }} 种药物</text>
|
||||
</view>
|
||||
<view v-if="medications.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无用药记录</text>
|
||||
</view>
|
||||
<view v-else class="medications-list">
|
||||
<view v-for="medication in medications" :key="medication.id" class="medication-item">
|
||||
<view class="medication-info">
|
||||
<text class="medication-name">{{ medication.medication_name }}</text>
|
||||
<text class="medication-dosage">{{ medication.dosage }} | {{ medication.frequency }}</text>
|
||||
<text class="medication-time">下次用药: {{ formatTime(medication.next_dose_time) }}</text>
|
||||
</view>
|
||||
<view class="medication-status" :class="getMedicationStatusClass(medication.status)">
|
||||
<text class="status-text">{{ getMedicationStatusText(medication.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Emergency Contact -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">紧急联系人</text>
|
||||
</view>
|
||||
<view class="contact-info">
|
||||
<view class="contact-item">
|
||||
<text class="contact-label">姓名:</text>
|
||||
<text class="contact-value">{{ elderInfo.emergency_contact_name || '--' }}</text>
|
||||
</view>
|
||||
<view class="contact-item">
|
||||
<text class="contact-label">关系:</text>
|
||||
<text class="contact-value">{{ elderInfo.emergency_contact_relationship || '--' }}</text>
|
||||
</view>
|
||||
<view class="contact-item">
|
||||
<text class="contact-label">电话:</text>
|
||||
<text class="contact-value phone-number" @click="callEmergencyContact">
|
||||
{{ elderInfo.emergency_contact_phone || '--' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<view class="quick-actions">
|
||||
<button class="action-btn emergency" @click="reportEmergency">
|
||||
<text class="btn-icon">🚨</text>
|
||||
<text class="btn-text">紧急报告</text>
|
||||
</button>
|
||||
<button class="action-btn medication" @click="recordMedication">
|
||||
<text class="btn-icon">💊</text>
|
||||
<text class="btn-text">用药记录</text>
|
||||
</button>
|
||||
<button class="action-btn activity" @click="recordActivity">
|
||||
<text class="btn-icon">🏃</text>
|
||||
<text class="btn-text">活动记录</text>
|
||||
</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { ElderInfo, HealthRecord, Medication, CareTask, VitalSigns } from '../types.uts'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
elderId: '',
|
||||
elderInfo: {} as ElderInfo,
|
||||
latestVitals: {} as VitalSigns,
|
||||
todayTasks: [] as CareTask[],
|
||||
healthRecords: [] as HealthRecord[],
|
||||
medications: [] as Medication[]
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.id) {
|
||||
this.elderId = options.id
|
||||
this.loadElderDetails()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadElderDetails() {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadElderInfo(),
|
||||
this.loadVitalSigns(),
|
||||
this.loadTodayTasks(),
|
||||
this.loadHealthRecords(),
|
||||
this.loadMedications()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('加载老人详情失败:', error)
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async loadElderInfo() {
|
||||
const result = await supa.executeAs('elder_by_id', {
|
||||
elder_id: this.elderId
|
||||
})
|
||||
|
||||
if (result.success && result.data.length > 0) {
|
||||
this.elderInfo = result.data[0] as ElderInfo
|
||||
}
|
||||
},
|
||||
|
||||
async loadVitalSigns() {
|
||||
const result = await supa.executeAs('latest_vital_signs', {
|
||||
elder_id: this.elderId
|
||||
})
|
||||
|
||||
if (result.success && result.data.length > 0) {
|
||||
this.latestVitals = result.data[0] as VitalSigns
|
||||
}
|
||||
},
|
||||
|
||||
async loadTodayTasks() {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const result = await supa.executeAs('elder_tasks_by_date', {
|
||||
elder_id: this.elderId,
|
||||
date: today
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
this.todayTasks = result.data as CareTask[]
|
||||
}
|
||||
},
|
||||
|
||||
async loadHealthRecords() {
|
||||
const result = await supa.executeAs('elder_health_records', {
|
||||
elder_id: this.elderId,
|
||||
limit: 10
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
this.healthRecords = result.data as HealthRecord[]
|
||||
}
|
||||
},
|
||||
|
||||
async loadMedications() {
|
||||
const result = await supa.executeAs('elder_medications', {
|
||||
elder_id: this.elderId
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
this.medications = result.data as Medication[]
|
||||
}
|
||||
},
|
||||
|
||||
getStatusClass(status: string): string {
|
||||
const statusMap = {
|
||||
'good': 'status-good',
|
||||
'fair': 'status-fair',
|
||||
'poor': 'status-poor',
|
||||
'critical': 'status-critical'
|
||||
}
|
||||
return statusMap[status] || 'status-good'
|
||||
},
|
||||
|
||||
getHealthStatusText(status: string): string {
|
||||
const statusMap = {
|
||||
'good': '良好',
|
||||
'fair': '一般',
|
||||
'poor': '较差',
|
||||
'critical': '危重'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
getCareLevelText(level: string): string {
|
||||
const levelMap = {
|
||||
'level1': '一级护理',
|
||||
'level2': '二级护理',
|
||||
'level3': '三级护理',
|
||||
'special': '特级护理'
|
||||
}
|
||||
return levelMap[level] || '未设置'
|
||||
},
|
||||
|
||||
getVitalStatusClass(type: string, value: string): string {
|
||||
// 根据生命体征类型和值判断状态
|
||||
if (!value) return 'vital-normal'
|
||||
|
||||
// 这里可以根据具体的医学标准来判断
|
||||
return 'vital-normal'
|
||||
},
|
||||
|
||||
getTaskStatusClass(status: string): string {
|
||||
const statusMap = {
|
||||
'pending': 'task-pending',
|
||||
'in_progress': 'task-progress',
|
||||
'completed': 'task-completed',
|
||||
'overdue': 'task-overdue'
|
||||
}
|
||||
return statusMap[status] || 'task-pending'
|
||||
},
|
||||
|
||||
getTaskStatusText(status: string): string {
|
||||
const statusMap = {
|
||||
'pending': '待执行',
|
||||
'in_progress': '进行中',
|
||||
'completed': '已完成',
|
||||
'overdue': '已逾期'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
getRecordTypeText(type: string): string {
|
||||
const typeMap = {
|
||||
'vital_signs': '生命体征',
|
||||
'medication': '用药记录',
|
||||
'activity': '活动记录',
|
||||
'incident': '事件记录',
|
||||
'observation': '观察记录'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
},
|
||||
|
||||
getPriorityClass(priority: string): string {
|
||||
const priorityMap = {
|
||||
'low': 'priority-low',
|
||||
'normal': 'priority-normal',
|
||||
'high': 'priority-high',
|
||||
'urgent': 'priority-urgent'
|
||||
}
|
||||
return priorityMap[priority] || 'priority-normal'
|
||||
},
|
||||
|
||||
getPriorityText(priority: string): string {
|
||||
const priorityMap = {
|
||||
'low': '低',
|
||||
'normal': '普通',
|
||||
'high': '高',
|
||||
'urgent': '紧急'
|
||||
}
|
||||
return priorityMap[priority] || '普通'
|
||||
},
|
||||
|
||||
getMedicationStatusClass(status: string): string {
|
||||
const statusMap = {
|
||||
'active': 'med-active',
|
||||
'paused': 'med-paused',
|
||||
'completed': 'med-completed'
|
||||
}
|
||||
return statusMap[status] || 'med-active'
|
||||
},
|
||||
|
||||
getMedicationStatusText(status: string): string {
|
||||
const statusMap = {
|
||||
'active': '正在服用',
|
||||
'paused': '暂停',
|
||||
'completed': '已完成'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
formatTime(timestamp: string): string {
|
||||
if (!timestamp) return '--'
|
||||
|
||||
const date = new Date(timestamp)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const minutes = Math.floor(diff / (1000 * 60))
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}天前`
|
||||
} else if (hours > 0) {
|
||||
return `${hours}小时前`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟前`
|
||||
} else {
|
||||
return '刚刚'
|
||||
}
|
||||
},
|
||||
|
||||
editElder() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/admin/elder-form?id=${this.elderId}`
|
||||
})
|
||||
},
|
||||
|
||||
recordVitals() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/health/vital-signs-form?elder_id=${this.elderId}`
|
||||
})
|
||||
},
|
||||
|
||||
async completeTask(task: CareTask) {
|
||||
try {
|
||||
const result = await supa.executeAs('update_task_status', {
|
||||
task_id: task.id,
|
||||
status: 'completed',
|
||||
completed_at: new Date().toISOString()
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
uni.showToast({
|
||||
title: '任务已完成',
|
||||
icon: 'success'
|
||||
})
|
||||
this.loadTodayTasks()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('完成任务失败:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
viewAllRecords() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/health/records?elder_id=${this.elderId}`
|
||||
})
|
||||
},
|
||||
|
||||
callEmergencyContact() {
|
||||
if (this.elderInfo.emergency_contact_phone) {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: this.elderInfo.emergency_contact_phone
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
reportEmergency() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/incident/report-form?elder_id=${this.elderId}&type=emergency`
|
||||
})
|
||||
},
|
||||
|
||||
recordMedication() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/medication/record-form?elder_id=${this.elderId}`
|
||||
})
|
||||
},
|
||||
|
||||
recordActivity() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/activity/record-form?elder_id=${this.elderId}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.elder-details-container {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.elder-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40rpx 30rpx;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.elder-avatar-section {
|
||||
position: relative;
|
||||
|
||||
.elder-avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
border: 4rpx solid rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.elder-status-badge {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 5rpx 10rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 20rpx;
|
||||
|
||||
&.status-good {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
&.status-fair {
|
||||
background-color: #FF9800;
|
||||
}
|
||||
|
||||
&.status-poor {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
&.status-critical {
|
||||
background-color: #9C27B0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.elder-basic-info {
|
||||
flex: 1;
|
||||
|
||||
.elder-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.elder-details, .elder-room, .care-level {
|
||||
font-size: 26rpx;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
|
||||
button {
|
||||
padding: 15rpx 20rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 24rpx;
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border: 2rpx solid rgba(255,255,255,0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.section-card {
|
||||
background: white;
|
||||
margin: 20rpx 30rpx;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.last-update, .task-count, .medication-count {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.view-all {
|
||||
font-size: 26rpx;
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
.vitals-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.vital-item {
|
||||
background-color: #f8f9ff;
|
||||
padding: 20rpx;
|
||||
border-radius: 15rpx;
|
||||
text-align: center;
|
||||
|
||||
.vital-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.vital-value {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
|
||||
&.vital-normal {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
&.vital-warning {
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
&.vital-danger {
|
||||
color: #f44336;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.tasks-list, .records-list, .medications-list {
|
||||
.task-item, .record-item, .medication-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-info, .record-info, .medication-info {
|
||||
flex: 1;
|
||||
|
||||
.task-title, .record-type, .medication-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.task-time, .record-content, .medication-dosage {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.record-time, .medication-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.task-status, .record-priority, .medication-status {
|
||||
padding: 10rpx 15rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
|
||||
&.task-pending, &.priority-low, &.med-paused {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&.task-progress, &.priority-normal, &.med-active {
|
||||
background-color: #e8f5e8;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
&.task-completed, &.med-completed {
|
||||
background-color: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
|
||||
&.task-overdue, &.priority-urgent {
|
||||
background-color: #ffebee;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
&.priority-high {
|
||||
background-color: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15rpx 0;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.contact-label {
|
||||
width: 120rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.contact-value {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
|
||||
&.phone-number {
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 120rpx;
|
||||
border-radius: 15rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
border: none;
|
||||
|
||||
&.emergency {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.medication {
|
||||
background: linear-gradient(135deg, #4ecdc4 0%, #26a69a 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.activity {
|
||||
background: linear-gradient(135deg, #45b7d1 0%, #2196f3 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
170
pages/ec/caregiver/my-elders.uvue
Normal file
170
pages/ec/caregiver/my-elders.uvue
Normal file
@@ -0,0 +1,170 @@
|
||||
<!-- 养老管理系统 - 我负责的老人列表 -->
|
||||
<template>
|
||||
<view class="my-elders-page">
|
||||
<view class="header">
|
||||
<text class="title">我负责的老人</text>
|
||||
</view>
|
||||
<view class="elders-list">
|
||||
<view v-if="elders.length === 0" class="empty-text">暂无负责老人</view>
|
||||
<view v-for="elder in elders" :key="elder.id" class="elder-item" @click="viewElderDetail(elder)">
|
||||
<view class="elder-avatar">
|
||||
<image class="avatar-image" :src="elder.profile_picture ?? ''" mode="aspectFill" v-if="elder.profile_picture !== null" />
|
||||
<text class="avatar-fallback" v-else>{{ elder.name.charAt(0) }}</text>
|
||||
</view>
|
||||
<view class="elder-info">
|
||||
<text class="elder-name">{{ elder.name }}</text>
|
||||
<text class="elder-room">{{ elder.room_number }}{{ elder.bed_number }}</text>
|
||||
<text class="elder-care-level">{{ getCareLevelText(elder.care_level) }}</text>
|
||||
</view>
|
||||
<view class="elder-status">
|
||||
<view class="health-indicator" :class="getHealthStatusClass(elder.health_status)">
|
||||
<text class="health-text">{{ getHealthStatusText(elder.health_status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import type { Elder } from '../types.uts'
|
||||
import { getCareLevelText, getHealthStatusText, getHealthStatusClass } from '../types.uts'
|
||||
import { state, getCurrentUserId } from '@/utils/store.uts'
|
||||
|
||||
const elders = ref<Array<Elder>>([])
|
||||
const profile = ref(state.userProfile)
|
||||
|
||||
const loadMyElders = async (currentUserId) => {
|
||||
try {
|
||||
// 查找当前护理员负责的所有老人ID
|
||||
const taskResult = await supa
|
||||
.from('ec_care_tasks')
|
||||
.select('*',{count:'exact'})
|
||||
.eq('assigned_to', currentUserId)
|
||||
.executeAs<Elder>()
|
||||
if (taskResult.error === null && taskResult.data !== null) {
|
||||
elders.value = taskResult.data
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载负责老人失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const viewElderDetail = (elder: Elder) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/elders/detail?id=${elder.id}`
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((options: OnLoadOptions) => {
|
||||
|
||||
const currentUserId = options['id'] ?? getCurrentUserId()
|
||||
|
||||
loadMyElders(currentUserId)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-elders-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.elders-list {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
.elder-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.elder-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.elder-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
margin-right: 12px;
|
||||
overflow: hidden;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.avatar-fallback {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
.elder-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.elder-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.elder-room {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.elder-care-level {
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
.elder-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.health-indicator {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.health-excellent {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
.health-good {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
.health-fair {
|
||||
background-color: #fff7e6;
|
||||
color: #d48806;
|
||||
}
|
||||
.health-poor {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
.empty-text {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 30px 0;
|
||||
}
|
||||
</style>
|
||||
1255
pages/ec/caregiver/task-execution.uvue
Normal file
1255
pages/ec/caregiver/task-execution.uvue
Normal file
File diff suppressed because it is too large
Load Diff
374
pages/ec/doctor/consultations.uvue
Normal file
374
pages/ec/doctor/consultations.uvue
Normal file
@@ -0,0 +1,374 @@
|
||||
<template>
|
||||
<view class="consultations-page">
|
||||
<text class="title">全部诊疗记录</text>
|
||||
|
||||
<!-- 查询区域 -->
|
||||
<view class="search-section">
|
||||
<view class="search-input-group">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
placeholder="请输入患者姓名或诊疗ID"
|
||||
class="search-input"
|
||||
@confirm="onInputConfirm"
|
||||
/>
|
||||
<button class="search-btn" @click="performSearch">查询</button>
|
||||
</view>
|
||||
|
||||
<view class="filter-group">
|
||||
<picker
|
||||
mode="date"
|
||||
:value="startDate"
|
||||
@change="onStartDateChange"
|
||||
class="date-picker date-picker-left"
|
||||
>
|
||||
<view class="picker-display">
|
||||
开始日期: {{ startDate != '' ? startDate : '请选择' }}
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<picker
|
||||
mode="date"
|
||||
:value="endDate"
|
||||
@change="onEndDateChange"
|
||||
class="date-picker"
|
||||
>
|
||||
<view class="picker-display">
|
||||
结束日期: {{ endDate != '' ? endDate : '请选择' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 诊疗记录列表 -->
|
||||
<view class="records-list" v-if="records.length > 0">
|
||||
<view
|
||||
v-for="record in records"
|
||||
:key="record.id"
|
||||
class="record-item"
|
||||
:class="getUrgencyClass(record.urgency)"
|
||||
>
|
||||
<view class="record-header">
|
||||
<view class="record-header-left">
|
||||
<text class="urgency-tag" v-if="record.urgency != 'normal'">{{ getUrgencyText(record.urgency) }}</text>
|
||||
<text class="patient-name">{{ record.patientName }}</text>
|
||||
</view>
|
||||
<text class="consultation-date">{{ record.date }}</text>
|
||||
</view>
|
||||
<view class="record-content">
|
||||
<text class="diagnosis">{{ record.diagnosis }}</text>
|
||||
</view>
|
||||
<view class="record-actions">
|
||||
<button class="detail-btn" @click="viewDetail(record)">查看详情</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty" v-else>暂无数据</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading" v-if="loading">加载中...</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// 全部诊疗记录页面
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
// 定义诊疗记录类
|
||||
type RecordItem = {
|
||||
id : number,
|
||||
patientName : string,
|
||||
date : string,
|
||||
diagnosis : string,
|
||||
urgency : string // emergency, urgent, normal
|
||||
}
|
||||
|
||||
// 数据
|
||||
const searchQuery = ref('')
|
||||
const startDate = ref('')
|
||||
const endDate = ref('')
|
||||
const records = ref([] as RecordItem[])
|
||||
const loading = ref(false)
|
||||
|
||||
/**
|
||||
* 查询诊疗记录
|
||||
*/
|
||||
function performSearch() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
|
||||
// 模拟异步请求
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// 暂时使用模拟数据
|
||||
const mockRecords : RecordItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
patientName: '张三',
|
||||
date: '2024-01-15',
|
||||
diagnosis: '感冒,发热,建议休息多喝水',
|
||||
urgency: 'normal'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
patientName: '李四',
|
||||
date: '2024-01-14',
|
||||
diagnosis: '高血压,建议定期检查',
|
||||
urgency: 'urgent'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
patientName: '王五',
|
||||
date: '2024-01-16',
|
||||
diagnosis: '急性胸痛,疑心肌梗死,需紧急处置',
|
||||
urgency: 'emergency'
|
||||
}
|
||||
]
|
||||
|
||||
// 过滤数据(模拟查询)
|
||||
records.value = mockRecords.filter((record : RecordItem) : boolean => {
|
||||
const matchesQuery = searchQuery.value == '' ||
|
||||
record.patientName.includes(searchQuery.value) ||
|
||||
record.id.toString().includes(searchQuery.value)
|
||||
|
||||
const matchesDate = (startDate.value == '' || record.date >= startDate.value) &&
|
||||
(endDate.value == '' || record.date <= endDate.value)
|
||||
|
||||
return matchesQuery && matchesDate
|
||||
})
|
||||
|
||||
} catch (e : any) {
|
||||
console.error('查询失败:', e)
|
||||
uni.showToast({
|
||||
title: '查询失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// 输入确认搜索
|
||||
function onInputConfirm(_ : any) {
|
||||
performSearch()
|
||||
}
|
||||
|
||||
// 日期选择器变化
|
||||
function onStartDateChange(e : any) {
|
||||
startDate.value = e.detail.value as string
|
||||
}
|
||||
|
||||
function onEndDateChange(e : any) {
|
||||
endDate.value = e.detail.value as string
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
function viewDetail(record : RecordItem) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/doctor/consultation-detail?id=${record.id}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取紧急程度对应的样式类
|
||||
*/
|
||||
function getUrgencyClass(urgency : string) : string {
|
||||
if (urgency == 'emergency') {
|
||||
return 'record-item-emergency'
|
||||
} else if (urgency == 'urgent') {
|
||||
return 'record-item-urgent'
|
||||
}
|
||||
return 'record-item-normal'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取紧急程度文字
|
||||
*/
|
||||
function getUrgencyText(urgency : string) : string {
|
||||
if (urgency == 'emergency') {
|
||||
return '【紧急】'
|
||||
} else if (urgency == 'urgent') {
|
||||
return '【优先】'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 页面加载时获取所有记录
|
||||
onMounted(() => {
|
||||
performSearch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.consultations-page {
|
||||
padding: 30px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-input-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #dddddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.date-picker-left {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.picker-display {
|
||||
padding: 10px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #dddddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.records-list {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
padding: 15px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #eeeeee;
|
||||
}
|
||||
|
||||
.record-item-emergency {
|
||||
background-color: #fff1f0;
|
||||
}
|
||||
|
||||
.record-item-urgent {
|
||||
background-color: #fff7e6;
|
||||
}
|
||||
|
||||
.record-item-normal {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.record-header-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.urgency-tag {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.record-item-emergency .urgency-tag {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.record-item-urgent .urgency-tag {
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.patient-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.consultation-date {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.record-content {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.diagnosis {
|
||||
font-size: 14px;
|
||||
color: #555555;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.record-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
background-color: #28a745;
|
||||
color: #ffffff;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #aaaaaa;
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #666666;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
932
pages/ec/doctor/dashboard.uvue
Normal file
932
pages/ec/doctor/dashboard.uvue
Normal file
@@ -0,0 +1,932 @@
|
||||
<!--
|
||||
UTS-Android 兼容性开发规范(重要,所有开发成员须遵循)
|
||||
- 表单优先用form,变量声明用let/const,不能用var。
|
||||
- 跟template交互的变量尽量用一维变量。
|
||||
- 不用foreach/map/safeget,只用for和UTSJSONObject。
|
||||
- 数组类型用Array<Type>,不用简写[]。
|
||||
- 不用interface,只用type。
|
||||
- 判断空用 !== null,不用!。
|
||||
- 不支持undefined,变量为null时需判空。
|
||||
- 逻辑或用??(空值合并),不用||。
|
||||
- for循环i需指定Int类型:for (let i:Int = 0; ...)
|
||||
- 不支持Intersection Type、Index Signature。
|
||||
- picker用picker-view或uni.showActionSheet。
|
||||
- scroll-view用direction="vertical"。
|
||||
- CSS只用display:flex; 不用gap、grid、calc()、伪类、vh等。
|
||||
- 复杂数据交互用utils/utis下的UTSJSONObject。
|
||||
- 时间选择用uni_modules/lime-date-time-picker。
|
||||
-->
|
||||
|
||||
|
||||
<template>
|
||||
<view class="doctor-dashboard">
|
||||
<!-- Header -->
|
||||
<view class="header">
|
||||
<text class="header-title">医生工作台</text>
|
||||
<text class="header-subtitle">{{ currentTime }}</text>
|
||||
<view class="header-actions">
|
||||
<button class="action-btn emergency" @click="showEmergencyPage">
|
||||
<text class="btn-text">🚨 急诊</text>
|
||||
</button>
|
||||
<button class="action-btn" @click="showNewConsultation">
|
||||
<text class="btn-text">➕ 新建诊疗</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<view class="stats-section">
|
||||
<view class="stat-card">
|
||||
<view class="stat-icon">👥</view>
|
||||
<view class="stat-content">
|
||||
<text class="stat-number">{{ stats.today_patients }}</text>
|
||||
<text class="stat-label">今日患者</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-icon">⏰</view>
|
||||
<view class="stat-content">
|
||||
<text class="stat-number">{{ stats.pending_consultations }}</text>
|
||||
<text class="stat-label">待诊疗</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-icon">💊</view>
|
||||
<view class="stat-content">
|
||||
<text class="stat-number">{{ stats.prescriptions_today }}</text>
|
||||
<text class="stat-label">今日处方</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-card urgent">
|
||||
<view class="stat-icon">🚨</view>
|
||||
<view class="stat-content">
|
||||
<text class="stat-number">{{ stats.urgent_cases }}</text>
|
||||
<text class="stat-label">紧急病例</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<view class="quick-actions">
|
||||
<text class="section-title">快速操作</text>
|
||||
<view class="actions-grid">
|
||||
<button class="quick-action-btn" @click="showPatientQueue">
|
||||
<text class="action-icon">👥</text>
|
||||
<text class="action-text">患者队列</text>
|
||||
</button>
|
||||
<button class="quick-action-btn" @click="showMedicalRecords">
|
||||
<text class="action-icon">📋</text>
|
||||
<text class="action-text">病历管理</text>
|
||||
</button>
|
||||
<button class="quick-action-btn" @click="showPrescriptions">
|
||||
<text class="action-icon">💊</text>
|
||||
<text class="action-text">处方管理</text>
|
||||
</button>
|
||||
<button class="quick-action-btn" @click="showHealthReports">
|
||||
<text class="action-icon">📊</text>
|
||||
<text class="action-text">健康报告</text>
|
||||
</button>
|
||||
<button class="quick-action-btn" @click="showVitalSigns">
|
||||
<text class="action-icon">❤️</text>
|
||||
<text class="action-text">生命体征</text>
|
||||
</button>
|
||||
<button class="quick-action-btn" @click="showMedicationManagement">
|
||||
<text class="action-icon">💉</text>
|
||||
<text class="action-text">用药管理</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Today's Schedule -->
|
||||
<view class="schedule-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">今日日程 ({{ todaySchedule.length }})</text>
|
||||
<button class="view-all-btn" @click="showFullSchedule">
|
||||
<text class="btn-text">查看全部</text>
|
||||
</button>
|
||||
</view>
|
||||
<scroll-view class="schedule-list" scroll-y="true" :style="{ height: '400px' }">
|
||||
<view
|
||||
v-for="appointment in todaySchedule"
|
||||
:key="appointment.id"
|
||||
class="schedule-item"
|
||||
:class="{
|
||||
'current': isCurrentAppointment(appointment),
|
||||
'urgent': appointment.priority === 'urgent',
|
||||
'completed': appointment.status === 'completed'
|
||||
}"
|
||||
@click="openAppointment(appointment)"
|
||||
>
|
||||
<view class="appointment-time">
|
||||
<text class="time-text">{{ formatTime(appointment.scheduled_time) }}</text>
|
||||
<text class="status-text" :class="appointment.status">{{ getStatusText(appointment.status) }}</text>
|
||||
</view>
|
||||
<view class="appointment-details">
|
||||
<text class="patient-name">{{ appointment.elder_name }}</text>
|
||||
<text class="appointment-type">{{ getAppointmentTypeText(appointment.appointment_type) }}</text>
|
||||
<text class="chief-complaint" v-if="appointment.chief_complaint">{{ appointment.chief_complaint }}</text>
|
||||
<text class="room-info" v-if="appointment.room_number">房间: {{ appointment.room_number }}</text>
|
||||
</view>
|
||||
<view class="appointment-actions">
|
||||
<button
|
||||
v-if="appointment.status === 'scheduled'"
|
||||
class="start-btn"
|
||||
@click.stop="startConsultation(appointment)"
|
||||
>
|
||||
<text class="btn-text">开始诊疗</text>
|
||||
</button>
|
||||
<button
|
||||
v-if="appointment.status === 'in_progress'"
|
||||
class="continue-btn"
|
||||
@click.stop="continueConsultation(appointment)"
|
||||
>
|
||||
<text class="btn-text">继续</text>
|
||||
</button>
|
||||
<button
|
||||
v-if="appointment.status === 'completed'"
|
||||
class="view-btn"
|
||||
@click.stop="viewConsultation(appointment)"
|
||||
>
|
||||
<text class="btn-text">查看</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="todaySchedule.length === 0" class="empty-state">
|
||||
<text class="empty-text">今日暂无安排</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- Recent Consultations -->
|
||||
<view class="recent-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最近诊疗记录</text>
|
||||
<button class="view-all-btn" @click="showAllConsultations">
|
||||
<text class="btn-text">查看全部</text>
|
||||
</button>
|
||||
</view>
|
||||
<scroll-view class="recent-list" scroll-y="true" :style="{ height: '300px' }">
|
||||
<view
|
||||
v-for="consultation in recentConsultations"
|
||||
:key="consultation.id"
|
||||
class="consultation-item"
|
||||
@click="viewConsultationDetails(consultation)"
|
||||
>
|
||||
<view class="consultation-header">
|
||||
<text class="patient-name">{{ consultation.elder_name }}</text>
|
||||
<text class="consultation-date">{{ formatDateTime(consultation.scheduled_time) }}</text>
|
||||
</view>
|
||||
<view class="consultation-content">
|
||||
<text class="diagnosis">诊断: {{ consultation.diagnosis || '未填写' }}</text>
|
||||
<text class="treatment">治疗: {{ consultation.treatment || '未填写' }}</text>
|
||||
</view>
|
||||
<view class="consultation-meta">
|
||||
<text class="consultation-type">{{ getConsultationTypeText(consultation.consultation_type) }}</text>
|
||||
<text class="follow-up" v-if="consultation.follow_up_date">复诊: {{ formatDate(consultation.follow_up_date) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="recentConsultations.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无诊疗记录</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- Urgent Alerts -->
|
||||
<view class="alerts-section" v-if="urgentAlerts.length > 0">
|
||||
<view class="section-header">
|
||||
<text class="section-title">紧急提醒</text>
|
||||
<button class="view-all-btn" @click="showAllAlerts">
|
||||
<text class="btn-text">查看全部</text>
|
||||
</button>
|
||||
</view>
|
||||
<scroll-view class="alerts-list" scroll-y="true">
|
||||
<view
|
||||
v-for="alert in urgentAlerts"
|
||||
:key="alert.id"
|
||||
class="alert-item"
|
||||
:class="alert.severity"
|
||||
@click="handleAlert(alert)"
|
||||
>
|
||||
<view class="alert-header">
|
||||
<text class="alert-title">{{ alert.title }}</text>
|
||||
<text class="alert-time">{{ formatTime(alert.created_at) }}</text>
|
||||
</view>
|
||||
<view class="alert-content">
|
||||
<text class="alert-description">{{ alert.description }}</text>
|
||||
<text class="alert-patient">患者: {{ alert.elder_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
import {
|
||||
formatTime,
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
getCurrentTimeString,
|
||||
getTodayStart,
|
||||
getTodayEnd,
|
||||
getRecentDate,
|
||||
getSeverityText
|
||||
} from '../types_new.uts'
|
||||
|
||||
// 数据类型定义
|
||||
type DoctorStats = {
|
||||
today_patients: number
|
||||
pending_consultations: number
|
||||
prescriptions_today: number
|
||||
urgent_cases: number
|
||||
}
|
||||
|
||||
type Appointment = {
|
||||
id: string
|
||||
elder_id: string
|
||||
elder_name: string
|
||||
doctor_id: string
|
||||
scheduled_time: string
|
||||
appointment_type: string
|
||||
chief_complaint: string
|
||||
status: string
|
||||
priority: string
|
||||
room_number: string
|
||||
estimated_duration: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type Consultation = {
|
||||
id: string
|
||||
elder_id: string
|
||||
elder_name: string
|
||||
doctor_id: string
|
||||
scheduled_time: string
|
||||
consultation_type: string
|
||||
chief_complaint: string
|
||||
diagnosis: string
|
||||
treatment: string
|
||||
prescription: string
|
||||
follow_up_date: string
|
||||
notes: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type HealthAlert = {
|
||||
id: string
|
||||
elder_id: string
|
||||
elder_name: string
|
||||
title: string
|
||||
description: string
|
||||
severity: string
|
||||
alert_type: string
|
||||
status: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const currentTime = ref<string>('')
|
||||
const stats = ref<DoctorStats>({
|
||||
today_patients: 0,
|
||||
pending_consultations: 0,
|
||||
prescriptions_today: 0,
|
||||
urgent_cases: 0
|
||||
})
|
||||
const todaySchedule = ref<Appointment[]>([])
|
||||
const recentConsultations = ref<Consultation[]>([])
|
||||
const urgentAlerts = ref<HealthAlert[]>([])
|
||||
|
||||
let timeInterval: number = 0
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
updateCurrentTime()
|
||||
timeInterval = setInterval(updateCurrentTime, 60000) // 每分钟更新时间
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval)
|
||||
}
|
||||
})
|
||||
|
||||
// 更新当前时间
|
||||
const updateCurrentTime = () => {
|
||||
currentTime.value = getCurrentTimeString()
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
await Promise.all([
|
||||
loadStats(),
|
||||
loadTodaySchedule(),
|
||||
loadRecentConsultations(),
|
||||
loadUrgentAlerts()
|
||||
])
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
// 今日患者数
|
||||
const patientsResult = await supa
|
||||
.from('ec_appointments')
|
||||
.select('*', { count: 'exact' })
|
||||
.gte('scheduled_time', getTodayStart())
|
||||
.lte('scheduled_time', getTodayEnd())
|
||||
.executeAs<Appointment[]>()
|
||||
|
||||
// 待诊疗数量
|
||||
const pendingResult = await supa
|
||||
.from('ec_appointments')
|
||||
.select('*', { count: 'exact' })
|
||||
.in('status', ['scheduled', 'in_progress'])
|
||||
.executeAs<Appointment[]>()
|
||||
|
||||
// 今日处方数量
|
||||
const prescriptionsResult = await supa
|
||||
.from('ec_medications')
|
||||
.select('*', { count: 'exact' })
|
||||
.gte('created_at', getTodayStart())
|
||||
.lte('created_at', getTodayEnd())
|
||||
.executeAs<any[]>()
|
||||
|
||||
// 紧急病例数量
|
||||
const urgentResult = await supa
|
||||
.from('ec_health_alerts')
|
||||
.select('*', { count: 'exact' })
|
||||
.in('severity', ['high', 'critical'])
|
||||
.eq('status', 'active')
|
||||
.executeAs<HealthAlert[]>()
|
||||
|
||||
stats.value = {
|
||||
today_patients: patientsResult.count ?? 0,
|
||||
pending_consultations: pendingResult.count ?? 0,
|
||||
prescriptions_today: prescriptionsResult.count ?? 0,
|
||||
urgent_cases: urgentResult.count ?? 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载今日日程
|
||||
const loadTodaySchedule = async () => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_appointments')
|
||||
.select(`
|
||||
id,
|
||||
elder_id,
|
||||
elder_name,
|
||||
doctor_id,
|
||||
scheduled_time,
|
||||
appointment_type,
|
||||
chief_complaint,
|
||||
status,
|
||||
priority,
|
||||
room_number,
|
||||
estimated_duration,
|
||||
created_at
|
||||
`)
|
||||
.gte('scheduled_time', getTodayStart())
|
||||
.lte('scheduled_time', getTodayEnd())
|
||||
.order('scheduled_time', { ascending: true })
|
||||
.executeAs<Appointment[]>()
|
||||
|
||||
if (result.error == null && result.data != null) {
|
||||
todaySchedule.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载今日日程失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最近诊疗记录
|
||||
const loadRecentConsultations = async () => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_consultations')
|
||||
.select(`
|
||||
id,
|
||||
elder_id,
|
||||
elder_name,
|
||||
doctor_id,
|
||||
scheduled_time,
|
||||
consultation_type,
|
||||
chief_complaint,
|
||||
diagnosis,
|
||||
treatment,
|
||||
prescription,
|
||||
follow_up_date,
|
||||
notes,
|
||||
created_at
|
||||
`)
|
||||
.gte('scheduled_time', getRecentDate(7))
|
||||
.order('scheduled_time', { ascending: false })
|
||||
.limit(10)
|
||||
.executeAs<Consultation[]>()
|
||||
|
||||
if (result.error == null && result.data != null) {
|
||||
recentConsultations.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载最近诊疗记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载紧急提醒
|
||||
const loadUrgentAlerts = async () => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_health_alerts')
|
||||
.select(`
|
||||
id,
|
||||
elder_id,
|
||||
elder_name,
|
||||
title,
|
||||
description,
|
||||
severity,
|
||||
alert_type,
|
||||
status,
|
||||
created_at
|
||||
`)
|
||||
.in('severity', ['high', 'critical'])
|
||||
.eq('status', 'active')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(5)
|
||||
.executeAs<HealthAlert[]>()
|
||||
|
||||
if (result.error == null && result.data != null) {
|
||||
urgentAlerts.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载紧急提醒失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
const getStatusText = (status: string): string => {
|
||||
const statusMap = new Map([
|
||||
['scheduled', '已预约'],
|
||||
['in_progress', '进行中'],
|
||||
['completed', '已完成'],
|
||||
['cancelled', '已取消'],
|
||||
['no_show', '未到']
|
||||
])
|
||||
return statusMap.get(status) ?? status
|
||||
}
|
||||
|
||||
const getAppointmentTypeText = (type: string): string => {
|
||||
const typeMap = new Map([
|
||||
['routine', '常规检查'],
|
||||
['follow_up', '复诊'],
|
||||
['emergency', '急诊'],
|
||||
['consultation', '会诊'],
|
||||
['physical', '体检']
|
||||
])
|
||||
return typeMap.get(type) ?? type
|
||||
}
|
||||
|
||||
const getConsultationTypeText = (type: string): string => {
|
||||
const typeMap = new Map([
|
||||
['initial', '初诊'],
|
||||
['follow_up', '复诊'],
|
||||
['emergency', '急诊'],
|
||||
['consultation', '会诊'],
|
||||
['phone', '电话咨询']
|
||||
])
|
||||
return typeMap.get(type) ?? type
|
||||
}
|
||||
|
||||
const isCurrentAppointment = (appointment: Appointment): boolean => {
|
||||
const now = new Date()
|
||||
const appointmentTime = new Date(appointment.scheduled_time)
|
||||
const diff = Math.abs(now.getTime() - appointmentTime.getTime())
|
||||
return diff <= 30 * 60 * 1000 && appointment.status === 'in_progress' // 30分钟内
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
const showEmergencyPage = () => {
|
||||
uni.navigateTo({ url: '/pages/ec/doctor/emergency' })
|
||||
}
|
||||
|
||||
const showNewConsultation = () => {
|
||||
uni.navigateTo({ url: '/pages/ec/doctor/consultation-form' })
|
||||
}
|
||||
|
||||
const showPatientQueue = () => {
|
||||
uni.navigateTo({ url: '/pages/ec/doctor/patient-queue' })
|
||||
}
|
||||
|
||||
const showMedicalRecords = () => {
|
||||
uni.navigateTo({ url: '/pages/ec/doctor/medical-records' })
|
||||
}
|
||||
|
||||
const showPrescriptions = () => {
|
||||
uni.navigateTo({ url: '/pages/ec/doctor/prescriptions' })
|
||||
}
|
||||
|
||||
const showHealthReports = () => {
|
||||
uni.navigateTo({ url: '/pages/ec/doctor/health-reports' })
|
||||
}
|
||||
|
||||
const showVitalSigns = () => {
|
||||
uni.navigateTo({ url: '/pages/ec/doctor/vital-signs' })
|
||||
}
|
||||
|
||||
const showMedicationManagement = () => {
|
||||
uni.navigateTo({ url: '/pages/ec/doctor/medication-management' })
|
||||
}
|
||||
|
||||
const showFullSchedule = () => {
|
||||
uni.navigateTo({ url: '/pages/ec/doctor/schedule' })
|
||||
}
|
||||
|
||||
const showAllConsultations = () => {
|
||||
uni.navigateTo({ url: '/pages/ec/doctor/consultations' })
|
||||
}
|
||||
|
||||
const showAllAlerts = () => {
|
||||
uni.navigateTo({ url: '/pages/ec/doctor/alerts' })
|
||||
}
|
||||
|
||||
const openAppointment = (appointment: Appointment) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/doctor/appointment-detail?id=${appointment.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const startConsultation = async (appointment: Appointment) => {
|
||||
try {
|
||||
await supa
|
||||
.from('ec_appointments')
|
||||
.update({ status: 'in_progress' })
|
||||
.eq('id', appointment.id)
|
||||
.executeAs<any>()
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/doctor/consultation?appointmentId=${appointment.id}`
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('开始诊疗失败:', error)
|
||||
uni.showToast({ title: '操作失败', icon: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
const continueConsultation = (appointment: Appointment) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/doctor/consultation?appointmentId=${appointment.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const viewConsultation = (appointment: Appointment) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/doctor/consultation-detail?appointmentId=${appointment.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const viewConsultationDetails = (consultation: Consultation) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/doctor/consultation-detail?id=${consultation.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const handleAlert = (alert: HealthAlert) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/doctor/alert-detail?id=${alert.id}`
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.doctor-dashboard {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: #667eea;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.header-subtitle {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
background-color: #fff;
|
||||
color: #667eea;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.action-btn.emergency {
|
||||
background-color: #ff4757;
|
||||
color: #fff;
|
||||
}
|
||||
.action-btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction:row;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stat-card {
|
||||
flex: 1 1 140px;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.stat-card:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.stat-card.urgent {
|
||||
background-color: #ff6b6b;
|
||||
color: #fff;
|
||||
}
|
||||
.stat-icon {
|
||||
font-size: 22px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.stat-card.urgent .stat-icon {
|
||||
background-color: #fff2f0;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
.stat-number {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
.actions-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction:row;
|
||||
}
|
||||
.quick-action-btn {
|
||||
width: 46%;
|
||||
margin-right: 4%;
|
||||
margin-bottom: 12px;
|
||||
padding: 16px 0;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.quick-action-btn:nth-child(2n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
.action-icon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.action-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.schedule-section, .recent-section, .alerts-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.view-all-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #fff;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.schedule-list, .recent-list, .alerts-list {
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.schedule-item {
|
||||
padding: 14px 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.schedule-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.schedule-item.current {
|
||||
background-color: #e8f5e8;
|
||||
border-left: 4px solid #4caf50;
|
||||
}
|
||||
.schedule-item.urgent {
|
||||
background-color: #fffbe6;
|
||||
border-left: 4px solid #fdcb6e;
|
||||
}
|
||||
.schedule-item.completed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.appointment-time {
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.time-text {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.status-text.scheduled { color: #3498db; }
|
||||
.status-text.in_progress { color: #f39c12; }
|
||||
.status-text.completed { color: #27ae60; }
|
||||
.status-text.cancelled { color: #e74c3c; }
|
||||
.appointment-details {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.patient-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.appointment-type {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.chief-complaint {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.room-info {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.appointment-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.start-btn, .continue-btn, .view-btn {
|
||||
padding: 6px 14px;
|
||||
border-radius: 16px;
|
||||
border: none;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.start-btn { background-color: #27ae60; }
|
||||
.continue-btn { background-color: #f39c12; }
|
||||
.view-btn { background-color: #3498db; }
|
||||
|
||||
.consultation-item {
|
||||
padding: 14px 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.consultation-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.consultation-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.consultation-date {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
.diagnosis, .treatment {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.consultation-meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.consultation-type, .follow-up {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-left: 4px solid #ddd;
|
||||
}
|
||||
.alert-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.alert-item.high {
|
||||
border-left-color: #f39c12;
|
||||
background-color: #fef9e7;
|
||||
}
|
||||
.alert-item.critical {
|
||||
border-left-color: #e74c3c;
|
||||
background-color: #fdf2f2;
|
||||
}
|
||||
.alert-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.alert-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.alert-time {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.alert-description {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.alert-patient {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
166
pages/ec/doctor/emergency.uvue
Normal file
166
pages/ec/doctor/emergency.uvue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<view class="emergency-page">
|
||||
<view class="header">
|
||||
<text class="title">急诊管理</text>
|
||||
<button class="add-btn" @click="showAddEmergency">➕ 新建急诊</button>
|
||||
</view>
|
||||
<view class="stats">
|
||||
<text>今日急诊: {{ stats.today }}</text>
|
||||
<text>处理中: {{ stats.processing }}</text>
|
||||
<text>已解决: {{ stats.resolved }}</text>
|
||||
</view>
|
||||
<scroll-view class="emergency-list" scroll-y="true">
|
||||
<view v-for="item in emergencies" :key="item.id" class="emergency-item" :class="item.severity">
|
||||
<view class="item-header">
|
||||
<text class="type">{{ getTypeText(item.emergency_type) }}</text>
|
||||
<text class="severity">{{ getSeverityText(item.severity) }}</text>
|
||||
<text class="status">{{ getStatusText(item.status) }}</text>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<text class="elder">患者: {{ item.elder_name }}</text>
|
||||
<text class="desc">{{ item.description }}</text>
|
||||
</view>
|
||||
<view class="item-footer">
|
||||
<text class="time">{{ formatDateTime(item.occurred_at) }}</text>
|
||||
<button v-if="item.status==='active'" class="handle-btn" @click="handleEmergency(item)">处理</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="emergencies.length===0" class="empty-state">
|
||||
<text>暂无急诊事件</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { formatDateTime } from '../types_new.uts'
|
||||
|
||||
type Emergency = {
|
||||
id: string
|
||||
appointment_id: string
|
||||
elder_id: string
|
||||
elder_name: string
|
||||
doctor_id: string
|
||||
emergency_type: string
|
||||
severity: string
|
||||
status: string
|
||||
description: string
|
||||
occurred_at: string
|
||||
handled_at: string
|
||||
handler_notes: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
type EmergencyStats = {
|
||||
today: number
|
||||
processing: number
|
||||
resolved: number
|
||||
}
|
||||
|
||||
const emergencies = ref<Emergency[]>([])
|
||||
const stats = ref<EmergencyStats>({ today: 0, processing: 0, resolved: 0 })
|
||||
|
||||
onMounted(() => {
|
||||
loadEmergencies()
|
||||
loadStats()
|
||||
})
|
||||
|
||||
const loadEmergencies = async () => {
|
||||
const result = await supa
|
||||
.from('ec_emergencies')
|
||||
.select('*')
|
||||
.order('occurred_at', { ascending: false })
|
||||
.limit(30)
|
||||
.executeAs<Emergency[]>()
|
||||
if (result.error == null && result.data != null) {
|
||||
emergencies.value = result.data
|
||||
}
|
||||
}
|
||||
|
||||
const loadStats = async () => {
|
||||
const todayStart = new Date()
|
||||
todayStart.setHours(0,0,0,0)
|
||||
const todayEnd = new Date()
|
||||
todayEnd.setHours(23,59,59,999)
|
||||
const todayResult = await supa
|
||||
.from('ec_emergencies')
|
||||
.select('*', { count: 'exact' })
|
||||
.gte('occurred_at', todayStart.toISOString())
|
||||
.lte('occurred_at', todayEnd.toISOString())
|
||||
.executeAs<Emergency[]>()
|
||||
const processingResult = await supa
|
||||
.from('ec_emergencies')
|
||||
.select('*', { count: 'exact' })
|
||||
.eq('status', 'processing')
|
||||
.executeAs<Emergency[]>()
|
||||
const resolvedResult = await supa
|
||||
.from('ec_emergencies')
|
||||
.select('*', { count: 'exact' })
|
||||
.eq('status', 'resolved')
|
||||
.executeAs<Emergency[]>()
|
||||
stats.value = {
|
||||
today: todayResult.count ?? 0,
|
||||
processing: processingResult.count ?? 0,
|
||||
resolved: resolvedResult.count ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeText = (type: string): string => {
|
||||
const map = new Map([
|
||||
['fall', '跌倒'],
|
||||
['stroke', '中风'],
|
||||
['cardiac', '心脏'],
|
||||
['other', '其他']
|
||||
])
|
||||
return map.get(type) ?? type
|
||||
}
|
||||
const getSeverityText = (sev: string): string => {
|
||||
const map = new Map([
|
||||
['low', '低'],
|
||||
['medium', '中'],
|
||||
['high', '高'],
|
||||
['critical', '危急']
|
||||
])
|
||||
return map.get(sev) ?? sev
|
||||
}
|
||||
const getStatusText = (status: string): string => {
|
||||
const map = new Map([
|
||||
['active', '待处理'],
|
||||
['processing', '处理中'],
|
||||
['resolved', '已解决'],
|
||||
['cancelled', '已取消']
|
||||
])
|
||||
return map.get(status) ?? status
|
||||
}
|
||||
const showAddEmergency = () => {
|
||||
uni.navigateTo({ url: '/pages/ec/doctor/emergency-form' })
|
||||
}
|
||||
const handleEmergency = (item: Emergency) => {
|
||||
uni.navigateTo({ url: `/pages/ec/doctor/emergency-handle?id=${item.id}` })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.emergency-page { padding: 20px; background: #f5f5f5; min-height: 100vh; }
|
||||
.header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.title { font-size: 20px; font-weight: bold; }
|
||||
.add-btn { background: #ff4757; color: #fff; border: none; border-radius: 16px; padding: 8px 18px; font-size: 15px; }
|
||||
.stats { display: flex; flex-direction: row; gap: 18px; margin-bottom: 12px; }
|
||||
.emergency-list { background: #fff; border-radius: 10px; }
|
||||
.emergency-item { padding: 14px 10px; border-bottom: 1px solid #f0f0f0; }
|
||||
.emergency-item:last-child { border-bottom: none; }
|
||||
.item-header { display: flex; flex-direction: row; gap: 10px; margin-bottom: 6px; }
|
||||
.type { font-size: 14px; font-weight: 600; }
|
||||
.severity { font-size: 13px; color: #e67e22; }
|
||||
.status { font-size: 13px; color: #3498db; }
|
||||
.item-content { font-size: 13px; color: #555; margin-bottom: 6px; }
|
||||
.elder { color: #888; margin-right: 10px; }
|
||||
.desc { color: #555; }
|
||||
.item-footer { display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||||
.time { font-size: 12px; color: #999; }
|
||||
.handle-btn { background: #27ae60; color: #fff; border: none; border-radius: 12px; padding: 6px 14px; font-size: 13px; }
|
||||
.empty-state { padding: 30px 0; text-align: center; color: #999; }
|
||||
</style>
|
||||
106
pages/ec/doctor/health-reports.uvue
Normal file
106
pages/ec/doctor/health-reports.uvue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<view class="health-reports-page">
|
||||
<view class="header">
|
||||
<text class="title">健康报告</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<view v-if="reports.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无健康报告</text>
|
||||
</view>
|
||||
<view v-for="report in reports" :key="report.id" class="report-item">
|
||||
<text class="report-title">{{ report.title }}</text>
|
||||
<text class="report-date">{{ formatDate(report.created_at) }}</text>
|
||||
<text class="report-summary">{{ report.summary }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { formatDate } from '../types_new.uts'
|
||||
|
||||
type HealthReport = {
|
||||
id: string
|
||||
elder_id: string
|
||||
elder_name: string
|
||||
title: string
|
||||
summary: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
const reports = ref<Array<HealthReport>>([])
|
||||
|
||||
onMounted(() => {
|
||||
loadReports()
|
||||
})
|
||||
|
||||
const loadReports = async () => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_health_reports')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(20)
|
||||
.executeAs<HealthReport[]>()
|
||||
if (result.error == null && result.data != null) {
|
||||
reports.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载健康报告失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.health-reports-page {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
.report-item {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.report-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.report-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
.report-date {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.report-summary {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
margin-top: 6px;
|
||||
display: block;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
.empty-text {
|
||||
color: #aaa;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
137
pages/ec/doctor/medical-records.uvue
Normal file
137
pages/ec/doctor/medical-records.uvue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<view class="medical-records">
|
||||
<view class="header">
|
||||
<text class="header-title">病历管理</text>
|
||||
</view>
|
||||
<scroll-view class="records-list" scroll-y="true" :style="{ height: '600px' }">
|
||||
<view
|
||||
v-for="record in records"
|
||||
:key="record.id"
|
||||
class="record-item"
|
||||
@click="openRecord(record)"
|
||||
>
|
||||
<view class="record-header">
|
||||
<text class="patient-name">{{ record.elder_name }}</text>
|
||||
<text class="visit-date">{{ formatDateTime(record.visit_date) }}</text>
|
||||
</view>
|
||||
<view class="record-content">
|
||||
<text class="visit-type">类型: {{ getVisitTypeText(record.visit_type) }}</text>
|
||||
<text class="chief-complaint">主诉: {{ record.chief_complaint || '无' }}</text>
|
||||
<text class="diagnosis">诊断: {{ record.diagnosis || '未填写' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="records.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无病历记录</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { formatDateTime } from '../types_new.uts'
|
||||
|
||||
type MedicalRecord = {
|
||||
id: string
|
||||
elder_id: string
|
||||
elder_name: string
|
||||
visit_type: string
|
||||
visit_date: string
|
||||
chief_complaint: string
|
||||
diagnosis: string
|
||||
}
|
||||
|
||||
const records = ref<Array<MedicalRecord>>([])
|
||||
|
||||
onMounted(() => {
|
||||
loadRecords()
|
||||
})
|
||||
|
||||
const loadRecords = async () => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_medical_records')
|
||||
.select('id, elder_id, elder_name, visit_type, visit_date, chief_complaint, diagnosis')
|
||||
.order('visit_date', { ascending: false })
|
||||
.executeAs<MedicalRecord[]>()
|
||||
if (result.error == null && result.data != null) {
|
||||
records.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载病历失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getVisitTypeText = (type: string): string => {
|
||||
const typeMap = new Map([
|
||||
['routine', '常规'],
|
||||
['emergency', '急诊'],
|
||||
['consultation', '会诊'],
|
||||
['follow_up', '复诊']
|
||||
])
|
||||
return typeMap.get(type) ?? type
|
||||
}
|
||||
|
||||
const openRecord = (record: MedicalRecord) => {
|
||||
uni.navigateTo({ url: `/pages/ec/doctor/medical-record-detail?id=${record.id}` })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.medical-records {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.records-list {
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.record-item {
|
||||
padding: 14px 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.record-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.record-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.patient-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.visit-date {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
.record-content {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}
|
||||
.visit-type, .chief-complaint, .diagnosis {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.empty-state {
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
28
pages/ec/doctor/medication-management.uvue
Normal file
28
pages/ec/doctor/medication-management.uvue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<view class="medication-management-page">
|
||||
<text class="title">用药管理</text>
|
||||
<view class="empty">暂无数据</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// 用药管理页面骨架
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.medication-management-page {
|
||||
padding: 30px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.empty {
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
}
|
||||
</style>
|
||||
163
pages/ec/doctor/patient-queue.uvue
Normal file
163
pages/ec/doctor/patient-queue.uvue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<view class="patient-queue">
|
||||
<view class="header">
|
||||
<text class="header-title">今日患者队列</text>
|
||||
</view>
|
||||
<scroll-view class="queue-list" scroll-y="true" :style="{ height: '600px' }">
|
||||
<view
|
||||
v-for="appointment in queue"
|
||||
:key="appointment.id"
|
||||
class="queue-item"
|
||||
:class="{ 'current': isCurrent(appointment), 'completed': appointment.status === 'completed' }"
|
||||
@click="openAppointment(appointment)"
|
||||
>
|
||||
<view class="queue-time">
|
||||
<text class="time-text">{{ formatTime(appointment.scheduled_time) }}</text>
|
||||
<text class="status-text" :class="appointment.status">{{ getStatusText(appointment.status) }}</text>
|
||||
</view>
|
||||
<view class="queue-details">
|
||||
<text class="patient-name">{{ appointment.elder_name }}</text>
|
||||
<text class="room-info" v-if="appointment.room_number">房间: {{ appointment.room_number }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="queue.length === 0" class="empty-state">
|
||||
<text class="empty-text">今日暂无预约</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { formatTime, getTodayStart, getTodayEnd } from '../types_new.uts'
|
||||
|
||||
type Appointment = {
|
||||
id: string
|
||||
elder_id: string
|
||||
elder_name: string
|
||||
scheduled_time: string
|
||||
status: string
|
||||
room_number: string
|
||||
}
|
||||
|
||||
const queue = ref<Array<Appointment>>([])
|
||||
|
||||
onMounted(() => {
|
||||
loadQueue()
|
||||
})
|
||||
|
||||
const loadQueue = async () => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_appointments')
|
||||
.select('id, elder_id, elder_name, scheduled_time, status, room_number')
|
||||
.gte('scheduled_time', getTodayStart())
|
||||
.lte('scheduled_time', getTodayEnd())
|
||||
.order('scheduled_time', { ascending: true })
|
||||
.executeAs<Appointment[]>()
|
||||
if (result.error == null && result.data != null) {
|
||||
queue.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载队列失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: string): string => {
|
||||
const statusMap = new Map([
|
||||
['scheduled', '待诊疗'],
|
||||
['in_progress', '进行中'],
|
||||
['completed', '已完成'],
|
||||
['cancelled', '已取消'],
|
||||
['no_show', '未到']
|
||||
])
|
||||
return statusMap.get(status) ?? status
|
||||
}
|
||||
|
||||
const isCurrent = (appointment: Appointment): boolean => {
|
||||
const now = new Date()
|
||||
const t = new Date(appointment.scheduled_time)
|
||||
return Math.abs(now.getTime() - t.getTime()) <= 30 * 60 * 1000 && appointment.status === 'in_progress'
|
||||
}
|
||||
|
||||
const openAppointment = (appointment: Appointment) => {
|
||||
uni.navigateTo({ url: `/pages/ec/doctor/appointment-detail?id=${appointment.id}` })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.patient-queue {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.queue-list {
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.queue-item {
|
||||
padding: 14px 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.queue-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.queue-item.current {
|
||||
background-color: #e8f5e8;
|
||||
border-left: 4px solid #4caf50;
|
||||
}
|
||||
.queue-item.completed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.queue-time {
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.time-text {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.status-text.scheduled { color: #3498db; }
|
||||
.status-text.in_progress { color: #f39c12; }
|
||||
.status-text.completed { color: #27ae60; }
|
||||
.status-text.cancelled { color: #e74c3c; }
|
||||
.queue-details {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.patient-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.room-info {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.empty-state {
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
136
pages/ec/doctor/prescriptions.uvue
Normal file
136
pages/ec/doctor/prescriptions.uvue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<view class="prescriptions">
|
||||
<view class="header">
|
||||
<text class="header-title">处方管理</text>
|
||||
</view>
|
||||
<scroll-view class="prescription-list" scroll-y="true" :style="{ height: '600px' }">
|
||||
<view
|
||||
v-for="item in prescriptions"
|
||||
:key="item.id"
|
||||
class="prescription-item"
|
||||
@click="openPrescription(item)"
|
||||
>
|
||||
<view class="prescription-header">
|
||||
<text class="patient-name">{{ item.elder_name }}</text>
|
||||
<text class="medication-name">{{ item.medication_name }}</text>
|
||||
</view>
|
||||
<view class="prescription-content">
|
||||
<text class="dosage">剂量: {{ item.dosage || '未填写' }}</text>
|
||||
<text class="status">状态: {{ getStatusText(item.status) }}</text>
|
||||
<text class="date-range">{{ item.start_date }} ~ {{ item.end_date || '...' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="prescriptions.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无处方记录</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type Prescription = {
|
||||
id: string
|
||||
elder_id: string
|
||||
elder_name: string
|
||||
medication_name: string
|
||||
dosage: string
|
||||
status: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
}
|
||||
|
||||
const prescriptions = ref<Array<Prescription>>([])
|
||||
|
||||
onMounted(() => {
|
||||
loadPrescriptions()
|
||||
})
|
||||
|
||||
const loadPrescriptions = async () => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_medications')
|
||||
.select('id, elder_id, elder_name, medication_name, dosage, status, start_date, end_date')
|
||||
.order('created_at', { ascending: false })
|
||||
.executeAs<Prescription[]>()
|
||||
if (result.error == null && result.data != null) {
|
||||
prescriptions.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载处方失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: string): string => {
|
||||
const statusMap = new Map([
|
||||
['active', '进行中'],
|
||||
['completed', '已完成'],
|
||||
['discontinued', '已停用']
|
||||
])
|
||||
return statusMap.get(status) ?? status
|
||||
}
|
||||
|
||||
const openPrescription = (item: Prescription) => {
|
||||
uni.navigateTo({ url: `/pages/ec/doctor/prescription-detail?id=${item.id}` })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.prescriptions {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.prescription-list {
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.prescription-item {
|
||||
padding: 14px 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.prescription-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.prescription-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.patient-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.medication-name {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.prescription-content {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}
|
||||
.dosage, .status, .date-range {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.empty-state {
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
28
pages/ec/doctor/schedule.uvue
Normal file
28
pages/ec/doctor/schedule.uvue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<view class="schedule-page">
|
||||
<text class="title">今日日程</text>
|
||||
<view class="empty">暂无数据</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// 日程页面骨架
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.schedule-page {
|
||||
padding: 30px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.empty {
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
}
|
||||
</style>
|
||||
28
pages/ec/doctor/vital-signs.uvue
Normal file
28
pages/ec/doctor/vital-signs.uvue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<view class="vital-signs-page">
|
||||
<text class="title">生命体征</text>
|
||||
<view class="empty">暂无数据</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// 生命体征页面骨架
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vital-signs-page {
|
||||
padding: 30px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.empty {
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
}
|
||||
</style>
|
||||
303
pages/ec/elder/care-records.uvue
Normal file
303
pages/ec/elder/care-records.uvue
Normal file
@@ -0,0 +1,303 @@
|
||||
<!-- 服务记录页面 - uts-android 兼容版 -->
|
||||
<template>
|
||||
<view class="service-records">
|
||||
<view class="header">
|
||||
<text class="header-title">服务记录</text>
|
||||
<button class="refresh-btn" @click="refreshData">
|
||||
<text class="refresh-text">🔄 刷新</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="filters-section">
|
||||
<view class="filter-row">
|
||||
<view class="filter-group">
|
||||
<text class="filter-label">老人</text>
|
||||
<button class="picker-btn" @click="showElderActionSheet">
|
||||
<text class="picker-text">{{ selectedElder?.name ?? '全部' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="filter-group">
|
||||
<text class="filter-label">服务类型</text>
|
||||
<button class="picker-btn" @click="showTypeActionSheet">
|
||||
<text class="picker-text">{{ selectedType?.label ?? '全部' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="filter-group">
|
||||
<text class="filter-label">时间范围</text>
|
||||
<button class="picker-btn" @click="showTimeRangeActionSheet">
|
||||
<text class="picker-text">{{ selectedTimeRange?.label ?? '近7天' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view class="records-list" direction="vertical" :style="{ height: '500px' }">
|
||||
<view v-for="record in filteredRecords" :key="record.id" class="record-item" @click="viewDetail(record)">
|
||||
<view class="record-header">
|
||||
<text class="elder-name">{{ record.elder_name ?? '未知' }}</text>
|
||||
<text class="service-type">{{ record.service_type ?? '未知类型' }}</text>
|
||||
<text class="record-time">{{ formatDateTime(record.created_at ?? '') }}</text>
|
||||
</view>
|
||||
<view class="record-content">
|
||||
<text class="caregiver">护理员: {{ record.caregiver_name ?? '未分配' }}</text>
|
||||
<text class="notes" v-if="record.notes">备注: {{ record.notes }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="filteredRecords.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无服务记录</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { formatDateTime as formatDateTimeUtil } from '../types.uts'
|
||||
|
||||
type ServiceRecord = {
|
||||
id: string
|
||||
task_id: string | null
|
||||
elder_id: string
|
||||
caregiver_id: string
|
||||
elder_name?: string
|
||||
caregiver_name?: string
|
||||
start_time: string | null
|
||||
end_time: string | null
|
||||
actual_duration: number | null
|
||||
care_content: string | null
|
||||
elder_condition: string | null
|
||||
issues_notes: string | null
|
||||
photo_urls: string[] | null
|
||||
status: string
|
||||
rating: number | null
|
||||
supervisor_notes: string | null
|
||||
created_at: string
|
||||
}
|
||||
type Elder = { id: string, name: string }
|
||||
type FilterOption = { value: string, label: string }
|
||||
|
||||
const records = ref<ServiceRecord[]>([])
|
||||
const elders = ref<Elder[]>([])
|
||||
const selectedElderIndex = ref<number>(-1)
|
||||
const selectedTypeIndex = ref<number>(-1)
|
||||
const selectedTimeRangeIndex = ref<number>(1)
|
||||
|
||||
const typeOptions = ref<FilterOption[]>([
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'nursing', label: '护理' },
|
||||
{ value: 'meal', label: '餐饮' },
|
||||
{ value: 'activity', label: '活动' },
|
||||
{ value: 'cleaning', label: '清洁' }
|
||||
])
|
||||
const timeRangeOptions = ref<FilterOption[]>([
|
||||
{ value: '3days', label: '近3天' },
|
||||
{ value: '7days', label: '近7天' },
|
||||
{ value: '30days', label: '近30天' }
|
||||
])
|
||||
|
||||
const elderOptions = computed<Elder[]>(() => [ { id: 'all', name: '全部' }, ...elders.value ])
|
||||
const selectedElder = computed(() => elderOptions.value[selectedElderIndex.value] ?? elderOptions.value[0])
|
||||
const selectedType = computed(() => typeOptions.value[selectedTypeIndex.value] ?? typeOptions.value[0])
|
||||
const selectedTimeRange = computed(() => timeRangeOptions.value[selectedTimeRangeIndex.value] ?? timeRangeOptions.value[1])
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
let list = records.value
|
||||
if (selectedElder.value.id !== 'all') {
|
||||
list = list.filter(r => r.elder_id === selectedElder.value.id)
|
||||
}
|
||||
if (selectedType.value.value !== 'all') {
|
||||
list = list.filter(r => r.service_type === selectedType.value.value)
|
||||
}
|
||||
// 时间范围
|
||||
const now = new Date()
|
||||
let startDate = new Date()
|
||||
if (selectedTimeRange.value.value === '3days') startDate.setDate(now.getDate() - 3)
|
||||
else if (selectedTimeRange.value.value === '7days') startDate.setDate(now.getDate() - 7)
|
||||
else if (selectedTimeRange.value.value === '30days') startDate.setDate(now.getDate() - 30)
|
||||
list = list.filter(r => r.created_at >= startDate.toISOString())
|
||||
return list
|
||||
})
|
||||
|
||||
const formatDateTime = (dt: string) => formatDateTimeUtil(dt)
|
||||
|
||||
const refreshData = () => { loadRecords(); loadElders(); }
|
||||
|
||||
const loadRecords = async () => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_care_records')
|
||||
.select('id, elder_id, ec_care_records_elder_id_fkey(name), record_type, ec_care_records_caregiver_id_fkey(username), created_at,issues_notes, supervisor_notes', {})
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(100)
|
||||
.executeAs<ServiceRecord[]>()
|
||||
if (result.error == null && result.data != null) {
|
||||
records.value = result.data
|
||||
}
|
||||
} catch (e) { console.error('加载服务记录失败', e) }
|
||||
}
|
||||
const loadElders = async () => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_elders')
|
||||
.select('id, name', {})
|
||||
.eq('status', 'active')
|
||||
.order('name', { ascending: true })
|
||||
.executeAs<Elder[]>()
|
||||
if (result.error == null && result.data != null) {
|
||||
elders.value = result.data
|
||||
}
|
||||
} catch (e) { console.error('加载老人列表失败', e) }
|
||||
}
|
||||
const showElderActionSheet = () => {
|
||||
const options = elderOptions.value.map(e => e.name)
|
||||
uni.showActionSheet({
|
||||
itemList: options,
|
||||
success: (res:any) => { selectedElderIndex.value = res.tapIndex }
|
||||
})
|
||||
}
|
||||
const showTypeActionSheet = () => {
|
||||
const options = typeOptions.value.map(t => t.label)
|
||||
uni.showActionSheet({
|
||||
itemList: options,
|
||||
success: (res:any) => { selectedTypeIndex.value = res.tapIndex }
|
||||
})
|
||||
}
|
||||
const showTimeRangeActionSheet = () => {
|
||||
const options = timeRangeOptions.value.map(t => t.label)
|
||||
uni.showActionSheet({
|
||||
itemList: options,
|
||||
success: (res:any) => { selectedTimeRangeIndex.value = res.tapIndex }
|
||||
})
|
||||
}
|
||||
const viewDetail = (record: ServiceRecord) => {
|
||||
uni.navigateTo({ url: `/pages/ec/admin/service-record-detail?id=${record.id}` })
|
||||
}
|
||||
onMounted(() => { refreshData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* uts-android 兼容性重构:
|
||||
1. 移除所有嵌套选择器、伪类(如 :last-child),全部 class 扁平化。
|
||||
2. 所有间距用 margin-right/margin-bottom 控制,禁止 gap、flex-wrap、嵌套。
|
||||
3. 所有布局 display: flex,禁止 grid、gap、伪类。
|
||||
4. 组件间距、分隔线全部用 border/margin 控制。
|
||||
*/
|
||||
.service-records {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.refresh-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #52c41a;
|
||||
background-color: #52c41a;
|
||||
color: white;
|
||||
}
|
||||
.filters-section {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.filter-group {
|
||||
flex: 1;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.filter-group.is-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
.picker-btn {
|
||||
width: 180rpx;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.picker-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: #f9f9f9;
|
||||
display: block;
|
||||
}
|
||||
.records-list {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
min-height: 300px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.record-item {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.record-item.is-last {
|
||||
border-bottom: none;
|
||||
}
|
||||
.record-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.elder-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.service-type {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.record-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.record-content {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.caregiver {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.notes {
|
||||
color: #faad14;
|
||||
}
|
||||
.empty-state {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
928
pages/ec/elder/dashboard.uvue
Normal file
928
pages/ec/elder/dashboard.uvue
Normal file
@@ -0,0 +1,928 @@
|
||||
<template>
|
||||
<view class="elder-dashboard">
|
||||
<!-- 头部欢迎区域 -->
|
||||
<view class="header-section">
|
||||
<view class="welcome-card">
|
||||
<text class="welcome-text">{{ greeting }},{{ elderInfo.name }}</text>
|
||||
<text class="weather-info">今天天气:{{ weatherInfo.description }}</text>
|
||||
<text class="date-info">{{ formatDate(new Date()) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康状态卡片 -->
|
||||
<view class="health-section">
|
||||
<view class="section-title">
|
||||
<text class="title-text">我的健康</text>
|
||||
<button class="view-all-btn" @tap="goToHealthDetails">
|
||||
<text class="btn-text">查看详情</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="health-cards">
|
||||
<view class="health-card" v-for="vital in vitals" :key="vital.type">
|
||||
<text class="health-icon">{{ getVitalIcon(vital.type) }}</text>
|
||||
<text class="health-label">{{ getVitalLabel(vital.type) }}</text>
|
||||
<text class="health-value">{{ vital.value }}{{ getVitalUnit(vital.type) }}</text>
|
||||
<text class="health-status" :class="vital.status">{{ getStatusText(vital.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日活动 -->
|
||||
<view class="activity-section">
|
||||
<view class="section-title">
|
||||
<text class="title-text">今日活动</text>
|
||||
<text class="activity-count">{{ todayActivities.length }}项</text>
|
||||
</view>
|
||||
<view class="activity-list" v-if="todayActivities.length > 0">
|
||||
<view class="activity-item" v-for="activity in todayActivities" :key="activity.id">
|
||||
<view class="activity-time">
|
||||
<text class="time-text">{{ formatTime(activity.scheduled_time) }}</text>
|
||||
</view>
|
||||
<view class="activity-info">
|
||||
<text class="activity-name">{{ activity.title }}</text>
|
||||
<text class="activity-location">{{ activity.location }}</text>
|
||||
</view>
|
||||
<view class="activity-status" :class="activity.status">
|
||||
<text class="status-text">{{ getActivityStatusText(activity.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-state" v-else>
|
||||
<text class="empty-text">今天没有安排活动</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用药提醒 -->
|
||||
<view class="medication-section">
|
||||
<view class="section-title">
|
||||
<text class="title-text">用药提醒</text>
|
||||
<view class="medication-alert" v-if="upcomingMedications.length > 0">
|
||||
<text class="alert-text">{{ upcomingMedications.length }}个即将到期</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="medication-list" v-if="todayMedications.length > 0">
|
||||
<view class="medication-item" v-for="medication in todayMedications" :key="medication.id">
|
||||
<view class="medication-time">
|
||||
<text class="time-text">{{ formatTime(medication.scheduled_time) }}</text>
|
||||
</view>
|
||||
<view class="medication-info">
|
||||
<text class="medication-name">{{ medication.medication_name }}</text>
|
||||
<text class="medication-dosage">{{ medication.dosage }}</text>
|
||||
</view>
|
||||
<view class="medication-actions">
|
||||
<button class="action-btn taken" @tap="markMedicationTaken(medication.id)" v-if="medication.status !== 'taken'">
|
||||
<text class="btn-text">已服用</text>
|
||||
</button>
|
||||
<view class="taken-indicator" v-else>
|
||||
<text class="taken-text">✓ 已服用</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-state" v-else>
|
||||
<text class="empty-text">今天没有用药安排</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 护理服务 -->
|
||||
<view class="care-section">
|
||||
<view class="section-title">
|
||||
<text class="title-text">护理服务</text>
|
||||
</view>
|
||||
<view class="care-summary">
|
||||
<view class="care-card" @tap="goToCareRecords">
|
||||
<text class="care-icon">👩⚕️</text>
|
||||
<text class="care-label">护理员</text>
|
||||
<text class="care-value">{{ caregiverInfo.name }}</text>
|
||||
<text class="care-status">在线</text>
|
||||
</view>
|
||||
<view class="care-card" @tap="goToServiceRequests">
|
||||
<text class="care-icon">🔔</text>
|
||||
<text class="care-label">服务请求</text>
|
||||
<text class="care-value">{{ pendingRequests }}</text>
|
||||
<text class="care-status">待处理</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<view class="quick-actions">
|
||||
<view class="section-title">
|
||||
<text class="title-text">快捷操作</text>
|
||||
</view>
|
||||
<view class="action-grid">
|
||||
<view class="action-item" @tap="callEmergency">
|
||||
<text class="action-icon">🚨</text>
|
||||
<text class="action-label">紧急呼叫</text>
|
||||
</view>
|
||||
<view class="action-item" @tap="callNurse">
|
||||
<text class="action-icon">🔔</text>
|
||||
<text class="action-label">呼叫护理员</text>
|
||||
</view>
|
||||
<view class="action-item" @tap="viewMenu">
|
||||
<text class="action-icon">🍽️</text>
|
||||
<text class="action-label">今日菜单</text>
|
||||
</view>
|
||||
<view class="action-item" @tap="contactFamily">
|
||||
<text class="action-icon">📞</text>
|
||||
<text class="action-label">联系家人</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 紧急呼叫悬浮按钮 -->
|
||||
<view class="emergency-fab" @tap="showEmergencyOptions">
|
||||
<text class="fab-icon">🚨</text>
|
||||
</view>
|
||||
|
||||
<!-- 紧急呼叫选项弹窗 -->
|
||||
<view class="emergency-modal" v-if="showEmergencyModal" @tap="hideEmergencyOptions">
|
||||
<view class="modal-content" @tap.stop>
|
||||
<text class="modal-title">紧急呼叫</text>
|
||||
<view class="emergency-options">
|
||||
<button class="emergency-btn medical" @tap="callMedicalEmergency">
|
||||
<text class="btn-text">医疗急救</text>
|
||||
</button>
|
||||
<button class="emergency-btn nurse" @tap="callNurseEmergency">
|
||||
<text class="btn-text">护理员</text>
|
||||
</button>
|
||||
<button class="emergency-btn family" @tap="callFamilyEmergency">
|
||||
<text class="btn-text">联系家人</text>
|
||||
</button>
|
||||
</view>
|
||||
<button class="cancel-btn" @tap="hideEmergencyOptions">
|
||||
<text class="btn-text">取消</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { state, getCurrentUserId } from '@/utils/store.uts'
|
||||
|
||||
import { formatDate, formatTime, getStatusText, getActivityStatusText } from '../types.uts'
|
||||
import type { Elder, VitalSign, Activity, Medication, CaregiverInfo } from '../types.uts'
|
||||
|
||||
// 数据状态
|
||||
const elderInfo = ref<Elder>({
|
||||
id: '',
|
||||
name: '',
|
||||
age: 0,
|
||||
gender: 'male',
|
||||
room_number: '',
|
||||
bed_number: '',
|
||||
admission_date: '',
|
||||
health_status: 'stable',
|
||||
care_level: 1,
|
||||
emergency_contact: '',
|
||||
profile_picture: '',
|
||||
family_contact: '',
|
||||
status: 'normal',
|
||||
|
||||
})
|
||||
|
||||
const vitals = ref<VitalSign[]>([])
|
||||
const todayActivities = ref<Activity[]>([])
|
||||
const todayMedications = ref<Medication[]>([])
|
||||
const caregiverInfo = ref<CaregiverInfo>({
|
||||
id: '',
|
||||
employee_id:'',
|
||||
name: '',
|
||||
phone: '',
|
||||
department: '',
|
||||
specialization: '',
|
||||
shift: 'day'
|
||||
})
|
||||
|
||||
const weatherInfo = ref({
|
||||
description: '晴朗',
|
||||
temperature: 22,
|
||||
humidity: 65
|
||||
})
|
||||
|
||||
const pendingRequests = ref(0)
|
||||
const showEmergencyModal = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const greeting = computed(() => {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 12) return '早上好'
|
||||
if (hour < 18) return '下午好'
|
||||
return '晚上好'
|
||||
})
|
||||
|
||||
const upcomingMedications = computed(() => {
|
||||
const now = new Date()
|
||||
const oneHour = 60 * 60 * 1000
|
||||
return todayMedications.value.filter(med => {
|
||||
const scheduledTime = new Date(med.scheduled_time)
|
||||
return scheduledTime.getTime() - now.getTime() <= oneHour && scheduledTime.getTime() > now.getTime()
|
||||
})
|
||||
})
|
||||
|
||||
// 辅助函数
|
||||
function getVitalIcon(type: string): string {
|
||||
const icons = {
|
||||
'heart_rate': '❤️',
|
||||
'blood_pressure': '🩸',
|
||||
'temperature': '🌡️',
|
||||
'blood_sugar': '🍯',
|
||||
'oxygen_saturation': '🫁'
|
||||
}
|
||||
return icons[type] || '📊'
|
||||
}
|
||||
|
||||
function getVitalLabel(type: string): string {
|
||||
const labels = {
|
||||
'heart_rate': '心率',
|
||||
'blood_pressure': '血压',
|
||||
'temperature': '体温',
|
||||
'blood_sugar': '血糖',
|
||||
'oxygen_saturation': '血氧'
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
function getVitalUnit(type: string): string {
|
||||
const units = {
|
||||
'heart_rate': 'bpm',
|
||||
'blood_pressure': 'mmHg',
|
||||
'temperature': '°C',
|
||||
'blood_sugar': 'mmol/L',
|
||||
'oxygen_saturation': '%'
|
||||
}
|
||||
return units[type] || ''
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
function goToHealthDetails() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ec/elder/health-details'
|
||||
})
|
||||
}
|
||||
|
||||
function goToCareRecords() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ec/elder/care-records'
|
||||
})
|
||||
}
|
||||
|
||||
function goToServiceRequests() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ec/elder/service-requests'
|
||||
})
|
||||
}
|
||||
|
||||
async function markMedicationTaken(medicationId: string) {
|
||||
try {
|
||||
const { error } = await supa
|
||||
.from('ec_medications')
|
||||
.update({ status: 'taken', updated_at: new Date().toISOString() })
|
||||
.eq('id', medicationId)
|
||||
.execute()
|
||||
if (!error) {
|
||||
const index = todayMedications.value.findIndex(med => med.id === medicationId)
|
||||
if (index !== -1) {
|
||||
todayMedications.value[index].status = 'taken'
|
||||
}
|
||||
uni.showToast({
|
||||
title: '已标记为已服用',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('标记用药失败:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function showEmergencyOptions() {
|
||||
showEmergencyModal.value = true
|
||||
}
|
||||
|
||||
function hideEmergencyOptions() {
|
||||
showEmergencyModal.value = false
|
||||
}
|
||||
|
||||
async function callEmergency() {
|
||||
try {
|
||||
console.log(elderInfo.value.id)
|
||||
await supa
|
||||
.from('ec_service_requests')
|
||||
.insert({
|
||||
elder_id: elderInfo.value.id,
|
||||
type: 'emergency_call',
|
||||
priority:'normal',
|
||||
description: '老人主动发起紧急呼叫',
|
||||
created_at: new Date().toISOString()
|
||||
})
|
||||
.execute()
|
||||
uni.showToast({
|
||||
title: '紧急呼叫已发送',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('紧急呼叫失败:', error)
|
||||
uni.showToast({
|
||||
title: '呼叫失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function callMedicalEmergency() {
|
||||
hideEmergencyOptions()
|
||||
try {
|
||||
await supa
|
||||
.from('ec_service_requests')
|
||||
.insert({
|
||||
elder_id: elderInfo.value.id,
|
||||
type: 'medical',
|
||||
description: '医疗急救',
|
||||
created_at: new Date().toISOString()
|
||||
})
|
||||
.execute()
|
||||
uni.showToast({
|
||||
title: '医疗急救已呼叫',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('医疗急救呼叫失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function callNurse() {
|
||||
hideEmergencyOptions()
|
||||
try {
|
||||
await supa
|
||||
.from('ec_service_requests')
|
||||
.insert({
|
||||
elder_id: elderInfo.value.id,
|
||||
type: 'nurse_call',
|
||||
priority: 'high',
|
||||
description: '老人呼叫护理员',
|
||||
status: 'pending',
|
||||
created_at: new Date().toISOString()
|
||||
})
|
||||
.execute()
|
||||
uni.showToast({
|
||||
title: '护理员呼叫已发送',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('呼叫护理员失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function viewMenu() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ec/elder/daily-menu'
|
||||
})
|
||||
}
|
||||
|
||||
async function contactFamily() {
|
||||
if (elderInfo.value.family_contact) {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: elderInfo.value.family_contact
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '未设置家属联系方式',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 数据加载
|
||||
async function loadElderInfo() {
|
||||
try {
|
||||
const user = await supa.auth.getUser()
|
||||
if (user?.data?.user?.id) {
|
||||
const { data, error } = await supa
|
||||
.from('ec_elders')
|
||||
.select('*')
|
||||
.or(`id.eq.${user.data.user.id},user_id.eq.${user.data.user.id}`)
|
||||
.limit(1)
|
||||
.single()
|
||||
.executeAs<Elder>()
|
||||
if (!error && data) {
|
||||
elderInfo.value = data
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载老人信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVitalSign() {
|
||||
try {
|
||||
const { data, error } = await supa
|
||||
.from('ec_vital_signs')
|
||||
.select('*')
|
||||
.eq('elder_id', elderInfo.value.id)
|
||||
.order('measured_at', { ascending: false })
|
||||
.limit(3)
|
||||
.executeAs<VitalSign[]>()
|
||||
if (!error && data) {
|
||||
vitals.value = data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载健康数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTodayActivities() {
|
||||
try {
|
||||
// 第一步:先查询老人报名的活动ID列表
|
||||
const participationRes = await supa.from('ec_activity_participations')
|
||||
.select('activity_id')
|
||||
.eq('elder_id', elderInfo.value.id)
|
||||
.eq('participation_status', 'registered')
|
||||
.executeAs<UTSJSONObject[]>()
|
||||
|
||||
if (participationRes.error != null) {
|
||||
console.error('加载活动参与情况失败:', participationRes.error)
|
||||
return
|
||||
}
|
||||
|
||||
const activityIds = participationRes.data?.map((item):string => item['activity_id'] as string) ?? []
|
||||
|
||||
if (activityIds.length == 0) {
|
||||
todayActivities.value = []
|
||||
return
|
||||
}
|
||||
|
||||
// 第二步:根据ID列表查询活动详情
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const { data, error } = await supa
|
||||
.from('ec_activities')
|
||||
.select('*')
|
||||
.in('id', activityIds)
|
||||
.gte('start_time', `${today} 00:00:00`)
|
||||
.lte('start_time', `${today} 23:59:59`)
|
||||
.order('start_time', { ascending: true })
|
||||
.executeAs<Activity[]>()
|
||||
if (!error && data) {
|
||||
todayActivities.value = data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载今日活动失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTodayMedications() {
|
||||
try {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const { data, error } = await supa
|
||||
.from('ec_medications')
|
||||
.select('*')
|
||||
.eq('elder_id', elderInfo.value.id)
|
||||
.or(`start_date.lte.${today},start_date.is.null`)
|
||||
.or(`end_date.gte.${today},end_date.is.null`)
|
||||
.order('medication_name', { ascending: true })
|
||||
.executeAs<Medication[]>()
|
||||
if (!error && data) {
|
||||
todayMedications.value = data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用药信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCaregiverInfo() {
|
||||
try {
|
||||
const { data, error } = await supa
|
||||
.from('ec_caregivers')
|
||||
.select('*')
|
||||
.eq('id', elderInfo.value.caregiver_id)
|
||||
.limit(1)
|
||||
.single()
|
||||
.executeAs<CaregiverInfo>()
|
||||
if (!error && data) {
|
||||
caregiverInfo.value = data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载护理员信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPendingRequests() {
|
||||
try {
|
||||
const { data, error } = await supa
|
||||
.from('ec_service_requests')
|
||||
.select('id')
|
||||
.eq('elder_id', elderInfo.value.id)
|
||||
.eq('status', 'pending')
|
||||
.executeAs<any[]>()
|
||||
if (!error && data) {
|
||||
pendingRequests.value = data.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载待处理请求数失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// // 初始化
|
||||
// onMounted(async () => {
|
||||
// await loadElderInfo()
|
||||
// if (elderInfo.value.id) {
|
||||
// await Promise.all([
|
||||
// loadVitalSign(),
|
||||
// loadTodayActivities(),
|
||||
// loadTodayMedications(),
|
||||
// loadCaregiverInfo(),
|
||||
// loadPendingRequests()
|
||||
// ])
|
||||
// }
|
||||
// })
|
||||
onLoad(async(options: OnLoadOptions) => {
|
||||
elderInfo.value.id = options['id'] ?? getCurrentUserId()
|
||||
|
||||
if (elderInfo.value.id !='' ) {
|
||||
await Promise.all([
|
||||
loadVitalSign(),
|
||||
loadTodayActivities(),
|
||||
loadTodayMedications(),
|
||||
loadCaregiverInfo(),
|
||||
loadPendingRequests()
|
||||
])
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.elder-dashboard {
|
||||
padding: 40rpx;
|
||||
background-color: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header-section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.welcome-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40rpx;
|
||||
border-radius: 24rpx;
|
||||
color: white;
|
||||
}
|
||||
.welcome-text {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.weather-info {
|
||||
font-size: 32rpx;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.date-info {
|
||||
font-size: 28rpx;
|
||||
display: block;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.section-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.title-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.view-all-btn {
|
||||
background: #007AFF;
|
||||
color: white;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 20rpx;
|
||||
border: none;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.activity-count {
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.health-section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.health-cards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.health-card {
|
||||
flex: 1;
|
||||
min-width: 200rpx;
|
||||
background: white;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
text-align: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.health-card.is-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
.health-icon {
|
||||
font-size: 48rpx;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.health-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.health-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.health-status {
|
||||
font-size: 24rpx;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
display: inline-block;
|
||||
}
|
||||
.health-status.is-normal {
|
||||
background: #e8f5e8;
|
||||
color: #4caf50;
|
||||
}
|
||||
.health-status.is-warning {
|
||||
background: #fff3e0;
|
||||
color: #ff9800;
|
||||
}
|
||||
.health-status.is-danger {
|
||||
background: #ffebee;
|
||||
color: #f44336;
|
||||
}
|
||||
.activity-section,
|
||||
.medication-section,
|
||||
.care-section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.activity-list,
|
||||
.medication-list {
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.activity-item,
|
||||
.medication-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.activity-item.is-last,
|
||||
.medication-item.is-last {
|
||||
border-bottom: none;
|
||||
}
|
||||
.activity-time,
|
||||
.medication-time {
|
||||
width: 140rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.time-text {
|
||||
font-size: 28rpx;
|
||||
color: #007AFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
.activity-info,
|
||||
.medication-info {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
.activity-name,
|
||||
.medication-name {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
.activity-location,
|
||||
.medication-dosage {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
}
|
||||
.activity-status,
|
||||
.medication-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.activity-status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.activity-status.is-pending {
|
||||
background: #fff3e0;
|
||||
color: #ff9800;
|
||||
}
|
||||
.activity-status.is-completed {
|
||||
background: #e8f5e8;
|
||||
color: #4caf50;
|
||||
}
|
||||
.medication-alert {
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.action-btn {
|
||||
padding: 12rpx 20rpx;
|
||||
border-radius: 16rpx;
|
||||
border: none;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.action-btn.is-taken {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
.taken-indicator {
|
||||
padding: 12rpx 20rpx;
|
||||
}
|
||||
.taken-text {
|
||||
color: #4caf50;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.care-summary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.care-card {
|
||||
flex: 1;
|
||||
background: white;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
text-align: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.care-card.is-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
.care-icon {
|
||||
font-size: 48rpx;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.care-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.care-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.care-status {
|
||||
font-size: 24rpx;
|
||||
color: #4caf50;
|
||||
background: #e8f5e8;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
display: inline-block;
|
||||
}
|
||||
.quick-actions {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.action-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.action-item {
|
||||
flex: 1;
|
||||
min-width: 150rpx;
|
||||
background: white;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
text-align: center;
|
||||
margin-right: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.action-item.is-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
.action-icon {
|
||||
font-size: 48rpx;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.action-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
.empty-state {
|
||||
background: white;
|
||||
padding: 60rpx;
|
||||
border-radius: 16rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: 30rpx;
|
||||
color: #999;
|
||||
}
|
||||
.emergency-fab {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 40rpx;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background: #ff4444;
|
||||
border-radius: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
.fab-icon {
|
||||
font-size: 48rpx;
|
||||
color: white;
|
||||
}
|
||||
.emergency-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 40rpx;
|
||||
border-radius: 24rpx;
|
||||
width: 600rpx;
|
||||
max-width: 90%;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
.emergency-options {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
.emergency-btn {
|
||||
width: 100%;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
border: none;
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.emergency-btn.is-medical {
|
||||
background: #ff4444;
|
||||
color: white;
|
||||
}
|
||||
.emergency-btn.is-nurse {
|
||||
background: #007AFF;
|
||||
color: white;
|
||||
}
|
||||
.emergency-btn.is-family {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
.cancel-btn {
|
||||
width: 100%;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
background: white;
|
||||
color: #666;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
</style>
|
||||
209
pages/ec/elder/health-details.uvue
Normal file
209
pages/ec/elder/health-details.uvue
Normal file
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<view class="health-details">
|
||||
<view class="header">
|
||||
<text class="title">健康详情</text>
|
||||
</view>
|
||||
<view class="summary-section">
|
||||
<view class="summary-item">
|
||||
<text class="label">姓名:</text>
|
||||
<text class="value">{{ elderInfo.name }}</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="label">性别:</text>
|
||||
<text class="value">{{ elderInfo.gender }}</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="label">年龄:</text>
|
||||
<text class="value">{{ getAge(elderInfo.birthday) }}</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="label">房间号:</text>
|
||||
<text class="value">{{ elderInfo.room_number }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="vital-history-section">
|
||||
<text class="section-title">生命体征历史</text>
|
||||
<view v-if="vitalHistory.length > 0">
|
||||
<view class="vital-row" v-for="vital in vitalHistory" :key="vital.id">
|
||||
<text class="vital-date">{{ formatDateTime(vital.measured_at) }}</text>
|
||||
<text class="vital-type">{{ getVitalLabel(vital.vital_type) }}</text>
|
||||
<text class="vital-value">{{ getVitalValue(vital) }}</text>
|
||||
<text class="vital-status" :class="vital.is_abnormal ? 'abnormal' : 'normal'">
|
||||
{{ vital.is_abnormal ? '异常' : '正常' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-state">
|
||||
<text>暂无生命体征记录</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="health-record-section">
|
||||
<text class="section-title">健康档案</text>
|
||||
<view v-if="healthRecords.length > 0">
|
||||
<view class="record-row" v-for="record in healthRecords" :key="record.id">
|
||||
<text class="record-date">{{ formatDate(record.record_date) }}</text>
|
||||
<text class="record-type">{{ getRecordTypeText(record.record_type) }}</text>
|
||||
<text class="record-summary">身高: {{ record.height_cm }}cm 体重: {{ record.weight_kg }}kg</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-state">
|
||||
<text>暂无健康档案</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="alert-section">
|
||||
<text class="section-title">健康预警</text>
|
||||
<view v-if="alerts.length > 0">
|
||||
<view class="alert-row" v-for="alert in alerts" :key="alert.id">
|
||||
<text class="alert-title">{{ alert.title }}</text>
|
||||
<text class="alert-severity" :class="getSeverityClass(alert.severity)">{{ getSeverityText(alert.severity) }}</text>
|
||||
<text class="alert-time">{{ formatDateTime(alert.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-state">
|
||||
<text>暂无健康预警</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { formatDate, formatDateTime, getAge, getRecordTypeText, getSeverityText, getSeverityClass } from '../types.uts'
|
||||
import type { Elder, VitalSign, HealthAlert } from '../types.uts'
|
||||
|
||||
const elderInfo = ref<Elder>({
|
||||
id: '',
|
||||
name: '',
|
||||
gender: '',
|
||||
birthday: '',
|
||||
room_number: '',
|
||||
// ...可补充其它字段
|
||||
})
|
||||
const vitalHistory = ref<VitalSign[]>([])
|
||||
const healthRecords = ref<any[]>([])
|
||||
const alerts = ref<HealthAlert[]>([])
|
||||
|
||||
function getVitalLabel(type: string): string {
|
||||
const labels = {
|
||||
'heart_rate': '心率',
|
||||
'blood_pressure': '血压',
|
||||
'temperature': '体温',
|
||||
'blood_sugar': '血糖',
|
||||
'oxygen_saturation': '血氧'
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
function getVitalValue(vital: VitalSign): string {
|
||||
switch (vital.vital_type) {
|
||||
case 'heart_rate': return vital.heart_rate ? vital.heart_rate + ' bpm' : '-'
|
||||
case 'blood_pressure': return vital.systolic_pressure && vital.diastolic_pressure ? `${vital.systolic_pressure}/${vital.diastolic_pressure} mmHg` : '-'
|
||||
case 'temperature': return vital.temperature ? vital.temperature + '°C' : '-'
|
||||
case 'blood_sugar': return vital.glucose_level ? vital.glucose_level + ' mmol/L' : '-'
|
||||
case 'oxygen_saturation': return vital.oxygen_saturation ? vital.oxygen_saturation + '%' : '-'
|
||||
default: return '-'
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(async(options: OnLoadOptions) => {
|
||||
const elderId = options['id']
|
||||
if (!elderId) return
|
||||
// 获取老人基本信息
|
||||
const elderRes = await supa.rpc('get_elder_info', { elder_id: elderId }).execute()
|
||||
if (elderRes && elderRes.length > 0) elderInfo.value = elderRes[0]
|
||||
// 获取生命体征历史
|
||||
const vitalsRes = await supa.rpc('get_vital_history', { elder_id: elderId, limit: 20 }).execute()
|
||||
if (vitalsRes) vitalHistory.value = vitalsRes
|
||||
// 获取健康档案
|
||||
const healthRes = await supa.rpc('get_health_records', { elder_id: elderId }).execute()
|
||||
if (healthRes) healthRecords.value = healthRes
|
||||
// 获取健康预警
|
||||
const alertRes = await supa.rpc('get_health_alerts', { elder_id: elderId }).execute()
|
||||
if (alertRes) alerts.value = alertRes
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.health-details {
|
||||
padding: 40rpx;
|
||||
background: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 44rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.summary-section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.summary-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.label {
|
||||
color: #666;
|
||||
width: 120rpx;
|
||||
}
|
||||
.value {
|
||||
color: #222;
|
||||
font-weight: 500;
|
||||
}
|
||||
.vital-history-section, .health-record-section, .alert-section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #007AFF;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.vital-row, .record-row, .alert-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.vital-date, .record-date, .alert-time {
|
||||
width: 180rpx;
|
||||
color: #888;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.vital-type, .record-type, .alert-title {
|
||||
width: 120rpx;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.vital-value, .record-summary {
|
||||
flex: 1;
|
||||
color: #222;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.vital-status.normal {
|
||||
color: #4caf50;
|
||||
}
|
||||
.vital-status.abnormal {
|
||||
color: #f44336;
|
||||
}
|
||||
.alert-severity {
|
||||
margin-left: 16rpx;
|
||||
font-size: 26rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
.empty-state {
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
</style>
|
||||
1068
pages/ec/elder/profile.uvue
Normal file
1068
pages/ec/elder/profile.uvue
Normal file
File diff suppressed because it is too large
Load Diff
0
pages/ec/elders/add.uvue
Normal file
0
pages/ec/elders/add.uvue
Normal file
189
pages/ec/elders/my-elders.uvue
Normal file
189
pages/ec/elders/my-elders.uvue
Normal file
@@ -0,0 +1,189 @@
|
||||
<!-- 养老管理系统 - 我负责的老人列表 -->
|
||||
<template>
|
||||
<view class="my-elders-page">
|
||||
<view class="header">
|
||||
<text class="title">我负责的老人</text>
|
||||
</view>
|
||||
<view class="elders-list">
|
||||
<view v-if="elders.length === 0" class="empty-text">暂无负责老人</view>
|
||||
<view v-for="elder in elders" :key="elder.id" class="elder-item" @click="viewElderDetail(elder)">
|
||||
<view class="elder-avatar">
|
||||
<image class="avatar-image" :src="elder.profile_picture ?? ''" mode="aspectFill" v-if="elder.profile_picture !== null" />
|
||||
<text class="avatar-fallback" v-else>{{ elder.name.charAt(0) }}</text>
|
||||
</view>
|
||||
<view class="elder-info">
|
||||
<text class="elder-name">{{ elder.name }}</text>
|
||||
<text class="elder-room">{{ elder.room_number }}{{ elder.bed_number }}</text>
|
||||
<text class="elder-care-level">{{ getCareLevelText(elder.care_level) }}</text>
|
||||
</view>
|
||||
<view class="elder-status">
|
||||
<view class="health-indicator" :class="getHealthStatusClass(elder.health_status)">
|
||||
<text class="health-text">{{ getHealthStatusText(elder.health_status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import type { Elder } from '../types.uts'
|
||||
import { getCareLevelText, getHealthStatusText, getHealthStatusClass } from '../types.uts'
|
||||
import { state, getCurrentUserId } from '@/utils/store.uts'
|
||||
|
||||
const elders = ref<Array<Elder>>([])
|
||||
const profile = ref(state.userProfile)
|
||||
|
||||
const loadMyElders = async () => {
|
||||
try {
|
||||
const currentUserId = profile.value.id ?? getCurrentUserId()
|
||||
// 查找当前护理员负责的所有老人ID
|
||||
const taskResult = await supa
|
||||
.from('ec_care_tasks')
|
||||
.select('elder_id')
|
||||
.eq('assigned_to', currentUserId)
|
||||
.executeAs<Array<{ elder_id: string }>>()
|
||||
if (taskResult.error === null && taskResult.data !== null) {
|
||||
const uniqueElderIds = Array.from(new Set(taskResult.data.map(e => e.elder_id)))
|
||||
if (uniqueElderIds.length === 0) {
|
||||
elders.value = []
|
||||
return
|
||||
}
|
||||
// 查询老人信息
|
||||
const eldersResult = await supa
|
||||
.from('ec_elders')
|
||||
.select(`
|
||||
id,
|
||||
name,
|
||||
room_number,
|
||||
bed_number,
|
||||
health_status,
|
||||
care_level,
|
||||
profile_picture
|
||||
`)
|
||||
.in('id', uniqueElderIds)
|
||||
.eq('status', 'active')
|
||||
.executeAs<Array<Elder>>()
|
||||
if (eldersResult.error === null && eldersResult.data !== null) {
|
||||
elders.value = eldersResult.data
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载负责老人失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const viewElderDetail = (elder: Elder) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/elders/detail?id=${elder.id}`
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMyElders()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-elders-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.elders-list {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
.elder-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.elder-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.elder-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
margin-right: 12px;
|
||||
overflow: hidden;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.avatar-fallback {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
.elder-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.elder-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.elder-room {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.elder-care-level {
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
.elder-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.health-indicator {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.health-excellent {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
.health-good {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
.health-fair {
|
||||
background-color: #fff7e6;
|
||||
color: #d48806;
|
||||
}
|
||||
.health-poor {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
.empty-text {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 30px 0;
|
||||
}
|
||||
</style>
|
||||
1438
pages/ec/equipment/management.uvue
Normal file
1438
pages/ec/equipment/management.uvue
Normal file
File diff suppressed because it is too large
Load Diff
837
pages/ec/family/care-records.uvue
Normal file
837
pages/ec/family/care-records.uvue
Normal file
@@ -0,0 +1,837 @@
|
||||
<template>
|
||||
<scroll-view class="care-records-container">
|
||||
<!-- Header with Filter -->
|
||||
<view class="records-header">
|
||||
<text class="header-title">护理记录</text>
|
||||
<text class="header-subtitle">{{ elderInfo.name }}的详细护理历史</text>
|
||||
<view class="filter-bar">
|
||||
<picker class="filter-picker" mode="selector" :value="selectedTimeFilter" :range="timeFilterLabels" @change="onTimeFilterChange">
|
||||
<view class="picker-item">
|
||||
<text class="picker-text">{{ timeFilterLabels[selectedTimeFilter] }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
<picker class="filter-picker" mode="selector" :value="selectedTypeFilter" :range="typeFilterLabels" @change="onTypeFilterChange">
|
||||
<view class="picker-item">
|
||||
<text class="picker-text">{{ typeFilterLabels[selectedTypeFilter] }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Records Summary -->
|
||||
<view class="summary-cards">
|
||||
<view class="summary-card">
|
||||
<text class="summary-number">{{ recordsSummary.total }}</text>
|
||||
<text class="summary-label">总记录数</text>
|
||||
</view>
|
||||
<view class="summary-card">
|
||||
<text class="summary-number">{{ recordsSummary.today }}</text>
|
||||
<text class="summary-label">今日记录</text>
|
||||
</view>
|
||||
<view class="summary-card">
|
||||
<text class="summary-number">{{ recordsSummary.this_week }}</text>
|
||||
<text class="summary-label">本周记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Records Timeline -->
|
||||
<view v-if="filteredRecords.length > 0" class="records-timeline">
|
||||
<view v-for="(dateGroup, dateKey) in groupedRecords" :key="dateKey" class="date-group">
|
||||
<view class="date-header">
|
||||
<text class="date-text">{{ formatDateHeader(dateKey) }}</text>
|
||||
<text class="date-count">{{ dateGroup.length }}条记录</text>
|
||||
</view>
|
||||
|
||||
<view v-for="record in dateGroup" :key="record.id" class="record-item" :class="getRecordTypeClass(record.type)">
|
||||
<view class="record-timeline-marker" :class="getRecordTypeClass(record.type)"></view>
|
||||
<view class="record-content-wrapper">
|
||||
<view class="record-header">
|
||||
<text class="record-type">{{ getCareRecordTypeText(record.type) }}</text>
|
||||
<text class="record-time">{{ formatTime(record.created_at) }}</text>
|
||||
</view>
|
||||
<view class="record-content">
|
||||
<text class="record-notes">{{ record.notes }}</text>
|
||||
<view class="record-details" v-if="record.details">
|
||||
<view class="detail-item" v-for="(value, key) in parseRecordDetails(record.details)" :key="key">
|
||||
<text class="detail-label">{{ getDetailLabel(key) }}:</text>
|
||||
<text class="detail-value">{{ value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="record-footer">
|
||||
<text class="record-caregiver">护理员:{{ record.caregiver_name }}</text>
|
||||
<view class="record-actions">
|
||||
<button class="action-btn" @tap="viewRecordDetail(record)" v-if="record.type === 'vital_signs'">
|
||||
<text class="btn-text">查看详情</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Empty State -->
|
||||
<view v-else class="empty-records">
|
||||
<text class="empty-icon">📋</text>
|
||||
<text class="empty-title">暂无护理记录</text>
|
||||
<text class="empty-subtitle">选择不同的筛选条件查看记录</text>
|
||||
</view>
|
||||
|
||||
<!-- Record Detail Modal -->
|
||||
<view class="record-modal" v-if="showRecordDetail" @tap="hideRecordDetail">
|
||||
<view class="modal-content" @tap.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ getCareRecordTypeText(selectedRecord.type) }}</text>
|
||||
<button class="close-btn" @tap="hideRecordDetail">
|
||||
<text class="close-icon">×</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="detail-section">
|
||||
<text class="section-title">基本信息</text>
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">记录时间:</text>
|
||||
<text class="detail-value">{{ formatDateTime(selectedRecord.created_at) }}</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">护理员:</text>
|
||||
<text class="detail-value">{{ selectedRecord.caregiver_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-section" v-if="selectedRecord.type === 'vital_signs'">
|
||||
<text class="section-title">生命体征</text>
|
||||
<view class="vitals-grid" v-if="selectedRecord.vital_signs">
|
||||
<view class="vital-item" v-for="vital in selectedRecord.vital_signs" :key="vital.type">
|
||||
<text class="vital-label">{{ getVitalLabel(vital.type) }}</text>
|
||||
<text class="vital-value">{{ vital.value }}{{ getVitalUnit(vital.type) }}</text>
|
||||
<text class="vital-status" :class="vital.status">{{ getStatusText(vital.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-section" v-if="selectedRecord.notes">
|
||||
<text class="section-title">护理记录</text>
|
||||
<text class="notes-content">{{ selectedRecord.notes }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.care-records-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.records-header {
|
||||
background: white;
|
||||
padding: 40rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 44rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.filter-picker {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.picker-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.summary-cards {
|
||||
display: flex;
|
||||
padding: 40rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
flex: 1;
|
||||
background: white;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.summary-number {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #007AFF;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.records-timeline {
|
||||
padding: 0 40rpx 40rpx;
|
||||
}
|
||||
|
||||
.date-group {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.date-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 20rpx;
|
||||
background: white;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.date-count {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
background: #f0f0f0;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
margin-bottom: 20rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.record-timeline-marker {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-right: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.record-timeline-marker.vital_signs {
|
||||
background: #ff6b6b;
|
||||
}
|
||||
|
||||
.record-timeline-marker.medication {
|
||||
background: #4ecdc4;
|
||||
}
|
||||
|
||||
.record-timeline-marker.nursing {
|
||||
background: #45b7d1;
|
||||
}
|
||||
|
||||
.record-timeline-marker.meal {
|
||||
background: #f9ca24;
|
||||
}
|
||||
|
||||
.record-timeline-marker.activity {
|
||||
background: #6c5ce7;
|
||||
}
|
||||
|
||||
.record-timeline-marker.incident {
|
||||
background: #fd79a8;
|
||||
}
|
||||
|
||||
.record-content-wrapper {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.record-type {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.record-content {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.record-notes {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.record-details {
|
||||
background: #f8f9fa;
|
||||
padding: 16rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
width: 160rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.record-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.record-caregiver {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.record-actions {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 12rpx 20rpx;
|
||||
background: #007AFF;
|
||||
color: white;
|
||||
border-radius: 16rpx;
|
||||
border: none;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.empty-records {
|
||||
padding: 120rpx 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.record-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 24rpx;
|
||||
width: 90%;
|
||||
max-width: 800rpx;
|
||||
max-height: 80%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx 40rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
background: #f0f0f0;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 40rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 40rpx;
|
||||
max-height: 600rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.detail-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.detail-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.vitals-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.vital-item {
|
||||
flex: 1;
|
||||
min-width: 200rpx;
|
||||
background: #f8f9fa;
|
||||
padding: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vital-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.vital-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.vital-status {
|
||||
font-size: 24rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 8rpx;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.vital-status.normal {
|
||||
background: #e8f5e8;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.vital-status.warning {
|
||||
background: #fff3e0;
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.vital-status.danger {
|
||||
background: #ffebee;
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.notes-content {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
background: #f8f9fa;
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { formatDate, formatTime, formatDateTime, getStatusText } from '../types.uts'
|
||||
import type { ElderInfo, CareRecord, HealthVitals } from '../types.uts'
|
||||
|
||||
// 数据状态
|
||||
const elderInfo = ref<ElderInfo>({
|
||||
id: '',
|
||||
name: '',
|
||||
age: 0,
|
||||
gender: 'male',
|
||||
room_number: '',
|
||||
bed_number: '',
|
||||
admission_date: '',
|
||||
health_status: 'stable',
|
||||
care_level: 1,
|
||||
emergency_contact: '',
|
||||
profile_picture: '',
|
||||
family_contact: ''
|
||||
})
|
||||
|
||||
const allRecords = ref<CareRecord[]>([])
|
||||
const recordsSummary = ref({
|
||||
total: 0,
|
||||
today: 0,
|
||||
this_week: 0
|
||||
})
|
||||
|
||||
const selectedTimeFilter = ref(0)
|
||||
const selectedTypeFilter = ref(0)
|
||||
const showRecordDetail = ref(false)
|
||||
const selectedRecord = ref<CareRecord>({
|
||||
id: '',
|
||||
elder_id: '',
|
||||
type: 'nursing',
|
||||
notes: '',
|
||||
created_at: '',
|
||||
caregiver_id: '',
|
||||
caregiver_name: '',
|
||||
details: null,
|
||||
vital_signs: null
|
||||
})
|
||||
|
||||
// 筛选选项
|
||||
const timeFilters = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '今天', value: 'today' },
|
||||
{ label: '本周', value: 'week' },
|
||||
{ label: '本月', value: 'month' }
|
||||
]
|
||||
|
||||
const typeFilters = [
|
||||
{ label: '全部类型', value: 'all' },
|
||||
{ label: '生命体征', value: 'vital_signs' },
|
||||
{ label: '用药记录', value: 'medication' },
|
||||
{ label: '护理服务', value: 'nursing' },
|
||||
{ label: '用餐记录', value: 'meal' },
|
||||
{ label: '活动记录', value: 'activity' },
|
||||
{ label: '事件记录', value: 'incident' }
|
||||
]
|
||||
|
||||
const timeFilterLabels = computed(() => timeFilters.map(f => f.label))
|
||||
const typeFilterLabels = computed(() => typeFilters.map(f => f.label))
|
||||
|
||||
// 筛选记录
|
||||
const filteredRecords = computed(() => {
|
||||
let records = [...allRecords.value]
|
||||
|
||||
// 时间筛选
|
||||
const timeFilter = timeFilters[selectedTimeFilter.value]
|
||||
if (timeFilter.value !== 'all') {
|
||||
const now = new Date()
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
|
||||
records = records.filter(record => {
|
||||
const recordDate = new Date(record.created_at)
|
||||
|
||||
switch (timeFilter.value) {
|
||||
case 'today':
|
||||
return recordDate >= today
|
||||
case 'week':
|
||||
const weekStart = new Date(today)
|
||||
weekStart.setDate(today.getDate() - today.getDay())
|
||||
return recordDate >= weekStart
|
||||
case 'month':
|
||||
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||
return recordDate >= monthStart
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 类型筛选
|
||||
const typeFilter = typeFilters[selectedTypeFilter.value]
|
||||
if (typeFilter.value !== 'all') {
|
||||
records = records.filter(record => record.type === typeFilter.value)
|
||||
}
|
||||
|
||||
return records
|
||||
})
|
||||
|
||||
// 按日期分组记录
|
||||
const groupedRecords = computed(() => {
|
||||
const groups: Record<string, CareRecord[]> = {}
|
||||
|
||||
filteredRecords.value.forEach(record => {
|
||||
const date = new Date(record.created_at).toISOString().split('T')[0]
|
||||
if (!groups[date]) {
|
||||
groups[date] = []
|
||||
}
|
||||
groups[date].push(record)
|
||||
})
|
||||
|
||||
// 按日期排序
|
||||
const sortedGroups: Record<string, CareRecord[]> = {}
|
||||
Object.keys(groups)
|
||||
.sort((a, b) => new Date(b).getTime() - new Date(a).getTime())
|
||||
.forEach(date => {
|
||||
// 每组内按时间排序
|
||||
sortedGroups[date] = groups[date].sort((a, b) =>
|
||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
)
|
||||
})
|
||||
|
||||
return sortedGroups
|
||||
})
|
||||
|
||||
// 辅助函数
|
||||
function getCareRecordTypeText(type: string): string {
|
||||
const typeMap = {
|
||||
'vital_signs': '生命体征',
|
||||
'medication': '用药记录',
|
||||
'nursing': '护理服务',
|
||||
'meal': '用餐记录',
|
||||
'activity': '活动记录',
|
||||
'incident': '事件记录'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
function getRecordTypeClass(type: string): string {
|
||||
return type
|
||||
}
|
||||
|
||||
function getVitalLabel(type: string): string {
|
||||
const labels = {
|
||||
'heart_rate': '心率',
|
||||
'blood_pressure': '血压',
|
||||
'temperature': '体温',
|
||||
'blood_sugar': '血糖',
|
||||
'oxygen_saturation': '血氧'
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
function getVitalUnit(type: string): string {
|
||||
const units = {
|
||||
'heart_rate': 'bpm',
|
||||
'blood_pressure': 'mmHg',
|
||||
'temperature': '°C',
|
||||
'blood_sugar': 'mmol/L',
|
||||
'oxygen_saturation': '%'
|
||||
}
|
||||
return units[type] || ''
|
||||
}
|
||||
|
||||
function formatDateHeader(dateStr: string): string {
|
||||
const date = new Date(dateStr)
|
||||
const today = new Date()
|
||||
const yesterday = new Date(today)
|
||||
yesterday.setDate(today.getDate() - 1)
|
||||
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return '今天'
|
||||
} else if (date.toDateString() === yesterday.toDateString()) {
|
||||
return '昨天'
|
||||
} else {
|
||||
return formatDate(dateStr)
|
||||
}
|
||||
}
|
||||
|
||||
function parseRecordDetails(details: string | null): Record<string, string> {
|
||||
if (!details) return {}
|
||||
|
||||
try {
|
||||
return JSON.parse(details)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function getDetailLabel(key: string): string {
|
||||
const labels = {
|
||||
'duration': '持续时间',
|
||||
'dosage': '剂量',
|
||||
'location': '位置',
|
||||
'temperature': '温度',
|
||||
'amount': '数量',
|
||||
'notes': '备注'
|
||||
}
|
||||
return labels[key] || key
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
function onTimeFilterChange(e: any) {
|
||||
selectedTimeFilter.value = e.detail.value
|
||||
}
|
||||
|
||||
function onTypeFilterChange(e: any) {
|
||||
selectedTypeFilter.value = e.detail.value
|
||||
}
|
||||
|
||||
function viewRecordDetail(record: CareRecord) {
|
||||
selectedRecord.value = record
|
||||
showRecordDetail.value = true
|
||||
}
|
||||
|
||||
function hideRecordDetail() {
|
||||
showRecordDetail.value = false
|
||||
}
|
||||
|
||||
// 数据加载
|
||||
async function loadElderInfo() {
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const elderId = currentPage.$route.query?.elder_id as string
|
||||
|
||||
if (!elderId) {
|
||||
uni.showToast({
|
||||
title: '缺少老人ID',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const supa = (globalThis as any).supa
|
||||
const result = await supa.executeAs('get_elder_info', {
|
||||
elder_id: elderId
|
||||
})
|
||||
|
||||
if (result && result.length > 0) {
|
||||
elderInfo.value = result[0]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载老人信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCareRecords() {
|
||||
try {
|
||||
const supa = (globalThis as any).supa
|
||||
const result = await supa.executeAs('get_elder_care_records', {
|
||||
elder_id: elderInfo.value.id,
|
||||
limit: 100
|
||||
})
|
||||
|
||||
if (result && result.length > 0) {
|
||||
allRecords.value = result
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载护理记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecordsSummary() {
|
||||
try {
|
||||
const supa = (globalThis as any).supa
|
||||
const result = await supa.executeAs('get_care_records_summary', {
|
||||
elder_id: elderInfo.value.id
|
||||
})
|
||||
|
||||
if (result && result.length > 0) {
|
||||
recordsSummary.value = result[0]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载记录统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
await loadElderInfo()
|
||||
if (elderInfo.value.id) {
|
||||
await Promise.all([
|
||||
loadCareRecords(),
|
||||
loadRecordsSummary()
|
||||
])
|
||||
}
|
||||
})
|
||||
</script>
|
||||
1527
pages/ec/family/communication.uvue
Normal file
1527
pages/ec/family/communication.uvue
Normal file
File diff suppressed because it is too large
Load Diff
829
pages/ec/family/dashboard.uvue
Normal file
829
pages/ec/family/dashboard.uvue
Normal file
@@ -0,0 +1,829 @@
|
||||
<!-- 养老管理系统 - 家属仪表板 (简化版) -->
|
||||
<template>
|
||||
<view class="family-dashboard">
|
||||
<view class="header">
|
||||
<text class="title">家属关怀</text>
|
||||
<text class="welcome">{{ familyName }},{{ currentTime }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 老人状态卡片 -->
|
||||
<view class="elder-status-card">
|
||||
<view class="elder-profile">
|
||||
<view class="elder-avatar">
|
||||
<image class="avatar-image" :src="elderInfo.profile_picture ?? ''" mode="aspectFill"
|
||||
v-if="elderInfo.profile_picture !== null" />
|
||||
<text class="avatar-fallback" v-else>{{ elderInfo.name.charAt(0) }}</text>
|
||||
</view>
|
||||
<view class="elder-basic">
|
||||
<text class="elder-name">{{ elderInfo.name }}</text>
|
||||
<text class="elder-info">{{ elderInfo.age }}岁 · {{ elderInfo.gender === 'male' ? '男' : '女' }}</text>
|
||||
<text class="elder-room">{{ elderInfo.room_number }}{{ elderInfo.bed_number }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="elder-health">
|
||||
<view class="health-status" :class="getHealthStatusClass(elderInfo.health_status)">
|
||||
<text class="status-text">{{ getHealthStatusText(elderInfo.health_status) }}</text>
|
||||
</view>
|
||||
<text class="last-update">最后更新:{{ formatDateTime(elderInfo.updated_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日护理概览 -->
|
||||
<view class="today-care-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">今日护理</text>
|
||||
<text class="section-more" @click="viewAllCareRecords">查看详情</text>
|
||||
</view>
|
||||
<view class="care-timeline">
|
||||
<view v-for="record in todayCareRecords" :key="record.id" class="timeline-item">
|
||||
<view class="timeline-dot" :class="record.record_type"></view>
|
||||
<view class="timeline-content">
|
||||
<text class="care-title">{{ record.description }}</text>
|
||||
<text class="care-time">{{ formatTime(record.created_at) }}</text>
|
||||
</view>
|
||||
<view class="care-type">
|
||||
<text class="type-text">{{ getRecordTypeText(record.record_type) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康监测 -->
|
||||
<view class="health-monitoring-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">健康监测</text>
|
||||
<text class="section-more" @click="viewHealthDetails">查看详情</text>
|
||||
</view>
|
||||
<view class="health-metrics">
|
||||
<view class="metric-item" v-for="vital in recentVitals" :key="vital.id">
|
||||
<view class="metric-icon">{{ getVitalIcon(vital.vital_type) }}</view>
|
||||
<view class="metric-info">
|
||||
<text class="metric-name">{{ getVitalName(vital.vital_type) }}</text>
|
||||
<text class="metric-value">{{ getVitalValue(vital) }}</text>
|
||||
<text class="metric-time">{{ formatDateTime(vital.measured_at) }}</text>
|
||||
</view>
|
||||
<view class="metric-status" :class="vital.is_abnormal ? 'abnormal' : 'normal'">
|
||||
<text class="status-text">{{ vital.is_abnormal ? '异常' : '正常' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 探访安排 -->
|
||||
<view class="visit-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">探访安排</text>
|
||||
<text class="section-more" @click="scheduleVisit">预约探访</text>
|
||||
</view>
|
||||
<view class="visit-info" v-if="nextVisit !== null">
|
||||
<view class="visit-item">
|
||||
<view class="visit-icon"></view>
|
||||
<view class="visit-details">
|
||||
<text class="visit-title">下次探访</text>
|
||||
<text class="visit-time">{{ formatDateTime(nextVisit.visit_time) }}</text>
|
||||
<text class="visit-note">{{ nextVisit.notes ?? '常规探访' }}</text>
|
||||
</view>
|
||||
<button class="visit-btn" @click="modifyVisit">修改</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="no-visit" v-else>
|
||||
<text class="no-visit-text">暂无探访安排</text>
|
||||
<button class="schedule-btn" @click="scheduleVisit">预约探访</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 服务费用 */
|
||||
<view class="billing-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">本月费用</text>
|
||||
<text class="section-more" @click="viewBillingDetails">查看账单</text>
|
||||
</view>
|
||||
<view class="billing-summary">
|
||||
<view class="billing-item">
|
||||
<text class="billing-label">护理费</text>
|
||||
<text class="billing-amount">¥{{ monthlyBilling.care_fee }}</text>
|
||||
</view>
|
||||
<view class="billing-item">
|
||||
<text class="billing-label">餐费</text>
|
||||
<text class="billing-amount">¥{{ monthlyBilling.meal_fee }}</text>
|
||||
</view>
|
||||
<view class="billing-item">
|
||||
<text class="billing-label">其他费用</text>
|
||||
<text class="billing-amount">¥{{ monthlyBilling.other_fee }}</text>
|
||||
</view>
|
||||
<view class="billing-total">
|
||||
<text class="total-label">本月总计</text>
|
||||
<text class="total-amount">¥{{ monthlyBilling.total_fee }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<view class="quick-actions">
|
||||
<view class="action-item" @click="contactCaregiver">
|
||||
<view class="action-icon"></view>
|
||||
<text class="action-text">联系护理员</text>
|
||||
</view>
|
||||
<view class="action-item" @click="viewPhotos">
|
||||
<view class="action-icon"></view>
|
||||
<text class="action-text">生活照片</text>
|
||||
</view>
|
||||
<view class="action-item" @click="feedbackSuggestion">
|
||||
<view class="action-icon"></view>
|
||||
<text class="action-text">意见反馈</text>
|
||||
</view>
|
||||
<view class="action-item" @click="emergencyContact">
|
||||
<view class="action-icon"></view>
|
||||
<text class="action-text">紧急联系</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
import type { Elder, CareRecord, VitalSign } from '../types.uts'
|
||||
import { formatDateTime, formatTime, getHealthStatusText, getHealthStatusClass, getRecordTypeText } from '../types.uts'
|
||||
|
||||
// 响应式数据
|
||||
const familyName = ref<string>('家属')
|
||||
const currentTime = ref<string>('')
|
||||
|
||||
// 老人信息
|
||||
const elderInfo = ref<Elder>({
|
||||
id: '',
|
||||
name: '',
|
||||
age: 0,
|
||||
gender: '',
|
||||
room_number: '',
|
||||
bed_number: '',
|
||||
health_status: '',
|
||||
care_level: '',
|
||||
admission_date: '',
|
||||
profile_picture: null,
|
||||
emergency_contact: '',
|
||||
emergency_phone: '',
|
||||
status: '',
|
||||
created_at: '',
|
||||
updated_at: ''
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const todayCareRecords = ref<Array<CareRecord>>([])
|
||||
const recentVitals = ref<Array<VitalSign>>([])
|
||||
|
||||
// 探访信息
|
||||
const nextVisit = ref<any>(null)
|
||||
|
||||
// 费用信息
|
||||
const monthlyBilling = ref({
|
||||
care_fee: 0,
|
||||
meal_fee: 0,
|
||||
other_fee: 0,
|
||||
total_fee: 0
|
||||
})
|
||||
|
||||
// 更新当前时间
|
||||
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 = () => {
|
||||
const today = new Date()
|
||||
const start = new Date(today.getFullYear(), today.getMonth(), today.getDate())
|
||||
const end = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1)
|
||||
return {
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载老人基本信息
|
||||
const loadElderInfo = async () => {
|
||||
try {
|
||||
const elderId = 'elder-001' // 替换为实际的老人ID,可以从页面参数获取
|
||||
|
||||
const result = await supa
|
||||
.from('ec_elders')
|
||||
.select(`
|
||||
id,
|
||||
name,
|
||||
age,
|
||||
gender,
|
||||
room_number,
|
||||
bed_number,
|
||||
health_status,
|
||||
care_level,
|
||||
admission_date,
|
||||
profile_picture,
|
||||
emergency_contact,
|
||||
emergency_phone,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
`)
|
||||
.eq('id', elderId)
|
||||
.single()
|
||||
.executeAs<Elder>()
|
||||
|
||||
if (result.error === null && result.data !== null) {
|
||||
elderInfo.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载老人信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载今日护理记录
|
||||
const loadTodayCareRecords = async () => {
|
||||
try {
|
||||
const { start, end } = getTodayRange()
|
||||
const elderId = elderInfo.value.id
|
||||
|
||||
const result = await supa
|
||||
.from('ec_care_records')
|
||||
.select('id, description, record_type, created_at')
|
||||
.eq('elder_id', elderId)
|
||||
.gte('created_at', start)
|
||||
.lt('created_at', end)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(5)
|
||||
.executeAs<Array<CareRecord>>()
|
||||
|
||||
if (result.error === null && result.data !== null) {
|
||||
todayCareRecords.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载护理记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最近生命体征
|
||||
const loadRecentVitals = async () => {
|
||||
try {
|
||||
const elderId = elderInfo.value.id
|
||||
|
||||
const result = await supa
|
||||
.from('ec_vital_signs')
|
||||
.select(`
|
||||
id,
|
||||
elder_id,
|
||||
elder_name,
|
||||
vital_type,
|
||||
systolic_pressure,
|
||||
diastolic_pressure,
|
||||
heart_rate,
|
||||
temperature,
|
||||
oxygen_saturation,
|
||||
glucose_level,
|
||||
measured_at,
|
||||
is_abnormal
|
||||
`)
|
||||
.eq('elder_id', elderId)
|
||||
.order('measured_at', { ascending: false })
|
||||
.limit(4)
|
||||
.executeAs<Array<VitalSign>>()
|
||||
|
||||
if (result.error === null && result.data !== null) {
|
||||
recentVitals.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载生命体征失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载下次探访安排
|
||||
const loadNextVisit = async () => {
|
||||
try {
|
||||
const elderId = elderInfo.value.id
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const result = await supa
|
||||
.from('ec_visits')
|
||||
.select('id, visit_time, notes')
|
||||
.eq('elder_id', elderId)
|
||||
.gte('visit_time', now)
|
||||
.order('visit_time', { ascending: true })
|
||||
.limit(1)
|
||||
.executeAs<Array<any>>()
|
||||
|
||||
if (result.error === null && result.data !== null && result.data.length > 0) {
|
||||
nextVisit.value = result.data[0]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载探访安排失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载本月费用
|
||||
const loadMonthlyBilling = async () => {
|
||||
try {
|
||||
// 模拟费用数据,实际应该从数据库加载
|
||||
monthlyBilling.value = {
|
||||
care_fee: 3500,
|
||||
meal_fee: 1200,
|
||||
other_fee: 300,
|
||||
total_fee: 5000
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载费用信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取生命体征图标
|
||||
const getVitalIcon = (vitalType: string): string => {
|
||||
switch (vitalType) {
|
||||
case 'blood_pressure': return ''
|
||||
case 'heart_rate': return ''
|
||||
case 'temperature': return '️'
|
||||
case 'oxygen': return ''
|
||||
case 'glucose': return ''
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 获取生命体征名称
|
||||
const getVitalName = (vitalType: string): string => {
|
||||
switch (vitalType) {
|
||||
case 'blood_pressure': return '血压'
|
||||
case 'heart_rate': return '心率'
|
||||
case 'temperature': return '体温'
|
||||
case 'oxygen': return '血氧'
|
||||
case 'glucose': return '血糖'
|
||||
default: return vitalType
|
||||
}
|
||||
}
|
||||
|
||||
// 获取生命体征值
|
||||
const getVitalValue = (vital: VitalSign): string => {
|
||||
switch (vital.vital_type) {
|
||||
case 'blood_pressure':
|
||||
return `${vital.systolic_pressure ?? 0}/${vital.diastolic_pressure ?? 0} mmHg`
|
||||
case 'heart_rate':
|
||||
return `${vital.heart_rate ?? 0} 次/分`
|
||||
case 'temperature':
|
||||
return `${vital.temperature ?? 0}°C`
|
||||
case 'oxygen':
|
||||
return `${vital.oxygen_saturation ?? 0}%`
|
||||
case 'glucose':
|
||||
return `${vital.glucose_level ?? 0} mmol/L`
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 导航和操作函数
|
||||
const viewAllCareRecords = () => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/family/care-records?elderId=${elderInfo.value.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const viewHealthDetails = () => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/family/health-monitoring?elderId=${elderInfo.value.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const scheduleVisit = () => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/family/schedule-visit?elderId=${elderInfo.value.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const modifyVisit = () => {
|
||||
if (nextVisit.value !== null) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/family/modify-visit?visitId=${nextVisit.value.id}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const viewBillingDetails = () => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/family/billing?elderId=${elderInfo.value.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 快速操作
|
||||
const contactCaregiver = () => {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: '13800138000'
|
||||
})
|
||||
}
|
||||
|
||||
const viewPhotos = () => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/family/photos?elderId=${elderInfo.value.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const feedbackSuggestion = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ec/family/feedback'
|
||||
})
|
||||
}
|
||||
|
||||
const emergencyContact = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ['联系护理员', '联系医生', '联系管理员', '拨打急救电话'],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0:
|
||||
uni.makePhoneCall({ phoneNumber: '13800138000' })
|
||||
break
|
||||
case 1:
|
||||
uni.makePhoneCall({ phoneNumber: '13800138001' })
|
||||
break
|
||||
case 2:
|
||||
uni.makePhoneCall({ phoneNumber: '13800138002' })
|
||||
break
|
||||
case 3:
|
||||
uni.makePhoneCall({ phoneNumber: '120' })
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
updateCurrentTime()
|
||||
loadElderInfo().then(() => {
|
||||
loadTodayCareRecords()
|
||||
loadRecentVitals()
|
||||
loadNextVisit()
|
||||
})
|
||||
loadMonthlyBilling()
|
||||
|
||||
// 定时更新时间
|
||||
setInterval(() => {
|
||||
updateCurrentTime()
|
||||
}, 60000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* uts-android 兼容性重构:
|
||||
1. 移除所有嵌套选择器、伪类(如 :last-child),全部 class 扁平化。
|
||||
2. 所有间距用 margin-right/margin-bottom 控制,禁止 gap、flex-wrap、嵌套。
|
||||
3. 所有布局 display: flex,禁止 grid、gap、伪类。
|
||||
4. 组件间距、分隔线全部用 border/margin 控制。
|
||||
*/
|
||||
.family-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;
|
||||
}
|
||||
.elder-status-card {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.elder-profile {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.elder-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 40px;
|
||||
margin-right: 20px;
|
||||
overflow: hidden;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.avatar-fallback {
|
||||
font-size: 32px;
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
.elder-basic {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.elder-name {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.elder-info {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.elder-room {
|
||||
font-size: 16px;
|
||||
color: #1890ff;
|
||||
}
|
||||
.elder-health {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.health-status {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.health-excellent {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
.health-good {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
.health-fair {
|
||||
background-color: #fff7e6;
|
||||
color: #d48806;
|
||||
}
|
||||
.health-poor {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
.last-update {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.today-care-section,
|
||||
.health-monitoring-section,
|
||||
.visit-section,
|
||||
.billing-section {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.section-more {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
}
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.timeline-item.is-last {
|
||||
border-bottom: none;
|
||||
}
|
||||
.timeline-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
margin-right: 15px;
|
||||
background-color: #1890ff;
|
||||
}
|
||||
.timeline-dot.medication {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
.timeline-dot.hygiene {
|
||||
background-color: #722ed1;
|
||||
}
|
||||
.timeline-dot.nutrition {
|
||||
background-color: #fa8c16;
|
||||
}
|
||||
.timeline-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.care-title {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.care-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.care-type {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.type-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.metric-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.metric-item.is-last {
|
||||
border-bottom: none;
|
||||
}
|
||||
.metric-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.metric-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.metric-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.metric-value {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.metric-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.metric-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.metric-status.normal {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
.metric-status.abnormal {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
.visit-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background-color: #f0f9ff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.visit-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.visit-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.visit-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.visit-time {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.visit-note {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.visit-btn {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.no-visit {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
}
|
||||
.no-visit-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.schedule-btn {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.billing-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.billing-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.billing-item.is-last {
|
||||
border-bottom: none;
|
||||
}
|
||||
.billing-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.billing-amount {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
.billing-total {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 0 10px;
|
||||
border-top: 2px solid #f0f0f0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.total-label {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
.total-amount {
|
||||
font-size: 20px;
|
||||
color: #ff4d4f;
|
||||
font-weight: bold;
|
||||
}
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
.action-item {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 15px 10px;
|
||||
}
|
||||
.action-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.action-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
1010
pages/ec/family/elder-status.uvue
Normal file
1010
pages/ec/family/elder-status.uvue
Normal file
File diff suppressed because it is too large
Load Diff
28
pages/ec/family/health-monitoring.uvue
Normal file
28
pages/ec/family/health-monitoring.uvue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<view class="health-monitoring-page">
|
||||
<text class="title">健康监测</text>
|
||||
<view class="empty">暂无数据</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// 健康监测页面骨架
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.health-monitoring-page {
|
||||
padding: 30px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.empty {
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
}
|
||||
</style>
|
||||
28
pages/ec/family/schedule-visit.uvue
Normal file
28
pages/ec/family/schedule-visit.uvue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<view class="schedule-visit-page">
|
||||
<text class="title">预约探访</text>
|
||||
<view class="empty">暂无数据</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// 预约探访页面骨架
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.schedule-visit-page {
|
||||
padding: 30px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.empty {
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
}
|
||||
</style>
|
||||
891
pages/ec/finance/management.uvue
Normal file
891
pages/ec/finance/management.uvue
Normal file
@@ -0,0 +1,891 @@
|
||||
<!-- 养老管理系统 - 财务管理 -->
|
||||
<template>
|
||||
<view class="finance-management">
|
||||
<view class="header">
|
||||
<text class="title">财务管理</text>
|
||||
<button class="add-btn" @click="showAddBill">添加账单</button>
|
||||
</view>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<view class="stats-section">
|
||||
<view class="stat-card">
|
||||
<view class="stat-icon"></view>
|
||||
<view class="stat-content">
|
||||
<text class="stat-number">{{ stats.total_amount.toFixed(2) }}</text>
|
||||
<text class="stat-label">总金额</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-icon">✅</view>
|
||||
<view class="stat-content">
|
||||
<text class="stat-number">{{ stats.paid_amount.toFixed(2) }}</text>
|
||||
<text class="stat-label">已收金额</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-icon">⏰</view>
|
||||
<view class="stat-content">
|
||||
<text class="stat-number">{{ stats.pending_amount.toFixed(2) }}</text>
|
||||
<text class="stat-label">待收金额</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-icon">⚠️</view>
|
||||
<view class="stat-content">
|
||||
<text class="stat-number">{{ stats.overdue_amount.toFixed(2) }}</text>
|
||||
<text class="stat-label">逾期金额</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<view class="filter-section">
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">老人:</text>
|
||||
<picker-view class="picker" :value="selectedElderIndex" @change="onElderChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(elder, index) in elderOptions" :key="elder.id" class="picker-item">
|
||||
{{ elder.name }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">类型:</text>
|
||||
<picker-view class="picker" :value="selectedTypeIndex" @change="onTypeChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(type, index) in typeOptions" :key="index" class="picker-item">
|
||||
{{ type.label }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">状态:</text>
|
||||
<picker-view class="picker" :value="selectedStatusIndex" @change="onStatusChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(status, index) in statusOptions" :key="index" class="picker-item">
|
||||
{{ status.label }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
<button class="search-btn" @click="searchBills">搜索</button>
|
||||
</view>
|
||||
|
||||
<!-- 账单列表 -->
|
||||
<view class="bills-list">
|
||||
<view v-for="bill in bills" :key="bill.id" class="bill-item" @click="viewBillDetail(bill)">
|
||||
<view class="bill-header">
|
||||
<text class="bill-type">{{ getTypeText(bill.bill_type) }}</text>
|
||||
<view class="status-badge" :class="getStatusClass(bill.status)">
|
||||
<text class="status-text">{{ getStatusText(bill.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bill-info">
|
||||
<text class="elder-name">{{ getElderName(bill.elder_id) }}</text>
|
||||
<text class="bill-amount">金额: ¥{{ bill.amount.toFixed(2) }}</text>
|
||||
<text class="bill-description">{{ bill.description ?? '无描述' }}</text>
|
||||
</view>
|
||||
<view class="bill-dates">
|
||||
<text class="date-text">到期: {{ formatDate(bill.due_date) }}</text>
|
||||
<text class="date-text" v-if="bill.paid_date">支付: {{ formatDate(bill.paid_date) }}</text>
|
||||
</view>
|
||||
<view class="bill-actions">
|
||||
<button class="action-btn edit-btn" @click.stop="editBill(bill)">编辑</button>
|
||||
<button class="action-btn pay-btn" v-if="bill.status === 'pending'" @click.stop="markAsPaid(bill)">标记已付</button>
|
||||
<button class="action-btn print-btn" @click.stop="printBill(bill)">打印</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加/编辑账单弹窗 -->
|
||||
<view v-if="showBillModal" class="modal-overlay" @click="closeBillModal">
|
||||
<view class="modal-content" @click.stop="">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ isEditMode ? '编辑账单' : '添加账单' }}</text>
|
||||
<button class="close-btn" @click="closeBillModal">×</button>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-group">
|
||||
<text class="form-label">老人:</text>
|
||||
<picker-view class="form-picker" :value="formData.elderIndex" @change="onFormElderChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(elder, index) in elderOptions" :key="elder.id" class="picker-item">
|
||||
{{ elder.name }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">账单类型:</text>
|
||||
<picker-view class="form-picker" :value="formData.typeIndex" @change="onFormTypeChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(type, index) in billTypes" :key="index" class="picker-item">
|
||||
{{ type.label }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">金额:</text>
|
||||
<input class="form-input" v-model="formData.amount" type="number" placeholder="请输入金额" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">描述:</text>
|
||||
<textarea class="form-textarea" v-model="formData.description" placeholder="请输入账单描述"></textarea>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">到期日期:</text>
|
||||
<lime-date-time-picker v-model="formData.due_date" type="date" :placeholder="'选择日期'" class="date-picker" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">支付方式:</text>
|
||||
<picker-view class="form-picker" :value="formData.paymentMethodIndex" @change="onFormPaymentMethodChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(method, index) in paymentMethods" :key="index" class="picker-item">
|
||||
{{ method.label }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">备注:</text>
|
||||
<textarea class="form-textarea" v-model="formData.notes" placeholder="请输入备注"></textarea>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="cancel-btn" @click="closeBillModal">取消</button>
|
||||
<button class="save-btn" @click="saveBill">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import type { Bill, Elder } from '../types.uts'
|
||||
import { formatDate, getStatusClass } from '../types.uts'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
|
||||
// 统计数据类型
|
||||
type FinanceStats = {
|
||||
total_amount: number
|
||||
paid_amount: number
|
||||
pending_amount: number
|
||||
overdue_amount: number
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const bills = ref<Bill[]>([])
|
||||
const elderOptions = ref<Elder[]>([])
|
||||
const eldersMap = ref<Map<string, string>>(new Map())
|
||||
const stats = ref<FinanceStats>({
|
||||
total_amount: 0,
|
||||
paid_amount: 0,
|
||||
pending_amount: 0,
|
||||
overdue_amount: 0
|
||||
})
|
||||
|
||||
// 筛选相关
|
||||
const selectedElderIndex = ref([0])
|
||||
const selectedTypeIndex = ref([0])
|
||||
const selectedStatusIndex = ref([0])
|
||||
|
||||
const typeOptions = [
|
||||
{ value: 'all', label: '全部类型' },
|
||||
{ value: 'monthly_fee', label: '月费' },
|
||||
{ value: 'medical', label: '医疗费' },
|
||||
{ value: 'nursing', label: '护理费' },
|
||||
{ value: 'meal', label: '餐费' },
|
||||
{ value: 'activity', label: '活动费' },
|
||||
{ value: 'other', label: '其他费用' }
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'pending', label: '待支付' },
|
||||
{ value: 'paid', label: '已支付' },
|
||||
{ value: 'overdue', label: '已逾期' },
|
||||
{ value: 'cancelled', label: '已取消' }
|
||||
]
|
||||
|
||||
const billTypes = [
|
||||
{ value: 'monthly_fee', label: '月费' },
|
||||
{ value: 'medical', label: '医疗费' },
|
||||
{ value: 'nursing', label: '护理费' },
|
||||
{ value: 'meal', label: '餐费' },
|
||||
{ value: 'activity', label: '活动费' },
|
||||
{ value: 'other', label: '其他费用' }
|
||||
]
|
||||
|
||||
const paymentMethods = [
|
||||
{ value: 'cash', label: '现金' },
|
||||
{ value: 'card', label: '银行卡' },
|
||||
{ value: 'transfer', label: '转账' },
|
||||
{ value: 'wechat', label: '微信支付' },
|
||||
{ value: 'alipay', label: '支付宝' }
|
||||
]
|
||||
|
||||
// 弹窗相关
|
||||
const showBillModal = ref(false)
|
||||
const isEditMode = ref(false)
|
||||
const currentBillId = ref<string | null>(null)
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
elderIndex: [0],
|
||||
typeIndex: [0],
|
||||
amount: '',
|
||||
description: '',
|
||||
due_date: '',
|
||||
paymentMethodIndex: [0],
|
||||
notes: ''
|
||||
})
|
||||
|
||||
// 页面加载
|
||||
onLoad(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
async function loadData(): Promise<void> {
|
||||
try {
|
||||
await Promise.all([
|
||||
loadElders(),
|
||||
loadBills(),
|
||||
loadStats()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
uni.showToast({
|
||||
title: '加载数据失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加载老人列表
|
||||
async function loadElders(): Promise<void> {
|
||||
const result = await supa.executeAs<Elder>('eldercare_admin', `
|
||||
SELECT id, name FROM ec_elders
|
||||
WHERE status = 'active'
|
||||
ORDER BY name
|
||||
`)
|
||||
elderOptions.value = [{ id: '', name: '全部老人' } as Elder, ...result]
|
||||
|
||||
// 建立映射
|
||||
const map = new Map<string, string>()
|
||||
for (let i: Int = 0; i < result.length; i++) {
|
||||
const elder = result[i]
|
||||
map.set(elder.id, elder.name)
|
||||
}
|
||||
eldersMap.value = map
|
||||
}
|
||||
|
||||
// 加载账单列表
|
||||
async function loadBills(): Promise<void> {
|
||||
let whereClause = "WHERE 1=1"
|
||||
|
||||
// 老人筛选
|
||||
if (selectedElderIndex.value[0] > 0) {
|
||||
const selectedElder = elderOptions.value[selectedElderIndex.value[0]]
|
||||
whereClause += ` AND elder_id = '${selectedElder.id}'`
|
||||
}
|
||||
|
||||
// 类型筛选
|
||||
if (selectedTypeIndex.value[0] > 0) {
|
||||
const selectedType = typeOptions[selectedTypeIndex.value[0]]
|
||||
whereClause += ` AND bill_type = '${selectedType.value}'`
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (selectedStatusIndex.value[0] > 0) {
|
||||
const selectedStatus = statusOptions[selectedStatusIndex.value[0]]
|
||||
whereClause += ` AND status = '${selectedStatus.value}'`
|
||||
}
|
||||
|
||||
const result = await supa.executeAs<Bill>('eldercare_admin', `
|
||||
SELECT * FROM ec_bills
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
`)
|
||||
bills.value = result
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
async function loadStats(): Promise<void> {
|
||||
const result = await supa.executeAs<FinanceStats>('eldercare_admin', `
|
||||
SELECT
|
||||
COALESCE(SUM(amount), 0) as total_amount,
|
||||
COALESCE(SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END), 0) as paid_amount,
|
||||
COALESCE(SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END), 0) as pending_amount,
|
||||
COALESCE(SUM(CASE WHEN status = 'overdue' THEN amount ELSE 0 END), 0) as overdue_amount
|
||||
FROM ec_bills
|
||||
`)
|
||||
|
||||
if (result.length > 0) {
|
||||
stats.value = result[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 获取老人姓名
|
||||
function getElderName(elderId: string): string {
|
||||
return eldersMap.value.get(elderId) ?? '未知老人'
|
||||
}
|
||||
|
||||
// 获取类型文本
|
||||
function getTypeText(type: string): string {
|
||||
const typeMap: Record<string, string> = {
|
||||
'monthly_fee': '月费',
|
||||
'medical': '医疗费',
|
||||
'nursing': '护理费',
|
||||
'meal': '餐费',
|
||||
'activity': '活动费',
|
||||
'other': '其他费用'
|
||||
}
|
||||
return typeMap[type] ?? type
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
'pending': '待支付',
|
||||
'paid': '已支付',
|
||||
'overdue': '已逾期',
|
||||
'cancelled': '已取消'
|
||||
}
|
||||
return statusMap[status] ?? status
|
||||
}
|
||||
|
||||
// 筛选事件
|
||||
function onElderChange(e: any): void {
|
||||
selectedElderIndex.value = e.detail.value
|
||||
}
|
||||
|
||||
function onTypeChange(e: any): void {
|
||||
selectedTypeIndex.value = e.detail.value
|
||||
}
|
||||
|
||||
function onStatusChange(e: any): void {
|
||||
selectedStatusIndex.value = e.detail.value
|
||||
}
|
||||
|
||||
// 搜索账单
|
||||
function searchBills(): void {
|
||||
loadBills()
|
||||
loadStats()
|
||||
}
|
||||
|
||||
// 查看账单详情
|
||||
function viewBillDetail(bill: Bill): void {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/finance/detail?id=${bill.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑账单
|
||||
function editBill(bill: Bill): void {
|
||||
isEditMode.value = true
|
||||
currentBillId.value = bill.id
|
||||
|
||||
// 填充表单数据
|
||||
const elderIndex = elderOptions.value.findIndex(elder => elder.id === bill.elder_id)
|
||||
const typeIndex = billTypes.findIndex(type => type.value === bill.bill_type)
|
||||
const paymentMethodIndex = paymentMethods.findIndex(method => method.value === bill.payment_method)
|
||||
|
||||
formData.value = {
|
||||
elderIndex: [elderIndex > 0 ? elderIndex : 0],
|
||||
typeIndex: [typeIndex > 0 ? typeIndex : 0],
|
||||
amount: bill.amount.toString(),
|
||||
description: bill.description ?? '',
|
||||
due_date: bill.due_date ?? '',
|
||||
paymentMethodIndex: [paymentMethodIndex > 0 ? paymentMethodIndex : 0],
|
||||
notes: bill.notes ?? ''
|
||||
}
|
||||
|
||||
showBillModal.value = true
|
||||
}
|
||||
|
||||
// 标记为已支付
|
||||
async function markAsPaid(bill: Bill): Promise<void> {
|
||||
uni.showModal({
|
||||
title: '确认支付',
|
||||
content: '确定标记此账单为已支付吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await supa.executeAs('eldercare_admin', `
|
||||
UPDATE ec_bills
|
||||
SET status = 'paid', paid_date = CURRENT_DATE, updated_at = NOW()
|
||||
WHERE id = '${bill.id}'
|
||||
`)
|
||||
|
||||
uni.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
loadBills()
|
||||
loadStats()
|
||||
} catch (error) {
|
||||
console.error('标记支付失败:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 打印账单
|
||||
function printBill(bill: Bill): void {
|
||||
// 这里可以实现打印功能
|
||||
uni.showToast({
|
||||
title: '打印功能待实现',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 显示添加账单弹窗
|
||||
function showAddBill(): void {
|
||||
isEditMode.value = false
|
||||
currentBillId.value = null
|
||||
|
||||
// 重置表单
|
||||
const today = new Date()
|
||||
const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate())
|
||||
|
||||
formData.value = {
|
||||
elderIndex: [0],
|
||||
typeIndex: [0],
|
||||
amount: '',
|
||||
description: '',
|
||||
due_date: formatDate(nextMonth.toISOString()),
|
||||
paymentMethodIndex: [0],
|
||||
notes: ''
|
||||
}
|
||||
|
||||
showBillModal.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeBillModal(): void {
|
||||
showBillModal.value = false
|
||||
}
|
||||
|
||||
// 表单事件
|
||||
function onFormElderChange(e: any): void {
|
||||
formData.value.elderIndex = e.detail.value
|
||||
}
|
||||
|
||||
function onFormTypeChange(e: any): void {
|
||||
formData.value.typeIndex = e.detail.value
|
||||
}
|
||||
|
||||
function onFormPaymentMethodChange(e: any): void {
|
||||
formData.value.paymentMethodIndex = e.detail.value
|
||||
}
|
||||
|
||||
function onDueDateChange(date: string): void {
|
||||
formData.value.due_date = date
|
||||
}
|
||||
|
||||
// 保存账单
|
||||
async function saveBill(): Promise<void> {
|
||||
// 验证表单
|
||||
if (formData.value.elderIndex[0] === 0) {
|
||||
uni.showToast({
|
||||
title: '请选择老人',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.value.amount.trim() === '' || parseFloat(formData.value.amount) <= 0) {
|
||||
uni.showToast({
|
||||
title: '请输入有效金额',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const selectedElder = elderOptions.value[formData.value.elderIndex[0]]
|
||||
const selectedType = billTypes[formData.value.typeIndex[0]]
|
||||
const selectedPaymentMethod = paymentMethods[formData.value.paymentMethodIndex[0]]
|
||||
|
||||
if (isEditMode.value && currentBillId.value !== null) {
|
||||
// 更新账单
|
||||
await supa.executeAs('eldercare_admin', `
|
||||
UPDATE ec_bills SET
|
||||
elder_id = '${selectedElder.id}',
|
||||
bill_type = '${selectedType.value}',
|
||||
amount = ${formData.value.amount},
|
||||
description = '${formData.value.description}',
|
||||
due_date = ${formData.value.due_date ? `'${formData.value.due_date}'` : 'NULL'},
|
||||
payment_method = '${selectedPaymentMethod.value}',
|
||||
notes = '${formData.value.notes}',
|
||||
updated_at = NOW()
|
||||
WHERE id = '${currentBillId.value}'
|
||||
`)
|
||||
} else {
|
||||
// 新增账单
|
||||
await supa.executeAs('eldercare_admin', `
|
||||
INSERT INTO ec_bills (
|
||||
elder_id, bill_type, amount, description, due_date,
|
||||
payment_method, notes, status
|
||||
) VALUES (
|
||||
'${selectedElder.id}',
|
||||
'${selectedType.value}',
|
||||
${formData.value.amount},
|
||||
'${formData.value.description}',
|
||||
${formData.value.due_date ? `'${formData.value.due_date}'` : 'NULL'},
|
||||
'${selectedPaymentMethod.value}',
|
||||
'${formData.value.notes}',
|
||||
'pending'
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
closeBillModal()
|
||||
loadBills()
|
||||
loadStats()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* uts-android 兼容性重构:
|
||||
1. 移除所有嵌套选择器、伪类(如 :last-child、&.xxx),全部 class 扁平化。
|
||||
2. 所有间距用 margin-right/margin-bottom 控制,禁止 gap、flex-wrap、嵌套。
|
||||
3. 所有布局 display: flex,禁止 grid、gap、伪类。
|
||||
4. 组件间距、分隔线全部用 border/margin 控制。
|
||||
5. 新增.is-last、.is-active、.is-overdue 等辅助 class。
|
||||
*/
|
||||
.finance-management {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.add-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #2196f3;
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
.stats-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
margin-right: 15px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.stat-card.is-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
.stat-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
.stat-number {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.filter-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.filter-item.is-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.picker {
|
||||
width: 150px;
|
||||
}
|
||||
.search-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
.bills-list {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
min-height: 300px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.bill-item {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.bill-item.is-last {
|
||||
border-bottom: none;
|
||||
}
|
||||
.bill-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.bill-type {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.status-badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background-color: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
.status-badge.paid {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
.status-badge.pending {
|
||||
background-color: #fffbe6;
|
||||
color: #faad14;
|
||||
}
|
||||
.status-badge.overdue {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
.status-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
.bill-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.elder-name {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.bill-amount {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.bill-description {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.bill-dates {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.date-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.bill-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.action-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 15px;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.edit-btn {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
.pay-btn {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
.print-btn {
|
||||
background-color: #faad14;
|
||||
color: #fff;
|
||||
}
|
||||
.action-btn.is-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
width: 500px;
|
||||
max-width: 95vw;
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
}
|
||||
.modal-body {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.form-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.form-picker {
|
||||
width: 150px;
|
||||
}
|
||||
.form-input {
|
||||
flex: 1;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.form-textarea {
|
||||
flex: 1;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
min-height: 60px;
|
||||
}
|
||||
.form-date-picker {
|
||||
width: 150px;
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.cancel-btn {
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.save-btn {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.finance-management {
|
||||
padding: 15px;
|
||||
}
|
||||
.stats-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
.stat-card {
|
||||
margin: 5px 0;
|
||||
min-width: auto;
|
||||
}
|
||||
.filter-section {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.filter-item {
|
||||
margin-right: 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.picker {
|
||||
width: 150px;
|
||||
}
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
184
pages/ec/health/alertparse.uts
Normal file
184
pages/ec/health/alertparse.uts
Normal file
@@ -0,0 +1,184 @@
|
||||
// 告警消息解析工具
|
||||
// 参考 doc_eldercare/alert.md 设计
|
||||
|
||||
export type AlertParseResult {
|
||||
type : string; // 解析后的类型,如 'SOS', '健康数据', '定位', '通知', '计步', '围栏', '语音', '睡眠', '手表', '未知'
|
||||
title : string; // 简要标题
|
||||
content : string; // 展示内容
|
||||
time ?: string; // 时间
|
||||
raw : any; // 原始数据
|
||||
level ?: 'normal' | 'warn' | 'danger'; // 可选,紧急等级
|
||||
mid ?: string; // 设备ID
|
||||
smid ?: string; // 消息ID
|
||||
extra ?: UTSJSONObject; // 其它解析字段
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析推送消息主入口
|
||||
* @param msg 传入ps_push_msg_raw的raw_data对象或完整record
|
||||
*/
|
||||
export const parseAlertMessage = (msg : UTSJSONObject) : AlertParseResult => {
|
||||
// 兼容 record/raw_data
|
||||
let data : UTSJSONObject = msg.getJSON('raw_data') ?? {};
|
||||
|
||||
|
||||
const pushType = data.getNumber('pushType') ?? 0;
|
||||
const action = data.getNumber('actionRaw') ?? 0;
|
||||
const time = data.getString('Time') ?? data.getString('time') ?? data.getString('created_at') ?? '';
|
||||
// 1. SOS
|
||||
if (pushType === 1) {
|
||||
return {
|
||||
type: 'SOS',
|
||||
title: 'SOS求救',
|
||||
content: `${data.getString('Name') ?? ''}(${data.getString('MID') ?? ''}) 向您发出求救`,
|
||||
time,
|
||||
raw: msg,
|
||||
level: 'danger',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { lon: data.getNumber('Lon'), lat: data.getNumber('Lat'), address: data.getString('Str') }
|
||||
};
|
||||
}
|
||||
// 2. 健康数据
|
||||
if (pushType === 2) {
|
||||
let arr : string[] = [];
|
||||
const h = data.getNumber('H');
|
||||
const o = data.getNumber('O');
|
||||
const w = data.getNumber('W');
|
||||
const x = data.getNumber('X');
|
||||
const y = data.getNumber('Y');
|
||||
if (h != null) arr.push(`心率:${h}`);
|
||||
if (o != null) arr.push(`血氧:${o}`);
|
||||
if (w != null) arr.push(`体温:${w}`);
|
||||
if (x != null) arr.push(`高压:${x}`);
|
||||
if (y != null) arr.push(`低压:${y}`);
|
||||
return {
|
||||
type: '健康数据',
|
||||
title: '健康数据推送',
|
||||
content: arr.length > 0 ? arr.join(',') : JSON.stringify(data),
|
||||
time,
|
||||
raw: msg,
|
||||
level: 'normal',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { mid: data.getString('MID') ?? data.getString('mid') }
|
||||
};
|
||||
}
|
||||
// 3. 定位
|
||||
if (pushType === 3) {
|
||||
return {
|
||||
type: '定位',
|
||||
title: '定位推送',
|
||||
content: `${data.getString('Pro') ?? ''}${data.getString('City') ?? ''}${data.getString('Dist') ?? ''}${data.getString('Str') ?? ''} (${data.getNumber('Lon')},${data.getNumber('Lat')})`,
|
||||
time: data.getString('CT') ?? data.getString('UT') ?? time,
|
||||
raw: msg,
|
||||
level: 'normal',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { lon: data.getNumber('Lon'), lat: data.getNumber('Lat') }
|
||||
};
|
||||
}
|
||||
// 4. 通知
|
||||
if (pushType === 4) {
|
||||
const actionMapObj = {
|
||||
'-1': '设备在线离线', '4': '围栏内停留', '5': '离开围栏', '6': '进入围栏', '9': '低电报警', '11': '跌倒报警', '23': '高温报警',
|
||||
'26': '断开wifi', '28': 'wifi离线', '36': '防盗报警', '42': '布防告警', '44': '在家布防告警', '7': 'SOS报警', '10': '摘除报警',
|
||||
'22': '低温报警', '24': '更换SIM卡', '27': '连接wifi', '35': '社区养老报警', '37': '状态通知', '43': '撤防告警', '45': '八件套报警',
|
||||
'47': 'wifi不一致报警', '49': '红外报警', '50': 'NB按键报警', '51': 'NB防拆报警', '52': 'NB报警复位', '61': 'NB设备报警',
|
||||
'63': '人体存在报警', '67': 'NB测试报警', '85': '网关上线', '87': '删除子设备', '114': '烟感/气感/门磁事件', '116': 'SCA事件',
|
||||
'118': '防跌倒雷达', '121': '智能胸牌告警', '84': '网关离线', '86': '添加子设备', '113': '门磁事件', '115': '拉绳SOS',
|
||||
'117': '4G视频门磁', '119': 'd5网关子设备报警', '122': 'NB温湿度报警', '123': '气感报警', '125': '水浸报警', '127': '跌倒报警',
|
||||
'129': '燃气报警', '131': '对讲SOS', '134': 'AI智能报警', '124': '烟感报警', '126': '摄像头报警', '128': '井盖报警',
|
||||
'130': '红外报警', '132': 'ZML_SOS报警', '200': '设备信息变更'
|
||||
};
|
||||
let actionStr:string = actionMapObj.getString(action.toString()) ?? `通知类型:${action}`;
|
||||
return {
|
||||
type: '通知',
|
||||
title: actionStr,
|
||||
content: `${data.getString('Name') ?? ''} ${data.getString('Content') ?? ''}`.trim() ?? JSON.stringify(data),
|
||||
time,
|
||||
raw: msg,
|
||||
level: (action === 9 || action === 11 || action === 23 || action === 7) ? 'danger' : 'warn',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { action }
|
||||
};
|
||||
}
|
||||
// 5. 计步/翻转(pushType=5 或有 Step/Roll 字段)
|
||||
if (pushType === 6) {
|
||||
return {
|
||||
type: '计步',
|
||||
title: '计步/翻转',
|
||||
content: `步数:${data.getNumber('Step') ?? '-'} 翻转:${data.getNumber('Roll') ?? '-'}`,
|
||||
time,
|
||||
raw: msg,
|
||||
level: 'normal',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { step: data.getNumber('Step'), roll: data.getNumber('Roll'), mid: data.getString('MID') ?? data.getString('mid'), smid: data.getString('SMID') ?? data.getString('smid') }
|
||||
};
|
||||
}
|
||||
// 6. 围栏(进出围栏、进入围栏、离开围栏等)
|
||||
if (pushType === 7 || (pushType === 4 && (action === 6 || action === 5))) {
|
||||
let actionStr = '';
|
||||
if (pushType === 4) {
|
||||
if (action === 6) actionStr = '进入围栏';
|
||||
else if (action === 5) actionStr = '离开围栏';
|
||||
else actionStr = '围栏事件';
|
||||
} else {
|
||||
actionStr = '进出围栏';
|
||||
}
|
||||
return {
|
||||
type: '围栏',
|
||||
title: actionStr,
|
||||
content: data.getString('Content') ?? JSON.stringify(data),
|
||||
time,
|
||||
raw: msg,
|
||||
level: 'warn',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { lon: data.getNumber('Lon'), lat: data.getNumber('Lat'), action }
|
||||
};
|
||||
}
|
||||
// 7. 微聊语音
|
||||
if (pushType === 8) {
|
||||
let msgType = data.getNumber('msgType') ?? data.getNumber('MType') ?? 0;
|
||||
let typeStr = msgType === 2 ? '语音' : '文字';
|
||||
return {
|
||||
type: '微聊',
|
||||
title: `微聊${typeStr}消息`,
|
||||
content: data.getString('content') ?? JSON.stringify(data),
|
||||
time,
|
||||
raw: msg,
|
||||
level: 'normal',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { msgType }
|
||||
};
|
||||
}
|
||||
// 8. 睡眠带报警
|
||||
if (pushType === 9) {
|
||||
return {
|
||||
type: '睡眠报警',
|
||||
title: '睡眠带报警',
|
||||
content: data.getString('Content') ?? JSON.stringify(data),
|
||||
time,
|
||||
raw: msg,
|
||||
level: 'warn',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { action }
|
||||
};
|
||||
}
|
||||
// 9. 其它类型可继续补充...
|
||||
// 默认
|
||||
return {
|
||||
type: '未知',
|
||||
title: '未知推送',
|
||||
content: JSON.stringify(data),
|
||||
time,
|
||||
raw: msg,
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid')
|
||||
};
|
||||
};
|
||||
80
pages/ec/health/alerts.uvue
Normal file
80
pages/ec/health/alerts.uvue
Normal file
@@ -0,0 +1,80 @@
|
||||
<!-- 健康提醒列表 - uts-android 兼容版 -->
|
||||
<template>
|
||||
<view class="alerts-list-page">
|
||||
<view class="header">
|
||||
<text class="header-title">健康提醒</text>
|
||||
</view>
|
||||
<view v-for="alert in alerts" :key="alert.id" class="alert-item">
|
||||
<text class="alert-title">{{ alert.title }}</text>
|
||||
<text class="alert-desc">{{ alert.description }}</text>
|
||||
<text class="alert-patient">患者: {{ alert.elder_name }}</text>
|
||||
<text class="alert-time">{{ alert.created_at }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
const alerts = ref<any[]>([])
|
||||
const loadAlert = async () => {
|
||||
const result = await supa.from('ec_health_alerts')
|
||||
.select('*', { count: 'exact' })
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(100)
|
||||
.execute()
|
||||
if (result.data != null) alerts.value = result.data
|
||||
}
|
||||
onLoad(() => {
|
||||
loadAlert()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.alerts-list-page {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px 0 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.alert-desc {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin: 6px 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alert-patient {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alert-time {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
98
pages/ec/health/ecalert-history.uvue
Normal file
98
pages/ec/health/ecalert-history.uvue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<view class="alert-container">
|
||||
<view class="top-bar">
|
||||
<button @click="goRealtime" style="margin-right: 16rpx;">实时告警</button>
|
||||
<text class="title">历史健康告警</text>
|
||||
</view>
|
||||
<view class="section" style="margin-bottom: 24rpx;">
|
||||
<button @click="loadHistory">刷新历史</button>
|
||||
</view>
|
||||
<scroll-view style="height: 600rpx; border: 1px solid #ccc; padding: 8rpx;" scroll-y scroll-with-animation>
|
||||
<view v-for="(msg, idx) in messages" :key="idx" style="font-size: 26rpx; color: #333; margin-bottom: 12rpx;">
|
||||
<text>{{ msg.timeStr }}:</text>
|
||||
<text>{{ msg.content }}</text>
|
||||
<text>{{ msg.raw_data }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
|
||||
type AlertMessage = {
|
||||
type: string;
|
||||
mid?:string;
|
||||
content: string;
|
||||
timeStr: string;
|
||||
raw_data:string
|
||||
};
|
||||
|
||||
import { parseAlertMessage } from './alertparse.uts';
|
||||
import supa from '@/components/supadb/aksupainstance.uts';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
/** 历史告警消息列表,强类型 */
|
||||
messages: [] as AlertMessage[],
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
goRealtime() {
|
||||
uni.navigateTo({ url: '/pages/ec/health/ecalert' });
|
||||
},
|
||||
async loadHistory() {
|
||||
this.loading = true;
|
||||
let historyList: any[] = [];
|
||||
try {
|
||||
// UTS Android 风格获取 Supabase 查询结果
|
||||
const resp = await supa.from('ps_push_msg_raw').select('*',{}).order('created_at', { ascending: false }).limit(100).execute();
|
||||
if (resp && resp.error) {
|
||||
uni.showToast({ title: '获取历史告警失败', icon: 'none' });
|
||||
} else if (resp && Array.isArray(resp.data)) {
|
||||
historyList = resp.data;
|
||||
} else {
|
||||
historyList = [];
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '请求异常', icon: 'none' });
|
||||
}
|
||||
this.messages = (historyList || []).map((item): AlertMessage => {
|
||||
// item.raw_data 可能为 UTSJSONObject,parseAlertMessage 返回强类型对象
|
||||
const parseResult = parseAlertMessage(item.raw_data ? item.raw_data : item);
|
||||
return {
|
||||
type: parseResult.type,
|
||||
mid:parseResult.mid,
|
||||
content: (parseResult.mid ? `[${parseResult.mid}] ` : '') + parseResult.title + (parseResult.content ? (': ' + parseResult.content) : ''),
|
||||
timeStr: parseResult.time || '',
|
||||
raw_data: item.raw_data
|
||||
};
|
||||
});
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadHistory();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.alert-container {
|
||||
padding: 32rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
</style>
|
||||
157
pages/ec/health/ecalert.uvue
Normal file
157
pages/ec/health/ecalert.uvue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<view class="alert-container">
|
||||
<view class="top-bar">
|
||||
<button @click="goHistory" style="margin-right: 16rpx;">历史告警</button>
|
||||
<text class="title">健康告警推送</text>
|
||||
</view>
|
||||
<scroll-view style="height: 600rpx; border: 1px solid #ccc; padding: 8rpx;" direction="vertical" scroll-with-animation>
|
||||
<view v-for="(msg, idx) in messages" :key="idx"
|
||||
style="font-size: 26rpx; color: #333; margin-bottom: 12rpx;">
|
||||
<text>{{ msg.timeStr }}:</text>
|
||||
<text>{{ msg.content }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { parseAlertMessage } from './alertparse.uts';
|
||||
import { AkSupaRealtime } from '@/components/supadb/aksuparealtime.uts';
|
||||
import { SUPA_KEY, WS_URL } from '@/ak/config.uts';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
wsUrl: WS_URL,
|
||||
channel: 'realtime:public:ps_push_msg_raw',
|
||||
messages: [] as Array<{ content : string; timeStr : string }>,
|
||||
realtime: null as AkSupaRealtime | null
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.initRealtime();
|
||||
},
|
||||
onUnload() {
|
||||
if (this.realtime) {
|
||||
this.realtime.close({});
|
||||
this.realtime = null;
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
// 页面回到前台时检查 WebSocket 连接状态,必要时重连
|
||||
if (!this.realtime || !this.realtime.isOpen) {
|
||||
console.log('onShow: WebSocket未连接,尝试重连');
|
||||
// this.initRealtime();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goHistory() {
|
||||
uni.navigateTo({ url: '/pages/ec/health/ecalert-history' });
|
||||
},
|
||||
// 重连相关参数
|
||||
reconnectDelay: 3000,
|
||||
reconnectMax: 10,
|
||||
reconnectCount: 0,
|
||||
reconnectTimer: null as any,
|
||||
initRealtime() {
|
||||
if (this.realtime) {
|
||||
this.realtime.close({});
|
||||
this.realtime = null;
|
||||
}
|
||||
const wsUrl: string = this.wsUrl as string;
|
||||
const channel: string = this.channel;
|
||||
const self = this;
|
||||
this.reconnectCount = 0;
|
||||
const createRealtime = () => {
|
||||
const newRealtime = new AkSupaRealtime({
|
||||
url: wsUrl,
|
||||
channel: channel,
|
||||
apikey: SUPA_KEY,
|
||||
onOpen() {
|
||||
self.reconnectCount = 0;
|
||||
if (self.reconnectTimer) {
|
||||
clearTimeout(self.reconnectTimer);
|
||||
self.reconnectTimer = null;
|
||||
}
|
||||
},
|
||||
onClose() {
|
||||
// 断开后自动重连
|
||||
self.tryReconnect();
|
||||
},
|
||||
onError(err) {
|
||||
// 错误后也尝试重连
|
||||
self.tryReconnect();
|
||||
},
|
||||
onMessage(data) {
|
||||
console.log(data)
|
||||
if (data && typeof data === 'object' && data.event === 'INSERT' && data.payload && data.payload.record) {
|
||||
const payload = data.payload;
|
||||
let content = '';
|
||||
let timeStr = '';
|
||||
const record = payload.record;
|
||||
if (record.raw_data) {
|
||||
content = record.raw_data;
|
||||
} else if (record.message) {
|
||||
content = record.message;
|
||||
} else {
|
||||
content = JSON.stringify(record);
|
||||
}
|
||||
if (record.created_at) {
|
||||
try {
|
||||
const d = new Date(record.created_at);
|
||||
timeStr = d.toLocaleString();
|
||||
} catch (e) {
|
||||
timeStr = record.created_at;
|
||||
}
|
||||
}
|
||||
const parseResult = parseAlertMessage(record.raw_data ? record.raw_data : record);
|
||||
console.log(parseResult)
|
||||
console.log(this.messages, self.messages)
|
||||
self.messages.unshift({
|
||||
content: (parseResult.mid ? `[${parseResult.mid}] ` : '') + parseResult.title + (parseResult.content ? (': ' + parseResult.content) : ''),
|
||||
timeStr: timeStr || parseResult.time || ''
|
||||
});
|
||||
if (self.messages.length > 100) self.messages.length = 100;
|
||||
}
|
||||
}
|
||||
});
|
||||
self.realtime = newRealtime;
|
||||
newRealtime.connect();
|
||||
};
|
||||
createRealtime();
|
||||
},
|
||||
tryReconnect() {
|
||||
if (this.reconnectCount >= this.reconnectMax) {
|
||||
console.warn('WebSocket重连已达最大次数');
|
||||
return;
|
||||
}
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
this.reconnectCount++;
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
console.log('WebSocket重连中...', this.reconnectCount);
|
||||
// this.initRealtime();
|
||||
}, this.reconnectDelay);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.alert-container {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
</style>
|
||||
41
pages/ec/health/quick-check.uvue
Normal file
41
pages/ec/health/quick-check.uvue
Normal file
@@ -0,0 +1,41 @@
|
||||
<!-- 养老管理系统 - 健康检查快捷入口 -->
|
||||
<template>
|
||||
<view class="quick-check-page">
|
||||
<view class="header">
|
||||
<text class="title">健康检查</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="desc">此处可快速录入健康检查数据或跳转到健康详情页面。</text>
|
||||
<!-- 可根据实际需求补充表单或快捷入口 -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// 可根据实际需求补充逻辑
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.quick-check-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
.desc {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
1108
pages/ec/incident/management.uvue
Normal file
1108
pages/ec/incident/management.uvue
Normal file
File diff suppressed because it is too large
Load Diff
955
pages/ec/incident/report.uvue
Normal file
955
pages/ec/incident/report.uvue
Normal file
@@ -0,0 +1,955 @@
|
||||
<template>
|
||||
<view class="incident-report">
|
||||
<!-- Header -->
|
||||
<view class="header">
|
||||
<view class="header-content">
|
||||
<text class="header-title">事件报告</text>
|
||||
<text class="header-subtitle">记录和跟踪护理事件</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<view class="stats-section">
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card urgent">
|
||||
<text class="stat-number">{{ stats.urgent_incidents }}</text>
|
||||
<text class="stat-label">紧急事件</text>
|
||||
<text class="stat-trend">今日新增</text>
|
||||
</view>
|
||||
<view class="stat-card pending">
|
||||
<text class="stat-number">{{ stats.pending_reports }}</text>
|
||||
<text class="stat-label">待处理</text>
|
||||
<text class="stat-trend">需要跟进</text>
|
||||
</view>
|
||||
<view class="stat-card resolved">
|
||||
<text class="stat-number">{{ stats.resolved_today }}</text>
|
||||
<text class="stat-label">今日解决</text>
|
||||
<text class="stat-trend">已完成</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- New Incident Button -->
|
||||
<view class="new-incident-section">
|
||||
<view class="new-incident-btn" @tap="showNewIncidentModal">
|
||||
<text class="new-incident-icon">📝</text>
|
||||
<text class="new-incident-text">新建事件报告</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Filter Tabs -->
|
||||
<view class="filter-section">
|
||||
<scroll-view class="filter-scroll" scroll-x="true">
|
||||
<view class="filter-tabs">
|
||||
<view v-for="filter in filterOptions" :key="filter.value"
|
||||
class="filter-tab" :class="{active: currentFilter === filter.value}"
|
||||
@tap="setFilter(filter.value)">
|
||||
<text class="filter-text">{{ filter.label }}</text>
|
||||
<text v-if="filter.count > 0" class="filter-count">{{ filter.count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- Incidents List -->
|
||||
<view class="incidents-section">
|
||||
<view v-if="filteredIncidents.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="incidents-list">
|
||||
<view v-for="incident in filteredIncidents" :key="incident.id"
|
||||
class="incident-card" @tap="viewIncidentDetail(incident)">
|
||||
<view class="incident-header">
|
||||
<view class="incident-info">
|
||||
<text class="incident-title">{{ incident.title }}</text>
|
||||
<text class="incident-elder">{{ incident.elder_name }}</text>
|
||||
</view>
|
||||
<view class="incident-priority" :class="getPriorityClass(incident.priority)">
|
||||
<text class="priority-text">{{ getPriorityText(incident.priority) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="incident-content">
|
||||
<text class="incident-description">{{ incident.description }}</text>
|
||||
</view>
|
||||
|
||||
<view class="incident-meta">
|
||||
<view class="incident-time">
|
||||
<text class="time-label">发生时间:</text>
|
||||
<text class="time-value">{{ formatDateTime(incident.incident_time) }}</text>
|
||||
</view>
|
||||
<view class="incident-reporter">
|
||||
<text class="reporter-label">报告人:</text>
|
||||
<text class="reporter-value">{{ incident.reporter_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="incident-footer">
|
||||
<view class="incident-status" :class="getStatusClass(incident.status)">
|
||||
<text class="status-text">{{ getStatusText(incident.status) }}</text>
|
||||
</view>
|
||||
<view class="incident-actions">
|
||||
<text class="action-time">{{ formatTime(incident.created_at) }}</text>
|
||||
<text v-if="incident.status === 'pending'" class="action-follow" @tap.stop="followUpIncident(incident)">跟进</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- New Incident Modal -->
|
||||
<view v-if="showModal" class="modal-overlay" @tap="hideModal">
|
||||
<view class="modal-content" @tap.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">新建事件报告</text>
|
||||
<text class="modal-close" @tap="hideModal">✕</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="modal-body">
|
||||
<view class="form-group">
|
||||
<text class="form-label">事件标题 *</text>
|
||||
<input class="form-input" v-model="newIncident.title" placeholder="请输入事件标题" />
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">相关老人 *</text>
|
||||
<picker class="form-picker" mode="selector" :value="elderIndex" :range="elderOptions" range-key="name" @change="onElderChange">
|
||||
<view class="picker-display">
|
||||
<text class="picker-text">{{ elderOptions[elderIndex]?.name || '请选择老人' }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">优先级 *</text>
|
||||
<picker class="form-picker" mode="selector" :value="priorityIndex" :range="priorityOptions" @change="onPriorityChange">
|
||||
<view class="picker-display">
|
||||
<text class="picker-text">{{ priorityOptions[priorityIndex] }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">事件类型</text>
|
||||
<picker class="form-picker" mode="selector" :value="typeIndex" :range="typeOptions" @change="onTypeChange">
|
||||
<view class="picker-display">
|
||||
<text class="picker-text">{{ typeOptions[typeIndex] }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">发生时间</text>
|
||||
<picker class="form-picker" mode="datetime" :value="newIncident.incident_time" @change="onTimeChange">
|
||||
<view class="picker-display">
|
||||
<text class="picker-text">{{ formatDateTime(newIncident.incident_time) || '选择时间' }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">事件描述 *</text>
|
||||
<textarea class="form-textarea" v-model="newIncident.description" placeholder="请详细描述事件经过..." />
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">处理措施</text>
|
||||
<textarea class="form-textarea" v-model="newIncident.action_taken" placeholder="请描述已采取的处理措施..." />
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="modal-footer">
|
||||
<button class="btn btn-cancel" @tap="hideModal">取消</button>
|
||||
<button class="btn btn-submit" @tap="submitIncident" :disabled="isSubmitting">
|
||||
{{ isSubmitting ? '提交中...' : '提交报告' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { IncidentReport, ElderInfo, IncidentStats } from '../types.uts'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentFilter: 'all',
|
||||
showModal: false,
|
||||
isSubmitting: false,
|
||||
elderIndex: 0,
|
||||
priorityIndex: 0,
|
||||
typeIndex: 0,
|
||||
stats: {
|
||||
urgent_incidents: 0,
|
||||
pending_reports: 0,
|
||||
resolved_today: 0
|
||||
} as IncidentStats,
|
||||
incidents: [] as IncidentReport[],
|
||||
elderOptions: [] as ElderInfo[],
|
||||
newIncident: {
|
||||
title: '',
|
||||
elder_id: '',
|
||||
priority: 'normal',
|
||||
incident_type: 'medical',
|
||||
description: '',
|
||||
action_taken: '',
|
||||
incident_time: new Date().toISOString()
|
||||
} as IncidentReport,
|
||||
filterOptions: [
|
||||
{ label: '全部', value: 'all', count: 0 },
|
||||
{ label: '待处理', value: 'pending', count: 0 },
|
||||
{ label: '处理中', value: 'in_progress', count: 0 },
|
||||
{ label: '已解决', value: 'resolved', count: 0 },
|
||||
{ label: '紧急', value: 'urgent', count: 0 }
|
||||
],
|
||||
priorityOptions: ['普通', '高', '紧急', '危急'],
|
||||
typeOptions: ['医疗事件', '安全事件', '行为事件', '设备故障', '其他']
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredIncidents(): IncidentReport[] {
|
||||
if (this.currentFilter === 'all') {
|
||||
return this.incidents
|
||||
}
|
||||
|
||||
if (this.currentFilter === 'urgent') {
|
||||
return this.incidents.filter(incident => incident.priority === 'urgent' || incident.priority === 'critical')
|
||||
}
|
||||
|
||||
return this.incidents.filter(incident => incident.status === this.currentFilter)
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadIncidentData()
|
||||
},
|
||||
onShow() {
|
||||
this.loadIncidentData()
|
||||
},
|
||||
methods: {
|
||||
async loadIncidentData() {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadIncidentStats(),
|
||||
this.loadIncidents(),
|
||||
this.loadElderOptions()
|
||||
])
|
||||
this.updateFilterCounts()
|
||||
} catch (error) {
|
||||
console.error('加载事件数据失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
async loadIncidentStats() {
|
||||
const result = await supa.executeAs('incident_stats', {})
|
||||
if (result.success && result.data.length > 0) {
|
||||
this.stats = result.data[0] as IncidentStats
|
||||
}
|
||||
},
|
||||
|
||||
async loadIncidents() {
|
||||
const result = await supa.executeAs('incident_reports', {})
|
||||
if (result.success) {
|
||||
this.incidents = result.data as IncidentReport[]
|
||||
}
|
||||
},
|
||||
|
||||
async loadElderOptions() {
|
||||
const result = await supa.executeAs('active_elders', {})
|
||||
if (result.success) {
|
||||
this.elderOptions = result.data as ElderInfo[]
|
||||
}
|
||||
},
|
||||
|
||||
updateFilterCounts() {
|
||||
this.filterOptions.forEach(filter => {
|
||||
if (filter.value === 'all') {
|
||||
filter.count = this.incidents.length
|
||||
} else if (filter.value === 'urgent') {
|
||||
filter.count = this.incidents.filter(incident =>
|
||||
incident.priority === 'urgent' || incident.priority === 'critical'
|
||||
).length
|
||||
} else {
|
||||
filter.count = this.incidents.filter(incident => incident.status === filter.value).length
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
setFilter(filter: string) {
|
||||
this.currentFilter = filter
|
||||
},
|
||||
|
||||
showNewIncidentModal() {
|
||||
this.showModal = true
|
||||
this.resetForm()
|
||||
},
|
||||
|
||||
hideModal() {
|
||||
this.showModal = false
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.newIncident = {
|
||||
title: '',
|
||||
elder_id: '',
|
||||
priority: 'normal',
|
||||
incident_type: 'medical',
|
||||
description: '',
|
||||
action_taken: '',
|
||||
incident_time: new Date().toISOString()
|
||||
} as IncidentReport
|
||||
this.elderIndex = 0
|
||||
this.priorityIndex = 0
|
||||
this.typeIndex = 0
|
||||
},
|
||||
|
||||
onElderChange(e) {
|
||||
this.elderIndex = e.detail.value
|
||||
if (this.elderOptions[this.elderIndex]) {
|
||||
this.newIncident.elder_id = this.elderOptions[this.elderIndex].id
|
||||
}
|
||||
},
|
||||
|
||||
onPriorityChange(e) {
|
||||
this.priorityIndex = e.detail.value
|
||||
const priorityMap = ['normal', 'high', 'urgent', 'critical']
|
||||
this.newIncident.priority = priorityMap[e.detail.value]
|
||||
},
|
||||
|
||||
onTypeChange(e) {
|
||||
this.typeIndex = e.detail.value
|
||||
const typeMap = ['medical', 'safety', 'behavioral', 'equipment', 'other']
|
||||
this.newIncident.incident_type = typeMap[e.detail.value]
|
||||
},
|
||||
|
||||
onTimeChange(e) {
|
||||
this.newIncident.incident_time = e.detail.value
|
||||
},
|
||||
|
||||
async submitIncident() {
|
||||
if (!this.validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isSubmitting = true
|
||||
|
||||
try {
|
||||
const result = await supa.executeAs('create_incident_report', {
|
||||
...this.newIncident,
|
||||
status: 'pending'
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
uni.showToast({
|
||||
title: '报告提交成功',
|
||||
icon: 'success'
|
||||
})
|
||||
this.hideModal()
|
||||
this.loadIncidentData()
|
||||
} else {
|
||||
throw new Error(result.error || '提交失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交事件报告失败:', error)
|
||||
uni.showToast({
|
||||
title: '提交失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
this.isSubmitting = false
|
||||
}
|
||||
},
|
||||
|
||||
validateForm(): boolean {
|
||||
if (!this.newIncident.title.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入事件标题',
|
||||
icon: 'error'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!this.newIncident.elder_id) {
|
||||
uni.showToast({
|
||||
title: '请选择相关老人',
|
||||
icon: 'error'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!this.newIncident.description.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入事件描述',
|
||||
icon: 'error'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
|
||||
async followUpIncident(incident: IncidentReport) {
|
||||
try {
|
||||
const result = await supa.executeAs('update_incident_status', {
|
||||
incident_id: incident.id,
|
||||
status: 'in_progress'
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
uni.showToast({
|
||||
title: '已开始跟进',
|
||||
icon: 'success'
|
||||
})
|
||||
this.loadIncidents()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('跟进事件失败:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
viewIncidentDetail(incident: IncidentReport) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/incident/detail?id=${incident.id}`
|
||||
})
|
||||
},
|
||||
|
||||
getPriorityClass(priority: string): string {
|
||||
const priorityMap = {
|
||||
'normal': 'priority-normal',
|
||||
'high': 'priority-high',
|
||||
'urgent': 'priority-urgent',
|
||||
'critical': 'priority-critical'
|
||||
}
|
||||
return priorityMap[priority] || 'priority-normal'
|
||||
},
|
||||
|
||||
getPriorityText(priority: string): string {
|
||||
const priorityMap = {
|
||||
'normal': '普通',
|
||||
'high': '高',
|
||||
'urgent': '紧急',
|
||||
'critical': '危急'
|
||||
}
|
||||
return priorityMap[priority] || '普通'
|
||||
},
|
||||
|
||||
getStatusClass(status: string): string {
|
||||
const statusMap = {
|
||||
'pending': 'status-pending',
|
||||
'in_progress': 'status-progress',
|
||||
'resolved': 'status-resolved',
|
||||
'closed': 'status-closed'
|
||||
}
|
||||
return statusMap[status] || 'status-pending'
|
||||
},
|
||||
|
||||
getStatusText(status: string): string {
|
||||
const statusMap = {
|
||||
'pending': '待处理',
|
||||
'in_progress': '处理中',
|
||||
'resolved': '已解决',
|
||||
'closed': '已关闭'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
formatTime(timestamp: string): string {
|
||||
if (!timestamp) return ''
|
||||
|
||||
const date = new Date(timestamp)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const minutes = Math.floor(diff / (1000 * 60))
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}天前`
|
||||
} else if (hours > 0) {
|
||||
return `${hours}小时前`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟前`
|
||||
} else {
|
||||
return '刚刚'
|
||||
}
|
||||
},
|
||||
|
||||
formatDateTime(timestamp: string): string {
|
||||
if (!timestamp) return ''
|
||||
|
||||
const date = new Date(timestamp)
|
||||
return `${date.getFullYear()}/${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.incident-report {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40rpx 30rpx 30rpx;
|
||||
color: white;
|
||||
|
||||
.header-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 26rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 30rpx 20rpx;
|
||||
border-radius: 15rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.1);
|
||||
|
||||
.stat-number {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&.urgent {
|
||||
.stat-number {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
}
|
||||
|
||||
&.pending {
|
||||
.stat-number {
|
||||
color: #4ecdc4;
|
||||
}
|
||||
}
|
||||
|
||||
&.resolved {
|
||||
.stat-number {
|
||||
color: #45b7d1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-incident-section {
|
||||
padding: 0 30rpx 30rpx;
|
||||
}
|
||||
|
||||
.new-incident-btn {
|
||||
background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%);
|
||||
padding: 30rpx;
|
||||
border-radius: 15rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15rpx;
|
||||
color: white;
|
||||
|
||||
.new-incident-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.new-incident-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 0 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.filter-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
padding: 20rpx 30rpx;
|
||||
background: white;
|
||||
border-radius: 25rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
|
||||
.filter-count {
|
||||
background: rgba(255,255,255,0.3);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-text {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.filter-count {
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
padding: 5rpx 10rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 20rpx;
|
||||
min-width: 30rpx;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.incidents-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;
|
||||
}
|
||||
}
|
||||
|
||||
.incidents-list {
|
||||
.incident-card {
|
||||
background: white;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 15rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.incident-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.incident-info {
|
||||
flex: 1;
|
||||
|
||||
.incident-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.incident-elder {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.incident-priority {
|
||||
padding: 10rpx 15rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
|
||||
&.priority-normal {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&.priority-high {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
&.priority-urgent {
|
||||
background: #ffebee;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
&.priority-critical {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
}
|
||||
|
||||
.incident-content {
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.incident-description {
|
||||
font-size: 26rpx;
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.incident-meta {
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.incident-time, .incident-reporter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.time-label, .reporter-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
width: 120rpx;
|
||||
}
|
||||
|
||||
.time-value, .reporter-value {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.incident-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 20rpx;
|
||||
border-top: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.incident-status {
|
||||
padding: 8rpx 15rpx;
|
||||
border-radius: 15rpx;
|
||||
font-size: 22rpx;
|
||||
|
||||
&.status-pending {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
&.status-progress {
|
||||
background: #e8f5e8;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
&.status-resolved {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&.status-closed {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.incident-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
|
||||
.action-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.action-follow {
|
||||
font-size: 24rpx;
|
||||
color: #667eea;
|
||||
padding: 5rpx 15rpx;
|
||||
border: 2rpx solid #667eea;
|
||||
border-radius: 15rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// Modal styles
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
width: 60rpx;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
max-height: 60vh;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.form-input, .form-textarea {
|
||||
width: 100%;
|
||||
border: 2rpx solid #e1e1e1;
|
||||
border-radius: 10rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
background: white;
|
||||
|
||||
&:focus {
|
||||
border-color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
.form-input {
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 150rpx;
|
||||
}
|
||||
|
||||
.form-picker {
|
||||
width: 100%;
|
||||
border: 2rpx solid #e1e1e1;
|
||||
border-radius: 10rpx;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.picker-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 80rpx;
|
||||
padding: 0 20rpx;
|
||||
|
||||
.picker-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 30rpx;
|
||||
border-top: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.btn-cancel {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border: 2rpx solid #e1e1e1;
|
||||
}
|
||||
|
||||
&.btn-submit {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
728
pages/ec/medication/management.uvue
Normal file
728
pages/ec/medication/management.uvue
Normal file
@@ -0,0 +1,728 @@
|
||||
<!-- 养老管理系统 - 用药管理 -->
|
||||
<template>
|
||||
<view class="medication-management">
|
||||
<view class="header">
|
||||
<text class="title">用药管理</text>
|
||||
<button class="add-btn" @click="showAddMedication">添加用药</button>
|
||||
</view>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<view class="filter-section">
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">老人:</text>
|
||||
<picker-view class="picker" :value="selectedElderIndex" @change="onElderChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(elder, index) in elderOptions" :key="elder.id" class="picker-item">
|
||||
{{ elder.name }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">状态:</text>
|
||||
<picker-view class="picker" :value="selectedStatusIndex" @change="onStatusChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(status, index) in statusOptions" :key="index" class="picker-item">
|
||||
{{ status.label }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
<button class="search-btn" @click="searchMedications">搜索</button>
|
||||
</view>
|
||||
|
||||
<!-- 用药列表 -->
|
||||
<view class="medications-list">
|
||||
<view v-for="medication in medications" :key="medication.id" class="medication-item" @click="viewMedicationDetail(medication)">
|
||||
<view class="medication-header">
|
||||
<text class="medication-name">{{ medication.medication_name }}</text>
|
||||
<view class="status-badge" :class="getStatusClass(medication.status)">
|
||||
<text class="status-text">{{ getStatusText(medication.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="medication-info">
|
||||
<text class="elder-name">{{ getElderName(medication.elder_id) }}</text>
|
||||
<text class="dosage">剂量: {{ medication.dosage ?? '未设置' }}</text>
|
||||
<text class="frequency">频率: {{ medication.frequency ?? '未设置' }}</text>
|
||||
</view>
|
||||
<view class="medication-dates">
|
||||
<text class="date-text">开始: {{ formatDate(medication.start_date) }}</text>
|
||||
<text class="date-text">结束: {{ formatDate(medication.end_date) }}</text>
|
||||
</view>
|
||||
<view class="medication-actions">
|
||||
<button class="action-btn edit-btn" @click.stop="editMedication(medication)">编辑</button>
|
||||
<button class="action-btn log-btn" @click.stop="viewMedicationLogs(medication)">用药记录</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加/编辑用药弹窗 -->
|
||||
<view v-if="showMedicationModal" class="modal-overlay" @click="closeMedicationModal">
|
||||
<view class="modal-content" @click.stop="">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ isEditMode ? '编辑用药' : '添加用药' }}</text>
|
||||
<button class="close-btn" @click="closeMedicationModal">×</button>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-group">
|
||||
<text class="form-label">老人:</text>
|
||||
<picker-view class="form-picker" :value="formData.elderIndex" @change="onFormElderChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(elder, index) in elderOptions" :key="elder.id" class="picker-item">
|
||||
{{ elder.name }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">药品名称:</text>
|
||||
<input class="form-input" v-model="formData.medication_name" placeholder="请输入药品名称" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">剂量:</text>
|
||||
<input class="form-input" v-model="formData.dosage" placeholder="如:500mg" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">频率:</text>
|
||||
<input class="form-input" v-model="formData.frequency" placeholder="如:每日3次" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">给药途径:</text>
|
||||
<picker-view class="form-picker" :value="formData.routeIndex" @change="onFormRouteChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(route, index) in routeOptions" :key="index" class="picker-item">
|
||||
{{ route.label }}
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">开始日期:</text>
|
||||
<lime-date-time-picker v-model="formData.medication_date" type="date" :placeholder="'选择用药日期'" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">结束日期:</text>
|
||||
<lime-date-time-picker v-model="formData.next_medication_date" type="date" :placeholder="'选择下次用药日期'" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">用药说明:</text>
|
||||
<textarea class="form-textarea" v-model="formData.instructions" placeholder="请输入用药说明"></textarea>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">副作用注意:</text>
|
||||
<textarea class="form-textarea" v-model="formData.side_effects" placeholder="请输入副作用注意事项"></textarea>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="cancel-btn" @click="closeMedicationModal">取消</button>
|
||||
<button class="save-btn" @click="saveMedication">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import type { Medication, Elder } from '../types.uts'
|
||||
import { formatDate, getStatusClass } from '../types.uts'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
|
||||
// 响应式数据
|
||||
const medications = ref<Medication[]>([])
|
||||
const elderOptions = ref<Elder[]>([])
|
||||
const eldersMap = ref<Map<string, string>>(new Map())
|
||||
|
||||
// 筛选相关
|
||||
const selectedElderIndex = ref([0])
|
||||
const selectedStatusIndex = ref([0])
|
||||
const statusOptions = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'active', label: '使用中' },
|
||||
{ value: 'completed', label: '已完成' },
|
||||
{ value: 'discontinued', label: '已停用' }
|
||||
]
|
||||
|
||||
// 弹窗相关
|
||||
const showMedicationModal = ref(false)
|
||||
const isEditMode = ref(false)
|
||||
const currentMedicationId = ref<string | null>(null)
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
elderIndex: [0],
|
||||
medication_name: '',
|
||||
dosage: '',
|
||||
frequency: '',
|
||||
routeIndex: [0],
|
||||
medication_date: '',
|
||||
next_medication_date: '',
|
||||
instructions: '',
|
||||
side_effects: ''
|
||||
})
|
||||
|
||||
const routeOptions = [
|
||||
{ value: 'oral', label: '口服' },
|
||||
{ value: 'injection', label: '注射' },
|
||||
{ value: 'topical', label: '外用' }
|
||||
]
|
||||
|
||||
// 页面加载
|
||||
onLoad(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
async function loadData(): Promise<void> {
|
||||
try {
|
||||
await Promise.all([
|
||||
loadElders(),
|
||||
loadMedications()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
uni.showToast({
|
||||
title: '加载数据失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加载老人列表
|
||||
async function loadElders(): Promise<void> {
|
||||
const result = await supa.executeAs<Elder>('eldercare_admin', `
|
||||
SELECT id, name FROM ec_elders
|
||||
WHERE status = 'active'
|
||||
ORDER BY name
|
||||
`)
|
||||
elderOptions.value = [{ id: '', name: '全部老人' } as Elder, ...result]
|
||||
|
||||
// 建立映射
|
||||
const map = new Map<string, string>()
|
||||
for (let i: Int = 0; i < result.length; i++) {
|
||||
const elder = result[i]
|
||||
map.set(elder.id, elder.name)
|
||||
}
|
||||
eldersMap.value = map
|
||||
}
|
||||
|
||||
// 加载用药记录
|
||||
async function loadMedications(): Promise<void> {
|
||||
let whereClause = "WHERE 1=1"
|
||||
|
||||
// 老人筛选
|
||||
if (selectedElderIndex.value[0] > 0) {
|
||||
const selectedElder = elderOptions.value[selectedElderIndex.value[0]]
|
||||
whereClause += ` AND elder_id = '${selectedElder.id}'`
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (selectedStatusIndex.value[0] > 0) {
|
||||
const selectedStatus = statusOptions[selectedStatusIndex.value[0]]
|
||||
whereClause += ` AND status = '${selectedStatus.value}'`
|
||||
}
|
||||
|
||||
const result = await supa.executeAs<Medication>('eldercare_admin', `
|
||||
SELECT * FROM ec_medications
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
`)
|
||||
medications.value = result
|
||||
}
|
||||
|
||||
// 获取老人姓名
|
||||
function getElderName(elderId: string): string {
|
||||
return eldersMap.value.get(elderId) ?? '未知老人'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
'active': '使用中',
|
||||
'completed': '已完成',
|
||||
'discontinued': '已停用'
|
||||
}
|
||||
return statusMap[status] ?? status
|
||||
}
|
||||
|
||||
// 筛选事件
|
||||
function onElderChange(e: any): void {
|
||||
selectedElderIndex.value = e.detail.value
|
||||
}
|
||||
|
||||
function onStatusChange(e: any): void {
|
||||
selectedStatusIndex.value = e.detail.value
|
||||
}
|
||||
|
||||
// 搜索用药记录
|
||||
function searchMedications(): void {
|
||||
loadMedications()
|
||||
}
|
||||
|
||||
// 查看用药详情
|
||||
function viewMedicationDetail(medication: Medication): void {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/medication/detail?id=${medication.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑用药
|
||||
function editMedication(medication: Medication): void {
|
||||
isEditMode.value = true
|
||||
currentMedicationId.value = medication.id
|
||||
|
||||
// 填充表单数据
|
||||
const elderIndex = elderOptions.value.findIndex(elder => elder.id === medication.elder_id)
|
||||
const routeIndex = routeOptions.findIndex(route => route.value === medication.route)
|
||||
|
||||
formData.value = {
|
||||
elderIndex: [elderIndex > 0 ? elderIndex : 0],
|
||||
medication_name: medication.medication_name,
|
||||
dosage: medication.dosage ?? '',
|
||||
frequency: medication.frequency ?? '',
|
||||
routeIndex: [routeIndex > 0 ? routeIndex : 0],
|
||||
medication_date: medication.start_date ?? '',
|
||||
next_medication_date: medication.end_date ?? '',
|
||||
instructions: medication.instructions ?? '',
|
||||
side_effects: medication.side_effects ?? ''
|
||||
}
|
||||
|
||||
showMedicationModal.value = true
|
||||
}
|
||||
|
||||
// 查看用药日志
|
||||
function viewMedicationLogs(medication: Medication): void {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/medication/logs?id=${medication.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 显示添加用药弹窗
|
||||
function showAddMedication(): void {
|
||||
isEditMode.value = false
|
||||
currentMedicationId.value = null
|
||||
|
||||
// 重置表单
|
||||
formData.value = {
|
||||
elderIndex: [0],
|
||||
medication_name: '',
|
||||
dosage: '',
|
||||
frequency: '',
|
||||
routeIndex: [0],
|
||||
medication_date: '',
|
||||
next_medication_date: '',
|
||||
instructions: '',
|
||||
side_effects: ''
|
||||
}
|
||||
|
||||
showMedicationModal.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeMedicationModal(): void {
|
||||
showMedicationModal.value = false
|
||||
}
|
||||
|
||||
// 表单事件
|
||||
function onFormElderChange(e: any): void {
|
||||
formData.value.elderIndex = e.detail.value
|
||||
}
|
||||
|
||||
function onFormRouteChange(e: any): void {
|
||||
formData.value.routeIndex = e.detail.value
|
||||
}
|
||||
|
||||
// 保存用药记录
|
||||
async function saveMedication(): Promise<void> {
|
||||
// 验证表单
|
||||
if (formData.value.medication_name.trim() === '') {
|
||||
uni.showToast({
|
||||
title: '请输入药品名称',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.value.elderIndex[0] === 0) {
|
||||
uni.showToast({
|
||||
title: '请选择老人',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const selectedElder = elderOptions.value[formData.value.elderIndex[0]]
|
||||
const selectedRoute = routeOptions[formData.value.routeIndex[0]]
|
||||
|
||||
if (isEditMode.value && currentMedicationId.value !== null) {
|
||||
// 更新用药记录
|
||||
await supa.executeAs('eldercare_admin', `
|
||||
UPDATE ec_medications SET
|
||||
elder_id = '${selectedElder.id}',
|
||||
medication_name = '${formData.value.medication_name}',
|
||||
dosage = '${formData.value.dosage}',
|
||||
frequency = '${formData.value.frequency}',
|
||||
route = '${selectedRoute.value}',
|
||||
start_date = ${formData.value.medication_date ? `'${formData.value.medication_date}'` : 'NULL'},
|
||||
end_date = ${formData.value.next_medication_date ? `'${formData.value.next_medication_date}'` : 'NULL'},
|
||||
instructions = '${formData.value.instructions}',
|
||||
side_effects = '${formData.value.side_effects}',
|
||||
updated_at = NOW()
|
||||
WHERE id = '${currentMedicationId.value}'
|
||||
`)
|
||||
} else {
|
||||
// 新增用药记录
|
||||
await supa.executeAs('eldercare_admin', `
|
||||
INSERT INTO ec_medications (
|
||||
elder_id, medication_name, dosage, frequency, route,
|
||||
start_date, end_date, instructions, side_effects, status
|
||||
) VALUES (
|
||||
'${selectedElder.id}',
|
||||
'${formData.value.medication_name}',
|
||||
'${formData.value.dosage}',
|
||||
'${formData.value.frequency}',
|
||||
'${selectedRoute.value}',
|
||||
${formData.value.medication_date ? `'${formData.value.medication_date}'` : 'NULL'},
|
||||
${formData.value.next_medication_date ? `'${formData.value.next_medication_date}'` : 'NULL'},
|
||||
'${formData.value.instructions}',
|
||||
'${formData.value.side_effects}',
|
||||
'active'
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
closeMedicationModal()
|
||||
loadMedications()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.medication-management {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.picker {
|
||||
width: 120px;
|
||||
height: 40px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.picker-item {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.medications-list {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.medication-item {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.medication-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.medication-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
|
||||
.status-discontinued {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
.medication-info {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.elder-name, .dosage, .frequency {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.medication-dates {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.medication-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.log-btn {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80%;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-picker {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.form-date-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.cancel-btn, .save-btn {
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 小屏幕适配 */
|
||||
@media (max-width: 768px) {
|
||||
.medication-management {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
margin-right: 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.picker {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.medication-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
94
pages/ec/nurse/bulk-entry.uvue
Normal file
94
pages/ec/nurse/bulk-entry.uvue
Normal file
@@ -0,0 +1,94 @@
|
||||
<!-- 批量体征录入 - uts-android 兼容版 -->
|
||||
<template>
|
||||
<view class="bulk-entry">
|
||||
<view class="header">
|
||||
<text class="header-title">批量体征录入</text>
|
||||
</view>
|
||||
<form @submit="onSubmit">
|
||||
<view v-for="(item, idx) in formList" :key="idx" class="form-group">
|
||||
<text class="form-label">患者</text>
|
||||
<button class="picker-btn" @click="showElderActionSheet(idx)">
|
||||
<text class="picker-text">{{ item.elder_name || '请选择患者' }}</text>
|
||||
</button>
|
||||
<text class="form-label">血压</text>
|
||||
<input class="form-input" v-model="item.blood_pressure" placeholder="如 120/80" />
|
||||
<text class="form-label">心率</text>
|
||||
<input class="form-input" v-model.number="item.heart_rate" type="number" placeholder="如 75" />
|
||||
<text class="form-label">体温</text>
|
||||
<input class="form-input" v-model.number="item.temperature" type="number" placeholder="如 36.5" />
|
||||
<text class="form-label">血糖</text>
|
||||
<input class="form-input" v-model.number="item.blood_sugar" type="number" placeholder="如 5.6" />
|
||||
<text class="form-label">血氧</text>
|
||||
<input class="form-input" v-model.number="item.oxygen_saturation" type="number" placeholder="如 98" />
|
||||
<text class="form-label">记录者</text>
|
||||
<input class="form-input" v-model="item.recorded_by" placeholder="请输入记录者姓名" />
|
||||
<text class="form-label">备注</text>
|
||||
<textarea class="form-textarea" v-model="item.notes" placeholder="可填写备注" />
|
||||
<button class="remove-btn" @click.prevent="removeItem(idx)"><text class="btn-text">移除</text></button>
|
||||
</view>
|
||||
<button class="add-btn" @click.prevent="addItem"><text class="btn-text">添加一行</text></button>
|
||||
<button class="submit-btn" form-type="submit"><text class="btn-text">批量提交</text></button>
|
||||
</form>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
const elders = ref<any[]>([])
|
||||
const formList = ref<any[]>([getEmptyItem()])
|
||||
function getEmptyItem() {
|
||||
return { elder_id: '', elder_name: '', blood_pressure: '', heart_rate: 0, temperature: 0, blood_sugar: 0, oxygen_saturation: 0, recorded_by: '', notes: '' }
|
||||
}
|
||||
onMounted(async () => {
|
||||
const result = await supa.from('ec_elders').select('id,name').eq('status','active').order('room_number',{ascending:true}).execute()
|
||||
if (result.data) elders.value = result.data
|
||||
})
|
||||
const showElderActionSheet = (idx:number) => {
|
||||
const options = elders.value.map(e => e.name)
|
||||
uni.showActionSheet({
|
||||
itemList: options,
|
||||
success: (res:any) => {
|
||||
formList.value[idx].elder_id = elders.value[res.tapIndex].id
|
||||
formList.value[idx].elder_name = elders.value[res.tapIndex].name
|
||||
}
|
||||
})
|
||||
}
|
||||
const addItem = () => { formList.value.push(getEmptyItem()) }
|
||||
const removeItem = (idx:number) => { if(formList.value.length>1)formList.value.splice(idx,1) }
|
||||
const onSubmit = async () => {
|
||||
const validList = formList.value.filter(i=>i.elder_id)
|
||||
if (validList.length === 0) {
|
||||
uni.showToast({ title: '请至少选择一位患者', icon: 'none' }); return
|
||||
}
|
||||
const insertList = validList.map(i=>({
|
||||
elder_id: i.elder_id,
|
||||
blood_pressure: i.blood_pressure||'',
|
||||
heart_rate: i.heart_rate||0,
|
||||
temperature: i.temperature||0,
|
||||
blood_sugar: i.blood_sugar||0,
|
||||
oxygen_saturation: i.oxygen_saturation||0,
|
||||
recorded_by: i.recorded_by||'',
|
||||
notes: i.notes||'',
|
||||
recorded_at: new Date().toISOString()
|
||||
}))
|
||||
const result = await supa.from('ec_vital_signs').insert(insertList).execute()
|
||||
if (!result.error) {
|
||||
uni.showToast({ title: '批量提交成功', icon: 'success' }); uni.navigateBack()
|
||||
} else {
|
||||
uni.showToast({ title: '提交失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.bulk-entry { padding: 20px; background: #f5f5f5; min-height: 100vh; }
|
||||
.header { padding: 20px 0 10px 0; text-align: center; }
|
||||
.header-title { font-size: 22px; font-weight: bold; }
|
||||
.form-group { margin-bottom: 18px; background: #fff; border-radius: 8px; padding: 12px; }
|
||||
.form-label { font-size: 15px; color: #333; margin-bottom: 6px; display: block; }
|
||||
.picker-btn { background: none; border: none; padding: 0; text-align: left; }
|
||||
.picker-text { font-size: 15px; color: #333; padding: 8px 10px; border: 1px solid #ddd; border-radius: 6px; background-color: #f9f9f9; display: block; }
|
||||
.form-input { width: 100%; padding: 8px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 15px; background: #fff; }
|
||||
.form-textarea { width: 100%; min-height: 60px; padding: 8px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 15px; background: #fff; }
|
||||
.add-btn, .remove-btn, .submit-btn { width: 100%; padding: 12px 0; border-radius: 20px; background: #667eea; color: #fff; font-size: 16px; font-weight: bold; border: none; margin-top: 10px; }
|
||||
.btn-text { color: #fff; font-size: 16px; }
|
||||
</style>
|
||||
1123
pages/ec/nurse/dashboard.uvue
Normal file
1123
pages/ec/nurse/dashboard.uvue
Normal file
File diff suppressed because it is too large
Load Diff
101
pages/ec/nurse/follow-up.uvue
Normal file
101
pages/ec/nurse/follow-up.uvue
Normal file
@@ -0,0 +1,101 @@
|
||||
<!-- 体征跟进 - uts-android 兼容版 -->
|
||||
<template>
|
||||
<view class="follow-up">
|
||||
<view class="header">
|
||||
<text class="header-title">体征跟进</text>
|
||||
</view>
|
||||
<form @submit="onSubmit">
|
||||
<view class="form-group">
|
||||
<text class="form-label">患者</text>
|
||||
<input class="form-input" v-model="form.elder_name" disabled />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">跟进内容</text>
|
||||
<textarea class="form-textarea" v-model="form.content" placeholder="请输入跟进内容" />
|
||||
</view>
|
||||
<button class="submit-btn" form-type="submit"><text class="btn-text">提交</text></button>
|
||||
</form>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
const form = ref({ elder_id: '', elder_name: '', content: '' })
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!form.value.elder_id || !form.value.content) {
|
||||
uni.showToast({ title: '请填写完整', icon: 'none' }); return
|
||||
}
|
||||
const insertData = { ...form.value, created_at: new Date().toISOString() }
|
||||
const result = await supa.from('ec_vital_followup').insert([insertData]).execute()
|
||||
if (!result.error) {
|
||||
uni.showToast({ title: '提交成功', icon: 'success' }); uni.navigateBack()
|
||||
} else {
|
||||
uni.showToast({ title: '提交失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.follow-up {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px 0 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 12px 0;
|
||||
border-radius: 20px;
|
||||
background: #667eea;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
73
pages/ec/nurse/vital-detail.uvue
Normal file
73
pages/ec/nurse/vital-detail.uvue
Normal file
@@ -0,0 +1,73 @@
|
||||
<!-- 体征详情 - uts-android 兼容版 -->
|
||||
<template>
|
||||
<view class="vital-detail">
|
||||
<view class="header">
|
||||
<text class="header-title">体征详情</text>
|
||||
</view>
|
||||
<view class="detail-group">
|
||||
<text class="detail-label">患者</text>
|
||||
<text class="detail-value">{{ vital.elder_name }}</text>
|
||||
</view>
|
||||
<view class="detail-group"><text class="detail-label">血压</text><text
|
||||
class="detail-value">{{ vital.blood_pressure }}</text></view>
|
||||
<view class="detail-group"><text class="detail-label">心率</text><text
|
||||
class="detail-value">{{ vital.heart_rate }}</text></view>
|
||||
<view class="detail-group"><text class="detail-label">体温</text><text
|
||||
class="detail-value">{{ vital.temperature }}</text></view>
|
||||
<view class="detail-group"><text class="detail-label">血糖</text><text
|
||||
class="detail-value">{{ vital.blood_sugar }}</text></view>
|
||||
<view class="detail-group"><text class="detail-label">血氧</text><text
|
||||
class="detail-value">{{ vital.oxygen_saturation }}</text></view>
|
||||
<view class="detail-group"><text class="detail-label">记录者</text><text
|
||||
class="detail-value">{{ vital.recorded_by }}</text></view>
|
||||
<view class="detail-group"><text class="detail-label">备注</text><text
|
||||
class="detail-value">{{ vital.notes }}</text></view>
|
||||
<view class="detail-group"><text class="detail-label">记录时间</text><text
|
||||
class="detail-value">{{ vital.recorded_at }}</text></view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
const vital = ref<any>({})
|
||||
onMounted(async () => {
|
||||
const id = uni.getCurrentPages().pop()?.options?.id
|
||||
if (!id) return
|
||||
const result = await supa.from('ec_vital_signs').select('*').eq('id', id).single().execute()
|
||||
if (result.data) vital.value = result.data
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.vital-detail {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px 0 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.detail-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 15px;
|
||||
color: #666;
|
||||
margin-bottom: 2px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 16px;
|
||||
color: #222;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
190
pages/ec/nurse/vital-signs-entry.uvue
Normal file
190
pages/ec/nurse/vital-signs-entry.uvue
Normal file
@@ -0,0 +1,190 @@
|
||||
<!-- 体征录入表单 - uts-android 兼容版 -->
|
||||
<template>
|
||||
<view class="vital-signs-entry">
|
||||
<view class="header">
|
||||
<text class="header-title">体征录入</text>
|
||||
</view>
|
||||
<form @submit="onSubmit">
|
||||
<view class="form-group">
|
||||
<text class="form-label">患者</text>
|
||||
<button class="picker-btn" @click="showElderActionSheet">
|
||||
<text class="picker-text">{{ selectedElder?.name ?? '请选择患者' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">血压 (mmHg)</text>
|
||||
<input class="form-input" v-model="form.blood_pressure" placeholder="如 120/80" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">心率 (bpm)</text>
|
||||
<input class="form-input" v-model.number="form.heart_rate" type="number" placeholder="如 75" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">体温 (°C)</text>
|
||||
<input class="form-input" v-model.number="form.temperature" type="number" placeholder="如 36.5" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">血糖 (mmol/L)</text>
|
||||
<input class="form-input" v-model.number="form.blood_sugar" type="number" placeholder="如 5.6" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">血氧 (%)</text>
|
||||
<input class="form-input" v-model.number="form.oxygen_saturation" type="number" placeholder="如 98" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">记录者</text>
|
||||
<input class="form-input" v-model="form.recorded_by" placeholder="请输入记录者姓名" />
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">备注</text>
|
||||
<textarea class="form-textarea" v-model="form.notes" placeholder="可填写备注" />
|
||||
</view>
|
||||
<button class="submit-btn" form-type="submit">
|
||||
<text class="btn-text">提交</text>
|
||||
</button>
|
||||
</form>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
// 患者列表
|
||||
const elders = ref<any[]>([])
|
||||
const selectedElderIndex = ref<number>(-1)
|
||||
const selectedElder = computed(() => {
|
||||
if (selectedElderIndex.value < 0 || selectedElderIndex.value >= elders.value.length) return null
|
||||
return elders.value[selectedElderIndex.value]
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
elder_id: '',
|
||||
blood_pressure: '',
|
||||
heart_rate: 0,
|
||||
temperature: 0,
|
||||
blood_sugar: 0,
|
||||
oxygen_saturation: 0,
|
||||
recorded_by: '',
|
||||
notes: ''
|
||||
})
|
||||
|
||||
// 加载患者
|
||||
onMounted(async () => {
|
||||
const result = await supa.from('ec_elders').select('id,name').eq('status','active').order('room_number',{ascending:true}).execute()
|
||||
if (result.data) elders.value = result.data
|
||||
})
|
||||
|
||||
// 选择患者
|
||||
const showElderActionSheet = () => {
|
||||
const options = elders.value.map(e => e.name)
|
||||
uni.showActionSheet({
|
||||
itemList: options,
|
||||
success: (res:any) => {
|
||||
selectedElderIndex.value = res.tapIndex
|
||||
form.value.elder_id = elders.value[res.tapIndex].id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
const onSubmit = async (e:any) => {
|
||||
if (!form.value.elder_id) {
|
||||
uni.showToast({ title: '请选择患者', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// 其他字段校验可补充
|
||||
const { elder_id, blood_pressure, heart_rate, temperature, blood_sugar, oxygen_saturation, recorded_by, notes } = form.value
|
||||
const insertData = {
|
||||
elder_id,
|
||||
blood_pressure: blood_pressure || '',
|
||||
heart_rate: heart_rate || 0,
|
||||
temperature: temperature || 0,
|
||||
blood_sugar: blood_sugar || 0,
|
||||
oxygen_saturation: oxygen_saturation || 0,
|
||||
recorded_by: recorded_by || '',
|
||||
notes: notes || '',
|
||||
recorded_at: new Date().toISOString()
|
||||
}
|
||||
const result = await supa.from('ec_vital_signs').insert([insertData]).execute()
|
||||
if (!result.error) {
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
uni.navigateBack()
|
||||
} else {
|
||||
uni.showToast({ title: '提交失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.vital-signs-entry {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
padding: 20px 0 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.form-label {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
.picker-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.picker-text {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background-color: #f9f9f9;
|
||||
display: block;
|
||||
}
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
background: #fff;
|
||||
}
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
background: #fff;
|
||||
}
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 12px 0;
|
||||
border-radius: 20px;
|
||||
background: #667eea;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.btn-text {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
71
pages/ec/reports/quick-add.uvue
Normal file
71
pages/ec/reports/quick-add.uvue
Normal file
@@ -0,0 +1,71 @@
|
||||
<!-- 养老管理系统 - 快速记录页面 -->
|
||||
<template>
|
||||
<view class="quick-add-page">
|
||||
<view class="header">
|
||||
<text class="title">快速记录</text>
|
||||
</view>
|
||||
<form @submit="handleSubmit">
|
||||
<view class="form-group">
|
||||
<text class="label">记录内容</text>
|
||||
<textarea v-model="content" class="input" placeholder="请输入记录内容" />
|
||||
</view>
|
||||
<button class="submit-btn" form-type="submit">提交</button>
|
||||
</form>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
const content = ref('')
|
||||
|
||||
const handleSubmit = (e: any) => {
|
||||
uni.showToast({
|
||||
title: '提交成功',
|
||||
icon: 'success'
|
||||
})
|
||||
content.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.quick-add-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
.input {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 12px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
1144
pages/ec/service/management.uvue
Normal file
1144
pages/ec/service/management.uvue
Normal file
File diff suppressed because it is too large
Load Diff
139
pages/ec/tasks/completed.uvue
Normal file
139
pages/ec/tasks/completed.uvue
Normal file
@@ -0,0 +1,139 @@
|
||||
<!-- 养老管理系统 - 已完成护理任务列表 -->
|
||||
<template>
|
||||
<view class="completed-tasks-page">
|
||||
<view class="header">
|
||||
<text class="title">已完成护理任务</text>
|
||||
</view>
|
||||
<view class="tasks-list">
|
||||
<view v-if="tasks.length === 0" class="empty-text">暂无已完成任务</view>
|
||||
<view v-for="task in tasks" :key="task.id" class="task-item">
|
||||
<view class="task-main">
|
||||
<view class="task-info">
|
||||
<text class="task-name">{{ task.task_name }}</text>
|
||||
<text class="task-elder">{{ task.elder_name }}</text>
|
||||
<text class="task-time">{{ formatTime(task.scheduled_time) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="task-status">
|
||||
<text class="status-text">已完成</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import type { CareTask } from '../types.uts'
|
||||
import { formatTime } from '../types.uts'
|
||||
import { state, getCurrentUserId } from '@/utils/store.uts'
|
||||
|
||||
const tasks = ref<Array<CareTask>>([])
|
||||
const profile = ref(state.userProfile)
|
||||
|
||||
const loadCompletedTasks = async (currentUserId) => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_care_tasks')
|
||||
.select(`
|
||||
id,
|
||||
task_name,
|
||||
elder_name,
|
||||
scheduled_time
|
||||
`,{count:'exact'})
|
||||
.eq('assigned_to', currentUserId)
|
||||
.eq('status', 'completed')
|
||||
.order('updated_at', { ascending: false })
|
||||
.executeAs<CareTask>()
|
||||
if (result.error === null && result.data !== null) {
|
||||
tasks.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载已完成任务失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options : OnLoadOptions) => {
|
||||
|
||||
const currentUserId = options['id'] ?? getCurrentUserId()
|
||||
|
||||
loadCompletedTasks(currentUserId)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.completed-tasks-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tasks-list {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
border-left: 3px solid #52c41a;
|
||||
background-color: #f6ffed;
|
||||
}
|
||||
|
||||
.task-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.task-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.task-elder {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.task-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.task-status .status-text {
|
||||
font-size: 12px;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 30px 0;
|
||||
}
|
||||
</style>
|
||||
868
pages/ec/tasks/list.uvue
Normal file
868
pages/ec/tasks/list.uvue
Normal file
@@ -0,0 +1,868 @@
|
||||
<template>
|
||||
<view class="task-management">
|
||||
<view class="header">
|
||||
<text class="title">护理任务</text>
|
||||
<button class="add-btn" @click="addNewTask">
|
||||
<text class="btn-text">➕ 新建</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 任务统计 -->
|
||||
<view class="task-stats">
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{ taskStats.pending }}</text>
|
||||
<text class="stat-label">待处理</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{ taskStats.in_progress }}</text>
|
||||
<text class="stat-label">进行中</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{ taskStats.completed }}</text>
|
||||
<text class="stat-label">已完成</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{ taskStats.overdue }}</text>
|
||||
<text class="stat-label">已逾期</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<view class="filter-section">
|
||||
<scroll-view class="filter-scroll" direction="horizontal" :show-scrollbar="false">
|
||||
<view class="filter-item" :class="{ active: selectedStatus === 'all' }" @click="filterByStatus('all')">
|
||||
全部
|
||||
</view>
|
||||
<view class="filter-item" :class="{ active: selectedStatus === 'pending' }" @click="filterByStatus('pending')">
|
||||
待处理
|
||||
</view>
|
||||
<view class="filter-item" :class="{ active: selectedStatus === 'in_progress' }" @click="filterByStatus('in_progress')">
|
||||
进行中
|
||||
</view>
|
||||
<view class="filter-item" :class="{ active: selectedStatus === 'completed' }" @click="filterByStatus('completed')">
|
||||
已完成
|
||||
</view>
|
||||
<view class="filter-item" :class="{ active: selectedPriority === 'urgent' }" @click="filterByPriority('urgent')">
|
||||
紧急任务
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- 任务列表 -->
|
||||
<view class="tasks-section">
|
||||
<scroll-view class="tasks-list" direction="vertical" :refresher-enabled="true"
|
||||
:refresher-triggered="isRefreshing" @refresherrefresh="refreshTasks">
|
||||
<view class="task-card" v-for="task in filteredTasks" :key="task.id"
|
||||
:class="getTaskCardClass(task)" @click="viewTaskDetail(task)">
|
||||
<view class="task-header">
|
||||
<view class="task-priority" :class="task.priority">
|
||||
<text class="priority-text">{{ getPriorityText(task.priority) }}</text>
|
||||
</view>
|
||||
<view class="task-status" :class="task.status">
|
||||
<text class="status-text">{{ getTaskStatusText(task.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="task-content">
|
||||
<text class="task-title">{{ task.title }}</text>
|
||||
<view class="task-info">
|
||||
<view class="info-item">
|
||||
<text class="info-icon">👤</text>
|
||||
<text class="info-text">{{ task.elder_name || '未分配' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-icon">🕐</text>
|
||||
<text class="info-text">{{ formatDateTime(task.scheduled_time) }}</text>
|
||||
</view>
|
||||
<view class="info-item" v-if="task.assigned_to_name">
|
||||
<text class="info-icon">👩⚕️</text>
|
||||
<text class="info-text">{{ task.assigned_to_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="task-actions">
|
||||
<button class="action-btn" v-if="task.status === 'pending'" @click.stop="startTask(task)">
|
||||
<text class="btn-text">开始</text>
|
||||
</button>
|
||||
<button class="action-btn" v-if="task.status === 'in_progress'" @click.stop="completeTask(task)">
|
||||
<text class="btn-text">完成</text>
|
||||
</button>
|
||||
<button class="action-btn secondary" @click.stop="editTask(task)">
|
||||
<text class="btn-text">编辑</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="filteredTasks.length === 0 && !isLoading">
|
||||
<text class="empty-icon">📋</text>
|
||||
<text class="empty-title">暂无任务</text>
|
||||
<text class="empty-description">{{ getEmptyStateText() }}</text>
|
||||
<button class="empty-action" @click="addNewTask">创建第一个任务</button>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-state" v-if="isLoading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { CareTask, TaskStats } from '../types.uts'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
tasks: [] as CareTask[],
|
||||
taskStats: {
|
||||
pending: 0,
|
||||
in_progress: 0,
|
||||
completed: 0,
|
||||
overdue: 0
|
||||
} as TaskStats,
|
||||
selectedStatus: 'all',
|
||||
selectedPriority: '',
|
||||
isLoading: false,
|
||||
isRefreshing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredTasks(): CareTask[] {
|
||||
let filtered = this.tasks
|
||||
|
||||
// 按状态筛选
|
||||
if (this.selectedStatus !== 'all') {
|
||||
filtered = filtered.filter(task => task.status === this.selectedStatus)
|
||||
}
|
||||
|
||||
// 按优先级筛选
|
||||
if (this.selectedPriority === 'urgent') {
|
||||
filtered = filtered.filter(task => task.priority === 'urgent' || task.priority === 'high')
|
||||
}
|
||||
|
||||
// 按时间排序,逾期的优先显示
|
||||
return filtered.sort((a, b) => {
|
||||
const now = new Date()
|
||||
const aScheduled = new Date(a.scheduled_time)
|
||||
const bScheduled = new Date(b.scheduled_time)
|
||||
|
||||
// 逾期任务优先
|
||||
const aOverdue = aScheduled < now && a.status !== 'completed'
|
||||
const bOverdue = bScheduled < now && b.status !== 'completed'
|
||||
|
||||
if (aOverdue && !bOverdue) return -1
|
||||
if (!aOverdue && bOverdue) return 1
|
||||
|
||||
// 按计划时间排序
|
||||
return aScheduled.getTime() - bScheduled.getTime()
|
||||
})
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadTasks()
|
||||
},
|
||||
onShow() {
|
||||
this.loadTasks()
|
||||
},
|
||||
methods: {
|
||||
async loadTasks() {
|
||||
this.isLoading = true
|
||||
|
||||
try {
|
||||
const [tasksResult, statsResult] = await Promise.all([
|
||||
supa.executeAs('care_tasks', {}),
|
||||
supa.executeAs('task_stats', {})
|
||||
])
|
||||
|
||||
if (tasksResult.success) {
|
||||
this.tasks = tasksResult.data as CareTask[]
|
||||
}
|
||||
|
||||
if (statsResult.success && statsResult.data.length > 0) {
|
||||
this.taskStats = statsResult.data[0] as TaskStats
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载任务失败:', error)
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
async refreshTasks() {
|
||||
this.isRefreshing = true
|
||||
await this.loadTasks()
|
||||
this.isRefreshing = false
|
||||
},
|
||||
|
||||
filterByStatus(status: string) {
|
||||
this.selectedStatus = status
|
||||
this.selectedPriority = ''
|
||||
},
|
||||
|
||||
filterByPriority(priority: string) {
|
||||
this.selectedPriority = priority
|
||||
this.selectedStatus = 'all'
|
||||
},
|
||||
|
||||
async startTask(task: CareTask) {
|
||||
try {
|
||||
const result = await supa.executeAs('update_task_status', {
|
||||
task_id: task.id,
|
||||
status: 'in_progress',
|
||||
started_at: new Date().toISOString()
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
uni.showToast({
|
||||
title: '任务已开始',
|
||||
icon: 'success'
|
||||
})
|
||||
this.loadTasks()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('开始任务失败:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async completeTask(task: CareTask) {
|
||||
try {
|
||||
const result = await supa.executeAs('update_task_status', {
|
||||
task_id: task.id,
|
||||
status: 'completed',
|
||||
completed_at: new Date().toISOString()
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
uni.showToast({
|
||||
title: '任务已完成',
|
||||
icon: 'success'
|
||||
})
|
||||
this.loadTasks()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('完成任务失败:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
addNewTask() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ec/tasks/form'
|
||||
})
|
||||
},
|
||||
|
||||
editTask(task: CareTask) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/tasks/form?id=${task.id}`
|
||||
})
|
||||
},
|
||||
|
||||
viewTaskDetail(task: CareTask) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/tasks/detail?id=${task.id}`
|
||||
})
|
||||
},
|
||||
|
||||
getTaskCardClass(task: CareTask): string {
|
||||
const classes = ['task-card']
|
||||
|
||||
if (task.status === 'overdue' || this.isTaskOverdue(task)) {
|
||||
classes.push('overdue')
|
||||
}
|
||||
|
||||
if (task.priority === 'urgent') {
|
||||
classes.push('urgent')
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
},
|
||||
|
||||
isTaskOverdue(task: CareTask): boolean {
|
||||
const now = new Date()
|
||||
const scheduled = new Date(task.scheduled_time)
|
||||
return scheduled < now && task.status !== 'completed'
|
||||
},
|
||||
|
||||
getPriorityText(priority: string): string {
|
||||
const priorityMap = {
|
||||
'low': '低',
|
||||
'normal': '普通',
|
||||
'high': '高',
|
||||
'urgent': '紧急'
|
||||
}
|
||||
return priorityMap[priority] || '普通'
|
||||
},
|
||||
|
||||
getTaskStatusText(status: string): string {
|
||||
const statusMap = {
|
||||
'pending': '待处理',
|
||||
'in_progress': '进行中',
|
||||
'completed': '已完成',
|
||||
'cancelled': '已取消',
|
||||
'overdue': '已逾期'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
formatDateTime(timestamp: string): string {
|
||||
if (!timestamp) return ''
|
||||
|
||||
const date = new Date(timestamp)
|
||||
const now = new Date()
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const taskDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||||
|
||||
const time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
|
||||
if (taskDate.getTime() === today.getTime()) {
|
||||
return `今天 ${time}`
|
||||
} else if (taskDate.getTime() === today.getTime() - 24 * 60 * 60 * 1000) {
|
||||
return `昨天 ${time}`
|
||||
} else if (taskDate.getTime() === today.getTime() + 24 * 60 * 60 * 1000) {
|
||||
return `明天 ${time}`
|
||||
} else {
|
||||
return `${date.getMonth() + 1}/${date.getDate()} ${time}`
|
||||
}
|
||||
},
|
||||
|
||||
getEmptyStateText(): string {
|
||||
if (this.selectedStatus !== 'all') {
|
||||
const statusMap = {
|
||||
'pending': '没有待处理的任务',
|
||||
'in_progress': '没有进行中的任务',
|
||||
'completed': '没有已完成的任务'
|
||||
}
|
||||
return statusMap[this.selectedStatus] || '没有相关任务'
|
||||
}
|
||||
|
||||
if (this.selectedPriority === 'urgent') {
|
||||
return '没有紧急任务'
|
||||
}
|
||||
|
||||
return '还没有创建任何任务'
|
||||
}
|
||||
}
|
||||
}
|
||||
const tasks = ref<Array<CareTask>>([])
|
||||
const filteredTasks = ref<Array<CareTask>>([])
|
||||
const selectedStatus = ref<string>('all')
|
||||
const selectedPriority = ref<string>('all')
|
||||
const isLoading = ref<boolean>(false)
|
||||
const isRefreshing = ref<boolean>(false)
|
||||
|
||||
// 任务统计
|
||||
const taskStats = ref({
|
||||
pending: 0,
|
||||
in_progress: 0,
|
||||
completed: 0,
|
||||
overdue: 0
|
||||
})
|
||||
|
||||
// 加载任务数据
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
const result = await supa
|
||||
.from('ec_care_tasks')
|
||||
.select(`
|
||||
id,
|
||||
task_name,
|
||||
elder_name,
|
||||
scheduled_time,
|
||||
status,
|
||||
priority,
|
||||
caregiver_name,
|
||||
due_date,
|
||||
created_at
|
||||
`)
|
||||
.order('scheduled_time', { ascending: true })
|
||||
.executeAs<Array<CareTask>>()
|
||||
|
||||
if (result.error === null && result.data !== null) {
|
||||
tasks.value = result.data
|
||||
applyFilters()
|
||||
updateTaskStats()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载任务数据失败:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新任务统计
|
||||
const updateTaskStats = () => {
|
||||
const now = new Date()
|
||||
|
||||
taskStats.value = {
|
||||
pending: tasks.value.filter(task => task.status === 'pending').length,
|
||||
in_progress: tasks.value.filter(task => task.status === 'in_progress').length,
|
||||
completed: tasks.value.filter(task => task.status === 'completed').length,
|
||||
overdue: tasks.value.filter(task => {
|
||||
const dueDate = new Date(task.scheduled_time)
|
||||
return task.status !== 'completed' && dueDate < now
|
||||
}).length
|
||||
}
|
||||
}
|
||||
|
||||
// 应用筛选
|
||||
const applyFilters = () => {
|
||||
let filtered = tasks.value
|
||||
|
||||
// 状态筛选
|
||||
if (selectedStatus.value !== 'all') {
|
||||
filtered = filtered.filter(task => task.status === selectedStatus.value)
|
||||
}
|
||||
|
||||
// 优先级筛选
|
||||
if (selectedPriority.value === 'urgent') {
|
||||
filtered = filtered.filter(task => task.priority === 'urgent')
|
||||
}
|
||||
|
||||
filteredTasks.value = filtered
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
const filterByStatus = (status: string) => {
|
||||
selectedStatus.value = status
|
||||
selectedPriority.value = 'all' // 重置优先级筛选
|
||||
applyFilters()
|
||||
}
|
||||
|
||||
// 优先级筛选
|
||||
const filterByPriority = (priority: string) => {
|
||||
selectedPriority.value = priority
|
||||
selectedStatus.value = 'all' // 重置状态筛选
|
||||
applyFilters()
|
||||
}
|
||||
|
||||
// 刷新任务
|
||||
const refreshTasks = async () => {
|
||||
isRefreshing.value = true
|
||||
await loadTasks()
|
||||
isRefreshing.value = false
|
||||
}
|
||||
|
||||
// 获取优先级文本
|
||||
const getPriorityText = (priority: string): string => {
|
||||
switch (priority) {
|
||||
case 'urgent': return '紧急'
|
||||
case 'high': return '高'
|
||||
case 'normal': return '普通'
|
||||
case 'low': return '低'
|
||||
default: return priority
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务卡片样式类
|
||||
const getTaskCardClass = (task: CareTask): string => {
|
||||
const classes = ['task-card']
|
||||
|
||||
// 优先级样式
|
||||
if (task.priority === 'urgent') {
|
||||
classes.push('urgent')
|
||||
} else if (task.priority === 'high') {
|
||||
classes.push('high-priority')
|
||||
}
|
||||
|
||||
// 逾期样式
|
||||
const now = new Date()
|
||||
const dueDate = new Date(task.scheduled_time)
|
||||
if (task.status !== 'completed' && dueDate < now) {
|
||||
classes.push('overdue')
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
}
|
||||
|
||||
// 开始任务
|
||||
const startTask = async (task: CareTask) => {
|
||||
try {
|
||||
await supa
|
||||
.from('ec_care_tasks')
|
||||
.update({
|
||||
status: 'in_progress',
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', task.id)
|
||||
.executeAs<any>()
|
||||
|
||||
// 更新本地数据
|
||||
task.status = 'in_progress'
|
||||
updateTaskStats()
|
||||
|
||||
uni.showToast({
|
||||
title: '任务已开始',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 导航到任务执行页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/tasks/execute?id=${task.id}`
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('开始任务失败:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 完成任务
|
||||
const completeTask = async (task: CareTask) => {
|
||||
try {
|
||||
await supa
|
||||
.from('ec_care_tasks')
|
||||
.update({
|
||||
status: 'completed',
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', task.id)
|
||||
.executeAs<any>()
|
||||
|
||||
// 更新本地数据
|
||||
task.status = 'completed'
|
||||
updateTaskStats()
|
||||
|
||||
uni.showToast({
|
||||
title: '任务已完成',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('完成任务失败:', error)
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 查看任务详情
|
||||
const viewTaskDetail = (task: CareTask) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/tasks/detail?id=${task.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑任务
|
||||
const editTask = (task: CareTask) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/tasks/edit?id=${task.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 新建任务
|
||||
const addNewTask = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ec/tasks/add'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取空状态文本
|
||||
const getEmptyStateText = (): string => {
|
||||
if (selectedStatus.value !== 'all') {
|
||||
return `没有${getTaskStatusText(selectedStatus.value)}的任务`
|
||||
}
|
||||
if (selectedPriority.value === 'urgent') {
|
||||
return '没有紧急任务'
|
||||
}
|
||||
return '还没有创建任何任务,点击下方按钮创建第一个任务'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadTasks()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-management {
|
||||
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;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 任务统计 */
|
||||
.task-stats {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 筛选器 */
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-scroll {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
margin-right: 10px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-item.active {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
/* 任务列表 */
|
||||
.tasks-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tasks-list {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
border-left: 4px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.task-card.urgent {
|
||||
border-left-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.task-card.high-priority {
|
||||
border-left-color: #fa8c16;
|
||||
}
|
||||
|
||||
.task-card.overdue {
|
||||
background-color: #fff2f0;
|
||||
border-left-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.task-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.task-priority {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.task-priority.urgent {
|
||||
background-color: #ff4d4f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-priority.high {
|
||||
background-color: #fa8c16;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-priority.normal {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-priority.low {
|
||||
background-color: #52c41a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.task-status.pending {
|
||||
background-color: #fff7e6;
|
||||
color: #d48806;
|
||||
}
|
||||
|
||||
.task-status.in_progress {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.task-status.completed {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.task-content {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.task-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background-color: #fff;
|
||||
color: #666;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.empty-action {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
187
pages/ec/tasks/my-tasks.uvue
Normal file
187
pages/ec/tasks/my-tasks.uvue
Normal file
@@ -0,0 +1,187 @@
|
||||
<!-- 养老管理系统 - 我的护理任务列表 -->
|
||||
<template>
|
||||
<view class="my-tasks-page">
|
||||
<view class="header">
|
||||
<text class="title">我的护理任务</text>
|
||||
</view>
|
||||
<view class="tasks-list">
|
||||
<view v-if="tasks.length === 0" class="empty-text">暂无任务</view>
|
||||
<view v-for="task in tasks" :key="task.id" class="task-item" :class="getTaskPriorityClass(task.priority)"
|
||||
@click="viewTaskDetail(task)">
|
||||
<view class="task-main">
|
||||
<view class="task-info">
|
||||
<text class="task-name">{{ task.task_name }}</text>
|
||||
<text class="task-elder">{{ task.elder_name }}</text>
|
||||
<text class="task-time">{{ formatTime(task.scheduled_time) }}</text>
|
||||
</view>
|
||||
<view class="task-priority">
|
||||
<view class="priority-badge" :class="task.priority">
|
||||
<text class="priority-text">{{ getPriorityText(task.priority) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="task-status">
|
||||
<text class="status-text">{{ getStatusText(task.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import type { CareTask } from '../types.uts'
|
||||
import { formatTime, getTaskPriorityClass, getPriorityText, getStatusText } from '../types.uts'
|
||||
import { state, getCurrentUserId } from '@/utils/store.uts'
|
||||
|
||||
const tasks = ref<Array<CareTask>>([])
|
||||
const profile = ref(state.userProfile)
|
||||
|
||||
const loadMyTasks = async (currentUserId) => {
|
||||
try {
|
||||
const result = await supa
|
||||
.from('ec_care_tasks')
|
||||
.select(`
|
||||
id,
|
||||
task_name,
|
||||
elder_name,
|
||||
scheduled_time,
|
||||
status,
|
||||
priority
|
||||
`)
|
||||
.eq('assigned_to', currentUserId)
|
||||
.order('scheduled_time', { ascending: true })
|
||||
.executeAs<CareTask>()
|
||||
if (result.error === null && result.data !== null) {
|
||||
tasks.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载任务失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const viewTaskDetail = (task : CareTask) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ec/tasks/detail?id=${task.id}`
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((options : OnLoadOptions) => {
|
||||
|
||||
const currentUserId = options['id'] ?? getCurrentUserId()
|
||||
|
||||
loadMyTasks(currentUserId)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-tasks-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tasks-list {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
border-left: 3px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.task-item.priority-urgent {
|
||||
border-left-color: #ff4d4f;
|
||||
background-color: #fff2f0;
|
||||
}
|
||||
|
||||
.task-item.priority-high {
|
||||
border-left-color: #fa8c16;
|
||||
background-color: #fff7e6;
|
||||
}
|
||||
|
||||
.task-item.priority-normal {
|
||||
border-left-color: #1890ff;
|
||||
background-color: #f0f9ff;
|
||||
}
|
||||
|
||||
.task-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.task-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.task-elder {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.task-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.priority-badge {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.priority-badge.urgent {
|
||||
background-color: #ff4d4f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.priority-badge.high {
|
||||
background-color: #fa8c16;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.priority-badge.normal {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-status .status-text {
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 30px 0;
|
||||
}
|
||||
</style>
|
||||
683
pages/ec/types.uts
Normal file
683
pages/ec/types.uts
Normal file
@@ -0,0 +1,683 @@
|
||||
// 养老管理系统类型定义 - 简化版本(直接映射数据库字段)
|
||||
// filepath: h:\blews\akmon\pages\ec\types.uts
|
||||
|
||||
// 老人基本信息
|
||||
export type Elder = {
|
||||
id: string
|
||||
user_id: string | null
|
||||
facility_id: string | null
|
||||
care_unit_id: string | null
|
||||
elder_code: string | null
|
||||
name: string
|
||||
id_card: string | null
|
||||
gender: string | null
|
||||
birthday: string | null
|
||||
avatar?: string | null // 头像字段,表单用
|
||||
birth_date?: string | null // 兼容表单字段
|
||||
phone?: string | null // 联系电话,表单用
|
||||
address?: string | null // 联系地址,表单用
|
||||
health_status?: string | null // 健康状态,表单用
|
||||
medical_history?: string | null // 疾病史,表单用
|
||||
allergies?: string | null // 过敏史,表单用
|
||||
special_needs?: string | null // 特殊需求,表单用
|
||||
emergency_contact_name?: string | null // 紧急联系人姓名
|
||||
emergency_contact_relationship?: string | null // 紧急联系人关系
|
||||
emergency_contact_phone?: string | null // 紧急联系人电话
|
||||
nationality: string | null
|
||||
religion: string | null
|
||||
marital_status: string | null
|
||||
education: string | null
|
||||
occupation: string | null
|
||||
admission_date: string | null
|
||||
care_level: string | null
|
||||
room_number: string | null
|
||||
bed_number: string | null
|
||||
payment_method: string | null
|
||||
monthly_fee: number | null
|
||||
deposit: number | null
|
||||
status: string | null
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
// 护理员
|
||||
export type Caregiver = {
|
||||
id: string
|
||||
user_id: string
|
||||
facility_id: string | null
|
||||
name: string
|
||||
phone: string | null
|
||||
position: string | null
|
||||
certification: string | null
|
||||
work_shift: string | null
|
||||
max_capacity: number | null
|
||||
status: string | null
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
// 护理记录
|
||||
export type CareRecord = {
|
||||
id: string
|
||||
task_id: string | null
|
||||
elder_id: string
|
||||
elder_name?: string | null // 用于连接查询时的老人姓名
|
||||
caregiver_id: string
|
||||
caregiver_name?: string | null // 用于连接查询时的护理员姓名
|
||||
start_time: string | null
|
||||
end_time: string | null
|
||||
actual_duration: number | null
|
||||
care_content: string | null
|
||||
description?: string | null // 用于显示护理内容的简化描述
|
||||
record_type?: string | null // 用于分类显示的记录类型
|
||||
elder_condition: string | null
|
||||
issues_notes: string | null
|
||||
photo_urls: string[] | null
|
||||
status: string
|
||||
rating: number | null
|
||||
supervisor_notes: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 健康提醒
|
||||
export type HealthAlert = {
|
||||
id: string
|
||||
elder_id: string
|
||||
elder_name?: string | null // 用于连接查询时的老人姓名
|
||||
alert_type: string
|
||||
severity: string
|
||||
title: string
|
||||
description: string | null
|
||||
vital_sign_id: string | null
|
||||
threshold_value: number | null
|
||||
actual_value: number | null
|
||||
status: string
|
||||
acknowledged_by: string | null
|
||||
acknowledged_at: string | null
|
||||
resolved_at: string | null
|
||||
notes: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 护理任务
|
||||
export type CareTask = {
|
||||
id: string
|
||||
elder_id: string
|
||||
elder_name?: string | null // 用于连接查询时的老人姓名
|
||||
care_plan_id: string | null
|
||||
task_name: string
|
||||
task_type: string | null
|
||||
description: string | null
|
||||
scheduled_time: string | null
|
||||
assigned_to: string | null
|
||||
caregiver_name?: string | null // 用于连接查询时的护理员姓名
|
||||
priority: string
|
||||
estimated_duration: number | null
|
||||
status: string
|
||||
due_date: string | null
|
||||
created_by: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 生命体征
|
||||
export type VitalSign = {
|
||||
id: string
|
||||
elder_id: string
|
||||
device_id: string | null
|
||||
vital_type: string
|
||||
systolic_pressure: number | null
|
||||
diastolic_pressure: number | null
|
||||
heart_rate: number | null
|
||||
temperature: number | null
|
||||
oxygen_saturation: number | null
|
||||
glucose_level: number | null
|
||||
measured_at: string
|
||||
measured_by: string | null
|
||||
notes: string | null
|
||||
is_abnormal: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 用药管理
|
||||
export type Medication = {
|
||||
id: string
|
||||
elder_id: string
|
||||
medical_record_id: string | null
|
||||
medication_name: string
|
||||
dosage: string | null
|
||||
frequency: string | null
|
||||
route: string | null
|
||||
start_date: string | null
|
||||
end_date: string | null
|
||||
prescribed_by: string | null
|
||||
instructions: string | null
|
||||
side_effects: string | null
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 活动记录
|
||||
export type Activity = {
|
||||
id: string
|
||||
facility_id: string
|
||||
activity_name: string
|
||||
activity_type: string | null
|
||||
description: string | null
|
||||
location: string | null
|
||||
start_time: string | null
|
||||
end_time: string | null
|
||||
max_participants: number | null
|
||||
instructor: string | null
|
||||
requirements: string | null
|
||||
materials_needed: string | null
|
||||
status: string
|
||||
created_by: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 访客记录
|
||||
export type Visit = {
|
||||
id: string
|
||||
elder_id: string
|
||||
visitor_name: string
|
||||
visitor_relationship: string | null
|
||||
visitor_id_card: string | null
|
||||
visitor_phone: string | null
|
||||
visit_date: string | null
|
||||
start_time: string | null
|
||||
end_time: string | null
|
||||
purpose: string | null
|
||||
notes: string | null
|
||||
status: string
|
||||
approved_by: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 账单记录
|
||||
export type Bill = {
|
||||
id: string
|
||||
elder_id: string
|
||||
bill_type: string
|
||||
amount: number
|
||||
description: string | null
|
||||
due_date: string | null
|
||||
paid_date: string | null
|
||||
status: string
|
||||
payment_method: string | null
|
||||
notes: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 机构信息
|
||||
export type Facility = {
|
||||
id: string
|
||||
name: string
|
||||
region_id: string | null
|
||||
type: string | null
|
||||
license_number: string | null
|
||||
contact_phone: string | null
|
||||
address: string | null
|
||||
capacity: number | null
|
||||
current_occupancy: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 工作排班
|
||||
export type WorkSchedule = {
|
||||
id: string
|
||||
caregiver_id: string
|
||||
facility_id: string | null
|
||||
date: string
|
||||
shift: string | null
|
||||
start_time: string | null
|
||||
end_time: string | null
|
||||
assigned_elders: string[] | null
|
||||
status: string
|
||||
notes: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 设备信息
|
||||
export type Equipment = {
|
||||
id: string
|
||||
name: string
|
||||
equipment_id: string
|
||||
type: string
|
||||
type_name: string
|
||||
model: string | null
|
||||
location_id: string | null
|
||||
location_name: string | null
|
||||
status: string
|
||||
description: string | null
|
||||
last_maintenance: string | null
|
||||
next_maintenance: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 维护记录
|
||||
export type MaintenanceRecord = {
|
||||
id: string
|
||||
equipment_id: string
|
||||
type: string
|
||||
description: string
|
||||
performed_by: string
|
||||
performed_at: string
|
||||
next_maintenance_date: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 事件报告
|
||||
export type Incident = {
|
||||
id: string
|
||||
elder_id: string
|
||||
facility_id: string | null
|
||||
incident_type: string
|
||||
severity: string
|
||||
title: string
|
||||
description: string | null
|
||||
location: string | null
|
||||
incident_time: string
|
||||
reported_by: string | null
|
||||
witnesses: string[] | null
|
||||
actions_taken: string | null
|
||||
follow_up_required: boolean
|
||||
status: string
|
||||
resolved_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 医生信息
|
||||
export type Doctor = {
|
||||
id: string
|
||||
user_id: string
|
||||
facility_id: string | null
|
||||
name: string
|
||||
specialization: string | null
|
||||
license_number: string | null
|
||||
phone: string | null
|
||||
email: string | null
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 护士信息
|
||||
export type Nurse = {
|
||||
id: string
|
||||
user_id: string
|
||||
facility_id: string | null
|
||||
name: string
|
||||
certification: string | null
|
||||
department: string | null
|
||||
phone: string | null
|
||||
email: string | null
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 统计数据类型
|
||||
export type DashboardStats = {
|
||||
total_elders: number
|
||||
total_caregivers: number
|
||||
on_duty_caregivers: number
|
||||
occupancy_rate: number
|
||||
available_beds: number
|
||||
urgent_alerts: number
|
||||
elders_trend: number
|
||||
}
|
||||
|
||||
// 健康统计数据类型
|
||||
export type HealthStats = {
|
||||
total_equipment: number
|
||||
online_equipment: number
|
||||
maintenance_needed: number
|
||||
faulty_equipment: number
|
||||
total_records_today: number
|
||||
abnormal_readings: number
|
||||
pending_reviews: number
|
||||
critical_alerts: number
|
||||
today_visitors: number
|
||||
current_visitors: number
|
||||
scheduled_visits: number
|
||||
pending_approvals: number
|
||||
}
|
||||
|
||||
// 数据分析相关类型
|
||||
export type AnalyticsMetric = {
|
||||
key: string
|
||||
label: string
|
||||
value: string
|
||||
icon: string
|
||||
trend: string
|
||||
change: string
|
||||
}
|
||||
|
||||
export type ActivityStat = {
|
||||
type: string
|
||||
label: string
|
||||
count: number
|
||||
percentage: number
|
||||
color: string
|
||||
}
|
||||
|
||||
export type CareQualityItem = {
|
||||
name: string
|
||||
value: number
|
||||
}
|
||||
|
||||
export type CareQualityMetric = {
|
||||
category: string
|
||||
score: number
|
||||
items: Array<CareQualityItem>
|
||||
}
|
||||
|
||||
export type Alert = {
|
||||
id: string
|
||||
title: string
|
||||
message: string
|
||||
level: string
|
||||
created_at: string
|
||||
handled: boolean
|
||||
}
|
||||
|
||||
export type DoctorInfo = {
|
||||
id: string
|
||||
name: string
|
||||
phone: string
|
||||
department: string
|
||||
specialization: string
|
||||
title: string
|
||||
}
|
||||
|
||||
// 老人统计数据
|
||||
export type ElderStats = {
|
||||
total: number
|
||||
new_this_month: number
|
||||
self_care: number
|
||||
assisted_care: number
|
||||
full_care: number
|
||||
}
|
||||
|
||||
// 事件报告相关类型
|
||||
export type IncidentReport = {
|
||||
id: string
|
||||
title: string
|
||||
elder_id: string
|
||||
elder_name?: string
|
||||
reporter_id: string
|
||||
reporter_name?: string
|
||||
incident_type: 'medical' | 'safety' | 'behavioral' | 'equipment' | 'other'
|
||||
priority: 'normal' | 'high' | 'urgent' | 'critical'
|
||||
status: 'pending' | 'in_progress' | 'resolved' | 'closed'
|
||||
description: string
|
||||
action_taken?: string
|
||||
incident_time: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type IncidentStats = {
|
||||
urgent_incidents: number
|
||||
pending_reports: number
|
||||
resolved_today: number
|
||||
total_incidents: number
|
||||
}
|
||||
|
||||
// 任务统计类型
|
||||
export type TaskStats = {
|
||||
pending: number
|
||||
in_progress: number
|
||||
completed: number
|
||||
overdue: number
|
||||
total: number
|
||||
}
|
||||
|
||||
// 护工相关类型
|
||||
export type CaregiverInfo = {
|
||||
id: string
|
||||
name: string
|
||||
employee_id: string
|
||||
phone?: string
|
||||
email?: string
|
||||
avatar?: string
|
||||
hire_date?: string
|
||||
care_level?: 'junior' | 'intermediate' | 'senior' | 'supervisor'
|
||||
status?: 'active' | 'on_leave' | 'inactive'
|
||||
assigned_elders?: number
|
||||
rating?: number
|
||||
specialties?: string
|
||||
certifications?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export type CaregiverStats = {
|
||||
total_caregivers: number
|
||||
active_caregivers: number
|
||||
on_leave: number
|
||||
workload_avg: number
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
export function formatDateTime(dateTime: string | null): string {
|
||||
if (dateTime === null || dateTime === '') return ''
|
||||
const date = new Date(dateTime)
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const hour = date.getHours().toString().padStart(2, '0')
|
||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
export function formatDate(date: string | null): string {
|
||||
if (date === null || date === '') return ''
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = (d.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = d.getDate().toString().padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
export function formatTime(time: string | null): string {
|
||||
if (time === null || time === '') return ''
|
||||
const date = new Date(time)
|
||||
const hour = date.getHours().toString().padStart(2, '0')
|
||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${hour}:${minute}`
|
||||
}
|
||||
|
||||
export function getAge(birthday: string | null): number {
|
||||
if (birthday === null || birthday === '') return 0
|
||||
const birth = new Date(birthday)
|
||||
const today = new Date()
|
||||
let age = today.getFullYear() - birth.getFullYear()
|
||||
const monthDiff = today.getMonth() - birth.getMonth()
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
|
||||
age--
|
||||
}
|
||||
return age
|
||||
}
|
||||
|
||||
export function getCareLevelText(level: string | null): string {
|
||||
if (level === null) return '未设置'
|
||||
const levelMap = new Map<string, string>([
|
||||
['self_care', '自理'],
|
||||
['assisted', '半护理'],
|
||||
['full_care', '全护理'],
|
||||
['dementia', '失智护理']
|
||||
])
|
||||
return levelMap.get(level) ?? level
|
||||
}
|
||||
|
||||
export function getHealthStatusText(status: string | null): string {
|
||||
if (status === null) return '未知'
|
||||
const statusMap = new Map<string, string>([
|
||||
['excellent', '优秀'],
|
||||
['good', '良好'],
|
||||
['fair', '一般'],
|
||||
['poor', '较差'],
|
||||
['critical', '严重']
|
||||
])
|
||||
return statusMap.get(status) ?? status
|
||||
}
|
||||
|
||||
export function getSeverityText(severity: string): string {
|
||||
const severityMap = new Map<string, string>([
|
||||
['low', '低'],
|
||||
['medium', '中'],
|
||||
['high', '高'],
|
||||
['critical', '紧急'] ])
|
||||
return severityMap.get(severity) ?? severity
|
||||
}
|
||||
|
||||
export function getTaskStatusText(status: string): string {
|
||||
const statusMap = new Map<string, string>([
|
||||
['pending', '待处理'],
|
||||
['in_progress', '进行中'],
|
||||
['completed', '已完成'],
|
||||
['cancelled', '已取消'],
|
||||
['overdue', '已超期']
|
||||
])
|
||||
return statusMap.get(status) ?? status
|
||||
}
|
||||
|
||||
export function getTaskPriorityText(priority: string): string {
|
||||
const priorityMap = new Map<string, string>([
|
||||
['low', '低'],
|
||||
['normal', '普通'],
|
||||
['high', '高'],
|
||||
['urgent', '紧急']
|
||||
])
|
||||
return priorityMap.get(priority) ?? priority
|
||||
}
|
||||
|
||||
export function getRecordTypeText(type: string | null): string {
|
||||
if (type === null) return '其他'
|
||||
const typeMap = new Map<string, string>([
|
||||
['medication', '用药'],
|
||||
['hygiene', '清洁'],
|
||||
['mobility', '康复'],
|
||||
['nutrition', '饮食'],
|
||||
['social', '社交'],
|
||||
['medical', '医疗']
|
||||
])
|
||||
return typeMap.get(type) ?? type
|
||||
}
|
||||
|
||||
export function getAlertIcon(severity: string): string {
|
||||
const iconMap = new Map<string, string>([
|
||||
['critical', '🚨'],
|
||||
['high', '⚠️'],
|
||||
['medium', 'ℹ️'],
|
||||
['low', '💡']
|
||||
])
|
||||
return iconMap.get(severity) ?? 'ℹ️'
|
||||
}
|
||||
|
||||
export function getPriorityIcon(priority: string): string {
|
||||
const iconMap = new Map<string, string>([
|
||||
['urgent', '🚨'],
|
||||
['high', '⚠️'],
|
||||
['normal', '📋'],
|
||||
['low', '💭']
|
||||
])
|
||||
return iconMap.get(priority) ?? '📋'
|
||||
}
|
||||
|
||||
export function getTaskPriorityClass(priority: string): string {
|
||||
const classMap = new Map<string, string>([
|
||||
['urgent', 'priority-urgent'],
|
||||
['high', 'priority-high'],
|
||||
['normal', 'priority-normal'],
|
||||
['low', 'priority-low']
|
||||
])
|
||||
return classMap.get(priority) ?? 'priority-normal'
|
||||
}
|
||||
|
||||
export function getSeverityClass(severity: string): string {
|
||||
const classMap = new Map<string, string>([
|
||||
['critical', 'severity-critical'],
|
||||
['high', 'severity-high'],
|
||||
['medium', 'severity-medium'],
|
||||
['low', 'severity-low']
|
||||
])
|
||||
return classMap.get(severity) ?? 'severity-medium'
|
||||
}
|
||||
|
||||
export function getStatusClass(status: string): string {
|
||||
const classMap = new Map<string, string>([
|
||||
['active', 'status-active'],
|
||||
['inactive', 'status-inactive'],
|
||||
['pending', 'status-pending'],
|
||||
['completed', 'status-completed'],
|
||||
['cancelled', 'status-cancelled'],
|
||||
['in_progress', 'status-progress']
|
||||
])
|
||||
return classMap.get(status) ?? 'status-unknown'
|
||||
}
|
||||
|
||||
// 通用状态文本转换
|
||||
export function getStatusText(status: string | null): string {
|
||||
if (!status) return '未知'
|
||||
const map = new Map<string, string>([
|
||||
['normal', '正常'],
|
||||
['warning', '预警'],
|
||||
['danger', '异常'],
|
||||
['stable', '稳定'],
|
||||
['critical', '危急'],
|
||||
['pending', '待处理'],
|
||||
['completed', '已完成'],
|
||||
['in_progress', '进行中'],
|
||||
['cancelled', '已取消'],
|
||||
['active', '活跃'],
|
||||
['inactive', '未激活']
|
||||
])
|
||||
return map.get(status) ?? status
|
||||
}
|
||||
|
||||
// 活动状态文本转换
|
||||
export function getActivityStatusText(status: string | null): string {
|
||||
if (!status) return '未知'
|
||||
const map = new Map<string, string>([
|
||||
['pending', '待开始'],
|
||||
['in_progress', '进行中'],
|
||||
['completed', '已完成'],
|
||||
['cancelled', '已取消']
|
||||
])
|
||||
return map.get(status) ?? status
|
||||
}
|
||||
|
||||
// 补充 getHealthStatusClass 工具函数,返回健康状态对应的 class
|
||||
export function getHealthStatusClass(status: string): string {
|
||||
switch (status) {
|
||||
case 'excellent': return 'health-excellent'
|
||||
case 'good': return 'health-good'
|
||||
case 'fair': return 'health-fair'
|
||||
case 'poor': return 'health-poor'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 获取优先级文本
|
||||
export const getPriorityText = (priority: string): string => {
|
||||
switch (priority) {
|
||||
case 'urgent': return '紧急'
|
||||
case 'high': return '高'
|
||||
case 'normal': return '普通'
|
||||
case 'low': return '低'
|
||||
default: return priority
|
||||
}
|
||||
}
|
||||
452
pages/ec/types_new.uts
Normal file
452
pages/ec/types_new.uts
Normal file
@@ -0,0 +1,452 @@
|
||||
// 养老管理系统类型定义 - 简化版本
|
||||
// 直接映射数据库字段,去除UTSJSONObject
|
||||
|
||||
// 基础类型
|
||||
export type Elder = {
|
||||
id: string
|
||||
tenant_id: string
|
||||
name: string
|
||||
age: number
|
||||
gender: string
|
||||
id_number: string
|
||||
room_number: string
|
||||
bed_number: string
|
||||
emergency_contact: string
|
||||
emergency_phone: string
|
||||
health_status: string
|
||||
care_level: string
|
||||
admission_date: string
|
||||
profile_picture: string
|
||||
medical_history: string
|
||||
current_medications: string
|
||||
allergies: string
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type Caregiver = {
|
||||
id: string
|
||||
tenant_id: string
|
||||
name: string
|
||||
phone: string
|
||||
position: string
|
||||
certification: string
|
||||
work_shift: string
|
||||
assigned_elders: string
|
||||
max_capacity: number
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type CareRecord = {
|
||||
id: string
|
||||
elder_id: string
|
||||
caregiver_id: string
|
||||
record_type: string
|
||||
description: string
|
||||
timestamp: string
|
||||
duration: number
|
||||
images: string
|
||||
vital_signs: string
|
||||
notes: string
|
||||
rating: number
|
||||
supervisor_notes: string
|
||||
created_at: string
|
||||
elder_name?: string
|
||||
caregiver_name?: string
|
||||
}
|
||||
|
||||
export type HealthAlert = {
|
||||
id: string
|
||||
elder_id: string
|
||||
title: string
|
||||
description: string
|
||||
severity: string
|
||||
alert_type: string
|
||||
status: string
|
||||
created_at: string
|
||||
resolved_at: string
|
||||
resolved_by: string
|
||||
elder_name?: string
|
||||
}
|
||||
|
||||
export type CareTask = {
|
||||
id: string
|
||||
elder_id: string
|
||||
caregiver_id: string
|
||||
task_type: string
|
||||
task_name: string
|
||||
description: string
|
||||
scheduled_time: string
|
||||
estimated_duration: number
|
||||
status: string
|
||||
priority: string
|
||||
completion_notes: string
|
||||
completed_at: string
|
||||
created_at: string
|
||||
elder_name?: string
|
||||
caregiver_name?: string
|
||||
}
|
||||
|
||||
export type VitalSign = {
|
||||
id: string
|
||||
elder_id: string
|
||||
blood_pressure: string
|
||||
heart_rate: number
|
||||
temperature: number
|
||||
blood_sugar: number
|
||||
weight: number
|
||||
height: number
|
||||
oxygen_saturation: number
|
||||
recorded_by: string
|
||||
recorded_at: string
|
||||
notes: string
|
||||
is_abnormal: boolean
|
||||
elder_name?: string
|
||||
}
|
||||
|
||||
export type Medication = {
|
||||
id: string
|
||||
elder_id: string
|
||||
medication_name: string
|
||||
dosage: string
|
||||
frequency: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
prescribed_by: string
|
||||
administered_by: string
|
||||
status: string
|
||||
side_effects: string
|
||||
notes: string
|
||||
created_at: string
|
||||
elder_name?: string
|
||||
}
|
||||
|
||||
export type Activity = {
|
||||
id: string
|
||||
elder_id: string
|
||||
activity_name: string
|
||||
activity_type: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
participation: string
|
||||
enjoyment_level: string
|
||||
notes: string
|
||||
staff_notes: string
|
||||
created_at: string
|
||||
elder_name?: string
|
||||
}
|
||||
|
||||
export type Visit = {
|
||||
id: string
|
||||
elder_id: string
|
||||
visitor_name: string
|
||||
relationship: string
|
||||
visit_date: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
purpose: string
|
||||
notes: string
|
||||
status: string
|
||||
created_at: string
|
||||
elder_name?: string
|
||||
}
|
||||
|
||||
export type Bill = {
|
||||
id: string
|
||||
elder_id: string
|
||||
tenant_id: string
|
||||
record_type: string
|
||||
amount: number
|
||||
description: string
|
||||
due_date: string
|
||||
paid_date: string
|
||||
status: string
|
||||
payment_method: string
|
||||
notes: string
|
||||
created_at: string
|
||||
elder_name?: string
|
||||
}
|
||||
|
||||
export type Facility = {
|
||||
id: string
|
||||
tenant_id: string
|
||||
name: string
|
||||
capacity: number
|
||||
current_occupancy: number
|
||||
available_beds: number
|
||||
room_count: number
|
||||
floor_count: number
|
||||
facility_type: string
|
||||
status: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type WorkSchedule = {
|
||||
id: string
|
||||
caregiver_id: string
|
||||
tenant_id: string
|
||||
shift_date: string
|
||||
shift_start: string
|
||||
shift_end: string
|
||||
shift_type: string
|
||||
status: string
|
||||
notes: string
|
||||
created_at: string
|
||||
caregiver_name?: string
|
||||
}
|
||||
|
||||
export type Equipment = {
|
||||
id: string
|
||||
tenant_id: string
|
||||
equipment_name: string
|
||||
equipment_type: string
|
||||
model: string
|
||||
serial_number: string
|
||||
location: string
|
||||
status: string
|
||||
last_maintenance: string
|
||||
next_maintenance: string
|
||||
responsible_person: string
|
||||
notes: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type Incident = {
|
||||
id: string
|
||||
elder_id: string
|
||||
tenant_id: string
|
||||
incident_type: string
|
||||
severity: string
|
||||
description: string
|
||||
location: string
|
||||
reported_by: string
|
||||
reported_at: string
|
||||
status: string
|
||||
resolution: string
|
||||
resolved_by: string
|
||||
resolved_at: string
|
||||
created_at: string
|
||||
elder_name?: string
|
||||
}
|
||||
|
||||
export type Doctor = {
|
||||
id: string
|
||||
tenant_id: string
|
||||
name: string
|
||||
phone: string
|
||||
email: string
|
||||
specialization: string
|
||||
license_number: string
|
||||
department: string
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type Nurse = {
|
||||
id: string
|
||||
tenant_id: string
|
||||
name: string
|
||||
phone: string
|
||||
email: string
|
||||
license_number: string
|
||||
department: string
|
||||
shift_type: string
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 统计类型
|
||||
export type FacilityStats = {
|
||||
total_elders: number
|
||||
total_caregivers: number
|
||||
on_duty_caregivers: number
|
||||
occupancy_rate: number
|
||||
available_beds: number
|
||||
urgent_alerts: number
|
||||
elders_trend: number
|
||||
}
|
||||
|
||||
export type TaskStats = {
|
||||
total_tasks: number
|
||||
pending_tasks: number
|
||||
in_progress_tasks: number
|
||||
completed_tasks: number
|
||||
overdue_tasks: number
|
||||
today_tasks: number
|
||||
}
|
||||
|
||||
export type HealthStats = {
|
||||
total_alerts: number
|
||||
critical_alerts: number
|
||||
high_alerts: number
|
||||
medium_alerts: number
|
||||
low_alerts: number
|
||||
resolved_alerts: number
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
export function formatDate(dateStr: string): string {
|
||||
if (dateStr == null || dateStr === '') return ''
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export function formatDateTime(dateStr: string): string {
|
||||
if (dateStr == null || dateStr === '') return ''
|
||||
const date = new Date(dateStr)
|
||||
return `${formatDate(dateStr)} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export function formatTime(dateStr: string): string {
|
||||
if (dateStr == null || dateStr === '') return ''
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export function getAge(birthDate: string): number {
|
||||
if (birthDate == null || birthDate === '') return 0
|
||||
const birth = new Date(birthDate)
|
||||
const now = new Date()
|
||||
let age = now.getFullYear() - birth.getFullYear()
|
||||
const monthDiff = now.getMonth() - birth.getMonth()
|
||||
if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birth.getDate())) {
|
||||
age--
|
||||
}
|
||||
return age
|
||||
}
|
||||
|
||||
export function getHealthStatusText(status: string): string {
|
||||
const statusMap = new Map([
|
||||
['excellent', '优秀'],
|
||||
['good', '良好'],
|
||||
['fair', '一般'],
|
||||
['poor', '较差'],
|
||||
['critical', '危重']
|
||||
])
|
||||
return statusMap.get(status) ?? status
|
||||
}
|
||||
|
||||
export function getCareLevelText(level: string): string {
|
||||
const levelMap = new Map([
|
||||
['level1', '一级护理'],
|
||||
['level2', '二级护理'],
|
||||
['level3', '三级护理'],
|
||||
['level4', '四级护理'],
|
||||
['intensive', '重症护理']
|
||||
])
|
||||
return levelMap.get(level) ?? level
|
||||
}
|
||||
|
||||
export function getSeverityText(severity: string): string {
|
||||
const severityMap = new Map([
|
||||
['low', '低'],
|
||||
['medium', '中'],
|
||||
['high', '高'],
|
||||
['critical', '危急']
|
||||
])
|
||||
return severityMap.get(severity) ?? severity
|
||||
}
|
||||
|
||||
export function getTaskStatusText(status: string): string {
|
||||
const statusMap = new Map([
|
||||
['pending', '待处理'],
|
||||
['in_progress', '进行中'],
|
||||
['completed', '已完成'],
|
||||
['cancelled', '已取消'],
|
||||
['overdue', '已逾期']
|
||||
])
|
||||
return statusMap.get(status) ?? status
|
||||
}
|
||||
|
||||
export function getPriorityText(priority: string): string {
|
||||
const priorityMap = new Map([
|
||||
['low', '低'],
|
||||
['medium', '中'],
|
||||
['high', '高'],
|
||||
['urgent', '紧急']
|
||||
])
|
||||
return priorityMap.get(priority) ?? priority
|
||||
}
|
||||
|
||||
export function getGenderText(gender: string): string {
|
||||
const genderMap = new Map([
|
||||
['male', '男'],
|
||||
['female', '女'],
|
||||
['other', '其他']
|
||||
])
|
||||
return genderMap.get(gender) ?? gender
|
||||
}
|
||||
|
||||
export function getTaskTypeText(type: string): string {
|
||||
const typeMap = new Map([
|
||||
['medication', '用药'],
|
||||
['hygiene', '清洁'],
|
||||
['mobility', '康复'],
|
||||
['nutrition', '饮食'],
|
||||
['social', '社交'],
|
||||
['medical', '医疗'],
|
||||
['emergency', '紧急']
|
||||
])
|
||||
return typeMap.get(type) ?? type
|
||||
}
|
||||
|
||||
export function getRecordTypeText(type: string): string {
|
||||
const typeMap = new Map([
|
||||
['medication', '用药'],
|
||||
['hygiene', '清洁'],
|
||||
['mobility', '康复'],
|
||||
['nutrition', '饮食'],
|
||||
['social', '社交'],
|
||||
['medical', '医疗'],
|
||||
['vital_signs', '生命体征'],
|
||||
['activity', '活动']
|
||||
])
|
||||
return typeMap.get(type) ?? type
|
||||
}
|
||||
|
||||
// 日期辅助函数
|
||||
export function getTodayStart(): string {
|
||||
const today = new Date()
|
||||
today.setHours(0)
|
||||
today.setMinutes(0)
|
||||
today.setSeconds(0)
|
||||
today.setMilliseconds(0)
|
||||
return today.toISOString()
|
||||
}
|
||||
|
||||
export function getTodayEnd(): string {
|
||||
const today = new Date()
|
||||
today.setHours(23)
|
||||
today.setMinutes(59)
|
||||
today.setSeconds(59)
|
||||
today.setMilliseconds(999)
|
||||
return today.toISOString()
|
||||
}
|
||||
|
||||
export function getRecentDate(days: number = 7): string {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - days)
|
||||
return date.toISOString()
|
||||
}
|
||||
|
||||
export function getDaysAgo(days: number): string {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - days)
|
||||
date.setHours(0)
|
||||
date.setMinutes(0)
|
||||
date.setSeconds(0)
|
||||
date.setMilliseconds(0)
|
||||
return date.toISOString()
|
||||
}
|
||||
|
||||
export function getCurrentTimeString(): string {
|
||||
const now = new Date()
|
||||
const hours = now.getHours().toString().padStart(2, '0')
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||
return `今天 ${hours}:${minutes}`
|
||||
}
|
||||
1241
pages/ec/visitor/management.uvue
Normal file
1241
pages/ec/visitor/management.uvue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user