Skip to content

Commit 29cf5bf

Browse files
authored
feat: admin panel frontend (#609)
1 parent e155931 commit 29cf5bf

File tree

6 files changed

+349
-58
lines changed

6 files changed

+349
-58
lines changed

components/custom/sidebar.vue

+13
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ import { Button } from '@/components/ui/button'
6666
</NuxtLink>
6767
</div>
6868
</div>
69+
<div class="px-3 py-2">
70+
<h2 class="relative px-4 text-lg font-semibold tracking-tight">
71+
管理员
72+
</h2>
73+
<div class="mt-2">
74+
<NuxtLink to="/admin/reservation">
75+
<Button :variant="$route.name === 'admin-reservation' ? 'secondary' : 'ghost'" class="w-full justify-start">
76+
<Icon class="mr-2 h-4 w-4" name="material-symbols:calendar-today-outline" />
77+
管理预约
78+
</Button>
79+
</NuxtLink>
80+
</div>
81+
</div>
6982
<div class="py-2 px-3">
7083
<h2 class="relative px-4 text-lg font-semibold tracking-tight">
7184
信息

pages/admin/reservation.vue

+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<script setup lang="ts">
2+
import { format, formatDistanceToNow, parse } from 'date-fns'
3+
import { zhCN } from 'date-fns/locale'
4+
import { LoaderCircle } from 'lucide-vue-next'
5+
import { VueElement, onMounted } from 'vue'
6+
import { enums } from '~/components/custom/enum2str'
7+
import { useToast } from '@/components/ui/toast/use-toast'
8+
import Toaster from '@/components/ui/toast/Toaster.vue'
9+
10+
definePageMeta({
11+
middleware: ['auth'],
12+
})
13+
14+
const { toast } = useToast()
15+
16+
enum Statuses {
17+
LOADING,
18+
ERROR,
19+
READY,
20+
}
21+
22+
const status = ref(Statuses.LOADING)
23+
let content: any[] = []
24+
25+
async function load() {
26+
status.value = Statuses.LOADING
27+
try {
28+
const data = await $fetch('/api/reservation/all-admin')
29+
if (!data) {
30+
status.value = Statuses.ERROR
31+
}
32+
else {
33+
status.value = Statuses.READY
34+
const response = JSON.parse(data)
35+
content = response.data
36+
}
37+
}
38+
catch (error) {
39+
status.value = Statuses.ERROR
40+
}
41+
}
42+
43+
const dlgOpen = ref(false)
44+
const alertContent = {
45+
title: '是否确认操作?',
46+
message: '',
47+
id: -1,
48+
action: 'UNKNOWN',
49+
}
50+
const managePending = ref(false)
51+
52+
function confirmManage(id: number, action: string, currentStatus = '') {
53+
if (action === 'DELALL') {
54+
alertContent.message = '将会删除全部预约记录,此操作不可撤销。'
55+
}
56+
else {
57+
alertContent.message = `将对 预约#${id} 执行以下操作: `
58+
if (action === 'DELETE') {
59+
alertContent.message += '撤销'
60+
}
61+
}
62+
alertContent.id = id
63+
alertContent.action = action
64+
dlgOpen.value = true
65+
}
66+
67+
async function manage() {
68+
managePending.value = true
69+
try {
70+
const data = await $fetch('/api/reservation/manage', {
71+
query: {
72+
id: alertContent.id,
73+
action: alertContent.action,
74+
admin: true,
75+
key: '',
76+
},
77+
})
78+
await load()
79+
}
80+
catch (error: any) {
81+
toast({
82+
description: error.data.message,
83+
variant: 'destructive',
84+
})
85+
}
86+
managePending.value = false
87+
dlgOpen.value = false
88+
}
89+
90+
onMounted(async () => {
91+
await load()
92+
})
93+
</script>
94+
95+
<template>
96+
<Alert variant="destructive">
97+
<AlertDescription>
98+
警告:这是一个管理员页面。此页面中的所有操作均有最高权限,你可以删除任何存在的记录。进行操作前务必再次确认,操作不可撤销!
99+
</AlertDescription>
100+
</Alert>
101+
<AlertDialog v-model:open="dlgOpen">
102+
<AlertDialogContent>
103+
<AlertDialogHeader>
104+
<AlertDialogTitle>{{ alertContent.title }}</AlertDialogTitle>
105+
<AlertDialogDescription>{{ alertContent.message }}</AlertDialogDescription>
106+
</AlertDialogHeader>
107+
<AlertDialogFooter>
108+
<AlertDialogCancel :disabled="managePending">
109+
取消
110+
</AlertDialogCancel>
111+
<Button :disabled="managePending" @click="manage()">
112+
<LoaderCircle v-if="managePending" class="animate-spin mr-2" />
113+
<span v-if="!managePending">确认</span>
114+
<span v-if="managePending">处理中...</span>
115+
</Button>
116+
</AlertDialogFooter>
117+
</AlertDialogContent>
118+
</AlertDialog>
119+
<Table class="mt-4">
120+
<TableHeader>
121+
<TableRow>
122+
<TableHead class="w-16">
123+
编号
124+
</TableHead>
125+
<TableHead>提交时间</TableHead>
126+
<TableHead>申请者</TableHead>
127+
<TableHead>社团</TableHead>
128+
<TableHead>日期</TableHead>
129+
<TableHead>时间</TableHead>
130+
<TableHead>教室</TableHead>
131+
<TableHead>备注</TableHead>
132+
<TableHead>
133+
<Skeleton v-if="status === Statuses.LOADING" class="h-5 my-2" />
134+
<!-- TODO: This is very dangerous and its behavior must be changed in the future -->
135+
<Button v-if="status === Statuses.READY" variant="link" class="p-0 text-red-500" @click="confirmManage(-1, 'DELALL')">
136+
清空全部
137+
</Button>
138+
</TableHead>
139+
</TableRow>
140+
</TableHeader>
141+
<TableBody v-if="status === Statuses.LOADING">
142+
<TableRow v-for="i in 5" :key="i">
143+
<TableCell v-for="j in 9" :key="j">
144+
<Skeleton class="h-5 my-2" />
145+
</TableCell>
146+
</TableRow>
147+
</TableBody>
148+
<TableBody v-if="status === Statuses.READY">
149+
<TableRow v-for="record in content" :key="record.id">
150+
<TableCell class="text-muted-foreground">
151+
#{{ record.id }}
152+
</TableCell>
153+
<TableCell>{{ formatDistanceToNow(new Date(record.creationTimestamp), { locale: zhCN, addSuffix: true }) }}</TableCell>
154+
<TableCell>{{ record.user.name }}</TableCell>
155+
<TableCell>{{ record.club.name.zh }}</TableCell>
156+
<TableCell>{{ enums.days.map[record.day] }}</TableCell>
157+
<TableCell>{{ enums.periods.map[record.period] }}</TableCell>
158+
<TableCell>{{ record.classroom.name }}</TableCell>
159+
<TableCell>
160+
{{ record.note ? '' : '–' }}
161+
<HoverCard v-if="record.note">
162+
<HoverCardTrigger>
163+
<Button variant="link" class="text-blue-500">
164+
查看
165+
</Button>
166+
</HoverCardTrigger>
167+
<HoverCardContent>
168+
{{ record.note }}
169+
</HoverCardContent>
170+
</HoverCard>
171+
</TableCell>
172+
<TableCell class="text-left">
173+
<div v-if="true" class="text-red-500">
174+
<Button variant="link" class="p-0 text-red-500" @click="confirmManage(record.id, 'DELETE')">
175+
撤销
176+
</Button>
177+
</div>
178+
</TableCell>
179+
</TableRow>
180+
</TableBody>
181+
</Table>
182+
<div v-if="status === Statuses.READY && !content.length" class="w-1/3 my-2 mx-auto text-center">
183+
<Alert>
184+
暂无记录
185+
</Alert>
186+
</div>
187+
<div v-if="status === Statuses.ERROR" class="w-1/3 my-2 mx-auto text-center">
188+
<Alert variant="destructive">
189+
加载失败
190+
</Alert>
191+
</div>
192+
<Toaster />
193+
</template>

pages/manage/manage.vue

+15-17
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ import { zhCN } from 'date-fns/locale'
44
import { LoaderCircle } from 'lucide-vue-next'
55
import { VueElement, onMounted } from 'vue'
66
import { enums } from '~/components/custom/enum2str'
7+
import { useToast } from '@/components/ui/toast/use-toast'
8+
import Toaster from '@/components/ui/toast/Toaster.vue'
79
810
definePageMeta({
911
middleware: ['auth'],
1012
})
1113
14+
const { toast } = useToast()
15+
1216
enum Statuses {
1317
LOADING,
1418
ERROR,
@@ -46,14 +50,9 @@ const alertContent = {
4650
const managePending = ref(false)
4751
4852
function confirmManage(id: number, action: string, currentStatus = '') {
49-
if (action === 'DELALL') {
50-
alertContent.message = '将会删除全部预约记录,此操作不可撤销。'
51-
}
52-
else {
53-
alertContent.message = `将对 预约#${id} 执行以下操作: `
54-
if (action === 'DELETE') {
55-
alertContent.message += '撤销'
56-
}
53+
alertContent.message = `将对 预约#${id} 执行以下操作: `
54+
if (action === 'DELETE') {
55+
alertContent.message += '撤销'
5756
}
5857
alertContent.id = id
5958
alertContent.action = action
@@ -67,12 +66,16 @@ async function manage() {
6766
query: {
6867
id: alertContent.id,
6968
action: alertContent.action,
69+
admin: false,
7070
},
7171
})
7272
await load()
7373
}
74-
catch (error) {
75-
console.log(error.message)
74+
catch (error: any) {
75+
toast({
76+
description: error.data.message,
77+
variant: 'destructive',
78+
})
7679
}
7780
managePending.value = false
7881
dlgOpen.value = false
@@ -115,13 +118,7 @@ onMounted(async () => {
115118
<TableHead>时间</TableHead>
116119
<TableHead>教室</TableHead>
117120
<TableHead>备注</TableHead>
118-
<TableHead>
119-
<Skeleton v-if="status === Statuses.LOADING" class="h-5 my-2" />
120-
<!-- TODO: This is very dangerous and its behavior must be changed in the future -->
121-
<Button v-if="status === Statuses.READY" variant="link" class="p-0 text-red-500" @click="confirmManage(-1, 'DELALL')">
122-
清空全部
123-
</Button>
124-
</TableHead>
121+
<TableHead>管理</TableHead>
125122
</TableRow>
126123
</TableHeader>
127124
<TableBody v-if="status === Statuses.LOADING">
@@ -175,4 +172,5 @@ onMounted(async () => {
175172
加载失败
176173
</Alert>
177174
</div>
175+
<Toaster />
178176
</template>
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Days, Periods, PrismaClient } from '@prisma/client'
2+
3+
const prisma = new PrismaClient()
4+
5+
export default eventHandler(async (event) => {
6+
const { auth } = event.context
7+
8+
if (!auth.userId)
9+
setResponseStatus(event, 403)
10+
11+
// TODO: admin auth
12+
const records = await prisma.reservationRecord.findMany({
13+
include: {
14+
user: true,
15+
classroom: true,
16+
club: true,
17+
},
18+
})
19+
return JSON.stringify({
20+
days: Days,
21+
periods: Periods,
22+
data: Array.from(records.values()),
23+
})
24+
})

server/api/reservation/all.get.ts

+21-14
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,26 @@ const prisma = new PrismaClient()
55
export default eventHandler(async (event) => {
66
const { auth } = event.context
77

8-
if (!auth.userId)
8+
if (!auth.userId) {
99
setResponseStatus(event, 403)
10-
11-
const records = await prisma.reservationRecord.findMany({
12-
include: {
13-
user: true,
14-
classroom: true,
15-
club: true,
16-
},
17-
})
18-
return JSON.stringify({
19-
days: Days,
20-
periods: Periods,
21-
data: Array.from(records.values()),
22-
})
10+
}
11+
else {
12+
const records = await prisma.reservationRecord.findMany({
13+
include: {
14+
user: true,
15+
classroom: true,
16+
club: true,
17+
},
18+
where: {
19+
user: {
20+
clerkUserId: auth.userId,
21+
},
22+
},
23+
})
24+
return JSON.stringify({
25+
days: Days,
26+
periods: Periods,
27+
data: Array.from(records.values()),
28+
})
29+
}
2330
})

0 commit comments

Comments
 (0)