feat: Implement core classroom functionality including video player, learning and quiz pages, course detail view, and i18n support.

This commit is contained in:
supalerk-ar66 2026-02-12 16:05:37 +07:00
parent 008f712480
commit 7f5119e5aa
9 changed files with 289 additions and 109 deletions

View file

@ -90,7 +90,7 @@ const getLocalizedText = (text: any) => {
</div>
<div v-else class="p-10 flex flex-col items-center justify-center text-slate-400">
<q-icon name="campaign" size="40px" class="mb-2 opacity-50" />
<p>{{ $t('classroom.noAnnouncements', 'ไม่มีประกาศในขณะนี้') }}</p>
<p>{{ $t('classroom.noAnnouncements') }}</p>
</div>
</q-card-section>
</q-card>

View file

@ -61,7 +61,7 @@ const progressPercentage = computed(() => {
<!-- Course Progress Header -->
<div class="p-5 border-b border-gray-200 dark:border-white/10 bg-slate-50/50 dark:bg-slate-900/50">
<div class="flex justify-between items-center mb-2">
<span class="text-xs font-black uppercase tracking-widest text-slate-500 dark:text-slate-400">{{ $t('course.progress', 'ความคืบหน้า') }}</span>
<span class="text-xs font-black uppercase tracking-widest text-slate-500 dark:text-slate-400">{{ $t('course.progress') }}</span>
<span class="text-sm font-black text-blue-600 dark:text-blue-400">{{ progressPercentage }}%</span>
</div>
<div class="h-2 w-full bg-slate-200 dark:bg-slate-800 rounded-full overflow-hidden shadow-inner">

View file

@ -165,9 +165,21 @@ const togglePlay = () => {
return;
}
if (!videoRef.value) return;
if (isPlaying.value) videoRef.value.pause();
else videoRef.value.play();
isPlaying.value = !isPlaying.value;
if (isPlaying.value) {
videoRef.value.pause();
isPlaying.value = false;
} else {
const playPromise = videoRef.value.play();
if (playPromise !== undefined) {
playPromise.then(() => {
isPlaying.value = true;
}).catch(error => {
// Auto-play was prevented or play was interrupted
// We can safely ignore this error
console.log("Video play request handled:", error.name);
});
}
}
};
const handleTimeUpdate = () => {
@ -238,12 +250,12 @@ watch([volume, isMuted], () => {
></iframe>
<!-- 2. Standard HTML5 Video Player -->
<div v-else class="w-full h-full relative">
<div v-else class="w-full h-full relative group/video cursor-pointer">
<video
ref="videoRef"
:src="src"
:poster="poster"
class="w-full h-full object-contain"
class="w-full h-full object-contain bg-slate-900"
@click="togglePlay"
@timeupdate="handleTimeUpdate"
@loadedmetadata="handleLoadedMetadata"
@ -251,26 +263,30 @@ watch([volume, isMuted], () => {
/>
<!-- Custom Controls Overlay (Only for HTML5 Video) -->
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/80 via-black/40 to-transparent transition-opacity opacity-0 group-hover:opacity-100">
<div class="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black/90 via-black/40 to-transparent transition-opacity opacity-0 group-hover/video:opacity-100 flex flex-col gap-3">
<!-- Progress Bar -->
<div class="relative flex-grow h-1.5 bg-white/20 rounded-full cursor-pointer group/progress overflow-hidden" @click="seek">
<div class="absolute top-0 left-0 h-full bg-blue-500 rounded-full group-hover/progress:bg-blue-400 transition-all shadow-[0_0_12px_rgba(59,130,246,0.6)]" :style="{ width: videoProgress + '%' }"></div>
</div>
<div class="flex items-center gap-4 text-white">
<q-btn flat round dense :icon="isPlaying ? 'pause' : 'play_arrow'" @click.stop="togglePlay" />
<div class="relative flex-grow h-1.5 bg-white/20 rounded-full cursor-pointer group/progress overflow-hidden" @click="seek">
<div class="absolute top-0 left-0 h-full bg-blue-500 rounded-full group-hover/progress:bg-blue-400 transition-all shadow-[0_0_10px_rgba(59,130,246,0.5)]" :style="{ width: videoProgress + '%' }"></div>
</div>
<span class="text-xs font-mono font-medium opacity-90">{{ currentTimeDisplay }} / {{ durationDisplay }}</span>
<q-btn flat round dense :icon="isPlaying ? 'pause' : 'play_arrow'" @click.stop="togglePlay" class="hover:scale-110 active:scale-95 transition-transform" />
<span class="text-xs font-mono font-bold opacity-80">{{ currentTimeDisplay }} / {{ durationDisplay }}</span>
<div class="flex-grow"></div>
<!-- Volume Control -->
<div class="flex items-center gap-2 group/volume">
<q-btn flat round dense :icon="volumeIcon" @click.stop="handleToggleMute" color="white" />
<div class="w-0 group-hover/volume:w-20 overflow-hidden transition-all duration-300 flex items-center">
<div class="flex items-center gap-2 group/volume relative">
<q-btn flat round dense :icon="volumeIcon" @click.stop="handleToggleMute" color="white" class="hover:scale-110 transition-transform" />
<div class="w-0 group-hover/volume:w-24 overflow-hidden transition-all duration-300 flex items-center bg-black/60 backdrop-blur-md rounded-full px-2">
<input
type="range"
min="0"
max="1"
step="0.1"
step="0.05"
:value="volume"
@input="handleVolumeChange"
class="w-20 h-1 bg-white/30 rounded-lg appearance-none cursor-pointer accent-blue-500"
class="w-20 h-1 bg-white/20 rounded-lg appearance-none cursor-pointer accent-blue-500 mx-2"
/>
</div>
</div>

View file

@ -156,7 +156,7 @@ const handleEnroll = () => {
<div class="relative">
<div v-if="course.price > 0" class="mb-4">
<span class="text-xs font-black uppercase tracking-widest text-slate-400 mb-1 block">{{ $t('course.price', 'ราคาคอร์ส') }}</span>
<span class="text-xs font-black uppercase tracking-widest text-slate-400 mb-1 block">{{ $t('course.price') }}</span>
<div class="text-4xl font-black font-display">
<span class="text-slate-900 dark:text-white">
{{ formatPrice(course.price) }}
@ -180,13 +180,13 @@ const handleEnroll = () => {
</q-btn>
<div class="mt-6 space-y-4">
<p class="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 mb-4">{{ $t('course.includes', 'คอร์สนี้รวมอะไรบ้าง') }}</p>
<p class="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 mb-4">{{ $t('course.includes') }}</p>
<div class="flex items-center gap-3 text-sm text-slate-600 dark:text-slate-300 font-bold">
<div class="w-6 h-6 rounded-lg bg-blue-50 dark:bg-blue-500/10 flex items-center justify-center">
<q-icon name="all_inclusive" size="14px" class="text-blue-600 dark:text-blue-400" />
</div>
{{ $t('course.fullLifetimeAccess', 'เข้าเรียนได้ตลอดชีพ') }}
{{ $t('course.fullLifetimeAccess') }}
</div>
<div class="flex items-center gap-3 text-sm text-slate-600 dark:text-slate-300 font-bold">
@ -200,7 +200,7 @@ const handleEnroll = () => {
<div class="w-6 h-6 rounded-lg bg-purple-50 dark:bg-purple-500/10 flex items-center justify-center">
<q-icon name="devices" size="14px" class="text-purple-600 dark:text-purple-400" />
</div>
{{ $t('course.accessOnMobile', 'เข้าเรียนได้ทุกอุปกรณ์') }}
{{ $t('course.accessOnMobile') }}
</div>
</div>
</div>