elearning/frontend_management/components/course/LessonPreviewDialog.vue
Missez f26a94076c
All checks were successful
Build and Deploy Frontend Management to Dev Server / Build Frontend Management Docker Image (push) Successful in 46s
Build and Deploy Frontend Management to Dev Server / Deploy E-learning Frontend Management to Dev Server (push) Successful in 4s
Build and Deploy Frontend Management to Dev Server / Notify Deployment Status (push) Successful in 2s
feat: Introduce comprehensive course management features for admin, including recommended, pending, and detailed course views, and instructor course listing with a lesson preview component.
2026-02-20 14:33:08 +07:00

254 lines
10 KiB
Vue

<template>
<q-dialog v-model="isOpen" maximizable>
<q-card class="column" style="width: 900px; max-width: 95vw; height: 90vh">
<q-card-section class="row items-center q-pb-none bg-gray-50 border-b">
<div class="text-h6 font-bold text-gray-900 flex items-center gap-2">
<q-icon :name="getIcon(lesson.type)" color="primary" />
{{ lesson.title.th }}
</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-card-section class="col q-pa-none scroll relative-position">
<div v-if="loading" class="absolute-center">
<q-spinner-dots size="40px" color="primary" />
</div>
<div v-else-if="lessonDetail" class="p-6 space-y-6">
<!-- Video Player -->
<div v-if="lesson.type === 'VIDEO' && lessonDetail.video_url" class="aspect-video bg-black rounded-lg overflow-hidden">
<iframe
v-if="isYoutubeUrl(lessonDetail.video_url)"
:src="getYoutubeEmbedUrl(lessonDetail.video_url)"
class="w-full h-full"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
<video
v-else
:src="lessonDetail.video_url"
controls
class="w-full h-full object-contain"
></video>
</div>
<!-- Content (Document/Text) -->
<div v-if="lessonDetail.content" class="prose max-w-none">
<div v-html="lessonDetail.content.th"></div>
</div>
<!-- Attachments -->
<div v-if="lessonDetail.attachments && lessonDetail.attachments.length > 0">
<h3 class="text-lg font-semibold mb-3 flex items-center gap-2">
<q-icon name="attach_file" />
เอกสารแนบ
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<a
v-for="file in lessonDetail.attachments.filter(f => !['video/mp4', 'video/youtube'].includes(f.mime_type))"
:key="file.id"
:href="file.file_path"
target="_blank"
class="flex items-center gap-3 p-3 border rounded-lg hover:bg-gray-50 transition-colors"
>
<q-icon name="description" color="primary" size="24px" />
<div class="overflow-hidden">
<div class="font-medium truncate">{{ file.file_name }}</div>
<div class="text-xs text-gray-500">{{ formatFileSize(file.file_size) }}</div>
</div>
</a>
</div>
</div>
<!-- Quiz Info -->
<div v-if="lesson.type === 'QUIZ'">
<div v-if="lessonDetail.quiz" class="bg-blue-50 p-6 rounded-lg border border-blue-100">
<h3 class="text-lg font-bold text-blue-900 mb-2">{{ lessonDetail.quiz.title?.th || 'แบบทดสอบ' }}</h3>
<p class="text-blue-800 mb-4">{{ lessonDetail.quiz.description?.th || '-' }}</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div class="bg-white p-3 rounded border border-blue-100">
<div class="text-gray-500 mb-1">เกณฑาน</div>
<div class="font-semibold">{{ lessonDetail.quiz.passing_score }}%</div>
</div>
<div class="bg-white p-3 rounded border border-blue-100">
<div class="text-gray-500 mb-1">เวลาทำขอสอบ</div>
<div class="font-semibold">{{ lessonDetail.quiz.time_limit ? lessonDetail.quiz.time_limit + ' นาที' : '-' }}</div>
</div>
<div class="bg-white p-3 rounded border border-blue-100">
<div class="text-gray-500 mb-1">จำนวนข</div>
<div class="font-semibold">{{ lessonDetail.quiz.questions?.length || 0 }} </div>
</div>
</div>
<!-- Questions List -->
<div v-if="lessonDetail.quiz.questions && lessonDetail.quiz.questions.length > 0" class="mt-6 space-y-6">
<!-- ... (questions rendering code unchanged) ... -->
<div v-for="(question, index) in lessonDetail.quiz.questions" :key="question.id" class="bg-white p-4 rounded-lg border border-blue-100">
<div class="flex gap-3">
<span class="font-bold text-blue-600 text-lg">{{ index + 1 }}.</span>
<div class="flex-1">
<p class="font-medium text-gray-900 text-lg mb-2">{{ question.question?.th || 'คำถาม' }}</p>
<!-- Choices -->
<div class="space-y-2 mt-3">
<div
v-for="choice in question.choices"
:key="choice.id"
class="p-3 rounded border flex items-center gap-3"
:class="choice.is_correct ? 'bg-green-50 border-green-200' : 'bg-gray-50 border-gray-200'"
>
<div
class="w-5 h-5 rounded-full border-2 flex items-center justify-center flex-shrink-0"
:class="choice.is_correct ? 'border-green-500' : 'border-gray-400'"
>
<div v-if="choice.is_correct" class="w-2.5 h-2.5 rounded-full bg-green-500"></div>
</div>
<div :class="choice.is_correct ? 'text-green-700 font-medium' : 'text-gray-700'">
<div>{{ choice.text?.th || '-' }}</div>
<div v-if="choice.text?.en" class="text-xs opacity-75">{{ choice.text.en }}</div>
</div>
<q-icon v-if="choice.is_correct" name="check_circle" color="green" class="ml-auto" />
</div>
</div>
<!-- Explanation -->
<div v-if="question.explanation?.th" class="mt-4 bg-yellow-50 p-3 rounded text-sm text-yellow-800 border border-yellow-100">
<div class="font-semibold mb-1 flex items-center gap-1">
<q-icon name="lightbulb" />
คำอธบาย:
</div>
{{ question.explanation.th }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fallback for missing Quiz Data -->
<div v-else class="flex flex-col items-center justify-center p-10 text-gray-500 bg-gray-50 rounded-lg border border-dashed">
<q-icon name="assignment_late" size="48px" class="mb-2" />
<p>ไมพบขอมลแบบทดสอบ หรอแบบทดสอบยงไมสมบรณ</p>
</div>
</div>
</div>
<div v-else class="flex flex-col items-center justify-center h-full text-gray-500">
<q-icon name="error_outline" size="48px" class="mb-2" />
<p>ไมพบขอมลบทเรยน</p>
</div>
</q-card-section>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue';
import { useQuasar } from 'quasar';
import { instructorService, type LessonResponse, type LessonDetailResponse } from '~/services/instructor.service';
const props = defineProps<{
modelValue: boolean;
lesson: LessonResponse;
courseId: number;
}>();
const emit = defineEmits(['update:modelValue']);
const $q = useQuasar();
const loading = ref(false);
const lessonDetail = ref<LessonDetailResponse | null>(null);
const isOpen = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
});
const getIcon = (type: string) => {
const icons: Record<string, string> = {
VIDEO: 'play_circle',
DOCUMENT: 'description',
QUIZ: 'quiz'
};
return icons[type] || 'article';
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const isYoutubeUrl = (url: string) => {
return url.includes('youtube.com') || url.includes('youtu.be');
};
const getYoutubeEmbedUrl = (url: string) => {
let videoId = '';
if (url.includes('youtu.be')) {
videoId = url.split('/').pop()?.split('?')[0] || '';
} else if (url.includes('youtube.com')) {
const params = new URLSearchParams(url.split('?')[1]);
videoId = params.get('v') || '';
}
return `https://www.youtube.com/embed/${videoId}`;
};
const fetchLessonDetail = async () => {
// Always verify lesson and courseId exist
if (!props.lesson || !props.courseId) return;
loading.value = true;
try {
const data = await instructorService.getLesson(
props.courseId,
props.lesson.chapter_id,
props.lesson.id
);
console.log('Fetched Lesson Details:', data);
// FIX: Handle case where API returns Quiz object directly instead of LessonDetailResponse
if (props.lesson.type === 'QUIZ' && !data.quiz && (data as any).questions) {
console.warn('API returned Quiz object directly, wrapping it.');
lessonDetail.value = {
...props.lesson,
attachments: [], // Assume no attachments if structure is unexpected
quiz: data as any
};
} else {
lessonDetail.value = data;
}
} catch (error) {
console.error('Failed to fetch lesson detail:', error);
$q.notify({
type: 'negative',
message: 'ไม่สามารถโหลดข้อมูลบทเรียนได้'
});
} finally {
loading.value = false;
}
};
watch(() => props.modelValue, (val) => {
if (val) {
fetchLessonDetail();
} else {
// Optional: clear detail to avoid flashing old content next time
// lessonDetail.value = null;
}
}, { immediate: true });
// Also watch lesson ID in case dialog stays open but lesson changes
watch(() => props.lesson.id, () => {
if (props.modelValue) {
fetchLessonDetail();
}
});
</script>