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

890 lines
22 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 养老管理系统 - 活动管理 -->
<template>
<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>