feat: Introduce core e-learning features with new pages for course details, dashboard, authentication, browsing, and learning, supported by a useCourse composable.
This commit is contained in:
parent
c982ab2c05
commit
0eb9b522f6
6 changed files with 109 additions and 38 deletions
|
|
@ -62,11 +62,20 @@ interface EnrolledCourseResponse {
|
|||
limit: number
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Composable: useCourse
|
||||
// หน้าที่: จัดการ Logic ทุกอย่างเกี่ยวกับคอร์สเรียน
|
||||
// - ดึงข้อมูลคอร์ส (Public & Protected)
|
||||
// - ลงทะเบียนเรียน (Enroll)
|
||||
// - ติดตามความคืบหน้าการเรียน (Progress tracking)
|
||||
// ==========================================
|
||||
export const useCourse = () => {
|
||||
const config = useRuntimeConfig()
|
||||
const API_BASE_URL = config.public.apiBase as string
|
||||
const { token } = useAuth()
|
||||
|
||||
// ฟังก์ชันดึงรายชื่อคอร์สทั้งหมด (Catalog)
|
||||
// Endpoint: GET /courses
|
||||
const fetchCourses = async () => {
|
||||
try {
|
||||
const data = await $fetch<CourseResponse>(`${API_BASE_URL}/courses`, {
|
||||
|
|
@ -90,6 +99,8 @@ export const useCourse = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// ฟังก์ชันดึงรายละเอียดคอร์สตาม ID
|
||||
// Endpoint: GET /courses/:id
|
||||
const fetchCourseById = async (id: number) => {
|
||||
try {
|
||||
const data = await $fetch<CourseResponse>(`${API_BASE_URL}/courses/${id}`, {
|
||||
|
|
@ -130,6 +141,8 @@ export const useCourse = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// ฟังก์ชันลงทะเบียนเรียน
|
||||
// Endpoint: POST /students/courses/:id/enroll
|
||||
const enrollCourse = async (courseId: number) => {
|
||||
try {
|
||||
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/enroll`, {
|
||||
|
|
@ -197,6 +210,8 @@ export const useCourse = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// ฟังก์ชันดึงข้อมูลสำหรับการเรียน (Chapters, Lessons, Progress)
|
||||
// Endpoint: GET /students/courses/:id/learn
|
||||
const fetchCourseLearningInfo = async (courseId: number) => {
|
||||
try {
|
||||
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/learn`, {
|
||||
|
|
@ -220,6 +235,8 @@ export const useCourse = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// ฟังก์ชันดึงเนื้อหาบทเรียน (Video, Content)
|
||||
// Endpoint: GET /students/courses/:cid/lessons/:lid
|
||||
const fetchLessonContent = async (courseId: number, lessonId: number) => {
|
||||
try {
|
||||
const data = await $fetch<{ code: number; message: string; data: any; progress?: any }>(`${API_BASE_URL}/students/courses/${courseId}/lessons/${lessonId}`, {
|
||||
|
|
@ -269,6 +286,8 @@ export const useCourse = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// ฟังก์ชันบันทึกเวลาที่ดูวิดีโอ (Video Progress)
|
||||
// Endpoint: POST /students/lessons/:id/progress
|
||||
const saveVideoProgress = async (lessonId: number, progressSeconds: number, durationSeconds: number) => {
|
||||
try {
|
||||
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/lessons/${lessonId}/progress`, {
|
||||
|
|
@ -320,6 +339,8 @@ export const useCourse = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// ฟังก์ชันบันทึกว่าเรียนจบบทเรียนแล้ว (Mark Complete)
|
||||
// Endpoint: POST /students/courses/:cid/lessons/:lid/complete
|
||||
const markLessonComplete = async (courseId: number, lessonId: number) => {
|
||||
try {
|
||||
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/lessons/${lessonId}/complete`, {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* @file login.vue
|
||||
* @description Login Page.
|
||||
* Handles user authentication with email/password and social login (mock).
|
||||
* Uses the 'auth' layout.
|
||||
* @description หน้าเข้าสู่ระบบ (Login Page)
|
||||
* รองรับการเข้าสู่ระบบด้วย Email/Password
|
||||
*/
|
||||
|
||||
definePageMeta({
|
||||
|
|
@ -28,6 +27,7 @@ const loginForm = reactive({
|
|||
})
|
||||
|
||||
// Validation rules definition
|
||||
// กำหนดกฎการตรวจสอบข้อมูล (Validation Rules)
|
||||
const loginRules = {
|
||||
email: {
|
||||
rules: {
|
||||
|
|
@ -56,11 +56,12 @@ const loginRules = {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validates form and attempts login.
|
||||
* Currently simulates an API call for demonstration.
|
||||
* ฟังก์ชันตรวจสอบความถูกต้องของฟอร์มและเรียก API login
|
||||
*/
|
||||
/**
|
||||
* Handles input updates, stripping Thai characters and clearing errors.
|
||||
* จัดการเมื่อมีการพิมพ์ข้อมูล (Input Handler)
|
||||
* - ลบ error เมื่อเริ่มพิมพ์ใหม่
|
||||
* - ตรวจสอบภาษาไทยแบบ real-time
|
||||
*/
|
||||
const handleInput = (field: keyof typeof loginForm, value: string) => {
|
||||
loginForm[field] = value
|
||||
|
|
|
|||
|
|
@ -15,23 +15,36 @@ useHead({
|
|||
title: "รายการคอร์ส - e-Learning",
|
||||
});
|
||||
|
||||
// UI State
|
||||
// ==========================================
|
||||
// 1. ตัวแปร State (สถานะของ UI)
|
||||
// ==========================================
|
||||
// showDetail: ควบคุมการแสดงผลหน้ารายละเอียดคอร์ส (true = แสดง, false = แสดงรายการ)
|
||||
const showDetail = ref(false);
|
||||
// searchQuery: เก็บคำค้นหาที่ผู้ใช้พิมพ์
|
||||
const searchQuery = ref("");
|
||||
// isCategoryOpen: ควบคุมการเปิด/ปิดเมนูหมวดหมู่ด้านข้าง
|
||||
const isCategoryOpen = ref(true);
|
||||
|
||||
// Helper to get localized text
|
||||
// ==========================================
|
||||
// 2. ฟังก์ชันช่วยเหลือ (Helpers)
|
||||
// ==========================================
|
||||
// getLocalizedText: เลือกแสดงภาษาไทยหรืออังกฤษตามข้อมูลที่มี
|
||||
// ถ้าเป็น object {th, en} จะพยายามหา th ก่อน, ถ้าไม่มีให้ใช้ en
|
||||
const getLocalizedText = (text: string | { th: string; en: string } | undefined) => {
|
||||
if (!text) return ''
|
||||
if (typeof text === 'string') return text
|
||||
return text.th || text.en || ''
|
||||
}
|
||||
|
||||
// Categories Data
|
||||
// ==========================================
|
||||
// 3. จัดการข้อมูลหมวดหมู่ (Categories)
|
||||
// ==========================================
|
||||
// ใช้ useCategory composable เพื่อดึงข้อมูลหมวดหมู่จาก API
|
||||
const { fetchCategories } = useCategory();
|
||||
const categories = ref<any[]>([]);
|
||||
const showAllCategories = ref(false);
|
||||
const showAllCategories = ref(false); // ควบคุมการแสดงหมวดหมู่ทั้งหมด (Show More/Less)
|
||||
|
||||
// ฟังก์ชันโหลดข้อมูลหมวดหมู่
|
||||
const loadCategories = async () => {
|
||||
const res = await fetchCategories();
|
||||
if (res.success) {
|
||||
|
|
@ -39,22 +52,28 @@ const loadCategories = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
// คำนวณหมวดหมู่ที่จะแสดงผล (ถ้ากด Show More จะแสดงทั้งหมด, ปกติแสดงแค่ 8 รายการ)
|
||||
const visibleCategories = computed(() => {
|
||||
return showAllCategories.value ? categories.value : categories.value.slice(0, 8);
|
||||
});
|
||||
|
||||
// Courses Data
|
||||
// ==========================================
|
||||
// 4. จัดการข้อมูลคอร์ส (Courses)
|
||||
// ==========================================
|
||||
// ใช้ useCourse composable สำหรับจัดการคอร์สเรียน (ดึงข้อมูล, ลงทะเบียน)
|
||||
const { fetchCourses, fetchCourseById, enrollCourse } = useCourse();
|
||||
const courses = ref<any[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const selectedCourse = ref<any>(null);
|
||||
const isLoadingDetail = ref(false);
|
||||
const isEnrolling = ref(false);
|
||||
const isLoading = ref(false); // สถานะกำลังโหลดรายการคอร์ส
|
||||
const selectedCourse = ref<any>(null); // คอร์สที่ถูกเลือกดูรายละเอียด
|
||||
const isLoadingDetail = ref(false); // สถานะกำลังโหลดรายละเอียดคอร์ส
|
||||
const isEnrolling = ref(false); // สถานะกำลังกดลงทะเบียน
|
||||
|
||||
// ฟังก์ชันโหลดข้อมูลคอร์สทั้งหมด
|
||||
const loadCourses = async () => {
|
||||
isLoading.value = true;
|
||||
const res = await fetchCourses();
|
||||
if (res.success) {
|
||||
// แปลงข้อมูลเบื้องต้นสำหรับแสดงผลในการ์ด
|
||||
courses.value = (res.data || []).map((c: any) => ({
|
||||
...c,
|
||||
rating: "0.0",
|
||||
|
|
@ -65,11 +84,13 @@ const loadCourses = async () => {
|
|||
isLoading.value = false;
|
||||
};
|
||||
|
||||
// ฟังก์ชันเลือกคอร์สเพื่อดูรายละเอียด
|
||||
const selectCourse = async (id: number) => {
|
||||
isLoadingDetail.value = true;
|
||||
selectedCourse.value = null;
|
||||
showDetail.value = true;
|
||||
showDetail.value = true; // เปลี่ยนหน้าเป็นแบบ Detail View
|
||||
|
||||
// ดึงข้อมูลรายละเอียดคอร์สจาก API โดยใช้ ID
|
||||
const res = await fetchCourseById(id);
|
||||
if (res.success) {
|
||||
selectedCourse.value = res.data;
|
||||
|
|
@ -77,14 +98,15 @@ const selectCourse = async (id: number) => {
|
|||
isLoadingDetail.value = false;
|
||||
};
|
||||
|
||||
// ฟังก์ชันลงทะเบียนเรียน (Enroll)
|
||||
const handleEnroll = async (id: number) => {
|
||||
if (isEnrolling.value) return;
|
||||
if (isEnrolling.value) return; // ป้องกันการกดรัว
|
||||
isEnrolling.value = true;
|
||||
|
||||
const res = await enrollCourse(id);
|
||||
|
||||
if (res.success) {
|
||||
// Navigate to my-courses where the success modal will be shown
|
||||
// ถ้าสำเร็จ ให้เปลี่ยนหน้าไปที่ "คอร์สของฉัน" พร้อมส่ง parameter enrolled=true เพื่อแสดง popup
|
||||
return navigateTo('/dashboard/my-courses?enrolled=true');
|
||||
} else {
|
||||
alert(res.error || 'Failed to enroll');
|
||||
|
|
@ -94,23 +116,28 @@ const handleEnroll = async (id: number) => {
|
|||
};
|
||||
|
||||
onMounted(() => {
|
||||
// โหลดข้อมูลเมื่อหน้าเว็บเริ่มทำงาน
|
||||
loadCategories();
|
||||
loadCourses();
|
||||
});
|
||||
|
||||
// Filter Logic based on search query
|
||||
// Filter Logic based on search query and category
|
||||
// ==========================================
|
||||
// 5. ระบบกรองและค้นหา (Filter & Search)
|
||||
// ==========================================
|
||||
// selectedCategoryIds: เก็บ ID ของหมวดหมู่ที่ถูกติ๊กเลือก
|
||||
const selectedCategoryIds = ref<number[]>([]);
|
||||
|
||||
// คำนวณคอร์สที่จะแสดงผลจริง (Filter Logic)
|
||||
const filteredCourses = computed(() => {
|
||||
let result = courses.value;
|
||||
|
||||
// Filter by Category
|
||||
// 1. กรองตามหมวดหมู่ (Category Filter)
|
||||
if (selectedCategoryIds.value.length > 0) {
|
||||
result = result.filter(c => selectedCategoryIds.value.includes(c.category_id));
|
||||
}
|
||||
|
||||
// Filter by Search Query
|
||||
// 2. กรองตามคำค้นหา (Search Query) - ค้นหาจากชื่อหรือคำอธิบาย
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
result = result.filter(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* @file learning.vue
|
||||
* @description Course Learning Interface ("Classroom" view).
|
||||
* Defines the main learning environment where users watch video lessons and track progress.
|
||||
* Layout mimics a typical LMS with a sidebar for curriculum and a main content area for video/details.
|
||||
* @important Matches the provided design mockups pixel-perfectly.
|
||||
* @description หน้าเรียนออนไลน์ (Classroom Interface)
|
||||
* จัดการแสดงผลวิดีโอรายการบทเรียน และติดตามความคืบหน้า
|
||||
* ออกแบบให้เหมือนระบบ LMS มาตรฐาน
|
||||
*/
|
||||
|
||||
|
||||
definePageMeta({
|
||||
layout: false, // Custom layout defined within this component
|
||||
middleware: 'auth'
|
||||
|
|
@ -24,18 +24,23 @@ const sidebarOpen = ref(false)
|
|||
const activeTab = ref<'details' | 'announcements'>('details')
|
||||
const courseId = computed(() => Number(route.query.course_id))
|
||||
|
||||
// ==========================================
|
||||
// 1. ตัวแปร State (สถานะของ UI)
|
||||
// ==========================================
|
||||
// courseData: เก็บข้อมูลโครงสร้างคอร์ส (บทเรียนต่างๆ)
|
||||
const courseData = ref<any>(null)
|
||||
// currentLesson: บทเรียนที่กำลังเรียนอยู่ปัจจุบัน
|
||||
const currentLesson = ref<any>(null)
|
||||
const isLoading = ref(true)
|
||||
const isLessonLoading = ref(false)
|
||||
const isLoading = ref(true) // โหลดข้อมูลคอร์ส
|
||||
const isLessonLoading = ref(false) // โหลดเนื้อหาบทเรียน
|
||||
|
||||
// Video Player Logic
|
||||
// Video Player State (สถานะวิดีโอ)
|
||||
const videoRef = ref<HTMLVideoElement | null>(null)
|
||||
const isPlaying = ref(false)
|
||||
const videoProgress = ref(0)
|
||||
const currentTime = ref(0)
|
||||
const duration = ref(0)
|
||||
const saveProgressInterval = ref<any>(null)
|
||||
const saveProgressInterval = ref<any>(null) // ตัวแปรเก็บ setInterval สำหรับบันทึกความคืบหน้าอัตโนมัติ
|
||||
|
||||
// Helper for localization
|
||||
const getLocalizedText = (text: any) => {
|
||||
|
|
@ -59,6 +64,11 @@ const switchTab = (tab: 'details' | 'announcements', lessonId: any = null) => {
|
|||
}
|
||||
|
||||
// Data Fetching
|
||||
// ==========================================
|
||||
// 2. ฟังก์ชันโหลดข้อมูล (Data Fetching)
|
||||
// ==========================================
|
||||
|
||||
// โหลดโครงสร้างคอร์สและบทเรียนทั้งหมด
|
||||
const loadCourseData = async () => {
|
||||
if (!courseId.value) return
|
||||
isLoading.value = true
|
||||
|
|
@ -67,7 +77,7 @@ const loadCourseData = async () => {
|
|||
if (res.success) {
|
||||
courseData.value = res.data
|
||||
|
||||
// Auto-load first unlocked lesson if no current lesson
|
||||
// Auto-load logic: ถ้ายังไม่ได้เลือกบทเรียน ให้โหลดบทแรกที่ไม่ล็อคมาแสดง
|
||||
if (!currentLesson.value) {
|
||||
const firstChapter = res.data.chapters[0]
|
||||
if (firstChapter && firstChapter.lessons.length > 0) {
|
||||
|
|
@ -164,7 +174,10 @@ const videoSrc = computed(() => {
|
|||
return ''
|
||||
})
|
||||
|
||||
// Save progress periodically
|
||||
// ==========================================
|
||||
// 3. ระบบบันทึกความคืบหน้า (Progress Tracking)
|
||||
// ==========================================
|
||||
// บันทึกอัตโนมัติทุกๆ 10 วินาทีเมื่อเล่นวิดีโอ
|
||||
watch(() => isPlaying.value, (playing) => {
|
||||
if (playing) {
|
||||
saveProgressInterval.value = setInterval(() => {
|
||||
|
|
@ -190,11 +203,12 @@ watch(() => isPlaying.value, (playing) => {
|
|||
}
|
||||
})
|
||||
|
||||
// เมื่อวิดีโอจบ ให้บันทึกว่าเรียนจบ (Complete)
|
||||
const onVideoEnded = async () => {
|
||||
isPlaying.value = false
|
||||
if (currentLesson.value) {
|
||||
await markLessonComplete(courseId.value, currentLesson.value.id)
|
||||
// Reload course data to update sidebar progress/locks
|
||||
// โหลดข้อมูลคอร์สใหม่เพื่ออัปเดตสถานะติ๊กถูกด้านข้าง
|
||||
await loadCourseData()
|
||||
|
||||
// Auto play next logic could go here
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@ definePageMeta({
|
|||
})
|
||||
|
||||
const route = useRoute()
|
||||
// ดึง courseId จาก URL params (แปลงเป็น integer)
|
||||
const courseId = computed(() => parseInt(route.params.id as string))
|
||||
const { fetchCourseById, enrollCourse } = useCourse()
|
||||
|
||||
// ใช้ useAsyncData ดึงข้อมูลคอร์ส Server-side rendering (SSR)
|
||||
// Key: 'course-{id}' เพื่อให้ cache แยกกันตาม ID
|
||||
const { data: courseData, error, refresh } = await useAsyncData(`course-${courseId.value}`, () => fetchCourseById(courseId.value))
|
||||
|
||||
const course = computed(() => {
|
||||
|
|
@ -22,18 +25,20 @@ const course = computed(() => {
|
|||
|
||||
const isEnrolling = ref(false)
|
||||
|
||||
// ฟังก์ชันสำหรับกดปุ่ม "ลงทะเบียนเรียน"
|
||||
const handleEnroll = async () => {
|
||||
if (!course.value) return
|
||||
if (isEnrolling.value) return
|
||||
isEnrolling.value = true
|
||||
|
||||
// เรียก API ลงทะเบียนเรียน
|
||||
const res = await enrollCourse(course.value.id)
|
||||
|
||||
if (res.success) {
|
||||
// Navigate to my-courses
|
||||
// ถ้าสำเร็จ ให้เปลี่ยนหน้าไปที่ "คอร์สของฉัน" พร้อม params enrolled=true
|
||||
return navigateTo('/dashboard/my-courses?enrolled=true')
|
||||
} else {
|
||||
// Handle error (alert for now, could be toast)
|
||||
// กรณี error แสดง alert (อนาคตอาจเปลี่ยนเป็น Toast notification)
|
||||
alert(res.error || 'Failed to enroll')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* @file home.vue
|
||||
* @description Dashboard / Home Page.
|
||||
* Displays the user's dashboard with a welcome message, current learning progress, and course recommendations.
|
||||
* @description หน้าแดชบอร์ดหลัก (Dashboard)
|
||||
* แสดงข้อความต้อนรับ และคอร์สแนะนำสำหรับผู้เรียน
|
||||
*/
|
||||
|
||||
definePageMeta({
|
||||
|
|
@ -26,10 +26,12 @@ const getLocalizedText = (text: string | { th: string; en: string } | undefined)
|
|||
}
|
||||
|
||||
// Recommended Courses State
|
||||
// เก็บข้อมูลคอร์สแนะนำ (สุ่มมา 3 คอร์ส)
|
||||
const recommendedCourses = ref<any[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
// 1. Fetch Categories for mapping
|
||||
// ดึงหมวดหมู่เพื่อเอามาแสดงชื่อหมวดหมู่ในการ์ด
|
||||
const catRes = await fetchCategories()
|
||||
const catMap = new Map()
|
||||
if (catRes.success) {
|
||||
|
|
@ -37,12 +39,13 @@ onMounted(async () => {
|
|||
}
|
||||
|
||||
// 2. Fetch All Courses and Randomize
|
||||
// ดึงคอร์สทั้งหมดและสุ่มเลือกมา 3 อัน
|
||||
const res = await fetchCourses()
|
||||
if (res.success && res.data?.length) {
|
||||
// Shuffle array
|
||||
// Shuffle array (สุ่มลำดับ)
|
||||
const shuffled = [...res.data].sort(() => 0.5 - Math.random())
|
||||
|
||||
// Pick first 3
|
||||
// Pick first 3 (เลือกมา 3 อันแรก)
|
||||
recommendedCourses.value = shuffled.slice(0, 3).map((c: any) => ({
|
||||
id: c.id,
|
||||
title: getLocalizedText(c.title),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue