feat: 初始化 bigdata 题库与 quiz-app 刷题系统

This commit is contained in:
2026-08-14 08:13:11 +08:00
commit c303426858
25 changed files with 25276 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
import { useState, useEffect } 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<Question[]>(questionsData);
const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null);
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
const [isSubmitted, setIsSubmitted] = useState(false);
const [weights, setWeights] = useState<Record<number, number>>({});
const [history, setHistory] = useState<boolean[]>([]);
const [totalAnswered, setTotalAnswered] = useState(0);
// Load weights from localStorage on init
useEffect(() => {
const savedWeights = localStorage.getItem('quiz_weights');
if (savedWeights) {
setWeights(JSON.parse(savedWeights));
} else {
const initialWeights: Record<number, number> = {};
questions.forEach(q => {
initialWeights[q.id] = 1;
});
setWeights(initialWeights);
}
const savedHistory = localStorage.getItem('quiz_history');
if (savedHistory) {
setHistory(JSON.parse(savedHistory));
}
const savedTotal = localStorage.getItem('quiz_total');
if (savedTotal) {
setTotalAnswered(JSON.parse(savedTotal));
}
}, [questions]);
// Pick a random question based on weights
const pickRandomQuestion = (latestWeights?: Record<number, number>) => {
if (questions.length === 0) return;
// Calculate total weight
let totalWeight = 0;
const currentWeights = latestWeights || weights;
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);
};
// Initial load
useEffect(() => {
if (Object.keys(weights).length > 0 && !currentQuestion) {
pickRandomQuestion();
}
}, [weights]);
const handleOptionClick = (optionLabel: string) => {
if (isSubmitted) return;
// extract letter A, B, C...
const letter = optionLabel.charAt(0);
if (currentQuestion?.type === '单选') {
setSelectedOptions([letter]);
handleSubmit([letter]);
} else {
// Multi-select
setSelectedOptions(prev =>
prev.includes(letter)
? prev.filter(l => l !== letter)
: [...prev, letter]
);
}
};
const handleSubmit = (autoSubmitOptions?: string[]) => {
const currentSelection = Array.isArray(autoSubmitOptions) ? autoSubmitOptions : selectedOptions;
if (currentSelection.length === 0 || !currentQuestion) return;
const userAns = [...currentSelection].sort().join('');
const isCorrect = userAns === currentQuestion.answer;
setIsSubmitted(true);
// Update stats (history of last 100)
const newHistory = [...history, isCorrect].slice(-100);
setHistory(newHistory);
localStorage.setItem('quiz_history', JSON.stringify(newHistory));
const newTotal = totalAnswered + 1;
setTotalAnswered(newTotal);
localStorage.setItem('quiz_total', JSON.stringify(newTotal));
// Update weights
const newWeights = { ...weights };
const currentWeight = newWeights[currentQuestion.id] || 1;
if (isCorrect) {
newWeights[currentQuestion.id] = Math.max(1, currentWeight - 1);
} else {
newWeights[currentQuestion.id] = currentWeight + 3; // Increase weight heavily on error
}
setWeights(newWeights);
localStorage.setItem('quiz_weights', JSON.stringify(newWeights));
if (isCorrect) {
setTimeout(() => {
pickRandomQuestion(newWeights);
}, 1000);
}
};
if (!currentQuestion) return <div className="glass-card">Loading...</div>;
const userAns = [...selectedOptions].sort().join('');
const isCorrect = isSubmitted && userAns === currentQuestion.answer;
return (
<div className="glass-card">
<div className="header">
<span className="title-tag">{currentQuestion.type}</span>
<span style={{color: '#94a3b8', fontSize: '0.875rem'}}>
Weight: {weights[currentQuestion.id] || 1}
</span>
</div>
<div className="question-title">
{currentQuestion.title}
</div>
<div className="options-container">
{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 (
<div
key={i}
className={className}
onClick={() => handleOptionClick(opt)}
>
{opt}
</div>
);
})}
</div>
{!isSubmitted ? (
<div className="action-container">
{currentQuestion.type !== '单选' && (
<button
onClick={() => handleSubmit()}
disabled={selectedOptions.length === 0}
>
提交答案
</button>
)}
</div>
) : (
<>
<div className={`feedback ${isCorrect ? 'correct' : 'incorrect'}`}>
<h3 style={{marginTop: 0, marginBottom: '0.5rem'}}>
{isCorrect ? '✅ 回答正确!' : '❌ 回答错误'}
</h3>
<p style={{margin: 0}}>
正确答案是:<strong>{currentQuestion.answer}</strong>
</p>
{!isCorrect && (
<div style={{marginTop: '0.5rem', fontSize: '0.9rem', color: '#64748b'}}>
<div>本题出处:题库第 {currentQuestion.source_id || currentQuestion.id} 题</div>
<div>来源文件:{currentQuestion.source_file || '未知'}</div>
<div>PDF页码:第 {currentQuestion.page_number || '?'} 页</div>
</div>
)}
</div>
<div className="action-container">
<button onClick={() => pickRandomQuestion()}>
{isCorrect ? '下一题 (1s后自动跳转)...' : '下一题'}
</button>
</div>
</>
)}
<div className="stats">
<span>已刷题:{totalAnswered} 道</span>
<span>近100题正确率:{history.length > 0 ? Math.round((history.filter(Boolean).length / history.length) * 100) : 0}%</span>
</div>
</div>
);
}