feat: Implement core classroom functionality including video player, learning and quiz pages, course detail view, and i18n support.
This commit is contained in:
parent
008f712480
commit
7f5119e5aa
9 changed files with 289 additions and 109 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue