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>