/** * 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 */} navigate(`/exams/${examId}`)} className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors" > 返回 进度: {exam.current_index + 1} / {exam.total_questions} {/* Question Card */} {/* Question Header */} {exam.current_index + 1} {getQuestionTypeText(question.type)} {inMistakeBook ? ( <> 移出错题本 > ) : ( <> 加入错题本 > )} {/* 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 ( { if (!result) { if (question.type === 'multiple') { handleMultipleChoice(option) } else { setUserAnswer(letter) } } }} disabled={!!result} className={`w-full text-left p-4 rounded-lg border-2 transition-all ${ isSelected ? 'border-primary-500 bg-primary-50' : 'border-gray-200 hover:border-gray-300' } ${result ? 'cursor-not-allowed opacity-75' : 'cursor-pointer'}`} > {option} ) })} )} {/* Short Answer Input */} {question.type === 'short' && !result && ( setUserAnswer(e.target.value)} rows={4} className="w-full px-4 py-3 border-2 border-gray-200 rounded-lg focus:border-primary-500 focus:outline-none" placeholder="请输入你的答案..." /> )} {/* Judge Input */} {question.type === 'judge' && !result && ( setUserAnswer('A')} className={`flex-1 py-3 rounded-lg border-2 transition-all ${ userAnswer === 'A' ? 'border-green-500 bg-green-50 text-green-700' : 'border-gray-200 hover:border-gray-300' }`} > 正确 setUserAnswer('B')} className={`flex-1 py-3 rounded-lg border-2 transition-all ${ userAnswer === 'B' ? 'border-red-500 bg-red-50 text-red-700' : 'border-gray-200 hover:border-gray-300' }`} > 错误 )} {/* Submit Button */} {!result && ( {submitting ? ( <> 提交中... > ) : ( <> 提交答案 > )} )} {/* Result */} {result && ( {result.correct ? ( ) : ( )} {result.correct ? '回答正确!' : '回答错误'} {!result.correct && ( 你的答案:{result.user_answer} 正确答案:{result.correct_answer} )} {/* AI Score for short answers */} {result.ai_score !== null && result.ai_score !== undefined && ( AI 评分:{(result.ai_score * 100).toFixed(0)}% {result.ai_feedback && ( {result.ai_feedback} )} )} {/* Analysis */} {result.analysis && ( 解析: {result.analysis} )} {/* Next Button */} 下一题 )} ) } export default QuizPlayer
没有更多题目了
{question.content}
你的答案:{result.user_answer}
正确答案:{result.correct_answer}
AI 评分:{(result.ai_score * 100).toFixed(0)}%
{result.ai_feedback}
解析:
{result.analysis}