/** * Quiz Player Page - Core quiz functionality */ import React, { useState, useEffect } from 'react' import { useParams, useNavigate, useSearchParams } from 'react-router-dom' import { examAPI, questionAPI, mistakeAPI } from '../api/client' import Layout from '../components/Layout' import { ArrowLeft, ArrowRight, Check, X, Loader, BookmarkPlus, BookmarkX, AlertCircle } from 'lucide-react' import toast from 'react-hot-toast' import { getQuestionTypeText } from '../utils/helpers' export const QuizPlayer = () => { const { examId } = useParams() const navigate = useNavigate() const [searchParams] = useSearchParams() const [exam, setExam] = useState(null) const [question, setQuestion] = useState(null) const [loading, setLoading] = useState(true) const [submitting, setSubmitting] = useState(false) const [userAnswer, setUserAnswer] = useState('') const [multipleAnswers, setMultipleAnswers] = useState([]) const [result, setResult] = useState(null) const [inMistakeBook, setInMistakeBook] = useState(false) useEffect(() => { loadQuiz() }, [examId]) const loadQuiz = async () => { try { // Check if reset flag is present const shouldReset = searchParams.get('reset') === 'true' if (shouldReset) { await examAPI.updateProgress(examId, 0) } const examRes = await examAPI.getDetail(examId) setExam(examRes.data) await loadCurrentQuestion() } catch (error) { console.error('Failed to load quiz:', error) toast.error('加载题目失败') } finally { setLoading(false) } } const loadCurrentQuestion = async () => { try { const response = await questionAPI.getCurrentQuestion(examId) setQuestion(response.data) setResult(null) setUserAnswer('') setMultipleAnswers([]) await checkIfInMistakeBook(response.data.id) } catch (error) { if (error.response?.status === 404) { toast.success('恭喜!所有题目已完成!') navigate(`/exams/${examId}`) } else { console.error('Failed to load question:', error) toast.error('加载题目失败') } } } const checkIfInMistakeBook = async (questionId) => { try { const response = await mistakeAPI.getList(0, 1000) // TODO: Optimize this const inBook = response.data.mistakes.some(m => m.question_id === questionId) setInMistakeBook(inBook) } catch (error) { console.error('Failed to check mistake book:', error) } } const handleSubmitAnswer = async () => { let answer = userAnswer // For multiple choice, join selected options if (question.type === 'multiple') { if (multipleAnswers.length === 0) { toast.error('请至少选择一个选项') return } answer = multipleAnswers.sort().join('') } if (!answer.trim()) { toast.error('请输入答案') return } setSubmitting(true) try { const response = await questionAPI.checkAnswer(question.id, answer) setResult(response.data) if (response.data.correct) { toast.success('回答正确!') } else { toast.error('回答错误') } } catch (error) { console.error('Failed to check answer:', error) toast.error('提交答案失败') } finally { setSubmitting(false) } } const handleNext = async () => { try { const newIndex = exam.current_index + 1 await examAPI.updateProgress(examId, newIndex) const examRes = await examAPI.getDetail(examId) setExam(examRes.data) await loadCurrentQuestion() } catch (error) { console.error('Failed to move to next question:', error) } } const handleToggleMistake = async () => { try { if (inMistakeBook) { await mistakeAPI.removeByQuestionId(question.id) setInMistakeBook(false) toast.success('已从错题本移除') } else { await mistakeAPI.add(question.id) setInMistakeBook(true) toast.success('已加入错题本') } } catch (error) { console.error('Failed to toggle mistake:', error) } } const handleMultipleChoice = (option) => { const letter = option.charAt(0) if (multipleAnswers.includes(letter)) { setMultipleAnswers(multipleAnswers.filter(a => a !== letter)) } else { setMultipleAnswers([...multipleAnswers, letter]) } } if (loading) { return (
) } if (!question) { return (

没有更多题目了

) } return (
{/* Header */}
进度: {exam.current_index + 1} / {exam.total_questions}
{/* Question Card */}
{/* Question Header */}
{exam.current_index + 1} {getQuestionTypeText(question.type)}
{/* Question Content */}

{question.content}

{/* Options (for choice questions) */} {question.options && question.options.length > 0 && (
{question.options.map((option, index) => { const letter = option.charAt(0) const isSelected = question.type === 'multiple' ? multipleAnswers.includes(letter) : userAnswer === letter return ( ) })}
)} {/* Short Answer Input */} {question.type === 'short' && !result && (