344 lines
10 KiB
Vue
344 lines
10 KiB
Vue
<script lang="ts" setup>
|
||
import { toast } from 'vue-sonner'
|
||
import { toTypedSchema } from '@vee-validate/zod'
|
||
import * as z from 'zod'
|
||
import type { FetchError } from 'ofetch'
|
||
import { createCourse, deleteCourse, listUserCourses } from '~/api/course'
|
||
|
||
definePageMeta({
|
||
requiresAuth: true,
|
||
})
|
||
|
||
useHead({
|
||
title: '课程中心',
|
||
})
|
||
|
||
const loginState = useLoginState()
|
||
const deleteMode = ref(false)
|
||
|
||
const {
|
||
data: coursesList,
|
||
refresh: refreshCoursesList,
|
||
status: _,
|
||
} = useAsyncData(() => listUserCourses(loginState.user.userId))
|
||
|
||
/**
|
||
* 生成学期列表
|
||
* @param years - 后推年数
|
||
* @returns 学期列表
|
||
*/
|
||
const getSemesters = (years: number) => {
|
||
const currentYear = new Date().getFullYear() - 1
|
||
const semesters = []
|
||
for (let i = 0; i < years + 1; i++) {
|
||
const year = currentYear + i
|
||
semesters.push(`${year}-${year + 1}-1`, `${year}-${year + 1}-2`)
|
||
}
|
||
return semesters
|
||
}
|
||
|
||
const createCourseDialogOpen = ref(false)
|
||
|
||
const courseFormSchema = toTypedSchema(
|
||
z.object({
|
||
courseName: z
|
||
.string()
|
||
.min(4, '课程名称不能为空')
|
||
.max(32, '最大长度32个字符'),
|
||
profile: z.string().optional(),
|
||
schoolName: z.string().min(4).max(32),
|
||
teacherName: z.string().optional(),
|
||
semester: z.enum([...getSemesters(3)] as [string, ...string[]]),
|
||
}),
|
||
)
|
||
|
||
const folderFormSchema = toTypedSchema(
|
||
z.object({
|
||
folderName: z.string().min(2).max(32),
|
||
}),
|
||
)
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
const onCourseSubmit = (values: any) => {
|
||
toast.promise(createCourse(values), {
|
||
loading: '正在创建课程...',
|
||
success: () => {
|
||
createCourseDialogOpen.value = false
|
||
return '创建课程成功'
|
||
},
|
||
error: (error: FetchError) => {
|
||
return `创建课程失败:${error.data?.msg || error.message}`
|
||
},
|
||
finally: () => {
|
||
refreshCoursesList()
|
||
},
|
||
})
|
||
}
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
const onFolderSubmit = (values: any) => {
|
||
toast('submit data:', {
|
||
description: JSON.stringify(values, null, 2),
|
||
})
|
||
}
|
||
|
||
const onDeleteCourse = (courseId: number) => {
|
||
toast.promise(deleteCourse(courseId), {
|
||
loading: '正在删除课程...',
|
||
success: () => {
|
||
return '删除课程成功'
|
||
},
|
||
error: () => {
|
||
return '删除课程失败'
|
||
},
|
||
finally: () => {
|
||
refreshCoursesList()
|
||
},
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<AppContainer>
|
||
<div class="flex flex-col gap-8">
|
||
<div class="flex justify-between items-center">
|
||
<div class="flex items-center gap-2">
|
||
<Form
|
||
v-slot="{ handleSubmit }"
|
||
as=""
|
||
:validation-schema="courseFormSchema"
|
||
>
|
||
<Dialog v-model:open="createCourseDialogOpen">
|
||
<DialogTrigger as-child>
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
>
|
||
<Icon
|
||
name="tabler:plus"
|
||
size="16px"
|
||
/>
|
||
新建课程
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent class="sm:max-w-[425px]">
|
||
<DialogHeader>
|
||
<DialogTitle>创建课程</DialogTitle>
|
||
<DialogDescription>
|
||
课程创建后,您可以在课程中添加章节等内容。
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<form
|
||
id="createCourseForm"
|
||
autocomplete="off"
|
||
class="space-y-2"
|
||
@submit="handleSubmit($event, onCourseSubmit)"
|
||
>
|
||
<FormField
|
||
v-slot="{ componentField }"
|
||
name="courseName"
|
||
>
|
||
<FormItem v-auto-animate>
|
||
<FormLabel>课程名称</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
type="text"
|
||
placeholder="请输入课程名称"
|
||
v-bind="componentField"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
</FormField>
|
||
<FormField
|
||
v-slot="{ componentField }"
|
||
name="profile"
|
||
>
|
||
<FormItem v-auto-animate>
|
||
<FormLabel>课程介绍</FormLabel>
|
||
<FormControl>
|
||
<Textarea
|
||
type="text"
|
||
placeholder="请输入课程介绍"
|
||
v-bind="componentField"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
</FormField>
|
||
<FormField
|
||
v-slot="{ componentField }"
|
||
name="schoolName"
|
||
>
|
||
<FormItem v-auto-animate>
|
||
<FormLabel>学校名称</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
type="text"
|
||
placeholder="请输入院校名称"
|
||
v-bind="componentField"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
</FormField>
|
||
<input
|
||
type="hidden"
|
||
name="teacherName"
|
||
:value="loginState.user.nickName"
|
||
/>
|
||
<FormField
|
||
v-slot="{ componentField }"
|
||
name="semester"
|
||
>
|
||
<FormItem v-auto-animate>
|
||
<FormLabel>学期</FormLabel>
|
||
<FormControl>
|
||
<Select v-bind="componentField">
|
||
<FormControl>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="请选择学期" />
|
||
</SelectTrigger>
|
||
</FormControl>
|
||
<SelectContent>
|
||
<SelectGroup>
|
||
<SelectItem
|
||
v-for="semester in getSemesters(3)"
|
||
:key="semester"
|
||
:value="semester"
|
||
>
|
||
{{ semester }}
|
||
</SelectItem>
|
||
</SelectGroup>
|
||
</SelectContent>
|
||
</Select>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
</FormField>
|
||
</form>
|
||
|
||
<DialogFooter>
|
||
<Button
|
||
type="submit"
|
||
form="createCourseForm"
|
||
>
|
||
创建
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</Form>
|
||
|
||
<Form
|
||
v-slot="{ handleSubmit }"
|
||
as=""
|
||
keep-values
|
||
:validation-schema="folderFormSchema"
|
||
>
|
||
<Dialog>
|
||
<DialogTrigger as-child>
|
||
<!-- TODO: disable temporarily -->
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
class="hidden"
|
||
>
|
||
<Icon
|
||
name="tabler:folder-plus"
|
||
size="16px"
|
||
/>
|
||
新建文件夹
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent class="sm:max-w-[425px]">
|
||
<DialogHeader>
|
||
<DialogTitle>创建文件夹</DialogTitle>
|
||
<DialogDescription>
|
||
可以将多门课程收纳在文件夹中
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<form
|
||
id="createCourseForm"
|
||
autocomplete="off"
|
||
class="space-y-2"
|
||
@submit="handleSubmit($event, onFolderSubmit)"
|
||
>
|
||
<FormField
|
||
v-slot="{ componentField }"
|
||
name="folderName"
|
||
>
|
||
<FormItem v-auto-animate>
|
||
<FormLabel>文件夹名称</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
type="text"
|
||
placeholder="请输入文件夹名称"
|
||
v-bind="componentField"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
</FormField>
|
||
</form>
|
||
|
||
<DialogFooter>
|
||
<Button
|
||
type="submit"
|
||
form="createCourseForm"
|
||
>
|
||
创建
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</Form>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<Button
|
||
:variant="deleteMode ? 'destructive' : 'secondary'"
|
||
size="sm"
|
||
@click="deleteMode = !deleteMode"
|
||
>
|
||
{{ deleteMode ? '退出删除' : '删除课程' }}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="coursesList?.rows && coursesList.rows.length > 0"
|
||
class="grid grid-cols-6 gap-8"
|
||
>
|
||
<CourseCard
|
||
v-for="course in coursesList?.rows"
|
||
:key="course.id"
|
||
:data="course"
|
||
:delete-mode="deleteMode"
|
||
@delete-course="onDeleteCourse"
|
||
/>
|
||
</div>
|
||
<EmptyScreen
|
||
v-else
|
||
title="暂无课程"
|
||
icon="fluent-color:people-list-24"
|
||
>
|
||
<p class="text-sm text-muted-foreground">还没有创建或加入课程</p>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
@click="createCourseDialogOpen = true"
|
||
>
|
||
<Icon
|
||
name="tabler:plus"
|
||
size="16px"
|
||
/>
|
||
新建课程
|
||
</Button>
|
||
</EmptyScreen>
|
||
</div>
|
||
</AppContainer>
|
||
</template>
|
||
|
||
<style scoped></style>
|