考试和测验系统
有效的评估工具能够准确测量学生的学习成果。本文讲解完整的考试系统设计。
测验类型设计
多种题目类型
const questionTypes = {
MULTIPLE_CHOICE: {
id: 'multiple_choice',
name: '多选题',
fields: {
question: '题目',
options: ['选项 A', '选项 B', '选项 C', '选项 D'],
correctAnswer: 0,
explanation: '解释',
},
},
TRUE_FALSE: {
id: 'true_false',
name: '判断题',
fields: {
question: '陈述',
correctAnswer: true,
},
},
SHORT_ANSWER: {
id: 'short_answer',
name: '简答题',
fields: {
question: '题目',
acceptableAnswers: ['答案1', '答案2'],
fuzzyMatching: true, // 允许部分匹配
},
},
ESSAY: {
id: 'essay',
name: '论文题',
fields: {
question: '题目',
maxWords: 500,
rubric: ['结构', '内容', '语言'],
},
},
MATCHING: {
id: 'matching',
name: '配对题',
fields: {
pairs: [
{ left: '项目 A', right: '定义 1' },
{ left: '项目 B', right: '定义 2' },
],
},
},
FILL_BLANK: {
id: 'fill_blank',
name: '填空题',
fields: {
text: '这是一个 _____ 的例子',
blanks: [{ answer: '正确', alternatives: ['对', 'OK'] }],
},
},
};
const quizQuestion = {
id: 'Q001',
type: 'multiple_choice',
question: 'JavaScript 中 var 和 let 的区别是什么?',
options: [
'没有区别',
'let 有块级作用域,var 没有',
'var 有块级作用域,let 没有',
'let 和 var 都没有块级作用域',
],
correctAnswer: 1,
points: 5,
explanation: 'let 在块级作用域内,而 var 只有函数作用域',
difficulty: 'medium', // easy, medium, hard
};
测验创建界面
教师创建测验
function CreateQuiz() {
const [quizData, setQuizData] = useState({
title: 'Web 基础测试',
description: '测试你对 Web 基础的理解',
timeLimit: 60, // 分钟
totalPoints: 100,
passingScore: 70,
shuffleQuestions: true,
shuffleOptions: true,
allowReview: true,
showCorrectAnswers: 'after_quiz', // immediate, after_quiz, never
});
const [questions, setQuestions] = useState([]);
const [currentQuestion, setCurrentQuestion] = useState(null);
const addQuestion = (question) => {
setQuestions([...questions, {
...question,
id: `Q_${Date.now()}`,
}]);
};
const handleCreateQuiz = async () => {
const quiz = {
...quizData,
questions,
totalPoints: questions.reduce((sum, q) => sum + q.points, 0),
};
try {
const response = await api.post('/quizzes', quiz);
console.log('Quiz created:', response);
} catch (error) {
console.error('Failed to create quiz:', error);
}
};
return (
<div className="quiz-creator">
<section className="quiz-settings">
<input
type="text"
value={quizData.title}
onChange={(e) => setQuizData({...quizData, title: e.target.value})}
placeholder="测验标题"
/>
<textarea
value={quizData.description}
onChange={(e) => setQuizData({...quizData, description: e.target.value})}
placeholder="测验描述"
/>
<div className="settings-grid">
<input
type="number"
value={quizData.timeLimit}
onChange={(e) => setQuizData({...quizData, timeLimit: parseInt(e.target.value)})}
placeholder="时间限制 (分钟)"
/>
<input
type="number"
value={quizData.passingScore}
onChange={(e) => setQuizData({...quizData, passingScore: parseInt(e.target.value)})}
placeholder="及格分数"
/>
<select
value={quizData.showCorrectAnswers}
onChange={(e) => setQuizData({...quizData, showCorrectAnswers: e.target.value})}
>
<option value="immediate">立即显示答案</option>
<option value="after_quiz">测验后显示答案</option>
<option value="never">不显示答案</option>
</select>
</div>
<label>
<input
type="checkbox"
checked={quizData.shuffleQuestions}
onChange={(e) => setQuizData({...quizData, shuffleQuestions: e.target.checked})}
/>
随机打乱题目顺序
</label>
</section>
<section className="questions-section">
<h2>题目 ({questions.length})</h2>
{questions.map((q, idx) => (
<div key={q.id} className="question-preview">
<span className="question-number">{idx + 1}</span>
<span>{q.question}</span>
<span className="points">{q.points} 分</span>
</div>
))}
<button onClick={() => setCurrentQuestion({})}>
添加题目
</button>
</section>
{currentQuestion !== null && (
<QuestionEditor
question={currentQuestion}
onSave={addQuestion}
onCancel={() => setCurrentQuestion(null)}
/>
)}
<button onClick={handleCreateQuiz} className="primary">
创建测验
</button>
</div>
);
}
学生参加测验
在线测验界面
function TakeQuiz() {
const { quizId } = useParams();
const [quiz, setQuiz] = useState(null);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [responses, setResponses] = useState({});
const [timeRemaining, setTimeRemaining] = useState(null);
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
fetchQuiz(quizId).then(quiz => {
setQuiz(quiz);
setTimeRemaining(quiz.timeLimit * 60); // 转换为秒
});
}, [quizId]);
// 倒计时
useEffect(() => {
if (timeRemaining === null || submitted) return;
const interval = setInterval(() => {
setTimeRemaining(t => t - 1);
if (timeRemaining <= 0) {
handleSubmit();
}
}, 1000);
return () => clearInterval(interval);
}, [timeRemaining, submitted]);
const currentQuestion = quiz?.questions[currentQuestionIndex];
const handleAnswerChange = (answer) => {
setResponses({
...responses,
[currentQuestion.id]: answer,
});
};
const handleNext = () => {
if (currentQuestionIndex < quiz.questions.length - 1) {
setCurrentQuestionIndex(currentQuestionIndex + 1);
}
};
const handlePrevious = () => {
if (currentQuestionIndex > 0) {
setCurrentQuestionIndex(currentQuestionIndex - 1);
}
};
const handleSubmit = async () => {
if (!window.confirm('你确定要提交测验吗?')) return;
try {
const submission = {
quizId,
responses,
submittedAt: new Date().toISOString(),
timeSpent: (quiz.timeLimit * 60) - timeRemaining,
};
const result = await api.post(`/quizzes/${quizId}/submit`, submission);
setSubmitted(true);
// 显示结果或重定向
} catch (error) {
console.error('Failed to submit quiz:', error);
}
};
if (!quiz) return <div>加载中...</div>;
const formatTime = (seconds) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
return (
<div className="quiz-container">
<div className="quiz-header">
<h1>{quiz.title}</h1>
<div className="timer" style={{
color: timeRemaining < 300 ? 'red' : 'black'
}}>
⏱️ {formatTime(timeRemaining)}
</div>
</div>
<div className="quiz-progress">
<span>问题 {currentQuestionIndex + 1} / {quiz.questions.length}</span>
<div className="progress-bar">
<div style={{width: `${((currentQuestionIndex + 1) / quiz.questions.length) * 100}%`}}></div>
</div>
</div>
<div className="question-container">
<QuestionDisplay
question={currentQuestion}
answer={responses[currentQuestion?.id]}
onAnswerChange={handleAnswerChange}
/>
</div>
<div className="navigation-buttons">
<button onClick={handlePrevious} disabled={currentQuestionIndex === 0}>
上一题
</button>
<button onClick={handleNext} disabled={currentQuestionIndex === quiz.questions.length - 1}>
下一题
</button>
<button onClick={handleSubmit} className="submit-btn">
提交测验
</button>
</div>
<div className="question-navigator">
<h3>题目导航</h3>
<div className="question-grid">
{quiz.questions.map((_, idx) => (
<button
key={idx}
className={`question-btn ${
idx === currentQuestionIndex ? 'current' : ''
} ${responses[quiz.questions[idx].id] ? 'answered' : ''}`}
onClick={() => setCurrentQuestionIndex(idx)}
>
{idx + 1}
</button>
))}
</div>
</div>
</div>
);
}
自动评分
答题评分引擎
class QuizScorer {
constructor(quiz) {
this.quiz = quiz;
}
// 评分所有答案
scoreQuiz(responses) {
const scores = this.quiz.questions.map(question =>
this.scoreQuestion(question, responses[question.id])
);
const totalScore = scores.reduce((sum, score) => sum + score.points, 0);
const percentage = (totalScore / this.getMaxPoints()) * 100;
return {
totalScore,
maxPoints: this.getMaxPoints(),
percentage,
passed: percentage >= this.quiz.passingScore,
details: scores,
};
}
// 评分单个问题
scoreQuestion(question, response) {
if (!response) {
return { questionId: question.id, points: 0, correct: false };
}
let correct = false;
switch (question.type) {
case 'multiple_choice':
case 'true_false':
correct = response === question.correctAnswer;
break;
case 'fill_blank':
correct = this.checkFillBlank(response, question);
break;
case 'short_answer':
correct = this.checkShortAnswer(response, question);
break;
case 'matching':
correct = this.checkMatching(response, question);
break;
case 'essay':
// 论文题需要人工评分
return {
questionId: question.id,
points: null,
correct: null,
requiresManualGrading: true,
};
}
return {
questionId: question.id,
points: correct ? question.points : 0,
correct,
};
}
checkFillBlank(response, question) {
return question.blanks.some(blank =>
this.fuzzyMatch(response, blank.answer) ||
blank.alternatives?.some(alt => this.fuzzyMatch(response, alt))
);
}
checkShortAnswer(response, question) {
return question.acceptableAnswers.some(answer =>
this.fuzzyMatch(response, answer, question.fuzzyMatching)
);
}
checkMatching(response, question) {
return response.every((r, idx) => r === question.pairs[idx].right);
}
fuzzyMatch(input, expected, fuzzy = false) {
const normalize = str => str.toLowerCase().trim();
if (!fuzzy) {
return normalize(input) === normalize(expected);
}
// 允许小的拼写错误
return this.levenshteinDistance(normalize(input), normalize(expected)) <= 1;
}
levenshteinDistance(a, b) {
const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(0));
for (let i = 0; i <= a.length; i++) matrix[0][i] = i;
for (let j = 0; j <= b.length; j++) matrix[j][0] = j;
for (let j = 1; j <= b.length; j++) {
for (let i = 1; i <= a.length; i++) {
const indicator = a[i - 1] === b[j - 1] ? 0 : 1;
matrix[j][i] = Math.min(
matrix[j][i - 1] + 1,
matrix[j - 1][i] + 1,
matrix[j - 1][i - 1] + indicator
);
}
}
return matrix[b.length][a.length];
}
getMaxPoints() {
return this.quiz.questions.reduce((sum, q) => sum + q.points, 0);
}
}
// 使用示例
const scorer = new QuizScorer(quiz);
const result = scorer.scoreQuiz(responses);
console.log(`得分: ${result.totalScore}/${result.maxPoints} (${result.percentage.toFixed(1)}%)`);
console.log(`及格: ${result.passed ? '是' : '否'}`);
最佳实践
✅ 应该做的事:
- 多样化的题目类型
- 清晰的题目表述
- 合理的时间限制
- 及时的反馈
- 防止作弊措施
❌ 不应该做的事:
- 题目含糊不清
- 时间限制过短
- 忽视学生反馈
- 没有安全措施
- 过度重视单一形式
检查清单
- 题目质量高
- 时间限制合理
- 评分系统准确
- 反馈有效
- 安全措施充分


