import { useState, useEffect, useCallback } from 'react'; import questionsData from './questions.json'; import './index.css'; interface Question { id: number; source_id?: string; source_file?: string; page_number?: number; title: string; options: string[]; type: string; answer: string; } export default function App() { const [questions] = useState(questionsData); const [currentQuestion, setCurrentQuestion] = useState(null); const [selectedOptions, setSelectedOptions] = useState([]); const [isSubmitted, setIsSubmitted] = useState(false); const [weights, setWeights] = useState>({}); const [history, setHistory] = useState([]); const [totalAnswered, setTotalAnswered] = useState(0); const [isSyncing, setIsSyncing] = useState(false); // 随机挑选一道题目(基于权重轮盘赌算法) const pickRandomQuestion = useCallback((latestWeights?: Record) => { if (questions.length === 0) return; const currentWeights = latestWeights || weights; let totalWeight = 0; questions.forEach(q => { totalWeight += (currentWeights[q.id] || 1); }); let randomNum = Math.random() * totalWeight; let selectedQ = questions[0]; for (const q of questions) { randomNum -= (currentWeights[q.id] || 1); if (randomNum <= 0) { selectedQ = q; break; } } setCurrentQuestion(selectedQ); setSelectedOptions([]); setIsSubmitted(false); }, [questions, weights]); // 从服务端拉取全局统计与权重数据 const fetchGlobalStats = useCallback(async () => { try { const res = await fetch('/api/stats'); if (res.ok) { const data = await res.json(); if (data.weights) setWeights(data.weights); if (data.history) setHistory(data.history); if (typeof data.totalAnswered === 'number') setTotalAnswered(data.totalAnswered); return data.weights; } } catch (e) { console.warn('获取全局统计数据失败,使用本地兜底:', e); } // 兜底本地逻辑 const initialWeights: Record = {}; questions.forEach(q => { initialWeights[q.id] = 1; }); setWeights(initialWeights); return initialWeights; }, [questions]); // 初始化加载 useEffect(() => { fetchGlobalStats().then((loadedWeights) => { pickRandomQuestion(loadedWeights); }); }, [fetchGlobalStats, pickRandomQuestion]); const handleOptionClick = (optionLabel: string) => { if (isSubmitted) return; const letter = optionLabel.charAt(0); if (currentQuestion?.type === '单选') { setSelectedOptions([letter]); handleSubmit([letter]); } else { setSelectedOptions(prev => prev.includes(letter) ? prev.filter(l => l !== letter) : [...prev, letter] ); } }; const handleSubmit = async (autoSubmitOptions?: string[]) => { const currentSelection = Array.isArray(autoSubmitOptions) ? autoSubmitOptions : selectedOptions; if (currentSelection.length === 0 || !currentQuestion || isSubmitted) return; const userAns = [...currentSelection].sort().join(''); const isCorrect = userAns === currentQuestion.answer; setIsSubmitted(true); // 本地乐观更新 const localNewHistory = [...history, isCorrect].slice(-100); setHistory(localNewHistory); setTotalAnswered(prev => prev + 1); const localWeights = { ...weights }; const currentWeight = localWeights[currentQuestion.id] || 1; localWeights[currentQuestion.id] = isCorrect ? Math.max(1, currentWeight - 1) : currentWeight + 3; setWeights(localWeights); // 异步同步到后端服务端 setIsSyncing(true); let nextWeights = localWeights; try { const res = await fetch('/api/answer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ questionId: currentQuestion.id, isCorrect }) }); if (res.ok) { const data = await res.json(); if (data.weights) { setWeights(data.weights); nextWeights = data.weights; } if (data.history) setHistory(data.history); if (typeof data.totalAnswered === 'number') setTotalAnswered(data.totalAnswered); } } catch (err) { console.warn('向服务端同步作答数据失败:', err); } finally { setIsSyncing(false); } if (isCorrect) { setTimeout(() => { pickRandomQuestion(nextWeights); }, 1000); } }; if (!currentQuestion) return
加载题库中...
; const userAns = [...selectedOptions].sort().join(''); const isCorrect = isSubmitted && userAns === currentQuestion.answer; return (
{currentQuestion.type} 全局权重: {weights[currentQuestion.id] || 1} {isSyncing && '(同步中...)'}
{currentQuestion.title}
{currentQuestion.options.map((opt, i) => { const letter = opt.charAt(0); const isSelected = selectedOptions.includes(letter); let className = 'option'; if (isSelected) className += ' selected'; if (isSubmitted) { className += ' disabled'; const isCorrectOption = currentQuestion.answer.includes(letter); if (isCorrectOption) { className += ' correct'; } else if (isSelected && !isCorrectOption) { className += ' incorrect'; } } return (
handleOptionClick(opt)} > {opt}
); })}
{!isSubmitted ? (
{currentQuestion.type !== '单选' && ( )}
) : ( <>

{isCorrect ? '✅ 回答正确!' : '❌ 回答错误'}

正确答案是:{currentQuestion.answer}

{!isCorrect && (
本题出处:题库第 {currentQuestion.source_id || currentQuestion.id} 题
来源文件:{currentQuestion.source_file || '未知'}
PDF页码:第 {currentQuestion.page_number || '?'} 页
)}
)}
全局已刷题:{totalAnswered} 道 近100题正确率:{history.length > 0 ? Math.round((history.filter(Boolean).length / history.length) * 100) : 0}%
); }