<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, editCourseChapter } 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) } const onIsPublishedSwitch = () => { toast.promise( editCourseChapter({ ...props.chapter, isPublished: !props.chapter.isPublished, }), { loading: '正在修改章节发布状态...', success: () => { return `已${props.chapter.isPublished ? '取消' : ''}发布章节` }, error: () => { return '修改章节发布状态失败' }, finally: () => { emit('refresh') }, }, ) } </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" @click="onIsPublishedSwitch" > <div v-if="chapter.isPublished" 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.isPublished ? "已发布" : "未发布" }}</span> </Button> </TooltipTrigger> <TooltipContent> 点击{{ chapter.isPublished ? "取消发布" : "发布章节" }} </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" @refresh="emit('refresh')" @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>