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
198 lines
5.3 KiB
Vue
198 lines
5.3 KiB
Vue
<script lang="ts" setup>
|
||
import { toTypedSchema } from "@vee-validate/zod";
|
||
import {
|
||
createCourseChatper,
|
||
deleteCourseChatper,
|
||
deleteCourseResource,
|
||
deleteCourseSection,
|
||
getCourseChatpers,
|
||
} from "~/api/course";
|
||
import * as z from "zod";
|
||
import { useForm } from "vee-validate";
|
||
import { toast } from "vue-sonner";
|
||
|
||
definePageMeta({
|
||
requiresAuth: true,
|
||
});
|
||
|
||
const {
|
||
params: { id: courseId },
|
||
} = useRoute();
|
||
|
||
const { data: chapters, refresh: refreshChapters } = useAsyncData(() =>
|
||
getCourseChatpers(parseInt(courseId as string))
|
||
);
|
||
|
||
const createChatperDialogOpen = ref(false);
|
||
|
||
const createChatperSchema = toTypedSchema(
|
||
z.object({
|
||
title: z.string().min(2, "章节名称至少2个字符").max(32, "最大长度32个字符"),
|
||
courseId: z.number().min(1, "课程ID不能为空"),
|
||
})
|
||
);
|
||
|
||
const createChatperForm = useForm({
|
||
validationSchema: createChatperSchema,
|
||
initialValues: {
|
||
title: "",
|
||
courseId: Number(courseId),
|
||
},
|
||
});
|
||
|
||
const onCreateChapterSubmit = createChatperForm.handleSubmit((values) => {
|
||
toast.promise(createCourseChatper(values), {
|
||
loading: "正在创建章节...",
|
||
success: () => {
|
||
refreshChapters();
|
||
createChatperForm.resetForm();
|
||
createChatperDialogOpen.value = false;
|
||
return "创建章节成功";
|
||
},
|
||
error: () => {
|
||
return "创建章节失败";
|
||
},
|
||
});
|
||
});
|
||
|
||
const onDeleteChatper = (chapterId: number) => {
|
||
toast.promise(deleteCourseChatper(chapterId), {
|
||
loading: "正在删除章节...",
|
||
success: () => {
|
||
refreshChapters();
|
||
return "删除章节成功";
|
||
},
|
||
error: () => {
|
||
return "删除章节失败";
|
||
},
|
||
});
|
||
};
|
||
|
||
const onDeleteSection = (sectionId: number) => {
|
||
toast.promise(deleteCourseSection(sectionId), {
|
||
loading: "正在删除小节...",
|
||
success: () => {
|
||
refreshChapters();
|
||
return "删除小节成功";
|
||
},
|
||
error: () => {
|
||
return "删除小节失败";
|
||
},
|
||
});
|
||
};
|
||
|
||
const onDeleteResource = (resourceId: number) => {
|
||
toast.promise(deleteCourseResource(resourceId), {
|
||
loading: "正在删除资源...",
|
||
success: () => {
|
||
refreshChapters();
|
||
return "删除资源成功";
|
||
},
|
||
error: () => {
|
||
return "删除资源失败";
|
||
},
|
||
});
|
||
};
|
||
</script>
|
||
|
||
<template>
|
||
<div class="flex flex-col gap-4 px-4 py-2">
|
||
<div class="flex justify-between items-start">
|
||
<h1 class="text-xl font-medium">课程章节管理</h1>
|
||
<div class="flex items-center gap-4">
|
||
<Tooltip :delay-duration="0">
|
||
<TooltipTrigger>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
class="flex items-center gap-2 text-muted-foreground"
|
||
>
|
||
<div class="w-2 h-2 rounded-full bg-emerald-500" />
|
||
<span>训练完成</span>
|
||
</Button>
|
||
</TooltipTrigger>
|
||
<TooltipContent>点击进行召回测试</TooltipContent>
|
||
</Tooltip>
|
||
|
||
<Dialog v-model:open="createChatperDialogOpen">
|
||
<DialogTrigger as-child>
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
class="flex items-center gap-1"
|
||
>
|
||
<Icon name="tabler:plus" size="16px" />
|
||
<span>添加章节</span>
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent class="sm:max-w-[425px]">
|
||
<DialogHeader>
|
||
<DialogTitle>添加章节</DialogTitle>
|
||
</DialogHeader>
|
||
|
||
<form
|
||
id="create-chapter-form"
|
||
autocomplete="off"
|
||
class="space-y-2"
|
||
@submit="onCreateChapterSubmit"
|
||
>
|
||
<FormField v-slot="{ componentField }" name="title">
|
||
<FormItem v-auto-animate>
|
||
<FormLabel>章节名称</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
type="text"
|
||
placeholder="请输入章节名称"
|
||
v-bind="componentField"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
</FormField>
|
||
<input type="hidden" name="courseId" />
|
||
</form>
|
||
|
||
<DialogFooter>
|
||
<Button type="submit" form="create-chapter-form">创建</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="chapters?.rows && chapters.rows.length > 0"
|
||
class="flex flex-col gap-8"
|
||
>
|
||
<!-- chatpter -->
|
||
<CourseChapter
|
||
v-for="chapter in chapters?.rows"
|
||
:key="chapter.id"
|
||
:tag="`${chapter.id}`"
|
||
:chapter="chapter"
|
||
@refresh="refreshChapters"
|
||
@delete-chapter="onDeleteChatper"
|
||
@delete-section="onDeleteSection"
|
||
@delete-resource="onDeleteResource"
|
||
/>
|
||
</div>
|
||
<EmptyScreen v-else title="暂无章节" icon="fluent-color:document-add-24">
|
||
<div class="flex flex-col gap-2">
|
||
<p class="text-sm text-muted-foreground">
|
||
课程章节列表为空,先创建章节吧
|
||
</p>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
@click="createChatperDialogOpen = true"
|
||
>
|
||
<Icon name="tabler:plus" size="16px" />
|
||
<span>添加章节</span>
|
||
</Button>
|
||
</div>
|
||
</EmptyScreen>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped></style>
|