IntelliClass_FE/components/course/Chapter.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

212 lines
6.8 KiB
Vue

<script lang="ts" setup>
import { toTypedSchema } from "@vee-validate/zod";
import { ChevronLeft } from "lucide-vue-next";
import { useForm } from "vee-validate";
import { toast } from "vue-sonner";
import { z } from "zod";
import { createCourseSection } from "~/api/course";
import type { ICourseChapter } from "~/types";
const props = defineProps<{
tag?: string;
chapter: ICourseChapter;
}>();
const emit = defineEmits<{
refresh: [];
"delete-chapter": [chapterId: number];
"delete-section": [sectionId: number];
"delete-resource": [resourceId: number];
}>();
const createSectionDialogOpen = ref(false);
const createSectionSchema = toTypedSchema(
z.object({
title: z.string().min(2, "小节名称至少2个字符").max(32, "最大长度32个字符"),
chapterId: z.number().min(1, "章节ID不能为空"),
})
);
const createSectionForm = useForm({
validationSchema: createSectionSchema,
initialValues: {
title: "",
chapterId: props.chapter.id,
},
});
const onCreateSectionSubmit = createSectionForm.handleSubmit((values) => {
toast.promise(createCourseSection(values), {
loading: "正在创建小节...",
success: () => {
createSectionForm.resetForm();
createSectionDialogOpen.value = false;
emit("refresh");
return "创建小节成功";
},
error: () => {
return "创建小节失败";
},
});
});
const handleDeleteChapter = () => {
if (props.chapter.sections.length > 0) {
const confirmDelete = confirm(
"该章节下有小节,删除后将无法恢复,是否继续?"
);
if (!confirmDelete) return;
}
emit("delete-chapter", props.chapter.id);
};
</script>
<template>
<div class="flex flex-col gap-1 relative">
<div
v-if="chapter.sections.length > 0"
class="absolute inset-y-0 left-9 bottom-6 w-[1px] bg-gray-300 dark:bg-gray-700 z-0"
/>
<Collapsible class="group/collapsible z-10" :default-open="true">
<div
class="w-full px-4 py-3 rounded-md bg-indigo-50 dark:bg-muted flex justify-between items-center"
>
<div class="flex items-center gap-2">
<div class="w-10 flex justify-center">
<Badge variant="secondary" class="text-xs text-white bg-indigo-400">
<span>
{{
tag || chapter.sections.length > 0
? chapter.sections.length
: 0
}}
</span>
</Badge>
</div>
<h1 class="text-base font-semibold text-ellipsis line-clamp-1">
{{ chapter.title }}
</h1>
</div>
<div class="flex items-center gap-2">
<!-- TODO: hide actions defaulty -->
<div
class="flex items-center gap-2 opacity-100 group-hover/collapsible:opacity-100 transition-opacity duration-200"
>
<Tooltip>
<TooltipTrigger>
<Button
variant="link"
size="xs"
class="flex items-center gap-2 text-muted-foreground"
>
<div
v-if="chapter.is_published"
class="w-2 h-2 rounded-full bg-emerald-500"
/>
<div
v-else
class="w-2 h-2 rounded-full bg-gray-400 dark:bg-gray-500"
/>
<span>{{ chapter.is_published ? "已发布" : "未发布" }}</span>
</Button>
</TooltipTrigger>
<TooltipContent>TBD.</TooltipContent>
</Tooltip>
<Button
variant="link"
size="xs"
class="flex items-center gap-1 text-muted-foreground"
>
<Icon name="tabler:automation" size="16px" />
<span>章节检测</span>
</Button>
<Dialog v-model:open="createSectionDialogOpen">
<DialogTrigger as-child>
<Button
variant="link"
size="xs"
class="flex items-center gap-1 text-muted-foreground"
>
<Icon name="tabler:plus" size="16px" />
<span>添加小节</span>
</Button>
</DialogTrigger>
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>添加小节</DialogTitle>
</DialogHeader>
<form
id="create-section-form"
autocomplete="off"
class="space-y-2"
@submit="onCreateSectionSubmit"
>
<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="chapterId" />
</form>
<DialogFooter>
<Button type="submit" form="create-section-form">创建</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Button
variant="link"
size="xs"
class="flex items-center gap-1 text-red-500"
@click="handleDeleteChapter"
>
<Icon name="tabler:trash" size="16px" />
<span>删除</span>
</Button>
</div>
<CollapsibleTrigger>
<ChevronLeft
class="transition-transform duration-200 group-data-[state=open]/collapsible:-rotate-90 text-muted-foreground"
/>
<span class="sr-only">Toggle</span>
</CollapsibleTrigger>
</div>
</div>
<CollapsibleContent class="pt-4">
<div v-if="chapter.sections.length > 0" class="flex flex-col gap-4">
<!-- Section -->
<CourseSection
v-for="section in chapter.sections"
:key="section.id"
:section="section"
@delete-section="emit('delete-section', section.id)"
@delete-resource="emit('delete-resource', $event)"
/>
</div>
<!-- <div
v-else
class="flex items-center justify-center gap-2 text-muted-foreground"
>
<Icon name="tabler:circle-minus" size="16px" />
<span class="text-sm">该章节没有内容</span>
</div> -->
</CollapsibleContent>
</Collapsible>
</div>
</template>
<style scoped></style>