IntelliClass_FE/pages/course/index.vue
Timothy Yin b05f954923
feat: add authentication requirements to course preparation and resources pages
fix: update home page background image and remove unnecessary redirect code

chore: update pnpm lock file with new dependencies for auto-animate and svg spinners

delete: remove unused images from public directory

refactor: modify course and user types for better clarity and structure

feat: implement course API with CRUD operations and teacher team management

feat: create user authentication page with login functionality and validation

feat: add login state management with Pinia for user session handling

style: create reusable UI components for cards and tabs

chore: implement HTTP utility for API requests with error handling
2025-04-06 00:25:20 +08:00

300 lines
9.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import { toast } from "vue-sonner";
import { toTypedSchema } from "@vee-validate/zod";
import * as z from "zod";
import { createCourse, deleteCourse, listUserCourses } from "~/api/course";
import type { FetchError } from "ofetch";
definePageMeta({
layout: "no-sidebar",
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>
<div class="container mx-auto 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-5 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>
</template>
<style scoped></style>