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 [answerSequences, setAnswerSequences] = 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)); } }; // 恢复答题进度(完全从后端数据库加载) const loadProgress = async (questions: Question[], type: string) => { console.log('[进度加载] 开始加载进度 (type:', type, ', questions:', questions.length, ')'); if (!type || questions.length === 0) { console.log('[进度加载] 参数无效,跳过加载'); return { index: 0, hasAnswer: false }; } try { // 从后端加载该类型的进度数据 console.log('[进度加载] 调用 API: GET /api/practice/progress?type=' + type); const res = await questionApi.getPracticeProgress(type); console.log('[进度加载] API 响应:', res); if (res.success && res.data && res.data.length > 0) { // 取第一条记录(每个type只有一条进度记录) const progressData = res.data[0]; console.log('[进度加载] 进度数据:', progressData); // 创建 question_id 到索引的映射 const questionIdToIndex = new Map(); questions.forEach((q, idx) => { questionIdToIndex.set(q.id, idx); }); // 根据 current_question_id 定位到题目索引 const currentIndex = questionIdToIndex.get(progressData.current_question_id); console.log('[进度加载] current_question_id:', progressData.current_question_id, ', 对应索引:', currentIndex); // 解析已答题目的状态 const statusMap = new Map(); const answersMap = new Map(); const sequencesMap = new Map(); const resultsMap = new Map(); let correct = 0; let wrong = 0; // 遍历所有已答题目 progressData.answered_questions.forEach((item) => { const index = questionIdToIndex.get(item.question_id); if (index !== undefined) { statusMap.set(index, item.is_correct); // 对于判断题,需要将布尔值转换为字符串(Radio.Group 需要字符串类型) let userAnswer = item.user_answer; if (questions[index].type === 'true-false' && typeof userAnswer === 'boolean') { userAnswer = userAnswer ? 'true' : 'false'; console.log('[进度加载] 判断题答案格式转换: boolean', item.user_answer, '-> string', userAnswer); } answersMap.set(index, userAnswer); // 构造答题结果对象 const result: AnswerResult = { correct: item.is_correct, user_answer: userAnswer, correct_answer: item.correct_answer, }; // 如果有 AI 评分信息,也加入结果中 if (item.ai_score !== undefined && item.ai_feedback !== undefined) { result.ai_grading = { score: item.ai_score, feedback: item.ai_feedback, suggestion: item.ai_suggestion || '', }; console.log('[进度加载] 恢复AI评分: score=', item.ai_score, ', feedback=', item.ai_feedback); } resultsMap.set(index, result); // 计算答题序号(查询该题目的总答题次数) // 注意:这里暂时用1,后续可以优化为查询历史记录数 sequencesMap.set(index, 1); if (item.is_correct) { correct++; } else { wrong++; } } }); console.log('[进度加载] 已答题目数:', progressData.answered_questions.length, ', 正确:', correct, ', 错误:', wrong); setAnsweredStatus(statusMap); setUserAnswers(answersMap); setAnswerSequences(sequencesMap); setQuestionResults(resultsMap); setCorrectCount(correct); setWrongCount(wrong); // 如果找到 current_question_id 对应的索引,返回它及其答案 if (currentIndex !== undefined) { console.log('[进度加载] 定位到题目索引:', currentIndex); const hasAnswer = answersMap.has(currentIndex); return { index: currentIndex, hasAnswer: hasAnswer, savedAnswer: answersMap.get(currentIndex), savedResult: resultsMap.get(currentIndex) }; } } else { console.log('[进度加载] 没有进度数据'); } } catch (error) { console.error('[进度加载] 加载失败:', error); } // 如果后端没有数据或加载失败,返回0(从第一题开始) console.log('[进度加载] 返回默认索引: 0'); return { index: 0, hasAnswer: false }; }; // 加载随机错题(使用智能推荐) 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 progressResult = type ? await loadProgress(res.data, type) : { index: 0, hasAnswer: false }; let startIndex = progressResult.index < res.data.length ? progressResult.index : 0; // 如果是随机模式且没有进度数据(全新开始),随机选择起始题目 if (randomMode && !progressResult.hasAnswer && startIndex === 0 && res.data.length > 0) { startIndex = Math.floor(Math.random() * res.data.length); console.log('[题目加载] 随机模式:随机选择起始题目 (index:', startIndex, ')'); } if (res.data.length > 0) { setCurrentQuestion(res.data[startIndex]); setCurrentIndex(startIndex); // 如果这道题已答,恢复答案和结果 if (progressResult.hasAnswer && progressResult.savedAnswer !== undefined && progressResult.savedResult !== undefined) { console.log('[题目加载] 恢复已答题目的答案和结果 (index:', startIndex, ')'); setSelectedAnswer(progressResult.savedAnswer); setAnswerResult(progressResult.savedResult); setShowResult(true); } else { // 如果未答,重置状态 console.log('[题目加载] 题目未答,重置状态 (index:', 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); } else { const newWrong = wrongCount + 1; setWrongCount(newWrong); } // 注意:进度已由后端的 /api/practice/submit 接口自动保存到 practice_progress 表 // 如果答案正确且开启自动跳转,根据设置的延迟时间后自动进入下一题 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"); const typeParam = searchParams.get("type"); console.log('[下一题] 当前模式:', mode, ', 题目类型:', typeParam); // 错题练习模式:加载下一道推荐错题 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); // 清除后端进度(只清除当前类型) questionApi.clearPracticeProgress(typeParam || undefined).catch(err => console.error('清除进度失败:', err)); return; } // 从未答题目中随机选择一题 const randomIdx = Math.floor(Math.random() * unansweredIndexes.length); nextIndex = unansweredIndexes[randomIdx]; } else { // 顺序模式:检查是否完成所有题目 if (currentIndex + 1 >= allQuestions.length) { // 显示统计摘要 setShowSummary(true); // 清除后端进度(只清除当前类型) questionApi.clearPracticeProgress(typeParam || undefined).catch(err => console.error('清除进度失败:', err)); return; } nextIndex = currentIndex + 1; } setCurrentIndex(nextIndex); setCurrentQuestion(allQuestions[nextIndex]); // 检查这道题是否已答 const savedAnswer = userAnswers.get(nextIndex); const savedResult = questionResults.get(nextIndex); if (savedAnswer !== undefined && savedResult !== undefined) { // 如果已答,恢复答案和结果 console.log('[下一题] 题目已答,恢复答案和结果 (index:', nextIndex, ')'); setSelectedAnswer(savedAnswer); setAnswerResult(savedResult); setShowResult(true); } else { // 如果未答,重置状态 console.log('[下一题] 题目未答,重置状态 (index:', nextIndex, ')'); setSelectedAnswer( allQuestions[nextIndex].type === "multiple-selection" ? [] : "" ); setShowResult(false); setAnswerResult(null); } // 进度已由后端自动保存,无需手动保存 // 滚动到页面顶部 window.scrollTo({ top: 0, behavior: 'smooth' }); } }; // 跳转到指定题目 const handleQuestionSelect = (index: number) => { if (index >= 0 && index < allQuestions.length) { const question = allQuestions[index]; // 检查是否有保存的答案和结果 const savedAnswer = userAnswers.get(index); const savedResult = questionResults.get(index); // 先更新题目索引和题目内容 setCurrentIndex(index); setCurrentQuestion(question); if (savedAnswer !== undefined && savedResult !== undefined) { // 恢复之前的答案和结果 setSelectedAnswer(savedAnswer); setAnswerResult(savedResult); setShowResult(true); } else { // 没有答案,重置状态 setSelectedAnswer( question.type === "multiple-selection" ? [] : "" ); setShowResult(false); setAnswerResult(null); } // 进度已由后端自动保存,无需手动保存 // 滚动到页面顶部 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 = async () => { setShowSummary(false); setCurrentIndex(0); setCorrectCount(0); setWrongCount(0); const typeParam = searchParams.get("type"); // 清除后端进度(只清除当前类型) try { await questionApi.clearPracticeProgress(typeParam || undefined); console.log('[重试] 已清除类型', typeParam, '的进度'); } catch (error) { console.error('清除后端进度失败:', error); } 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;