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

338 lines
11 KiB
Plaintext

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