在现代的在线教育平台中,在线考试系统是不可或缺的一部分。本文将通过一个完整的示例,演示如何使用 HTML、CSS 和 JavaScript 构建一个支持多种题型的在线考试系统。
效果演示
项目概述
本项目主要包含以下核心功能:
- 支持4种常见题型:单选题、多选题、判断题、填空题
- 答题卡导航功能
- 实时计时器
- 自动评分与结果反馈
页面结构与样式设计
创建 HTML 结构
<div class="container">
<!-- 考试内容 -->
<div class="exam-container">
<!-- 标题和计时器 -->
<div class="header">
<h2>在线考试系统</h2>
<div class="timer">剩余时间: <span id="time">30:00</span></div>
</div>
<!-- 题目 -->
<div id="subject"></div>
<!-- 导航按钮 -->
<div class="navigation">
<button id="prev-btn" disabled>上一题</button>
<button id="next-btn">下一题</button>
<button id="submit-btn" class="submit-btn">提交试卷</button>
</div>
<!-- 结果 -->
<div id="result" class="result">
<h2>考试结束</h2>
<p>你的得分是: <span class="score" id="final-score">0</span> 分</p>
<p id="result-message"></p>
</div>
</div>
<!-- 答题卡 -->
<div class="answer-sheet">
<h3>答题卡</h3>
<div class="answer-buttons" id="answer-buttons"></div>
</div>
</div>
设计 CSS 样式
整体布局
body {
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.container {
display: flex;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.exam-container {
flex: 3;
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
margin-right: 20px;
}
.answer-sheet {
flex: 1;
background-color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
height: fit-content;
position: sticky;
top: 20px;
}
题目区域样式
.header {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.timer {
font-weight: bold;
color: #e74c3c;
}
.question {
margin-bottom: 20px;
padding: 15px;
background-color: #f9f9f9;
border-radius: 5px;
}
.question h3 {
margin-top: 0;
color: #2c3e50;
}
.question-type {
display: inline-block;
padding: 2px 8px;
background-color: #3498db;
color: white;
border-radius: 4px;
font-size: 12px;
margin-left: 10px;
}
.options {
margin-left: 20px;
}
.option {
margin: 10px 0;
padding: 8px;
cursor: pointer;
border-radius: 4px;
}
.option:hover {
background-color: #eee;
}
.option.selected {
background-color: #3498db;
color: white;
}
.option.multi-selected {
background-color: #9b59b6;
color: white;
}
.true-false-options {
display: flex;
gap: 20px;
}
.true-false-option {
padding: 10px 20px;
border: 1px solid #ddd;
border-radius: 5px;
cursor: pointer;
}
.true-false-option.selected {
background-color: #3498db;
color: white;
border-color: #3498db;
}
.fill-blank-input {
width: 100%;
padding: 8px;
margin-top: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.navigation {
display: flex;
justify-content: space-between;
margin-top: 30px;
}
button {
padding: 10px 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #2980b9;
}
button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.submit-btn {
background-color: #2ecc71;
}
.submit-btn:hover {
background-color: #27ae60;
}
.result {
display: none;
text-align: center;
padding: 20px;
}
.score {
font-size: 24px;
font-weight: bold;
color: #2ecc71;
}
答题卡样式
.answer-sheet h3 {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
text-align: center;
}
.answer-buttons {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
}
.answer-btn {
width: 100%;
aspect-ratio: 1;
border: 1px solid #ddd;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-weight: bold;
background-color: white;
}
.answer-btn:hover {
background-color: #f0f0f0;
}
.answer-btn.current {
border: 2px solid #3498db;
color: #3498db;
}
.answer-btn.answered {
background-color: #3498db;
color: white;
border-color: #3498db;
}
.answer-btn.unanswered {
background-color: #f1f1f1;
color: #999;
}
核心功能实现
定义基础数据
// 题型常量
const QUESTION_TYPES = {
SINGLE_CHOICE: 'single-choice',
MULTI_CHOICE: 'multi-choice',
TRUE_FALSE: 'true-false',
FILL_BLANK: 'fill-blank'
};
// 考试数据
const examData = {
title: "JavaScript综合测试",
timeLimit: 30 * 60, // 30分钟,以秒为单位
questions: [
{
type: QUESTION_TYPES.SINGLE_CHOICE,
question: "JavaScript是什么类型的语言?",
options: ["编译型", "解释型", "混合型", "标记型"],
answer: 1,
score: 10
},
{
type: QUESTION_TYPES.MULTI_CHOICE,
question: "以下哪些是JavaScript的数据类型?(多选)",
options: ["String", "Boolean", "Number", "Float", "Object"],
answer: [0, 1, 2, 4],
score: 15
},
// ...
]
};
生成答题卡
function createAnswerSheet() {
answerButtonsContainer.innerHTML = '';
examData.questions.forEach((_, index) => {
const btn = document.createElement('button');
btn.className = 'answer-btn unanswered';
btn.textContent = index + 1;
btn.onclick = () => jumpToQuestion(index);
answerButtonsContainer.appendChild(btn);
});
}
渲染不同题型
function showQuestion() {
const question = examData.questions[currentQuestion];
const typeLabel = getTypeLabel(question.type);
let html = `<div class="question">
<h3>题目 ${currentQuestion + 1}/${examData.questions.length}: ${question.question}
<span class="question-type">${typeLabel}</span>
</h3>
<div class="options">`;
// 根据题型生成不同的HTML
switch(question.type) {
case QUESTION_TYPES.SINGLE_CHOICE:
html += generateSingleChoiceHTML(question);
break;
case QUESTION_TYPES.MULTI_CHOICE:
html += generateMultiChoiceHTML(question);
break;
case QUESTION_TYPES.TRUE_FALSE:
html += generateTrueFalseHTML(question);
break;
case QUESTION_TYPES.FILL_BLANK:
html += generateFillBlankHTML(question);
break;
}
html += `</div></div>`;
subjectContainer.innerHTML = html;
// 更新导航按钮状态
prevBtn.disabled = currentQuestion === 0;
nextBtn.disabled = currentQuestion === examData.questions.length - 1;
// 更新答题卡
updateAnswerSheet();
}
跳转到指定题目
function jumpToQuestion(index) {
currentQuestion = index;
showQuestion();
}
更新答题卡状态
function updateAnswerSheet() {
const buttons = answerButtonsContainer.querySelectorAll('.answer-btn');
buttons.forEach((btn, index) => {
btn.classList.remove('current', 'answered', 'unanswered');
if (index === currentQuestion) {
btn.classList.add('current');
}
if (userAnswers[index] === null || userAnswers[index] === '' || (Array.isArray(userAnswers[index]) && userAnswers[index].length === 0) ) {
btn.classList.add('unanswered');
} else {
btn.classList.add('answered');
}
});
}
监听做题事件
事件名 | 事件 |
---|---|
选择单选题选项 | selectSingleChoice |
选择多选题选项 | toggleMultiChoice |
选择判断题选项 | selectTrueFalse |
更新填空题答案 | updateFillBlankAnswer |
提交试卷
function submitExam() {
clearInterval(timer);
// 计算分数
let score = 0;
examData.questions.forEach((question, index) => {
const userAnswer = userAnswers[index];
let isCorrect = false;
switch(question.type) {
case QUESTION_TYPES.SINGLE_CHOICE:
isCorrect = userAnswer === question.answer;
break;
case QUESTION_TYPES.MULTI_CHOICE:
if (Array.isArray(userAnswer)) {
// 检查答案数组是否完全相同
const userSorted = [...userAnswer].sort();
const answerSorted = [...question.answer].sort();
isCorrect = JSON.stringify(userSorted) === JSON.stringify(answerSorted);
}
break;
case QUESTION_TYPES.TRUE_FALSE:
isCorrect = userAnswer === question.answer;
break;
case QUESTION_TYPES.FILL_BLANK:
isCorrect = userAnswer && userAnswer.toString().toLowerCase() === question.answer.toLowerCase();
break;
}
if (isCorrect) {
score += question.score;
}
});
// 显示结果
document.getElementById('subject').style.display = 'none';
document.querySelector('.navigation').style.display = 'none';
resultDiv.style.display = 'block';
finalScore.textContent = score;
// 根据分数显示不同消息
const totalScore = examData.questions.reduce((sum, q) => sum + q.score, 0);
const percentage = (score / totalScore) * 100;
if (percentage >= 80) {
resultMessage.textContent = "优秀!你掌握了大部分知识点。";
} else if (percentage >= 60) {
resultMessage.textContent = "良好!继续努力,你可以做得更好。";
} else if (percentage >= 40) {
resultMessage.textContent = "及格!建议复习相关知识点。";
} else {
resultMessage.textContent = "不及格!需要加强学习。";
}
}
扩展建议
- 防切屏功能
- 倒计时功能优化
- 实现错题回顾功能
- 添加用户登录与成绩保存功能
- 集成后端进行数据持久化
完整代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>在线考试系统 - 多题型版</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.container {
display: flex;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.exam-container {
flex: 3;
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
margin-right: 20px;
}
.answer-sheet {
flex: 1;
background-color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
height: fit-content;
position: sticky;
top: 20px;
}
.header {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.timer {
font-weight: bold;
color: #e74c3c;
}
.question {
margin-bottom: 20px;
padding: 15px;
background-color: #f9f9f9;
border-radius: 5px;
}
.question h3 {
margin-top: 0;
color: #2c3e50;
}
.question-type {
display: inline-block;
padding: 2px 8px;
background-color: #3498db;
color: white;
border-radius: 4px;
font-size: 12px;
margin-left: 10px;
}
.options {
margin-left: 20px;
}
.option {
margin: 10px 0;
padding: 8px;
cursor: pointer;
border-radius: 4px;
}
.option:hover {
background-color: #eee;
}
.option.selected {
background-color: #3498db;
color: white;
}
.option.multi-selected {
background-color: #9b59b6;
color: white;
}
.true-false-options {
display: flex;
gap: 20px;
}
.true-false-option {
padding: 10px 20px;
border: 1px solid #ddd;
border-radius: 5px;
cursor: pointer;
}
.true-false-option.selected {
background-color: #3498db;
color: white;
border-color: #3498db;
}
.fill-blank-input {
width: 100%;
padding: 8px;
margin-top: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.navigation {
display: flex;
justify-content: space-between;
margin-top: 30px;
}
button {
padding: 10px 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #2980b9;
}
button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.submit-btn {
background-color: #2ecc71;
}
.submit-btn:hover {
background-color: #27ae60;
}
.result {
display: none;
text-align: center;
padding: 20px;
}
.score {
font-size: 24px;
font-weight: bold;
color: #2ecc71;
}
.answer-sheet h3 {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
text-align: center;
}
.answer-buttons {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
}
.answer-btn {
width: 100%;
aspect-ratio: 1;
border: 1px solid #ddd;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-weight: bold;
background-color: white;
}
.answer-btn:hover {
background-color: #f0f0f0;
}
.answer-btn.current {
border: 2px solid #3498db;
color: #3498db;
}
.answer-btn.answered {
background-color: #3498db;
color: white;
border-color: #3498db;
}
.answer-btn.unanswered {
background-color: #f1f1f1;
color: #999;
}
</style>
</head>
<body>
<div class="container">
<!-- 考试内容 -->
<div class="exam-container">
<!-- 标题和计时器 -->
<div class="header">
<h2>在线考试系统</h2>
<div class="timer">剩余时间: <span id="time">30:00</span></div>
</div>
<!-- 题目 -->
<div id="subject"></div>
<!-- 导航按钮 -->
<div class="navigation">
<button id="prev-btn" disabled>上一题</button>
<button id="next-btn">下一题</button>
<button id="submit-btn" class="submit-btn">提交试卷</button>
</div>
<!-- 结果 -->
<div id="result" class="result">
<h2>考试结束</h2>
<p>你的得分是: <span class="score" id="final-score">0</span> 分</p>
<p id="result-message"></p>
</div>
</div>
<!-- 答题卡 -->
<div class="answer-sheet">
<h3>答题卡</h3>
<div class="answer-buttons" id="answer-buttons"></div>
</div>
</div>
<script>
// 题型常量
const QUESTION_TYPES = {
SINGLE_CHOICE: 'single-choice',
MULTI_CHOICE: 'multi-choice',
TRUE_FALSE: 'true-false',
FILL_BLANK: 'fill-blank'
};
// 考试数据
const examData = {
title: "JavaScript综合测试",
timeLimit: 30 * 60, // 30分钟,以秒为单位
questions: [
{
type: QUESTION_TYPES.SINGLE_CHOICE,
question: "JavaScript是什么类型的语言?",
options: ["编译型", "解释型", "混合型", "标记型"],
answer: 1,
score: 10
},
{
type: QUESTION_TYPES.MULTI_CHOICE,
question: "以下哪些是JavaScript的数据类型?(多选)",
options: ["String", "Boolean", "Number", "Float", "Object"],
answer: [0, 1, 2, 4], // 多选的答案使用数组
score: 15
},
{
type: QUESTION_TYPES.TRUE_FALSE,
question: "JavaScript和Java是同一种语言。",
answer: false, // true或false
score: 5
},
{
type: QUESTION_TYPES.FILL_BLANK,
question: "用于向数组末尾添加元素的方法是______。",
answer: "push", // 填空题的答案
score: 10
},
{
type: QUESTION_TYPES.SINGLE_CHOICE,
question: "哪个方法可以将字符串转换为整数?",
options: ["parseInt()", "parseString()", "toInteger()", "stringToInt()"],
answer: 0,
score: 10
},
{
type: QUESTION_TYPES.MULTI_CHOICE,
question: "以下哪些是JavaScript的循环语句?(多选)",
options: ["for", "while", "do...while", "repeat", "loop"],
answer: [0, 1, 2],
score: 15
},
{
type: QUESTION_TYPES.TRUE_FALSE,
question: "JavaScript中可以使用let和const声明变量。",
answer: true,
score: 5
},
{
type: QUESTION_TYPES.FILL_BLANK,
question: "用于检测变量类型的操作符是______。",
answer: "typeof",
score: 10
}
]
};
// 全局变量
let currentQuestion = 0;
let userAnswers = Array(examData.questions.length).fill(null);
let timeLeft = examData.timeLimit;
let timer;
// DOM元素
const subjectContainer = document.getElementById('subject');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const submitBtn = document.getElementById('submit-btn');
const timeDisplay = document.getElementById('time');
const resultDiv = document.getElementById('result');
const finalScore = document.getElementById('final-score');
const resultMessage = document.getElementById('result-message');
const answerButtonsContainer = document.getElementById('answer-buttons');
// 初始化考试
function initExam() {
createAnswerSheet();
showQuestion();
startTimer();
}
// 创建答题卡
function createAnswerSheet() {
answerButtonsContainer.innerHTML = '';
examData.questions.forEach((_, index) => {
const btn = document.createElement('button');
btn.className = 'answer-btn unanswered';
btn.textContent = index + 1;
btn.onclick = () => jumpToQuestion(index);
answerButtonsContainer.appendChild(btn);
});
}
// 更新答题卡状态
function updateAnswerSheet() {
const buttons = answerButtonsContainer.querySelectorAll('.answer-btn');
buttons.forEach((btn, index) => {
btn.classList.remove('current', 'answered', 'unanswered');
if (index === currentQuestion) {
btn.classList.add('current');
}
if (userAnswers[index] === null || userAnswers[index] === '' || (Array.isArray(userAnswers[index]) && userAnswers[index].length === 0) ) {
btn.classList.add('unanswered');
} else {
btn.classList.add('answered');
}
});
}
// 跳转到指定题目
function jumpToQuestion(index) {
currentQuestion = index;
showQuestion();
}
// 显示当前题目
function showQuestion() {
const question = examData.questions[currentQuestion];
const typeLabel = getTypeLabel(question.type);
let html = `<div class="question">
<h3>题目 ${currentQuestion + 1}/${examData.questions.length}: ${question.question}
<span class="question-type">${typeLabel}</span>
</h3>
<div class="options">`;
// 根据题型生成不同的HTML
switch(question.type) {
case QUESTION_TYPES.SINGLE_CHOICE:
html += generateSingleChoiceHTML(question);
break;
case QUESTION_TYPES.MULTI_CHOICE:
html += generateMultiChoiceHTML(question);
break;
case QUESTION_TYPES.TRUE_FALSE:
html += generateTrueFalseHTML(question);
break;
case QUESTION_TYPES.FILL_BLANK:
html += generateFillBlankHTML(question);
break;
}
html += `</div></div>`;
subjectContainer.innerHTML = html;
// 更新导航按钮状态
prevBtn.disabled = currentQuestion === 0;
nextBtn.disabled = currentQuestion === examData.questions.length - 1;
// 更新答题卡
updateAnswerSheet();
}
// 获取题型标签
function getTypeLabel(type) {
switch(type) {
case QUESTION_TYPES.SINGLE_CHOICE: return '单选题';
case QUESTION_TYPES.MULTI_CHOICE: return '多选题';
case QUESTION_TYPES.TRUE_FALSE: return '判断题';
case QUESTION_TYPES.FILL_BLANK: return '填空题';
default: return '';
}
}
// 生成单选题
function generateSingleChoiceHTML(question) {
let html = '';
question.options.forEach((option, index) => {
const isSelected = userAnswers[currentQuestion] === index;
html += `<div class="option ${isSelected ? 'selected' : ''}" onclick="selectSingleChoice(${index})">
${String.fromCharCode(65 + index)}. ${option}
</div>`;
});
return html;
}
// 生成多选题
function generateMultiChoiceHTML(question) {
let html = '';
question.options.forEach((option, index) => {
const isSelected = Array.isArray(userAnswers[currentQuestion]) && userAnswers[currentQuestion].includes(index);
html += `<div class="option ${isSelected ? 'multi-selected' : ''}" onclick="toggleMultiChoice(${index})">
${String.fromCharCode(65 + index)}. ${option}
</div>`;
});
return html;
}
// 生成判断题
function generateTrueFalseHTML(question) {
const userAnswer = userAnswers[currentQuestion];
return `<div class="true-false-options">
<div class="true-false-option ${userAnswer === true ? 'selected' : ''}"
onclick="selectTrueFalse(true)">正确</div>
<div class="true-false-option ${userAnswer === false ? 'selected' : ''}"
onclick="selectTrueFalse(false)">错误</div>
</div>`;
}
// 生成填空题
function generateFillBlankHTML(question) {
const userAnswer = userAnswers[currentQuestion] || '';
return `<input type="text" class="fill-blank-input"
value="${userAnswer}"
oninput="updateFillBlankAnswer(this.value)"
placeholder="请输入答案">`;
}
// 选择单选题选项
function selectSingleChoice(optionIndex) {
userAnswers[currentQuestion] = optionIndex;
showQuestion();
}
// 选择多选题选项
function toggleMultiChoice(optionIndex) {
if (!Array.isArray(userAnswers[currentQuestion])) {
userAnswers[currentQuestion] = [];
}
const index = userAnswers[currentQuestion].indexOf(optionIndex);
if (index === -1) {
userAnswers[currentQuestion].push(optionIndex);
} else {
userAnswers[currentQuestion].splice(index, 1);
}
showQuestion();
}
// 选择判断题选项
function selectTrueFalse(answer) {
userAnswers[currentQuestion] = answer;
showQuestion();
}
// 更新填空题答案
function updateFillBlankAnswer(answer) {
userAnswers[currentQuestion] = answer.trim();
updateAnswerSheet();
}
// 上一题
function prevQuestion() {
if (currentQuestion > 0) {
currentQuestion--;
showQuestion();
}
}
// 下一题
function nextQuestion() {
if (currentQuestion < examData.questions.length - 1) {
currentQuestion++;
showQuestion();
}
}
// 开始计时器
function startTimer() {
updateTimerDisplay();
timer = setInterval(() => {
timeLeft--;
updateTimerDisplay();
if (timeLeft <= 0) {
clearInterval(timer);
submitExam();
}
}, 1000);
}
// 更新计时器显示
function updateTimerDisplay() {
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
timeDisplay.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
}
// 提交试卷
function submitExam() {
clearInterval(timer);
// 计算分数
let score = 0;
examData.questions.forEach((question, index) => {
const userAnswer = userAnswers[index];
let isCorrect = false;
switch(question.type) {
case QUESTION_TYPES.SINGLE_CHOICE:
isCorrect = userAnswer === question.answer;
break;
case QUESTION_TYPES.MULTI_CHOICE:
if (Array.isArray(userAnswer)) {
// 检查答案数组是否完全相同
const userSorted = [...userAnswer].sort();
const answerSorted = [...question.answer].sort();
isCorrect = JSON.stringify(userSorted) === JSON.stringify(answerSorted);
}
break;
case QUESTION_TYPES.TRUE_FALSE:
isCorrect = userAnswer === question.answer;
break;
case QUESTION_TYPES.FILL_BLANK:
isCorrect = userAnswer && userAnswer.toString().toLowerCase() === question.answer.toLowerCase();
break;
}
if (isCorrect) {
score += question.score;
}
});
// 显示结果
document.getElementById('subject').style.display = 'none';
document.querySelector('.navigation').style.display = 'none';
resultDiv.style.display = 'block';
finalScore.textContent = score;
// 根据分数显示不同消息
const totalScore = examData.questions.reduce((sum, q) => sum + q.score, 0);
const percentage = (score / totalScore) * 100;
if (percentage >= 80) {
resultMessage.textContent = "优秀!你掌握了大部分知识点。";
} else if (percentage >= 60) {
resultMessage.textContent = "良好!继续努力,你可以做得更好。";
} else if (percentage >= 40) {
resultMessage.textContent = "及格!建议复习相关知识点。";
} else {
resultMessage.textContent = "不及格!需要加强学习。";
}
}
// 事件监听
prevBtn.addEventListener('click', prevQuestion);
nextBtn.addEventListener('click', nextQuestion);
submitBtn.addEventListener('click', () => {
if (confirm("确定要提交试卷吗?提交后将无法修改答案。")) {
submitExam();
}
});
// 开始考试
initExam();
</script>
</body>
</html>