import React, { useState, useEffect } from "react"; import { useSearchParams, useNavigate } from "react-router-dom"; import { Button, message, Typography, Switch, InputNumber, Modal, Space } from "antd"; import { ArrowLeftOutlined, SettingOutlined } from "@ant-design/icons"; import type { Question, AnswerResult } from "../types/question"; import * as questionApi from "../api/question"; import QuestionCard from "../components/QuestionCard"; import CompletionSummary from "../components/CompletionSummary"; import QuestionDrawer from "../components/QuestionDrawer"; import QuestionFloatButton from "../components/QuestionFloatButton"; import styles from "./Question.module.less"; const { Title } = Typography; const QuestionPage: React.FC = () => { const [searchParams] = useSearchParams(); const navigate = useNavigate(); const [currentQuestion, setCurrentQuestion] = useState(null); const [selectedAnswer, setSelectedAnswer] = useState(""); const [showResult, setShowResult] = useState(false); const [answerResult, setAnswerResult] = useState(null); const [loading, setLoading] = useState(false); const [autoNextLoading, setAutoNextLoading] = useState(false); const [allQuestions, setAllQuestions] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); // 答题统计 const [correctCount, setCorrectCount] = useState(0); const [wrongCount, setWrongCount] = useState(0); const [showSummary, setShowSummary] = useState(false); // 题目导航抽屉 const [drawerVisible, setDrawerVisible] = useState(false); const [answeredStatus, setAnsweredStatus] = useState>(new Map()); // 存储每道题的用户答案和答题结果 const [userAnswers, setUserAnswers] = useState>(new Map()); const [questionResults, setQuestionResults] = useState>(new Map()); // 设置弹窗 const [settingsVisible, setSettingsVisible] = useState(false); // 自动跳转开关(默认开启) const [autoNext, setAutoNext] = useState(() => { const saved = localStorage.getItem('autoNextEnabled'); return saved !== null ? saved === 'true' : true; }); // 自动跳转延迟时间(秒,默认2秒) const [autoNextDelay, setAutoNextDelay] = useState(() => { const saved = localStorage.getItem('autoNextDelay'); return saved !== null ? parseInt(saved, 10) : 2; }); // 随机题目开关(默认关闭) const [randomMode, setRandomMode] = useState(() => { const saved = localStorage.getItem('randomModeEnabled'); return saved !== null ? saved === 'true' : false; }); // 切换自动跳转开关 const toggleAutoNext = () => { const newValue = !autoNext; setAutoNext(newValue); localStorage.setItem('autoNextEnabled', String(newValue)); }; // 切换随机模式开关 const toggleRandomMode = () => { const newValue = !randomMode; setRandomMode(newValue); localStorage.setItem('randomModeEnabled', String(newValue)); }; // 修改自动跳转延迟时间 const handleDelayChange = (value: number | null) => { if (value !== null && value >= 1 && value <= 10) { setAutoNextDelay(value); localStorage.setItem('autoNextDelay', String(value)); } }; // 从localStorage恢复答题进度 const getStorageKey = () => { const type = searchParams.get("type"); const mode = searchParams.get("mode"); return `question_progress_${type || mode || "default"}`; }; // 保存答题进度 const saveProgress = (index: number, correct: number, wrong: number, statusMap?: Map) => { const key = getStorageKey(); const answeredStatusObj: Record = {}; const userAnswersObj: Record = {}; const questionResultsObj: Record = {}; // 将 Map 转换为普通对象以便 JSON 序列化 const mapToSave = statusMap || answeredStatus; mapToSave.forEach((value, key) => { answeredStatusObj[key] = value; }); userAnswers.forEach((value, key) => { userAnswersObj[key] = value; }); questionResults.forEach((value, key) => { questionResultsObj[key] = value; }); localStorage.setItem( key, JSON.stringify({ currentIndex: index, correctCount: correct, wrongCount: wrong, answeredStatus: answeredStatusObj, userAnswers: userAnswersObj, questionResults: questionResultsObj, timestamp: Date.now(), }) ); }; // 恢复答题进度 const loadProgress = () => { const key = getStorageKey(); const saved = localStorage.getItem(key); if (saved) { try { const progress = JSON.parse(saved); setCurrentIndex(progress.currentIndex || 0); setCorrectCount(progress.correctCount || 0); setWrongCount(progress.wrongCount || 0); // 恢复答题状态 if (progress.answeredStatus) { const statusMap = new Map(); Object.entries(progress.answeredStatus).forEach(([index, status]) => { statusMap.set(Number(index), status as boolean | null); }); setAnsweredStatus(statusMap); } // 恢复用户答案 if (progress.userAnswers) { const answersMap = new Map(); Object.entries(progress.userAnswers).forEach(([index, answer]) => { answersMap.set(Number(index), answer as string | string[]); }); setUserAnswers(answersMap); } // 恢复答题结果 if (progress.questionResults) { const resultsMap = new Map(); Object.entries(progress.questionResults).forEach(([index, result]) => { resultsMap.set(Number(index), result as AnswerResult); }); setQuestionResults(resultsMap); } return progress.currentIndex || 0; } catch (e) { console.error("恢复进度失败", e); } } return 0; }; // 加载随机错题(使用智能推荐) const loadRandomWrongQuestion = async () => { console.log('[错题练习] 开始加载下一道错题...'); console.log('[错题练习] 当前题目ID:', currentQuestion?.id); setLoading(true); try { // 传递当前题目ID,避免重复推荐 const res = await questionApi.getRecommendedWrongQuestions(1, currentQuestion?.id); console.log('[错题练习] API响应:', res); if (res.success && res.data && res.data.length > 0) { // 获取推荐的错题,然后加载对应的题目详情 const wrongQuestion = res.data[0]; console.log('[错题练习] 推荐的错题:', wrongQuestion); const questionRes = await questionApi.getQuestionById(wrongQuestion.question_id); console.log('[错题练习] 题目详情:', questionRes); if (questionRes.success && questionRes.data) { console.log('[错题练习] 设置新题目:', questionRes.data.question_id); setCurrentQuestion(questionRes.data); setSelectedAnswer(questionRes.data.type === "multiple-selection" ? [] : ""); setShowResult(false); setAnswerResult(null); } else { console.error('[错题练习] 加载题目详情失败'); message.error("加载题目详情失败"); } } else { console.warn('[错题练习] 暂无推荐错题'); message.warning("暂无需要复习的错题"); } } catch (error: any) { console.error('[错题练习] 加载失败:', error); if (error.response?.status === 401) { message.error("请先登录"); } else if (error.response?.status === 404) { message.error("暂无错题"); } else { message.error("加载题目失败"); } } finally { setLoading(false); console.log('[错题练习] 加载完成'); } }; // 加载题目列表(从第一题开始) const loadQuestions = async (type?: string) => { setLoading(true); try { const res = await questionApi.getQuestions({ type }); if (res.success && res.data) { setAllQuestions(res.data); // 恢复答题进度 const savedIndex = loadProgress(); const startIndex = savedIndex < res.data.length ? savedIndex : 0; if (res.data.length > 0) { setCurrentQuestion(res.data[startIndex]); setCurrentIndex(startIndex); setSelectedAnswer( res.data[startIndex].type === "multiple-selection" ? [] : "" ); setShowResult(false); setAnswerResult(null); } } } catch (error) { message.error("加载题目列表失败"); } finally { setLoading(false); } }; // 提交答案 const handleSubmit = async () => { if (!currentQuestion) return; // 检查是否选择了答案 if (currentQuestion.type === "multiple-selection") { if ((selectedAnswer as string[]).length === 0) { message.warning("请选择答案"); return; } } else if (currentQuestion.type === "fill-in-blank") { const answers = selectedAnswer as string[]; if (answers.length === 0 || answers.some((a) => !a || a.trim() === "")) { message.warning("请填写所有空格"); return; } } else { if ( !selectedAnswer || (typeof selectedAnswer === "string" && selectedAnswer.trim() === "") ) { message.warning("请填写答案"); return; } } setLoading(true); try { // 处理判断题答案:将字符串 "true"/"false" 转换为布尔值 let answerToSubmit: string | string[] | boolean = selectedAnswer; if (currentQuestion.type === "true-false" && typeof selectedAnswer === "string") { answerToSubmit = selectedAnswer === "true"; } const res = await questionApi.submitAnswer({ question_id: currentQuestion.id, answer: answerToSubmit, }); if (res.success && res.data) { setAnswerResult(res.data); setShowResult(true); // 保存用户答案和结果到 Map const newAnswersMap = new Map(userAnswers); newAnswersMap.set(currentIndex, selectedAnswer); setUserAnswers(newAnswersMap); const newResultsMap = new Map(questionResults); newResultsMap.set(currentIndex, res.data); setQuestionResults(newResultsMap); // 更新答题状态 const newStatusMap = new Map(answeredStatus); newStatusMap.set(currentIndex, res.data.correct); setAnsweredStatus(newStatusMap); // 更新统计 if (res.data.correct) { const newCorrect = correctCount + 1; setCorrectCount(newCorrect); saveProgress(currentIndex, newCorrect, wrongCount, newStatusMap); } else { const newWrong = wrongCount + 1; setWrongCount(newWrong); saveProgress(currentIndex, correctCount, newWrong, newStatusMap); } // 如果答案正确且开启自动跳转,根据设置的延迟时间后自动进入下一题 if (res.data.correct && autoNext) { setAutoNextLoading(true); setTimeout(() => { setAutoNextLoading(false); handleNext(); }, autoNextDelay * 1000); // 将秒转换为毫秒 } } } catch (error) { message.error("提交失败"); } finally { setLoading(false); } }; // 下一题 const handleNext = () => { const mode = searchParams.get("mode"); console.log('[下一题] 当前模式:', mode); // 错题练习模式:加载下一道推荐错题 if (mode === "wrong") { console.log('[下一题] 错题练习模式,加载下一道错题'); loadRandomWrongQuestion(); window.scrollTo({ top: 0, behavior: 'smooth' }); return; } // 普通模式:从题库数组中选择 if (allQuestions.length > 0) { let nextIndex: number; // 随机模式:从题库中随机选择一题 if (randomMode) { // 获取所有未答题目的索引(排除当前题目) const unansweredIndexes: number[] = []; for (let i = 0; i < allQuestions.length; i++) { // 排除当前题目索引,避免选到同一题 if (i !== currentIndex && !answeredStatus.has(i)) { unansweredIndexes.push(i); } } // 如果没有未答题目,显示总结页面 if (unansweredIndexes.length === 0) { setShowSummary(true); // 清除进度 localStorage.removeItem(getStorageKey()); return; } // 从未答题目中随机选择一题 const randomIdx = Math.floor(Math.random() * unansweredIndexes.length); nextIndex = unansweredIndexes[randomIdx]; } else { // 顺序模式:检查是否完成所有题目 if (currentIndex + 1 >= allQuestions.length) { // 显示统计摘要 setShowSummary(true); // 清除进度 localStorage.removeItem(getStorageKey()); return; } nextIndex = currentIndex + 1; } setCurrentIndex(nextIndex); setCurrentQuestion(allQuestions[nextIndex]); setSelectedAnswer( allQuestions[nextIndex].type === "multiple-selection" ? [] : "" ); setShowResult(false); setAnswerResult(null); // 保存进度 saveProgress(nextIndex, correctCount, wrongCount); // 滚动到页面顶部 window.scrollTo({ top: 0, behavior: 'smooth' }); } }; // 跳转到指定题目 const handleQuestionSelect = (index: number) => { if (index >= 0 && index < allQuestions.length) { setCurrentIndex(index); setCurrentQuestion(allQuestions[index]); // 检查是否有保存的答案和结果 const savedAnswer = userAnswers.get(index); const savedResult = questionResults.get(index); if (savedAnswer !== undefined && savedResult !== undefined) { // 恢复之前的答案和结果 setSelectedAnswer(savedAnswer); setAnswerResult(savedResult); setShowResult(true); } else { // 没有答案,重置状态 setSelectedAnswer( allQuestions[index].type === "multiple-selection" ? [] : "" ); setShowResult(false); setAnswerResult(null); } // 保存进度 saveProgress(index, correctCount, wrongCount); // 滚动到页面顶部 window.scrollTo({ top: 0, behavior: 'smooth' }); } }; // 初始化 useEffect(() => { const typeParam = searchParams.get("type"); const mode = searchParams.get("mode"); // 错题练习模式 if (mode === "wrong") { loadRandomWrongQuestion(); return; } // 普通练习模式 - 从第一题开始 loadQuestions(typeParam || undefined); }, [searchParams]); // 重试处理 const handleRetry = () => { setShowSummary(false); setCurrentIndex(0); setCorrectCount(0); setWrongCount(0); localStorage.removeItem(getStorageKey()); const typeParam = searchParams.get("type"); loadQuestions(typeParam || undefined); }; // 重新答题(错题练习模式) const handleRetryQuestion = () => { // 重置当前题目的答题状态 setShowResult(false); setAnswerResult(null); // 重置答案 if (currentQuestion) { setSelectedAnswer( currentQuestion.type === "multiple-selection" ? [] : "" ); } // 滚动到页面顶部 window.scrollTo({ top: 0, behavior: 'smooth' }); }; return (
{/* 固定顶栏:包含导航和进度 */}
{/* 头部导航 */}
AnKao 刷题 {/* 设置按钮 */}
{/* 主内容区 */}
{/* 题目卡片 */} {currentQuestion && ( )}
{/* 完成统计摘要 */} { setShowSummary(false); navigate("/"); }} onRetry={handleRetry} /> {/* 题目导航抽屉 */} setDrawerVisible(false)} questions={allQuestions} currentIndex={currentIndex} onQuestionSelect={handleQuestionSelect} answeredStatus={answeredStatus} /> {/* 悬浮球 - 题目导航 */} {allQuestions.length > 0 && ( setDrawerVisible(true)} /> )} {/* 设置弹窗 */} setSettingsVisible(false)} footer={null} width={400} >
自动下一题 答对题目后自动跳转到下一题
{autoNext ? '已开启' : '已关闭'}
{autoNext && (
延迟时间 设置答对后等待多久跳转到下一题
秒后自动跳转
)}
随机题目 开启后点击下一题时随机跳转
{randomMode ? '已开启' : '已关闭'}
); }; export default QuestionPage;