Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,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>

View File

View 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>

View File

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

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,748 @@
<template>
<view class="elder-management">
<!-- 顶部搜索和操作区 -->
<view class="header-section">
<view class="search-container">
<input class="search-input" placeholder="搜索老人姓名、房间号..." v-model="searchKeyword" @input="handleSearch" />
<view class="search-icon">🔍</view>
</view>
<view class="header-actions">
<button class="action-btn primary" @click="addNewElder">
<text class="btn-icon"></text>
<text class="btn-text">新增老人</text>
</button>
<button class="action-btn secondary" @click="exportElders">
<text class="btn-icon">📊</text>
<text class="btn-text">导出</text>
</button>
</view>
</view>
<!-- 统计卡片 -->
<view class="stats-container">
<view class="stat-card">
<view class="stat-number">{{ elderStats.total }}</view>
<view class="stat-label">总入住</view>
<view class="stat-trend positive">+{{ elderStats.new_this_month }}</view>
</view>
<view class="stat-card">
<view class="stat-number">{{ elderStats.self_care }}</view>
<view class="stat-label">自理老人</view>
<view class="stat-percent">{{ getSelfCarePercent() }}%</view>
</view>
<view class="stat-card">
<view class="stat-number">{{ elderStats.assisted_care }}</view>
<view class="stat-label">半护理</view>
<view class="stat-percent">{{ getAssistedCarePercent() }}%</view>
</view>
<view class="stat-card">
<view class="stat-number">{{ elderStats.full_care }}</view>
<view class="stat-label">全护理</view>
<view class="stat-percent">{{ getFullCarePercent() }}%</view>
</view>
</view>
<!-- 筛选器 -->
<view class="filter-container">
<scroll-view class="filter-scroll" direction="horizontal" :show-scrollbar="false">
<view class="filter-item" :class="{ active: selectedCareLevel === 'all' }" @click="filterByCareLevel('all')">
全部
</view>
<view class="filter-item" :class="{ active: selectedCareLevel === '1' }" @click="filterByCareLevel('1')">
一级护理
</view>
<view class="filter-item" :class="{ active: selectedCareLevel === '2' }" @click="filterByCareLevel('2')">
二级护理
</view>
<view class="filter-item" :class="{ active: selectedCareLevel === '3' }" @click="filterByCareLevel('3')">
三级护理
</view>
<view class="filter-item" :class="{ active: selectedCareLevel === '4' }" @click="filterByCareLevel('4')">
特级护理
</view>
<view class="filter-item" :class="{ active: selectedHealthStatus === 'stable' }" @click="filterByHealthStatus('stable')">
健康稳定
</view>
<view class="filter-item" :class="{ active: selectedHealthStatus === 'attention' }" @click="filterByHealthStatus('attention')">
需要关注
</view>
<view class="filter-item" :class="{ active: selectedHealthStatus === 'critical' }" @click="filterByHealthStatus('critical')">
危险状态
</view>
</scroll-view>
</view>
<!-- 老人列表 -->
<view class="elders-list" v-if="filteredElders.length > 0">
<view class="elder-card" v-for="elder in filteredElders" :key="elder.id">
<view class="elder-info">
<view class="elder-avatar">
<image class="avatar-image" :src="elder.profile_picture" mode="aspectFill"
@error="handleAvatarError" v-if="elder.profile_picture" />
<text class="avatar-fallback" v-else>{{ elder.name.charAt(0) }}</text>
</view>
<view class="elder-details">
<view class="elder-name-row">
<text class="elder-name">{{ elder.name }}</text>
<view class="elder-status" :class="elder.health_status">
<text class="status-text">{{ getHealthStatusText(elder.health_status) }}</text>
</view>
</view>
<text class="elder-info-text">{{ elder.age }}岁 · {{ elder.gender === 'male' ? '男' : '女' }}</text>
<text class="elder-room">{{ elder.room_number }}房 {{ elder.bed_number }}床</text>
<text class="elder-care-level">{{ getCareLevelText(elder.care_level) }}</text>
</view>
</view>
<view class="elder-actions">
<button class="action-btn-small" @click="viewElderDetail(elder.id)">
<text class="btn-text">详情</text>
</button>
<button class="action-btn-small edit" @click="editElder(elder.id)">
<text class="btn-text">编辑</text>
</button>
<button class="action-btn-small health" @click="viewHealthRecord(elder.id)">
<text class="btn-text">健康</text>
</button>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<text class="empty-icon">👥</text>
<text class="empty-title">暂无老人信息</text>
<text class="empty-subtitle">点击"新增老人"按钮添加第一位老人</text>
<button class="empty-action-btn" @click="addNewElder">
<text class="btn-text">新增老人</text>
</button>
</view>
<!-- 分页器 -->
<view class="pagination" v-if="totalPages > 1">
<button class="page-btn" :disabled="currentPage === 1" @click="goToPage(currentPage - 1)">
<text class="btn-text">上一页</text>
</button>
<view class="page-info">
<text class="page-text">第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</text>
</view>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">
<text class="btn-text">下一页</text>
</button>
</view>
<!-- 加载状态 -->
<view class="loading-overlay" v-if="isLoading">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
</view>
</template>
<style scoped>
.elder-management {
padding: 40rpx;
background-color: #f8f9fa;
min-height: 100vh;
}
.header-section {
display: flex;
align-items: center;
gap: 30rpx;
margin-bottom: 40rpx;
}
.search-container {
flex: 1;
position: relative;
}
.search-input {
width: 100%;
padding: 24rpx 60rpx 24rpx 24rpx;
background: white;
border-radius: 24rpx;
border: 1rpx solid #ddd;
font-size: 28rpx;
color: #333;
}
.search-icon {
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
font-size: 32rpx;
color: #666;
}
.header-actions {
display: flex;
gap: 20rpx;
}
.action-btn {
display: flex;
align-items: center;
gap: 12rpx;
padding: 24rpx 32rpx;
border-radius: 20rpx;
border: none;
font-size: 28rpx;
font-weight: 600;
}
.action-btn.primary {
background: #007AFF;
color: white;
}
.action-btn.secondary {
background: white;
color: #007AFF;
border: 1rpx solid #007AFF;
}
.btn-icon {
font-size: 24rpx;
}
.stats-container {
display: flex;
gap: 20rpx;
margin-bottom: 40rpx;
}
.stat-card {
flex: 1;
background: white;
padding: 40rpx;
border-radius: 24rpx;
text-align: center;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.stat-number {
font-size: 48rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 12rpx;
}
.stat-label {
font-size: 28rpx;
color: #666;
display: block;
margin-bottom: 8rpx;
}
.stat-trend {
font-size: 24rpx;
padding: 6rpx 12rpx;
border-radius: 12rpx;
display: inline-block;
}
.stat-trend.positive {
background: #e8f5e8;
color: #4caf50;
}
.stat-percent {
font-size: 24rpx;
color: #666;
background: #f0f0f0;
padding: 6rpx 12rpx;
border-radius: 12rpx;
display: inline-block;
}
.filter-container {
margin-bottom: 40rpx;
}
.filter-scroll {
flex-direction: row;
white-space: nowrap;
}
.filter-item {
display: inline-block;
padding: 20rpx 30rpx;
margin-right: 20rpx;
background: white;
border-radius: 20rpx;
border: 1rpx solid #ddd;
font-size: 26rpx;
color: #666;
white-space: nowrap;
}
.filter-item.active {
background: #007AFF;
color: white;
border-color: #007AFF;
}
.elders-list {
display: flex;
flex-direction: column;
gap: 24rpx;
margin-bottom: 40rpx;
}
.elder-card {
background: white;
border-radius: 24rpx;
padding: 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.elder-info {
display: flex;
align-items: center;
flex: 1;
}
.elder-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50rpx;
margin-right: 24rpx;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
flex-shrink: 0;
}
.avatar-image {
width: 100%;
height: 100%;
}
.avatar-fallback {
font-size: 36rpx;
color: #666;
font-weight: bold;
}
.elder-details {
flex: 1;
}
.elder-name-row {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 8rpx;
}
.elder-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.elder-status {
padding: 6rpx 12rpx;
border-radius: 12rpx;
font-size: 22rpx;
}
.elder-status.stable {
background: #e8f5e8;
color: #4caf50;
}
.elder-status.attention {
background: #fff3e0;
color: #ff9800;
}
.elder-status.critical {
background: #ffebee;
color: #f44336;
}
.elder-info-text {
font-size: 26rpx;
color: #666;
display: block;
margin-bottom: 4rpx;
}
.elder-room {
font-size: 26rpx;
color: #007AFF;
display: block;
margin-bottom: 4rpx;
}
.elder-care-level {
font-size: 24rpx;
color: #666;
background: #f0f0f0;
padding: 4rpx 8rpx;
border-radius: 8rpx;
display: inline-block;
}
.elder-actions {
display: flex;
gap: 12rpx;
}
.action-btn-small {
padding: 16rpx 20rpx;
border-radius: 16rpx;
border: none;
font-size: 24rpx;
font-weight: 600;
background: #f0f0f0;
color: #666;
}
.action-btn-small.edit {
background: #007AFF;
color: white;
}
.action-btn-small.health {
background: #4caf50;
color: white;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 30rpx;
margin-bottom: 40rpx;
}
.page-btn {
padding: 20rpx 30rpx;
background: white;
border: 1rpx solid #ddd;
border-radius: 16rpx;
font-size: 26rpx;
color: #333;
}
.page-btn:disabled {
opacity: 0.5;
}
.page-info {
background: white;
padding: 20rpx 30rpx;
border-radius: 16rpx;
border: 1rpx solid #ddd;
}
.page-text {
font-size: 26rpx;
color: #666;
}
.empty-state {
text-align: center;
padding: 120rpx 40rpx;
}
.empty-icon {
font-size: 120rpx;
display: block;
margin-bottom: 30rpx;
opacity: 0.3;
}
.empty-title {
font-size: 36rpx;
color: #333;
display: block;
margin-bottom: 12rpx;
font-weight: 600;
}
.empty-subtitle {
font-size: 28rpx;
color: #666;
display: block;
margin-bottom: 40rpx;
}
.empty-action-btn {
padding: 30rpx 60rpx;
background: #007AFF;
color: white;
border-radius: 20rpx;
border: none;
font-size: 30rpx;
font-weight: 600;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 6rpx solid #f0f0f0;
border-top: 6rpx solid #007AFF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 28rpx;
color: #666;
margin-top: 20rpx;
}
</style>
<script setup lang="uts">
import { ref, computed, onMounted } from 'vue'
import { getHealthStatusText, getCareLevelText } from '../types.uts'
import type { ElderInfo, ElderStats } from '../types.uts'
// 数据状态
const elders = ref<ElderInfo[]>([])
const elderStats = ref<ElderStats>({
total: 0,
new_this_month: 0,
self_care: 0,
assisted_care: 0,
full_care: 0
})
// UI状态
const searchKeyword = ref('')
const selectedCareLevel = ref('all')
const selectedHealthStatus = ref('all')
const currentPage = ref(1)
const pageSize = ref(20)
const isLoading = ref(false)
// 计算属性
const filteredElders = computed(() => {
let filtered = [...elders.value]
// 搜索筛选
if (searchKeyword.value.trim()) {
const keyword = searchKeyword.value.toLowerCase()
filtered = filtered.filter(elder =>
elder.name.toLowerCase().includes(keyword) ||
(elder.room_number && elder.room_number.includes(keyword)) ||
(elder.bed_number && elder.bed_number.includes(keyword))
)
}
// 护理等级筛选
if (selectedCareLevel.value !== 'all') {
filtered = filtered.filter(elder => elder.care_level === selectedCareLevel.value)
}
// 健康状态筛选
if (selectedHealthStatus.value !== 'all') {
filtered = filtered.filter(elder => elder.health_status === selectedHealthStatus.value)
}
// 分页
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filtered.slice(start, end)
})
const totalPages = computed(() => {
const totalFiltered = getTotalFilteredCount()
return Math.ceil(totalFiltered / pageSize.value)
})
// 辅助函数
function getTotalFilteredCount(): number {
let filtered = [...elders.value]
if (searchKeyword.value.trim()) {
const keyword = searchKeyword.value.toLowerCase()
filtered = filtered.filter(elder =>
elder.name.toLowerCase().includes(keyword) ||
(elder.room_number && elder.room_number.includes(keyword)) ||
(elder.bed_number && elder.bed_number.includes(keyword))
)
}
if (selectedCareLevel.value !== 'all') {
filtered = filtered.filter(elder => elder.care_level === selectedCareLevel.value)
}
if (selectedHealthStatus.value !== 'all') {
filtered = filtered.filter(elder => elder.health_status === selectedHealthStatus.value)
}
return filtered.length
}
function getSelfCarePercent(): number {
if (elderStats.value.total === 0) return 0
return Math.round((elderStats.value.self_care / elderStats.value.total) * 100)
}
function getAssistedCarePercent(): number {
if (elderStats.value.total === 0) return 0
return Math.round((elderStats.value.assisted_care / elderStats.value.total) * 100)
}
function getFullCarePercent(): number {
if (elderStats.value.total === 0) return 0
return Math.round((elderStats.value.full_care / elderStats.value.total) * 100)
}
function handleAvatarError() {
// 头像加载失败时的处理
}
// 事件处理
function handleSearch() {
currentPage.value = 1
}
function filterByCareLevel(level: string) {
selectedCareLevel.value = level
selectedHealthStatus.value = 'all'
currentPage.value = 1
}
function filterByHealthStatus(status: string) {
selectedHealthStatus.value = status
selectedCareLevel.value = 'all'
currentPage.value = 1
}
function goToPage(page: number) {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
function addNewElder() {
uni.navigateTo({
url: '/pages/ec/admin/elder-form'
})
}
function viewElderDetail(elderId: string) {
uni.navigateTo({
url: `/pages/ec/admin/elder-detail?elder_id=${elderId}`
})
}
function editElder(elderId: string) {
uni.navigateTo({
url: `/pages/ec/admin/elder-form?elder_id=${elderId}`
})
}
function viewHealthRecord(elderId: string) {
uni.navigateTo({
url: `/pages/ec/admin/health-record?elder_id=${elderId}`
})
}
async function exportElders() {
try {
isLoading.value = true
const supa = (globalThis as any).supa
const result = await supa.executeAs('export_elders', {
filters: {
care_level: selectedCareLevel.value,
health_status: selectedHealthStatus.value,
search_keyword: searchKeyword.value
}
})
if (result && result.length > 0) {
uni.showToast({
title: '导出成功',
icon: 'success'
})
// 这里可以处理导出文件
}
} catch (error) {
console.error('导出失败:', error)
uni.showToast({
title: '导出失败',
icon: 'error'
})
} finally {
isLoading.value = false
}
}
// 数据加载
async function loadElders() {
try {
isLoading.value = true
const supa = (globalThis as any).supa
const result = await supa.executeAs('get_elders_list', {
page: currentPage.value,
page_size: pageSize.value
})
if (result && result.length > 0) {
elders.value = result
}
} catch (error) {
console.error('加载老人列表失败:', error)
uni.showToast({
title: '加载失败',
icon: 'error'
})
} finally {
isLoading.value = false
}
}
async function loadElderStats() {
try {
const supa = (globalThis as any).supa
const result = await supa.executeAs('get_elder_stats')
if (result && result.length > 0) {
elderStats.value = result[0]
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
}
// 初始化
onMounted(async () => {
await Promise.all([
loadElders(),
loadElderStats()
])
})
</script>

File diff suppressed because it is too large Load Diff

View 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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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

File diff suppressed because it is too large Load Diff

0
pages/ec/elders/add.uvue Normal file
View File

View 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>

File diff suppressed because it is too large Load Diff

View 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>

File diff suppressed because it is too large Load Diff

View 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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View 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')
};
};

View 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>

View 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 可能为 UTSJSONObjectparseAlertMessage 返回强类型对象
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>

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View 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
View 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>

View 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
View 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
View 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}`
}

File diff suppressed because it is too large Load Diff