266 lines
7.9 KiB
Vue
266 lines
7.9 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, 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>
|