Skip to content

Commit c4a08fe

Browse files
committed
refactor(reservation): add suspense skeleton for reservation page
1 parent 9068019 commit c4a08fe

File tree

2 files changed

+332
-273
lines changed

2 files changed

+332
-273
lines changed
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
<script setup lang="ts">
2+
import type { ClassroomData } from '@prisma/client'
3+
import { Button } from '@/components/ui/button'
4+
import {
5+
Card,
6+
CardContent,
7+
CardDescription,
8+
CardHeader,
9+
CardTitle,
10+
} from '@/components/ui/card'
11+
import { useToast } from '@/components/ui/toast/use-toast'
12+
import { LoaderCircle } from 'lucide-vue-next'
13+
import { enums } from '~/components/custom/enum2str'
14+
import type { AllClubs } from '~/types/api/user/all_clubs'
15+
16+
definePageMeta({
17+
middleware: ['auth'],
18+
})
19+
20+
useHead({
21+
title: 'Classroom Reservation | Enspire',
22+
})
23+
24+
const { toast } = useToast()
25+
26+
const { data } = await useAsyncData<ClassroomData[]>('classroomStatuses', () => {
27+
return $fetch<ClassroomData[]>(`/api/reservation/classroomId`, {
28+
headers: useRequestHeaders(),
29+
method: 'GET',
30+
})
31+
})
32+
33+
if (!data.value) {
34+
toast({
35+
title: '错误',
36+
description: '获取教室信息出错',
37+
})
38+
}
39+
else {
40+
data.value = data.value.sort((a: any, b: any) => a.name < b.name ? -1 : 1)
41+
}
42+
43+
const { data: clubs } = await useAsyncData<AllClubs>('clubs', () => {
44+
return $fetch<AllClubs>(`/api/user/all_clubs`, {
45+
headers: useRequestHeaders(),
46+
method: 'GET',
47+
})
48+
})
49+
50+
if (!clubs.value) {
51+
toast({
52+
title: '错误',
53+
description: '获取社团信息出错',
54+
})
55+
}
56+
57+
let reloadKey = 0
58+
59+
const day = ref([false, false, false, false, false, false, false])
60+
61+
const pending = ref(false)
62+
63+
const formData = ref({
64+
day: computed(() => {
65+
let r = ''
66+
day.value.forEach((e) => {
67+
r += e ? '1' : '0'
68+
})
69+
return r
70+
}), // Sunday ~ Saturday
71+
period: '',
72+
classroom: '',
73+
applicant: '',
74+
note: '',
75+
})
76+
77+
async function handleSubmit(e: any) {
78+
e.preventDefault()
79+
if (pending.value) {
80+
toast({
81+
description: '请等待前一个提交完成',
82+
})
83+
return
84+
}
85+
if (formData.value.day === '0000000') {
86+
toast({
87+
description: '请填写完整预约时间',
88+
})
89+
return
90+
}
91+
pending.value = true
92+
try {
93+
const { data, error } = await useFetch('/api/reservation/new', {
94+
method: 'POST',
95+
headers: {
96+
'Content-Type': 'application/json',
97+
},
98+
body: JSON.stringify(formData.value),
99+
})
100+
if (error.value) {
101+
toast({
102+
title: '发生错误',
103+
description: error.value.data,
104+
variant: 'destructive',
105+
})
106+
}
107+
else if (data.value?.status === 'SUCCESS') {
108+
toast({
109+
title: '创建成功',
110+
description: '已成功创建预约记录,你可以在「管理预约」中查看',
111+
})
112+
// A VERY BAD PRACTICE TO RELOAD THE FORM
113+
// TODO: TO BE FIXED
114+
reloadKey++
115+
day.value = [false, false, false, false, false, false, false]
116+
formData.value.period = ''
117+
formData.value.classroom = ''
118+
formData.value.applicant = ''
119+
formData.value.note = ''
120+
}
121+
else if (data.value?.status === 'PRISMA_ERROR') {
122+
toast({
123+
title: '数据错误',
124+
description: '请稍后再试',
125+
variant: 'destructive',
126+
})
127+
}
128+
}
129+
catch (error) {
130+
toast({
131+
title: 'Error',
132+
description: error,
133+
variant: 'destructive',
134+
})
135+
}
136+
pending.value = false
137+
}
138+
</script>
139+
140+
<template>
141+
<Card>
142+
<CardHeader>
143+
<CardTitle>预约教室</CardTitle>
144+
<CardDescription>在此处预约教室</CardDescription>
145+
</CardHeader>
146+
<CardContent>
147+
<form class="space-y-2" @submit="handleSubmit">
148+
<FormField name="main">
149+
<FormItem>
150+
<FormLabel>预约时间</FormLabel>
151+
<FormControl>
152+
<div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-1 justify-start">
153+
<!-- This ToggleGroup should be implemented in a better way but anyway it works -->
154+
<ToggleGroup :key="reloadKey" type="multiple" variant="outline">
155+
<div class="text-muted-foreground text-sm text-center w-7">
156+
每周
157+
</div>
158+
<ToggleGroupItem value="mon" @click="day[1] = !day[1]">
159+
160+
</ToggleGroupItem>
161+
<ToggleGroupItem value="tue" @click="day[2] = !day[2]">
162+
163+
</ToggleGroupItem>
164+
<ToggleGroupItem value="wed" @click="day[3] = !day[3]">
165+
166+
</ToggleGroupItem>
167+
<ToggleGroupItem value="thu" @click="day[4] = !day[4]">
168+
169+
</ToggleGroupItem>
170+
<ToggleGroupItem value="fri" @click="day[5] = !day[5]">
171+
172+
</ToggleGroupItem>
173+
</ToggleGroup>
174+
<Select v-model="formData.period" required>
175+
<SelectTrigger class="lg:ml-3">
176+
<SelectValue placeholder="选择时段" />
177+
</SelectTrigger>
178+
<SelectContent>
179+
<SelectItem v-for="period in enums.periods.values" :key="period" :value="period">
180+
{{ enums.periods.map[period] }}
181+
</SelectItem>
182+
</SelectContent>
183+
</Select>
184+
</div>
185+
</FormControl>
186+
</FormItem>
187+
<div class="py-1" />
188+
<FormItem>
189+
<FormLabel>选择教室</FormLabel>
190+
<div v-if="!clubs || !data">
191+
<Skeleton class="h-5 p-3 my-3" />
192+
</div>
193+
<FormControl>
194+
<Select v-model="formData.classroom" required>
195+
<SelectTrigger>
196+
<SelectValue placeholder="选择教室" />
197+
</SelectTrigger>
198+
<!-- Only available classrooms should be filled in the following <SelectContent/> -->
199+
<SelectContent>
200+
<SelectGroup v-for="classroom in data" :key="classroom.id">
201+
<SelectItem :value="classroom.id.toString()">
202+
<span class="inline-block min-w-32 text-left">
203+
<span class="inline-block min-w-14">
204+
{{ classroom.name }}
205+
</span>
206+
<span class="inline-block text-gray-500">
207+
{{ classroom.alias }}
208+
</span>
209+
</span>
210+
<span v-if="classroom.size" class="text-gray-400">
211+
可容纳<span class="inline-block w-5 text-center">{{ classroom.size }}</span>人
212+
</span>
213+
</SelectItem>
214+
</SelectGroup>
215+
</SelectContent>
216+
</Select>
217+
</FormControl>
218+
</FormItem>
219+
<div class="py-1" />
220+
<FormItem>
221+
<FormLabel>社团</FormLabel>
222+
<FormControl>
223+
<Select v-model="formData.applicant" required>
224+
<SelectTrigger>
225+
<SelectValue placeholder="选择你的社团" />
226+
</SelectTrigger>
227+
<SelectContent>
228+
<SelectGroup v-if="clubs?.president.length">
229+
<SelectItem v-for="club in clubs.president" :key="club.id" :value="club.id">
230+
{{ club.name.zh }}
231+
<span class="inline-block text-gray-500">
232+
社长
233+
</span>
234+
</SelectItem>
235+
</SelectGroup>
236+
<SelectGroup v-if="clubs?.vice.length">
237+
<SelectItem v-for="club in clubs?.vice" :key="club.id" :value="club.id">
238+
{{ club.name.zh }}
239+
<span class="inline-block text-gray-500">
240+
副社
241+
</span>
242+
</SelectItem>
243+
</SelectGroup>
244+
<SelectGroup v-if="clubs?.member.length">
245+
<SelectItem v-for="club in clubs?.member" :key="club.id" :value="club.id">
246+
{{ club.name.zh }}
247+
<span class="inline-block text-gray-500">
248+
成员
249+
</span>
250+
</SelectItem>
251+
</SelectGroup>
252+
</SelectContent>
253+
</Select>
254+
</FormControl>
255+
<FormMessage />
256+
</FormItem>
257+
<div class="py-1" />
258+
<FormItem>
259+
<FormLabel>备注</FormLabel>
260+
<FormControl>
261+
<Textarea v-model="formData.note" type="text" placeholder="备注" class="resize-none" rows="3" />
262+
</FormControl>
263+
<FormDescription>
264+
非必填
265+
</FormDescription>
266+
<FormMessage />
267+
</FormItem>
268+
</FormField>
269+
<div class="py-2" />
270+
<Button type="submit" :disabled="pending" class="mr-3">
271+
<LoaderCircle v-if="pending" class="animate-spin mr-2" />
272+
<span v-if="!pending">提交预约</span>
273+
<span v-if="pending">处理中...</span>
274+
</Button>
275+
</form>
276+
</CardContent>
277+
</Card>
278+
</template>

0 commit comments

Comments
 (0)