feat: Implement initial e-learning platform frontend structure including dashboard, course management, authentication, and common UI components.
This commit is contained in:
parent
aceeb80d9a
commit
ad11c6b7c5
44 changed files with 720 additions and 578 deletions
|
|
@ -19,95 +19,107 @@ export interface AnswerState {
|
|||
|
||||
/**
|
||||
* @composable useQuizRunner
|
||||
* @description Manages the state and logic for running a quiz activity.
|
||||
* @description จัดการ State (สถานะ) และ Logic สำหรับการทำแบบทดสอบ (Quiz)
|
||||
* ครอบคลุมการแสดงคำถาม, บันทึกคำตอบ, และตรวจสอบการข้ามคำถาม
|
||||
*/
|
||||
export const useQuizRunner = () => {
|
||||
// State
|
||||
const questions = useState<QuizQuestion[]>('quiz-questions', () => []);
|
||||
const answers = useState<Record<number, AnswerState>>('quiz-answers', () => ({}));
|
||||
const currentQuestionIndex = useState<number>('quiz-current-index', () => 0);
|
||||
const loading = useState<boolean>('quiz-loading', () => false);
|
||||
const lastError = useState<string | null>('quiz-error', () => null);
|
||||
// ================= State (สถานะเก็บค่าต่างๆ ของข้อสอบ) =================
|
||||
const questions = useState<QuizQuestion[]>('quiz-questions', () => []); // เก็บรายการคำถามทั้งหมด
|
||||
const answers = useState<Record<number, AnswerState>>('quiz-answers', () => ({})); // เก็บคำตอบที่ผู้ใช้ตอบ แยกตาม ID คำถาม
|
||||
const currentQuestionIndex = useState<number>('quiz-current-index', () => 0); // ลำดับคำถามที่กำลังทำอยู่ปัจจุบัน (เริ่มที่ 0)
|
||||
const loading = useState<boolean>('quiz-loading', () => false); // สถานะตอนกำลังกดเซฟหรือโหลดข้อมูล
|
||||
const lastError = useState<string | null>('quiz-error', () => null); // เก็บข้อความแจ้งเตือนข้อผิดพลาดล่าสุด
|
||||
|
||||
// Getters
|
||||
const currentQuestion = computed(() => questions.value[currentQuestionIndex.value]);
|
||||
// ================= Getters (ดึงค่าที่ถูกประมวลผลแล้ว) =================
|
||||
const currentQuestion = computed(() => questions.value[currentQuestionIndex.value]); // ดึงคำถามข้อปัจจุบัน
|
||||
|
||||
const currentAnswer = computed(() => {
|
||||
const currentAnswer = computed(() => { // ดึงคำตอบในข้อปัจจุบัน
|
||||
if (!currentQuestion.value) return null;
|
||||
return answers.value[currentQuestion.value.id];
|
||||
});
|
||||
|
||||
const totalQuestions = computed(() => questions.value.length);
|
||||
const isLastQuestion = computed(() => currentQuestionIndex.value === questions.value.length - 1);
|
||||
const isFirstQuestion = computed(() => currentQuestionIndex.value === 0);
|
||||
const totalQuestions = computed(() => questions.value.length); // จำนวนคำถามทั้งหมดในแบบทดสอบ
|
||||
const isLastQuestion = computed(() => currentQuestionIndex.value === questions.value.length - 1); // เช็คว่าใช่คำถามข้อสุดท้ายหรือไม่
|
||||
const isFirstQuestion = computed(() => currentQuestionIndex.value === 0); // เช็คว่าใช่คำถามข้อแรกหรือไม่
|
||||
|
||||
// Actions
|
||||
// ================= Actions (ฟังก์ชันหลักสำหรับการทำงาน) =================
|
||||
|
||||
// ฟังก์ชันเริ่มต้นสร้าง/โหลดข้อสอบ (กำหนดโครงสร้างพื้นฐาน)
|
||||
function initQuiz(quizData: any) {
|
||||
if (!quizData || !quizData.questions) return;
|
||||
|
||||
questions.value = quizData.questions;
|
||||
currentQuestionIndex.value = 0;
|
||||
currentQuestionIndex.value = 0; // รีเซ็ตไปที่ข้อ 1 ใหม่
|
||||
answers.value = {};
|
||||
lastError.value = null;
|
||||
|
||||
// เตรียมโครงสร้างคำตอบรองรับทุกข้อ
|
||||
questions.value.forEach(q => {
|
||||
answers.value[q.id] = {
|
||||
questionId: q.id,
|
||||
value: null,
|
||||
is_saved: false,
|
||||
status: 'not_started',
|
||||
touched: false,
|
||||
is_saved: false, // บันทึกและส่ง API เรียบร้อยหรือยัง
|
||||
status: 'not_started', // สถานะเริ่มต้นของคำถาม
|
||||
touched: false, // ผู้ใช้เคยเปิดเข้ามาดูข้อนีัหรือยัง
|
||||
};
|
||||
});
|
||||
|
||||
// เริ่มต้นบันทึกเวลา/เข้าสู่ข้อที่ 1 ทันทีเมื่ออธิบายเสร็จ
|
||||
if (questions.value.length > 0) {
|
||||
enterQuestion(questions.value[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
// ฟังก์ชันสลับสถานะเมื่อกดเข้ามาที่คำถามนั้นๆ
|
||||
function enterQuestion(qId: number) {
|
||||
const ans = answers.value[qId];
|
||||
if (ans) {
|
||||
ans.touched = true;
|
||||
if (ans.status === 'not_started' || ans.status === 'skipped') {
|
||||
ans.status = 'in_progress';
|
||||
ans.status = 'in_progress'; // เปลี่ยนสถานะเป็น 'กำลังทำ'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ตรวจสอบเงื่อนไขว่าผู้ใช้สามารถออกจากคำถามปัจจุบันไปยังข้ออื่นได้หรือไม่
|
||||
function canLeaveCurrent(): { allowed: boolean; reason?: string } {
|
||||
if (!currentQuestion.value) return { allowed: true };
|
||||
const q = currentQuestion.value;
|
||||
const a = answers.value[q.id];
|
||||
|
||||
// สามารถออกได้ถ้าคำถามทำถูกหรือโจทย์อนุญาตให้ข้ามได้
|
||||
if (a.status === 'completed' || a.is_saved) return { allowed: true };
|
||||
if (q.is_skippable) return { allowed: true };
|
||||
|
||||
// บังคับให้ตอบถ้าไม่ได้อนุญาตให้ข้าม และไม่ได้ตอบ
|
||||
if (!a.is_saved && a.value === null) {
|
||||
return { allowed: false, reason: 'This question is required.' };
|
||||
return { allowed: false, reason: 'ต้องการคำตอบสำหรับข้อบังคับนี้' };
|
||||
}
|
||||
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
// ฟังก์ชันอัปเดตค่าตัวเลือกที่ผู้ใช้กดเลือกในข้อปัจจุบัน
|
||||
function updateAnswer(val: any) {
|
||||
if (!currentQuestion.value) return;
|
||||
const qId = currentQuestion.value.id;
|
||||
answers.value[qId].value = val;
|
||||
|
||||
// หากมีแก้ไขคำตอบหลังจากกดเซฟไปแล้ว ให้เปลี่ยนสถานะให้ระบบรู้ว่าต้องเซฟใหม่
|
||||
if (answers.value[qId].is_saved) {
|
||||
answers.value[qId].is_saved = false;
|
||||
answers.value[qId].status = 'in_progress';
|
||||
answers.value[qId].status = 'in_progress';
|
||||
}
|
||||
}
|
||||
|
||||
// ล็อกและบันทึกข้อสอบเมื่อกดปุ่ม "ตกลง/ส่งคำตอบ" สำหรับข้อนั้นๆ
|
||||
async function saveCurrentAnswer() {
|
||||
if (!currentQuestion.value) return;
|
||||
const qId = currentQuestion.value.id;
|
||||
const ans = answers.value[qId];
|
||||
|
||||
if (ans.value === null) {
|
||||
lastError.value = "Please provide an answer.";
|
||||
lastError.value = "กรุณาเลือกคำตอบอย่างน้อย 1 ตัวเลือก";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -115,30 +127,34 @@ export const useQuizRunner = () => {
|
|||
lastError.value = null;
|
||||
|
||||
try {
|
||||
// หมายเหตุ: การเชื่อมต่อ API หลักต้องทำที่ไฟล์ component, ตัวนี้จัดการแค่เรื่อง State
|
||||
ans.is_saved = true;
|
||||
ans.status = 'completed';
|
||||
ans.status = 'completed'; // มาร์คว่าเป็นข้อที่ทำเสร็จแล้ว
|
||||
ans.last_saved_at = new Date().toISOString();
|
||||
return true;
|
||||
} catch (e) {
|
||||
lastError.value = "Failed to save answer.";
|
||||
lastError.value = "เกิดข้อผิดพลาดในการบันทึกคำตอบ";
|
||||
return false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// วินิจฉัยก่อนสั่งผู้ใช้ย้ายไปยังคำถามอื่นหน้าอื่นตามดัชนีระบุ
|
||||
function handleLeaveLogic(targetIndex: number) {
|
||||
if (targetIndex === currentQuestionIndex.value) return;
|
||||
|
||||
// ตรวจสอบขั้นสุดท้าย ป้องกันคนคลิกแอบหนีข้อที่บังคับทำ
|
||||
const check = canLeaveCurrent();
|
||||
if (!check.allowed) {
|
||||
lastError.value = check.reason || "Required question.";
|
||||
lastError.value = check.reason || "จำเป็นต้องตอบข้อนี้ก่อนข้าม";
|
||||
return false;
|
||||
}
|
||||
|
||||
const currQ = currentQuestion.value;
|
||||
if (currQ) {
|
||||
const currAns = answers.value[currQ.id];
|
||||
// หากผู้ใช้ทิ้งขว้างโดยที่ไม่บังคับ ให้ทิ้งสถานะเป็นข้าม ('skipped')
|
||||
if (currAns.status !== 'completed' && !currAns.is_saved) {
|
||||
currAns.status = 'skipped';
|
||||
}
|
||||
|
|
@ -147,6 +163,7 @@ export const useQuizRunner = () => {
|
|||
currentQuestionIndex.value = targetIndex;
|
||||
lastError.value = null;
|
||||
|
||||
// ติดตามสถานะ 'touched' ในข้อใหม่ที่เข้าไปล่าสุด
|
||||
if (questions.value[targetIndex]) {
|
||||
enterQuestion(questions.value[targetIndex].id);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue