ActivityDetail.vue 19.6 KB
<template>
    <div class="min-h-screen bg-gray-50">
        <!-- Activity Header -->
        <div class="bg-white shadow-sm">
            <div class="container mx-auto px-4 py-4">
                <button @click="goBack" class="flex items-center text-gray-600 hover:text-green-500">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20"
                        fill="currentColor">
                        <path fill-rule="evenodd"
                            d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z"
                            clip-rule="evenodd" />
                    </svg>
                    返回
                </button>
            </div>
        </div>

        <!-- Loading State -->
        <div v-if="loading" class="min-h-screen flex justify-center items-center">
            <div class="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-green-500"></div>
        </div>

        <!-- Error State -->
        <div v-else-if="error || !activity"
            class="min-h-screen flex flex-col justify-center items-center bg-gray-50 px-4">
            <div class="text-red-500 text-6xl mb-4">
                <svg xmlns="http://www.w3.org/2000/svg" class="h-24 w-24" fill="none" viewBox="0 0 24 24"
                    stroke="currentColor">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                        d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
                </svg>
            </div>
            <h1 class="text-2xl font-bold mb-2">活动不存在</h1>
            <p class="text-gray-600 mb-6">{{ error || '该活动可能已被删除或不存在' }}</p>
            <Button @click="goBack" variant="primary">返回上一页</Button>
        </div>

        <!-- Activity Content -->
        <template v-else>
            <!-- Activity Cover -->
            <div class="relative h-64 md:h-80 bg-gray-300 overflow-hidden">
                <img :src="getActivityImage(activity.id)" :alt="activity.title" class="w-full h-full object-cover" />
                <div class="absolute inset-0 bg-black bg-opacity-40"></div>
                <div class="absolute bottom-0 left-0 right-0 p-6">
                    <div class="container mx-auto">
                        <span
                            :class="['inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium mb-2', activityStatus.class]">
                            {{ activityStatus.text }}
                        </span>
                        <h1 class="text-2xl md:text-3xl font-bold text-white">{{ activity.title }}</h1>
                    </div>
                </div>
            </div>

            <!-- Activity Content -->
            <div class="container mx-auto px-4 py-8">
                <div class="flex flex-col md:flex-row gap-8">
                    <!-- Main content -->
                    <div class="w-full md:w-2/3">
                        <!-- Organizer -->
                        <div class="mb-8 flex items-center">
                            <div class="flex items-center">
                                <div class="h-12 w-12 rounded-full overflow-hidden bg-gray-200 mr-3">
                                    <svg xmlns="http://www.w3.org/2000/svg" class="h-full w-full text-gray-400"
                                        fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
                                            d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
                                    </svg>
                                </div>
                                <div>
                                    <div class="font-medium">主办方:{{ activity.organizer_name }}</div>
                                    <div class="text-sm text-gray-500">活动组织者</div>
                                </div>
                            </div>
                        </div>

                        <!-- Activity Tabs -->
                        <Tabs :tabs="tabs" />
                    </div>

                    <!-- Sidebar -->
                    <div class="w-full md:w-1/3 mt-8 md:mt-0">
                        <div class="bg-white rounded-lg shadow-sm p-6 mb-6">
                            <h3 class="text-lg font-medium text-gray-900 mb-4">活动信息</h3>

                            <div class="space-y-4">
                                <!-- Date and Time -->
                                <div class="flex">
                                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500 mt-0.5 mr-3"
                                        fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                                            d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
                                    </svg>
                                    <div>
                                        <div class="font-medium">活动时间</div>
                                        <div class="text-gray-600">{{ formatDate(activity.start_time) }}</div>
                                    </div>
                                </div>

                                <!-- Location -->
                                <div class="flex">
                                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500 mt-0.5 mr-3"
                                        fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                                            d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                                            d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
                                    </svg>
                                    <div>
                                        <div class="font-medium">活动地点</div>
                                        <div class="text-gray-600">
                                            {{ activity.activity_type === 'online'
                                                ? '线上活动' + (activity.online_link ? `(${activity.online_link})` : '')
                                                : (activity.location ? (typeof activity.location === 'object' ?
                                                    activity.location.name : activity.location) : '地点未设置')
                                            }}
                                        </div>
                                    </div>
                                </div>

                                <!-- Registration -->
                                <div class="flex">
                                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500 mt-0.5 mr-3"
                                        fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                                            d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
                                    </svg>
                                    <div>
                                        <div class="font-medium">报名时间</div>
                                        <div class="text-gray-600">{{ formatDate(activity.registration_start) }} - {{
                                            formatDate(activity.registration_end) }}</div>
                                    </div>
                                </div>

                                <!-- Participants -->
                                <div class="flex">
                                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500 mt-0.5 mr-3"
                                        fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                                            d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
                                    </svg>
                                    <div>
                                        <div class="font-medium">参与人数</div>
                                        <div class="text-gray-600">{{ activity.participant_count }}/{{
                                            activity.max_participants }}</div>
                                    </div>
                                </div>

                                <!-- Registration progress -->
                                <div class="mt-2">
                                    <div class="flex items-center justify-between text-sm mb-1">
                                        <span>报名进度</span>
                                        <span>{{ Math.round((activity.participant_count / activity.max_participants) *
                                            100) }}%</span>
                                    </div>
                                    <div class="w-full bg-gray-200 rounded-full h-2">
                                        <div class="bg-gradient-to-r from-green-400 to-blue-500 h-2 rounded-full"
                                            :style="{ width: `${Math.min((activity.participant_count / activity.max_participants) * 100, 100)}%` }">
                                        </div>
                                    </div>
                                </div>

                                <!-- Registration Status -->
                                <div v-if="hasRegistered"
                                    class="mt-2 bg-green-50 border border-green-100 rounded-md p-4">
                                    <div class="flex items-center">
                                        <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500 mr-2"
                                            viewBox="0 0 20 20" fill="currentColor">
                                            <path fill-rule="evenodd"
                                                d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
                                                clip-rule="evenodd" />
                                        </svg>
                                        <span class="font-medium text-green-800">您已报名此活动</span>
                                    </div>
                                    <div class="mt-2 flex items-center">
                                        <span class="text-sm text-green-700 mr-2">报名状态:</span>
                                        <span :class="registrationStatusBadge.class">{{ registrationStatusBadge.text
                                            }}</span>
                                    </div>
                                </div>

                                <!-- Registration button or message -->
                                <template v-if="!hasRegistered">
                                    <Button variant="primary" size="lg" block
                                        :disabled="!isRegistrationOpen || activity.participant_count >= activity.max_participants"
                                        @click="handleRegistration" class="mt-4">
                                        {{ isRegistrationOpen
                                            ? activity.participant_count >= activity.max_participants
                                                ? '名额已满'
                                                : '立即报名'
                                            : '报名已截止' }}
                                    </Button>
                                </template>

                                <RouterLink v-if="hasRegistered" :to="`/check-in/${activity.id}`">
                                    <Button variant="secondary" size="md" block class="mt-2">
                                        活动签到
                                    </Button>
                                </RouterLink>
                            </div>
                        </div>

                        <!-- Similar Activities -->
                        <div v-if="similarActivities.length > 0" class="bg-white rounded-lg shadow-sm p-6">
                            <h3 class="text-lg font-medium text-gray-900 mb-4">相似活动推荐</h3>
                            <div class="space-y-4">
                                <ActivityCard v-for="activity in similarActivities" :key="activity.id"
                                    :activity="activity" variant="compact" />
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </template>

        <!-- Register Modal -->
        <Modal v-if="showRegisterModal" @close="showRegisterModal = false">
            <template #title>活动报名</template>
            <template #content>
                <form @submit.prevent="submitRegistration">
                    <!-- Registration form fields -->
                </form>
            </template>
        </Modal>
    </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAppStore } from '../stores/app'
import Button from '../components/shared/Button.vue'
import Modal from '../components/shared/Modal.vue'
import Tabs from '../components/shared/Tabs.vue'
import ActivityCard from '../components/shared/ActivityCard.vue'

const route = useRoute()
const router = useRouter()
const store = useAppStore()

// State
const activity = ref(null)
const loading = ref(true)
const error = ref(null)
const showRegisterModal = ref(false)
const similarActivities = ref([])
const hasRegistered = ref(false)
const registrationStatus = ref(null)

// Computed
const activityStatus = computed(() => {
    if (!activity.value) return {}

    const now = new Date()
    const startTime = new Date(activity.value.start_time.replace(' ', 'T'))
    const endTime = new Date(activity.value.end_time.replace(' ', 'T'))
    const registrationEnd = new Date(activity.value.registration_end.replace(' ', 'T'))

    if (now > endTime) {
        return {
            text: '已结束',
            class: 'bg-gray-100 text-gray-800'
        }
    } else if (now >= startTime && now <= endTime) {
        return {
            text: '进行中',
            class: 'bg-green-100 text-green-800'
        }
    } else if (now <= registrationEnd) {
        return {
            text: '报名中',
            class: 'bg-blue-100 text-blue-800'
        }
    } else {
        return {
            text: '即将开始',
            class: 'bg-yellow-100 text-yellow-800'
        }
    }
})

const isRegistrationOpen = computed(() => {
    if (!activity.value) return false
    const now = new Date()
    return now >= new Date(activity.value.registration_start.replace(' ', 'T')) &&
        now <= new Date(activity.value.registration_end.replace(' ', 'T'))
})

const registrationStatusBadge = computed(() => {
    if (!registrationStatus.value) return null

    const statusMap = {
        pending: { text: '审核中', class: 'bg-yellow-100 text-yellow-800' },
        approved: { text: '已通过', class: 'bg-green-100 text-green-800' },
        rejected: { text: '已拒绝', class: 'bg-red-100 text-red-800' },
        waitlist: { text: '候补', class: 'bg-purple-100 text-purple-800' }
    }

    return statusMap[registrationStatus.value] || {
        text: registrationStatus.value,
        class: 'bg-gray-100 text-gray-800'
    }
})

const tabs = computed(() => [
    {
        label: '活动详情',
        content: activity.value?.description.split('\n').map((paragraph, idx) => (
            `<p key="${idx}" class="mb-4">${paragraph}</p>`
        )).join('')
    },
    {
        label: '参与须知',
        content: `
      <h3 class="font-medium text-lg mb-4">参与须知</h3>
      <ul class="list-disc pl-5 space-y-2">
        <li>请准时到达活动地点或登录线上会议</li>
        <li>请提前阅读相关书籍或材料</li>
        <li>活动开始后,请将手机调至静音模式</li>
        <li>尊重他人发言,不打断他人</li>
        <li>可携带笔记本进行记录</li>
        <li>如需取消参与,请提前24小时通知主办方</li>
      </ul>
    `
    },
    {
        label: '常见问题',
        content: `
        <div className="py-4">
                      <div className="space-y-6">
                        <div>
                          <h4 className="font-medium text-gray-900">如何取消报名?</h4>
                          <p className="mt-2 text-gray-600">您可以在"我的活动"页面找到已报名的活动,点击"取消报名"按钮即可。请注意,活动开始前24小时内取消将无法获得退款。</p>
                        </div>
                        <div>
                          <h4 className="font-medium text-gray-900">活动材料如何获取?</h4>
                          <p className="mt-2 text-gray-600">报名成功后,您将在"我的活动"页面看到活动详情,相关材料可在页面底部下载或通过邮件接收。</p>
                        </div>
                        <div>
                          <h4 className="font-medium text-gray-900">线上活动如何参加?</h4>
                          <p className="mt-2 text-gray-600">线上活动将在活动开始前30分钟发送会议链接到您的邮箱和手机短信,您也可以在"我的活动"页面找到入口链接。</p>
                        </div>
                      </div>
                    </div>
      `}
])

// Functions
const goBack = () => {
    router.back()
}

const getActivityImage = (activityId) => {
    return `/ assets / images / activities / ${activityId}.jpg`
}

const formatDate = (dateString) => {
    if (!dateString) return ''
    const date = new Date(dateString.replace(' ', 'T'))
    return date.toLocaleString('zh-CN', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit'
    })
}

const handleRegistration = () => {
    showRegisterModal.value = true
}

const submitRegistration = async () => {
    // TODO: Implement registration submission
    showRegisterModal.value = false
}

// Fetch activity data
onMounted(async () => {
    try {
        const activityId = route.params.id
        const response = await store.fetchActivity(activityId)
        activity.value = response
        loading.value = false

        // Check registration status
        const registration = await store.checkRegistration(activityId)
        hasRegistered.value = registration !== null
        registrationStatus.value = registration?.status

        // Fetch similar activities
        similarActivities.value = await store.fetchSimilarActivities(activityId)
    } catch (err) {
        error.value = err.message
        loading.value = false
    }
})
</script>