hookehuyr

feat(活动): 添加活动报名页面及路由

新增活动报名页面,允许用户填写报名信息并提交。同时更新活动详情页,添加报名按钮跳转到报名页面。
......@@ -64,6 +64,14 @@ const routes = [
meta: { title: '活动详情' }
},
{
path: '/activities/:id/signup',
name: 'ActivitySignup',
component: () => import('../views/activities/ActivitySignupPage.vue'),
meta: {
title: '活动报名'
}
},
{
path: '/checkout',
name: 'CheckoutPage',
component: () => import('../views/checkout/CheckoutPage.vue'),
......
......@@ -109,7 +109,7 @@ const RightContent = defineComponent({
</script>
<template>
<AppLayout title="活动详情" :showBackButton="false" :rightContent="RightContent">
<AppLayout title="活动详情" :rightContent="RightContent" @back="router.back()">
<div class="pb-24">
<!-- Activity Cover Image -->
<div class="w-full h-56 relative">
......@@ -431,6 +431,7 @@ const RightContent = defineComponent({
</button>
<button
class="px-6 py-2 bg-gradient-to-r from-green-500 to-green-600 text-white rounded-full text-sm font-medium shadow-md"
@click="router.push(`/activities/${activity.id}/signup`)"
>
立即报名
</button>
......
<template>
<div class="min-h-screen flex flex-col bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="text-center text-2xl font-bold text-gray-800 mb-2">活动报名</h2>
<p class="text-center text-gray-600">{{ activity?.title }}</p>
</div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<FrostedGlass class="py-8 px-6 rounded-lg">
<!-- 活动基本信息 -->
<div class="mb-6 space-y-4">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-500">活动时间</span>
<span class="text-gray-700">{{ activity?.period }}</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-gray-500">活动地点</span>
<span class="text-gray-700">{{ activity?.location }}</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-gray-500">活动费用</span>
<span class="text-red-500 font-medium">¥{{ activity?.price }}/人</span>
</div>
</div>
<div v-if="error" class="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-md">
{{ error }}
</div>
<form class="space-y-6" @submit.prevent="handleSubmit">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">
联系人姓名 <span class="text-red-500">*</span>
</label>
<input
id="name"
v-model="formData.name"
type="text"
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
/>
</div>
<div>
<label for="phone" class="block text-sm font-medium text-gray-700">
联系电话 <span class="text-red-500">*</span>
</label>
<input
id="phone"
v-model="formData.phone"
type="tel"
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
/>
</div>
<div>
<label for="participants" class="block text-sm font-medium text-gray-700">
参与人数 <span class="text-red-500">*</span>
</label>
<input
id="participants"
v-model="formData.participants"
type="number"
min="1"
:max="activity?.maxParticipants - activity?.participantsCount"
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
/>
</div>
<div>
<label for="remark" class="block text-sm font-medium text-gray-700">
备注信息
</label>
<textarea
id="remark"
v-model="formData.remark"
rows="3"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
></textarea>
</div>
<div class="flex items-center">
<input
id="agreeTerms"
v-model="formData.agreeTerms"
type="checkbox"
class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
/>
<label for="agreeTerms" class="ml-2 block text-sm text-gray-700">
我已阅读并同意 <a href="#" class="text-green-600 hover:text-green-500">活动协议</a>
</label>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-500">费用合计</span>
<span class="text-red-500 font-bold">¥{{ totalAmount }}</span>
</div>
<button
type="submit"
:disabled="loading"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
:class="{ 'opacity-70 cursor-not-allowed': loading }"
>
{{ loading ? '提交中...' : '提交报名' }}
</button>
</div>
</form>
</FrostedGlass>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import FrostedGlass from '@/components/ui/FrostedGlass.vue'
import { activities } from '@/utils/mockData'
import { useTitle } from '@vueuse/core';
const $route = useRoute();
const $router = useRouter();
useTitle($route.meta.title);
const route = useRoute()
const router = useRouter()
const activity = ref(null)
const formData = reactive({
name: '',
phone: '',
participants: 1,
remark: '',
agreeTerms: false
})
const error = ref('')
const loading = ref(false)
// 获取活动数据
onMounted(() => {
const id = route.params.id
const foundActivity = activities.find((a) => a.id === id)
if (foundActivity) {
activity.value = foundActivity
} else {
router.push('/activities')
}
})
// 计算总金额
const totalAmount = computed(() => {
return activity.value ? activity.value.price * formData.participants : 0
})
const handleSubmit = async () => {
if (!formData.name || !formData.phone || !formData.participants) {
error.value = '请填写所有必填字段'
return
}
if (!formData.agreeTerms) {
error.value = '请阅读并同意活动协议'
return
}
try {
error.value = ''
loading.value = true
// TODO: 实现报名和支付逻辑
await new Promise(resolve => setTimeout(resolve, 1000))
// 报名成功后跳转回活动详情页,使用replace避免返回到报名页
router.replace(`/activities/${route.params.id}`)
} catch (e) {
error.value = '报名失败,请稍后重试'
} finally {
loading.value = false
}
}
</script>