Registration.vue
15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
<template>
<div class="min-h-screen bg-gray-50">
<!-- Loading State -->
<div v-if="loading" class="min-h-screen flex justify-center items-center bg-gray-50">
<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="$router.push('/')" variant="primary">返回首页</Button>
</div>
<!-- Registration Closed State -->
<div v-else-if="!isRegistrationOpen"
class="min-h-screen flex flex-col justify-center items-center bg-gray-50 px-4">
<div class="text-yellow-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">该活动的报名时间为 {{ formatDate(activity.registration_start) }} 至 {{
formatDate(activity.registration_end) }}</p>
<Button @click="$router.push(`/activity/${activityId}`)" variant="primary">查看活动详情</Button>
</div>
<!-- Full State -->
<div v-else-if="isFull" class="min-h-screen flex flex-col justify-center items-center bg-gray-50 px-4">
<div class="text-blue-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="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h1 class="text-2xl font-bold mb-2">名额已满</h1>
<p class="text-gray-600 mb-6">该活动名额已满,请关注其他精彩活动</p>
<div class="flex space-x-4">
<Button @click="$router.push(`/activity/${activityId}`)" variant="secondary">查看活动详情</Button>
<Button @click="$router.push('/')" variant="primary">浏览更多活动</Button>
</div>
</div>
<!-- Registration Form -->
<div v-else class="py-8">
<div class="container mx-auto px-4">
<div class="max-w-3xl mx-auto bg-white rounded-lg shadow-sm overflow-hidden">
<!-- Header -->
<div class="bg-gradient-to-r from-green-500 to-blue-500 px-6 py-4">
<h1 class="text-2xl font-bold text-white">活动报名</h1>
</div>
<!-- Activity Info -->
<div class="border-b border-gray-200 bg-gray-50 px-6 py-4">
<h2 class="text-xl font-semibold text-gray-800 mb-2">{{ activity.title }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600">
<div class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500 mr-2" 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>
<span>活动时间:{{ formatDate(activity.start_time) }}</span>
</div>
<div class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500 mr-2" 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>
<span>{{ activity.activity_type === 'online' ? '线上活动' : `活动地点:${activity.location}`
}}</span>
</div>
<div class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500 mr-2" 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>
<span>已报名:{{ activity.participant_count }}/{{ activity.max_participants }}</span>
</div>
<div class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500 mr-2" 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>
<span>报名截止:{{ formatDate(activity.registration_end) }}</span>
</div>
</div>
</div>
<!-- Registration Form -->
<form class="p-6" @submit.prevent="handleSubmit">
<h3 class="text-lg font-medium text-gray-900 mb-4">填写报名信息</h3>
<div class="space-y-6">
<Input id="name" label="姓名" v-model="formState.name" :error="formErrors.name"
placeholder="请输入您的姓名" required />
<Input id="phone" label="手机号码" v-model="formState.phone" :error="formErrors.phone"
placeholder="请输入您的手机号码" required />
<Input id="email" label="电子邮箱" type="email" v-model="formState.email"
:error="formErrors.email" placeholder="请输入您的电子邮箱" required />
<div>
<label for="reason" class="block text-sm font-medium text-gray-700 mb-1">
参加原因 <span class="text-red-500">*</span>
</label>
<textarea id="reason" v-model="formState.reason" rows="4" :class="[
'block w-full border-gray-300 rounded-md shadow-sm focus:ring-green-500 focus:border-green-500 sm:text-sm',
formErrors.reason ? 'border-red-300' : ''
]" placeholder="请简要说明您参加本次活动的原因或期望..."></textarea>
<p v-if="formErrors.reason" class="mt-1 text-sm text-red-600">{{ formErrors.reason }}
</p>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="agreeTerms" type="checkbox" v-model="formState.agreeTerms"
class="focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300 rounded" />
</div>
<div class="ml-3 text-sm">
<label for="agreeTerms"
:class="['font-medium', formErrors.agreeTerms ? 'text-red-600' : 'text-gray-700']">
我同意活动条款和隐私政策
</label>
<p class="text-gray-500">
注册即表示您同意我们的
<a href="#" class="text-green-600 hover:text-green-500">服务条款</a>和
<a href="#" class="text-green-600 hover:text-green-500">隐私政策</a>。
</p>
<p v-if="formErrors.agreeTerms" class="mt-1 text-sm text-red-600">{{
formErrors.agreeTerms }}</p>
</div>
</div>
</div>
<div class="mt-8 flex justify-between">
<Button type="button" variant="secondary" @click="$router.push(`/activity/${activityId}`)">
返回活动详情
</Button>
<Button type="submit" variant="primary" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : '确认报名' }}
</Button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import activitiesData from '../data/activities.json'
import registrationsData from '../data/registrations.json'
import Button from '../components/shared/Button.vue'
import Input from '../components/shared/Input.vue'
const route = useRoute()
const router = useRouter()
const activities = ref(activitiesData.activities)
const registrations = ref(registrationsData.registrations)
const activityId = route.params.activityId
const activity = ref(null)
const loading = ref(true)
const error = ref(null)
const isSubmitting = ref(false)
const formState = ref({
name: '',
phone: '',
email: '',
reason: '',
agreeTerms: false
})
const formErrors = ref({})
// Fetch activity details
onMounted(async () => {
try {
const foundActivity = activities.value.find(a => a.id === activityId)
if (foundActivity) {
activity.value = foundActivity
// Pre-fill form with user data if available
if (store.currentUser) {
formState.value = {
...formState.value,
name: store.currentUser.name || '',
phone: store.currentUser.phone || '',
email: store.currentUser.email || ''
}
}
} else {
error.value = '未找到活动信息'
}
loading.value = false
} catch (err) {
console.error('Failed to fetch activity details:', err)
error.value = '加载活动详情失败'
loading.value = false
}
})
// Validate form
const validateForm = () => {
const errors = {}
if (!formState.value.name.trim()) {
errors.name = '请输入您的姓名'
}
if (!formState.value.phone.trim()) {
errors.phone = '请输入您的手机号码'
} else if (!/^1[3-9]\d{9}$/.test(formState.value.phone)) {
errors.phone = '请输入有效的手机号码'
}
if (!formState.value.email.trim()) {
errors.email = '请输入您的电子邮箱'
} else if (!/\S+@\S+\.\S+/.test(formState.value.email)) {
errors.email = '请输入有效的电子邮箱'
}
if (!formState.value.reason.trim()) {
errors.reason = '请简要说明参加原因'
}
if (!formState.value.agreeTerms) {
errors.agreeTerms = '请同意活动条款和隐私政策'
}
formErrors.value = errors
return Object.keys(errors).length === 0
}
// Handle form submission
const handleSubmit = async () => {
if (!store.currentUser) {
alert('请先登录再进行报名')
// Redirect to login page in a real app
return
}
if (!validateForm()) {
// Focus on first error field
const firstErrorField = Object.keys(formErrors.value)[0]
const element = document.getElementById(firstErrorField)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
return
}
isSubmitting.value = true
try {
const registrationData = {
fields: {
姓名: true,
手机号: true,
邮箱: true,
参加原因: true
},
answers: {
姓名: formState.value.name,
手机号: formState.value.phone,
邮箱: formState.value.email,
参加原因: formState.value.reason
}
}
const result = await store.registerForActivity(activityId, registrationData)
if (result.success) {
alert('报名成功,请等待审核')
router.push(`/activity/${activityId}`)
} else {
alert(`报名失败: ${result.error}`)
isSubmitting.value = false
}
} catch (error) {
console.error('Registration error:', error)
alert('报名失败,请稍后再试')
isSubmitting.value = false
}
}
// Format date for display
const formatDate = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString.replace(' ', 'T'))
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
}).format(date)
}
// Check if registration is still open
const isRegistrationOpen = computed(() => {
if (!activity.value) return false
return (
new Date() >= new Date(activity.value.registration_start.replace(' ', 'T')) &&
new Date() <= new Date(activity.value.registration_end.replace(' ', 'T'))
)
})
// Check if activity is full
const isFull = computed(() => {
if (!activity.value) return false
return activity.value.participant_count >= activity.value.max_participants
})
</script>