hookehuyr

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

新增活动报名页面,允许用户填写报名信息并提交。同时更新活动详情页,添加报名按钮跳转到报名页面。
...@@ -64,6 +64,14 @@ const routes = [ ...@@ -64,6 +64,14 @@ const routes = [
64 meta: { title: '活动详情' } 64 meta: { title: '活动详情' }
65 }, 65 },
66 { 66 {
67 + path: '/activities/:id/signup',
68 + name: 'ActivitySignup',
69 + component: () => import('../views/activities/ActivitySignupPage.vue'),
70 + meta: {
71 + title: '活动报名'
72 + }
73 + },
74 + {
67 path: '/checkout', 75 path: '/checkout',
68 name: 'CheckoutPage', 76 name: 'CheckoutPage',
69 component: () => import('../views/checkout/CheckoutPage.vue'), 77 component: () => import('../views/checkout/CheckoutPage.vue'),
......
...@@ -109,7 +109,7 @@ const RightContent = defineComponent({ ...@@ -109,7 +109,7 @@ const RightContent = defineComponent({
109 </script> 109 </script>
110 110
111 <template> 111 <template>
112 - <AppLayout title="活动详情" :showBackButton="false" :rightContent="RightContent"> 112 + <AppLayout title="活动详情" :rightContent="RightContent" @back="router.back()">
113 <div class="pb-24"> 113 <div class="pb-24">
114 <!-- Activity Cover Image --> 114 <!-- Activity Cover Image -->
115 <div class="w-full h-56 relative"> 115 <div class="w-full h-56 relative">
...@@ -431,6 +431,7 @@ const RightContent = defineComponent({ ...@@ -431,6 +431,7 @@ const RightContent = defineComponent({
431 </button> 431 </button>
432 <button 432 <button
433 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" 433 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"
434 + @click="router.push(`/activities/${activity.id}/signup`)"
434 > 435 >
435 立即报名 436 立即报名
436 </button> 437 </button>
......
1 +<template>
2 + <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">
3 + <div class="sm:mx-auto sm:w-full sm:max-w-md">
4 + <h2 class="text-center text-2xl font-bold text-gray-800 mb-2">活动报名</h2>
5 + <p class="text-center text-gray-600">{{ activity?.title }}</p>
6 + </div>
7 +
8 + <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
9 + <FrostedGlass class="py-8 px-6 rounded-lg">
10 + <!-- 活动基本信息 -->
11 + <div class="mb-6 space-y-4">
12 + <div class="flex items-center justify-between text-sm">
13 + <span class="text-gray-500">活动时间</span>
14 + <span class="text-gray-700">{{ activity?.period }}</span>
15 + </div>
16 + <div class="flex items-center justify-between text-sm">
17 + <span class="text-gray-500">活动地点</span>
18 + <span class="text-gray-700">{{ activity?.location }}</span>
19 + </div>
20 + <div class="flex items-center justify-between text-sm">
21 + <span class="text-gray-500">活动费用</span>
22 + <span class="text-red-500 font-medium">¥{{ activity?.price }}/人</span>
23 + </div>
24 + </div>
25 +
26 + <div v-if="error" class="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-md">
27 + {{ error }}
28 + </div>
29 +
30 + <form class="space-y-6" @submit.prevent="handleSubmit">
31 + <div>
32 + <label for="name" class="block text-sm font-medium text-gray-700">
33 + 联系人姓名 <span class="text-red-500">*</span>
34 + </label>
35 + <input
36 + id="name"
37 + v-model="formData.name"
38 + type="text"
39 + required
40 + 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"
41 + />
42 + </div>
43 +
44 + <div>
45 + <label for="phone" class="block text-sm font-medium text-gray-700">
46 + 联系电话 <span class="text-red-500">*</span>
47 + </label>
48 + <input
49 + id="phone"
50 + v-model="formData.phone"
51 + type="tel"
52 + required
53 + 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"
54 + />
55 + </div>
56 +
57 + <div>
58 + <label for="participants" class="block text-sm font-medium text-gray-700">
59 + 参与人数 <span class="text-red-500">*</span>
60 + </label>
61 + <input
62 + id="participants"
63 + v-model="formData.participants"
64 + type="number"
65 + min="1"
66 + :max="activity?.maxParticipants - activity?.participantsCount"
67 + required
68 + 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"
69 + />
70 + </div>
71 +
72 + <div>
73 + <label for="remark" class="block text-sm font-medium text-gray-700">
74 + 备注信息
75 + </label>
76 + <textarea
77 + id="remark"
78 + v-model="formData.remark"
79 + rows="3"
80 + 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"
81 + ></textarea>
82 + </div>
83 +
84 + <div class="flex items-center">
85 + <input
86 + id="agreeTerms"
87 + v-model="formData.agreeTerms"
88 + type="checkbox"
89 + class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
90 + />
91 + <label for="agreeTerms" class="ml-2 block text-sm text-gray-700">
92 + 我已阅读并同意 <a href="#" class="text-green-600 hover:text-green-500">活动协议</a>
93 + </label>
94 + </div>
95 +
96 + <div class="space-y-2">
97 + <div class="flex items-center justify-between text-sm">
98 + <span class="text-gray-500">费用合计</span>
99 + <span class="text-red-500 font-bold">¥{{ totalAmount }}</span>
100 + </div>
101 + <button
102 + type="submit"
103 + :disabled="loading"
104 + 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"
105 + :class="{ 'opacity-70 cursor-not-allowed': loading }"
106 + >
107 + {{ loading ? '提交中...' : '提交报名' }}
108 + </button>
109 + </div>
110 + </form>
111 + </FrostedGlass>
112 + </div>
113 + </div>
114 +</template>
115 +
116 +<script setup>
117 +import { ref, reactive, computed, onMounted } from 'vue'
118 +import { useRoute, useRouter } from 'vue-router'
119 +import FrostedGlass from '@/components/ui/FrostedGlass.vue'
120 +import { activities } from '@/utils/mockData'
121 +import { useTitle } from '@vueuse/core';
122 +const $route = useRoute();
123 +const $router = useRouter();
124 +useTitle($route.meta.title);
125 +
126 +const route = useRoute()
127 +const router = useRouter()
128 +const activity = ref(null)
129 +
130 +const formData = reactive({
131 + name: '',
132 + phone: '',
133 + participants: 1,
134 + remark: '',
135 + agreeTerms: false
136 +})
137 +
138 +const error = ref('')
139 +const loading = ref(false)
140 +
141 +// 获取活动数据
142 +onMounted(() => {
143 + const id = route.params.id
144 + const foundActivity = activities.find((a) => a.id === id)
145 + if (foundActivity) {
146 + activity.value = foundActivity
147 + } else {
148 + router.push('/activities')
149 + }
150 +})
151 +
152 +// 计算总金额
153 +const totalAmount = computed(() => {
154 + return activity.value ? activity.value.price * formData.participants : 0
155 +})
156 +
157 +const handleSubmit = async () => {
158 + if (!formData.name || !formData.phone || !formData.participants) {
159 + error.value = '请填写所有必填字段'
160 + return
161 + }
162 +
163 + if (!formData.agreeTerms) {
164 + error.value = '请阅读并同意活动协议'
165 + return
166 + }
167 +
168 + try {
169 + error.value = ''
170 + loading.value = true
171 +
172 + // TODO: 实现报名和支付逻辑
173 + await new Promise(resolve => setTimeout(resolve, 1000))
174 +
175 + // 报名成功后跳转回活动详情页,使用replace避免返回到报名页
176 + router.replace(`/activities/${route.params.id}`)
177 + } catch (e) {
178 + error.value = '报名失败,请稍后重试'
179 + } finally {
180 + loading.value = false
181 + }
182 +}
183 +</script>