关于在线考试系统的技术博客-结对编程作业


【结对编程作业】408 基础知识在线考试系统设计与实现
结对信息:
编码者:2452925
审核者:2452927
项目: 408基础知识在线考试系统

开发环境:idea
前端: HTML + CSS + JavaScript(前端本地存储实现)
后端: java

一、项目概述

本项目为408 计算机学科专业基础综合在线考试系统,面向计算机专业学生设计,用于数据结构、计算机组成原理、操作系统、计算机网络四大科目基础知识,我们包含单选题、多选题、判断题以及填空题(满分100分)。

系统采用Web 本地存储,完成了题目管理、身份验证、在线答题、自动批改、成绩解析全流程,**本项目也含有DBUtil文件,方便后续数据量变大后接入数据库管理。**

我的系统支持管理员 / 学生两种身份登录验证、3 套试卷随机分配、点击答题卡快速跳转题目、答题进度监控、全题目自动批改与对应的题目可视化解析,完全满足课程设计要求与日常刷题自测需求,界面方面是用了简约主题,交互流畅人性化。

合作过程中,我们先共同讨论了系统模块划分、技术实现方案,再由 2452925 负责核心代码编写,2452927 全程审核逻辑与格式。

下面附上整体项目文件架构图:

二、系统设计思路

  1. 整体模块划分
    我们讨论后决定将系统划分为 4 个核心模块,模块间通过localStorage/sessionStorage实现数据交互:
模块名称 核心功能
用户登录模块 角色校验、账号密码验证、权限拦截、退出登录
题库管理模块 多套试卷内置、随机抽取、题目增删改查
在线考试模块 题型分组展示、答题卡、进度条、题目跳转、倒计时
自动批改模块 答案匹配、分值计算、对错判定、题型分类统计
成绩解析模块 全题目展示、答案对比、解析展示、对错样式区分

2.数据存储设计

localStorage:永久存储用户角色、考试题目数据
sessionStorage:临时存储学生答题答案,关闭页面自动清除
题目数据结构:统一包含type/title/optionA-D/answer/analysis字段,兼容所有题型

3.业务流程
管理员登录 → 查看 / 管理内置随机试卷
学生登录 → 系统随机分配试卷 → 进入考试页面
学生通过答题卡答题 → 交卷触发自动批改
跳转成绩页 → 查看总分、全题目答案对比与解析

三、关键功能亮点设计方案
1.多套随机试卷实现

方案:在管理员页面内置 3 套符合 408 统考结构的试卷(每套 25 题,单选 10 + 多选 5 + 判断 5 + 填空 5,满分 100),学生登录时通过js的Math.random()方法随机抽取一套存入本地存储,实现不同同学题目不同的防作弊效果。
部分代码(详细代码后面展示):

点击查看代码

// 内置3套408标准试卷
const examPaper1 = [/* 单选10+多选5+判断5+填空5 */];
const examPaper2 = [/* 同结构试卷2 */];
const examPaper3 = [/* 同结构试卷3 */];

// 随机抽取试卷并存入本地存储
const papers = [examPaper1, examPaper2, examPaper3];
const randomPaper = papers[Math.floor(Math.random() * papers.length)];
localStorage.setItem('examQuestions', JSON.stringify(randomPaper));

– 试卷的分值是这样安排的,以实现四大题型都合理覆盖到。

题型 题量 单题分值 总分
单选题 10 题 4 分 40 分
多选题 5 题 6 分 30 分
判断题 5 题 2 分 10 分
填空题 5 题 4 分 20 分
合计 25 题 100 分

2.答题卡与进度条实现

方案:考试页面左侧固定答题卡栏,根据题目总数生成题号按钮;通过onchange事件监听答题状态,标记已答题目为绿色;用scrollIntoView实现点击题号平滑跳转,同时通过已答题数 / 总题数计算进度条宽度。
部分代码(详细代码后面展示):

点击查看代码

// 渲染答题卡题号
function renderAnswerSheet(){
    const numBox = document.getElementById('questionNums');
    for(let i=0;i<questions.length;i++){
        numBox.innerHTML += `<button class="num-btn" onclick="jumpToQuestion(${i})" id="btn${i}">${i+1}</button>`;
    }
}

// 题号跳转题目
function jumpToQuestion(index){
    document.getElementById(`q${index}`).scrollIntoView({ behavior:'smooth' });
}

// 更新已答状态与进度条
function updateAnswer(index){
    document.getElementById(`btn${index}`).classList.add('answered');
    const percent = (Object.keys(userAnswers).length / questions.length) * 100;
    document.getElementById('progress').style.width = percent + '%';
}

3.自动批改逻辑

方案:按题目索引匹配用户答案与正确答案,根据题型(单选 / 多选 / 判断 / 填空)实现不同的答案匹配规则(如多选题答案排序后比较、填空题忽略大小写),并按题型分值计算总分,同时按题型分组展示全部题目,区分正确 / 错误样式。
部分代码(详细代码后面展示):

点击查看代码

function gradeAllQuestions(){
    questions.forEach((q, index) => {
        const userAns = userAnswers[index] || '未作答';
        let isCorrect = false;
        // 单选/判断匹配
        if(q.type === 'single' || q.type === 'judge'){
            isCorrect = userAns === q.answer;
        }
        // 多选题排序后匹配
        if(q.type === 'multiple'){
            const userArr = userAns.split(',').sort();
            const corrArr = q.answer.split(',').sort();
            isCorrect = JSON.stringify(userArr) === JSON.stringify(corrArr);
        }
        // 计算总分并分组存储
        isCorrect && (totalScore += scoreConfig[q.type]);
        allByType[q.type].push({ idx:index+1, q, userAns, isCorrect });
    });
}

四、系统功能演示
1 系统首页
动态背景,核心入口引导至登录页面,设置前往登录按钮交互引导。

2 用户登录页
双角色选择(管理员 / 学生),内置默认账号密码,后续可修改,页面显示默认密码方便展示,权限校验也可以拦截一些非法访问。

2.1管理员登录:

如果没点击确认,3秒后也会自动跳转管理员操作页面,增强交互,减少繁琐操作,提高了用户体验感。

2.2学生登录

同上

3 管理员题目管理页面
展示当前随机试卷,可以看到所有题目,并且支持题目新增、编辑、删除、清空表单,题型分类清晰。

可以添加修改四种题型

完成操作退出登录

4 学生在线考试页面
左侧答题卡显示现在的答题进度(已答是绿色,未答是灰色),点击题号,可以快速跳转到对应题目。倒计时显示。

交卷

5.5 成绩解析页面

-交卷后跳转到自动批改生成的成绩报告,显示每道题的考生回答状态和正确答案,给出相应的题目解析,展示总分,按题型分类展示全部题目,标注个人答案、正确答案,正确题绿色高亮、错误题红色提示 + 解析,清晰,用户体验感强。

看完解析可返回登录

六、结对编程实施过程
6.1 分工安排
开发阶段:2452925 余建文负责核心代码编写、页面布局、功能逻辑实现
审核阶段:2452927 负责代码逻辑校验、BUG 排查

6.2 协作解决的核心问题
我开发过程中也遇到一些困难,主要是以下一些:

  1. 题目与答案错位问题
  2. 本地存储缓存冲突导致无题目显示
  3. 多选题答案匹配逻辑异常
  4. 答题卡状态同步与跳转失效

解决:

遇到的问题 解决方案
学生登录后无题目显示 清除浏览器 LocalStorage 缓存,强制重置内置题库
交卷后不显示答案 / 解析 修复答案存储索引,统一题目与答案匹配规则
答题卡答题状态不同步 绑定 onchange 事件,实时更新题号已答样式
多选题自动批改错误 答案按字母排序后对比,忽略选项选择顺序
题目与答案错位错乱 按题目真实索引绑定答案,保证一一对应

6.3 结对编程优势
我认为结对编程主要有以下四点好处:

  1. 实时代码审核,大幅减少 BUG 数量
  2. 思路互补,优化功能实现方案
  3. 分工明确,提升开发效率
  4. 规范代码格式,提升可维护性

七、项目心得体会

实际开发过程中其实大部分时间用于debug,和修改代码实现逻辑,这个作业让我明白了这种结对编程的双人协作能有效规避单人开发的思维盲区,及时发现逻辑漏洞,同时提升沟通与团队协作能力,我感觉真正实现了1+1>2的开发效果。 这种结对编程的经历也有利于系统设计思维的培养吗,从模块划分,再到代码实现与测试,完整经历软件开发全流程,建立了规范化的项目设计思维。

八、项目总结

本 408 计算机基础在线考试系统,基于 HTML+CSS + 原生 JavaScript 实现,完成了用户权限验证和两个身份的管理、多套随机试卷、答题卡交互、自动批改、全题目解析全部核心功能,界面美观、逻辑稳定、无服务端依赖。 系统可直接在浏览器运行,无需部署环境,后续可扩展主观题批改、成绩导出、多科目题库等功能,具备良好的扩展性与实用性。

九、完整源码

  1. 首页 index.html

点击查看代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> 欢迎使用408基础知识在线考试系统 </title>
    <style>
        *{margin:0;padding:0;box-sizing:border-box}
        :root{
            --pink:#e8a4c4;
            --magenta:#c471ed;
            --violet:#6b5ce7;
            --deep:#0f0a14;
            --glass:rgba(255,255,255,0.08);
            --stroke:rgba(255,255,255,0.18);
        }
        body{
            font-family:"Microsoft YaHei","Segoe UI",sans-serif;
            min-height:100vh;
            display:flex;
            justify-content:center;
            align-items:center;
            overflow:hidden;
            position:relative;
            background:var(--deep);
        }
        /* 动态极光背景 */
        .aurora{
            position:fixed;
            inset:0;
            z-index:0;
            background:
                radial-gradient(ellipse 80% 50% at 20% 40%, rgba(196,113,237,0.35) 0%, transparent 55%),
                radial-gradient(ellipse 70% 45% at 80% 60%, rgba(107,92,231,0.3) 0%, transparent 50%),
                radial-gradient(ellipse 60% 40% at 50% 100%, rgba(232,164,196,0.25) 0%, transparent 45%),
                linear-gradient(180deg, #0a0610 0%, #120a18 50%, #0d0814 100%);
            animation: auroraShift 14s ease-in-out infinite alternate;
        }
        @keyframes auroraShift{
            0%{filter:hue-rotate(-8deg) saturate(1.05);}
            100%{filter:hue-rotate(12deg) saturate(1.15);}
        }
        .grid{
            position:fixed;
            inset:0;
            z-index:0;
            background-image:
                linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
                linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
            background-size:48px 48px;
            mask-image:radial-gradient(ellipse 70% 60% at 50% 50%, black 20%, transparent 70%);
            pointer-events:none;
        }
        /* 樱花画布 */
        #sakura{
            position:fixed;
            top:0;
            left:0;
            width:100%;
            height:100%;
            pointer-events:none;
            z-index:1;
            opacity:0.85;
        }
        .box{
            text-align:center;
            position:relative;
            z-index:2;
            padding:52px 56px 48px;
            max-width:min(92vw, 520px);
            border-radius:28px;
            background:var(--glass);
            border:1px solid var(--stroke);
            box-shadow:
                0 0 0 1px rgba(255,255,255,0.05) inset,
                0 25px 80px rgba(0,0,0,0.45),
                0 0 60px rgba(196,113,237,0.15);
            backdrop-filter:blur(18px);
            -webkit-backdrop-filter:blur(18px);
            animation: cardFloat 5s ease-in-out infinite, cardGlow 6s ease-in-out infinite;
        }
        @keyframes cardFloat{
            0%,100%{transform:translateY(0) scale(1);}
            50%{transform:translateY(-8px) scale(1.01);}
        }
        @keyframes cardGlow{
            0%,100%{box-shadow:
                0 0 0 1px rgba(255,255,255,0.05) inset,
                0 25px 80px rgba(0,0,0,0.45),
                0 0 50px rgba(196,113,237,0.12);}
            50%{box-shadow:
                0 0 0 1px rgba(255,255,255,0.08) inset,
                0 30px 90px rgba(0,0,0,0.5),
                0 0 70px rgba(107,92,231,0.2);}
        }
        .box::before{
            content:"";
            position:absolute;
            inset:-1px;
            border-radius:inherit;
            padding:1px;
            background:linear-gradient(135deg, rgba(232,164,196,0.5), rgba(196,113,237,0.35), rgba(107,92,231,0.4));
            -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
            mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
            -webkit-mask-composite:xor;
            mask-composite:exclude;
            pointer-events:none;
            opacity:0.75;
        }
        .tagline{
            display:inline-block;
            font-size:13px;
            letter-spacing:0.35em;
            text-transform:uppercase;
            color:rgba(255,255,255,0.45);
            margin-bottom:18px;
            animation: fadePulse 3s ease-in-out infinite;
        }
        @keyframes fadePulse{
            0%,100%{opacity:0.5;}
            50%{opacity:0.9;}
        }
        h1{
            margin-bottom:36px;
            font-size:clamp(22px, 4.2vw, 34px);
            line-height:1.45;
            font-weight:700;
            background:linear-gradient(120deg, #fce4ec 0%, #e1bee7 25%, #ce93d8 50%, #b39ddb 75%, #e8a4c4 100%);
            background-size:200% auto;
            -webkit-background-clip:text;
            background-clip:text;
            color:transparent;
            animation: shineText 8s linear infinite;
            text-shadow:none;
            filter:drop-shadow(0 0 24px rgba(196,113,237,0.35));
        }
        @keyframes shineText{
            0%{background-position:0% center;}
            100%{background-position:200% center;}
        }
        .go-login{
            position:relative;
            padding:16px 48px;
            border:none;
            border-radius:14px;
            font-size:18px;
            font-weight:600;
            cursor:pointer;
            color:#fff;
            overflow:hidden;
            background:linear-gradient(135deg, #a86a8c 0%, #9c6bb8 40%, #6b5ce7 100%);
            background-size:200% 200%;
            box-shadow:
                0 8px 32px rgba(107,92,231,0.45),
                0 0 0 1px rgba(255,255,255,0.12) inset;
            transition:transform 0.35s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.35s ease, background-position 0.5s ease;
        }
        .go-login:hover{
            transform:translateY(-3px) scale(1.04);
            background-position:100% 50%;
            box-shadow:
                0 14px 40px rgba(196,113,237,0.55),
                0 0 0 1px rgba(255,255,255,0.2) inset;
        }
        .go-login:active{transform:translateY(0) scale(1.02);}
        .go-login::after{
            content:"";
            position:absolute;
            top:0;
            left:-120%;
            width:60%;
            height:100%;
            background:linear-gradient(90deg, transparent, rgba(255,255,255,0.35), transparent);
            transform:skewX(-18deg);
            transition:left 0.6s ease;
        }
        .go-login:hover::after{left:120%;}
    </style>
</head>
<body>
<div class="aurora" aria-hidden="true"></div>
<div class="grid" aria-hidden="true"></div>
<!-- 樱花画布 -->
<canvas id="sakura"></canvas>

<div class="box">
    <p class="tagline">408 · ONLINE EXAM</p>
    <h1> 欢迎使用408基础知识在线考试系统 </h1>
    <button type="button" class="go-login" onclick="location.href='login.html'">前往登录</button>
</div>

<script>
    // 樱花飘落动效
    const canvas = document.getElementById('sakura');
    const ctx = canvas.getContext('2d');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    class Sakura {
        constructor(){
            this.x = Math.random() * canvas.width;
            this.y = Math.random() * -canvas.height;
            this.r = Math.random() * 10 + 5;
            this.speed = Math.random() * 1 + 0.5;
            this.wind = Math.random() * 1 - 0.5;
            this.color = '#e6c6d8';
            this.alpha = Math.random() * 0.35 + 0.35;
        }
        draw(){
            ctx.save();
            ctx.globalAlpha = this.alpha;
            const g = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.r);
            g.addColorStop(0, 'rgba(255,230,240,0.95)');
            g.addColorStop(0.6, this.color);
            g.addColorStop(1, 'rgba(230,198,216,0.15)');
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
            ctx.fillStyle = g;
            ctx.fill();
            ctx.restore();
        }
        update(){
            this.y += this.speed;
            this.x += this.wind;
            if(this.y > canvas.height){
                this.y = -10;
                this.x = Math.random() * canvas.width;
            }
        }
    }

    const sakuras = Array(50).fill().map(()=>new Sakura());
    function animate(){
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        sakuras.forEach(s=>{s.draw();s.update();});
        requestAnimationFrame(animate);
    }
    animate();

    window.addEventListener('resize',()=>{
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
    });
</script>
</body>
</html>

2.考试 exam.html

点击查看代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>在线考试</title>
    <style>
        *{margin:0;padding:0;box-sizing:border-box;font-family:"Microsoft YaHei"}
        body{background:#fdfbf7;display:flex;}
        /* 侧边答题卡 */
        .answer-sheet{
            width:220px;
            height:100vh;
            position:fixed;
            left:0;top:0;
            background:#fff;
            border-right:2px solid #e6c6d8;
            padding:20px;
            overflow-y:auto;
            z-index:10;
        }
        .sheet-title{
            color:#a86a8c;
            text-align:center;
            margin-bottom:15px;
            font-size:18px;
        }
        .progress-bar{
            height:8px;
            background:#eee;
            border-radius:4px;
            margin-bottom:15px;
            overflow:hidden;
        }
        .progress{
            height:100%;
            background:#27ae60;
            width:0%;
            transition:0.3s;
        }
        .question-nums{
            display:grid;
            grid-template-columns:repeat(5,1fr);
            gap:8px;
        }
        .num-btn{
            width:32px;height:32px;
            border:1px solid #ddd;
            border-radius:4px;
            background:#f8f8f8;
            cursor:pointer;
            font-size:12px;
        }
        .num-btn.answered{
            background:#27ae60;
            color:#fff;
            border-color:#27ae60;
        }
        /* 主内容区 */
        .main-content{
            margin-left:220px;
            flex:1;
            padding:20px;
        }
        .header{text-align:right;padding:15px;background:#fff;border-radius:10px;margin-bottom:15px;}
        .logout{padding:6px 15px;background:linear-gradient(45deg,#c45b65,#e68a9c);color:#fff;border:none;border-radius:6px;cursor:pointer;transition:0.3s;}
        .logout:hover{transform:scale(1.05);}
        .container{width:900px;margin:0 auto;padding:30px;background:rgba(255,255,255,.95);border-radius:15px;box-shadow:0 0 15px rgba(0,0,0,.1);border:2px solid #e6c6d8}
        .title{text-align:center;color:#a86a8c;margin-bottom:20px}
        .time-box{font-size:20px;color:#c45b65;margin:20px 0;text-align:center;font-weight:bold}
        .type-title{
            font-size:20px;
            color:#a86a8c;
            margin:30px 0 15px 0;
            padding-bottom:8px;
            border-bottom:2px solid #e6c6d8;
            font-weight:bold;
        }
        .question{
            margin:20px 0;
            padding:20px;
            border:1px solid #f0d9e5;
            border-radius:10px;
            background:#fff8fc;
            scroll-margin-top:20px;
        }
        .option{margin:10px 0;font-size:15px}
        .blank-input{width:60%;padding:8px;border:1px solid #ddd;border-radius:6px;margin-top:10px}
        #submitBtn{padding:12px 30px;background:linear-gradient(45deg,#a86a8c,#d488b2);color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;display:block;margin:30px auto;transition:0.3s;}
        #submitBtn:hover{transform:scale(1.05);}
        .modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.3);display:flex;justify-content:center;align-items:center;z-index:999;opacity:0;visibility:hidden;transition:0.3s;}
        .modal.show{opacity:1;visibility:visible;}
        .modal-box{width:320px;background:#fff;border-radius:12px;padding:25px;text-align:center;border:2px solid #e6c6d8;box-shadow:0 5px 15px rgba(0,0,0,.1);}
        .modal-title{font-size:18px;color:#a86a8c;margin-bottom:15px;}
        .modal-btns{display:flex;gap:10px;justify-content:center;margin-top:15px;}
        .modal-btn{padding:8px 25px;background:linear-gradient(45deg,#a86a8c,#d488b2);color:#fff;border:none;border-radius:6px;cursor:pointer;}
        .modal-btn.cancel{background:#ccc;color:#666;}
    </style>
</head>
<body>
<!-- 侧边答题卡 -->
<div class="answer-sheet">
    <h3 class="sheet-title">答题卡</h3>
    <div class="progress-bar">
        <div class="progress" id="progress"></div>
    </div>
    <div class="question-nums" id="questionNums"></div>
</div>

<!-- 主内容区 -->
<div class="main-content">
    <div class="header">
        <button class="logout" onclick="logout()">退出登录</button>
    </div>
    <div class="container">
        <h1 class="title"> 学生 - 在线考试 </h1>
        <div class="time-box">剩余时间:<span id="time">10:00</span></div>
        <div id="examContainer"></div>
        <button id="submitBtn" onclick="showModal('确定交卷?',true,'submitExam()')">交卷</button>
    </div>
</div>

<div class="modal" id="modal">
    <div class="modal-box">
        <div class="modal-title" id="modalTitle"></div>
        <div class="modal-btns" id="modalBtns"></div>
    </div>
</div>

<script>
    if(localStorage.getItem('userRole') !== 'student'){
        showModal('无权限,请使用学生账号登录!');
        setTimeout(()=>{location.href='login.html'},1500);
    }

    function showModal(title,showCancel=false,confirmCallback=null){
        document.getElementById('modalTitle').innerText = title;
        let btns = `<button class="modal-btn" onclick="closeModal()">确定</button>`;
        if(showCancel){ btns = `<button class="modal-btn cancel" onclick="closeModal()">取消</button><button class="modal-btn" onclick="${confirmCallback};closeModal()">确认</button>`; }
        document.getElementById('modalBtns').innerHTML = btns;
        document.getElementById('modal').classList.add('show');
    }
    function closeModal(){ document.getElementById('modal').classList.remove('show'); }
    function logout(){ showModal('确定退出考试?',true,'confirmLogout()'); }
    function confirmLogout(){ localStorage.removeItem('userRole'); showModal('退出成功!'); setTimeout(()=>{location.href='login.html'},1000); }

    let questions = JSON.parse(localStorage.getItem('examQuestions')) || [];
    let userAnswers = {};
    let time = 10*60;
    let timer;

    const typeConfig = {
        single: { name: '一、单选题(每题4分)', list: [] },
        multiple: { name: '二、多选题(每题6分)', list: [] },
        judge: { name: '三、判断题(每题2分)', list: [] },
        blank: { name: '四、填空题(每题4分)', list: [] }
    };

    window.onload = ()=>{
        groupQuestionsByType();
        renderExamByType();
        renderAnswerSheet();
        startCountDown();
    }

    // 按题型分组
    function groupQuestionsByType(){
        questions.forEach((q, index)=>{
            q.realIndex = index;
            typeConfig[q.type].list.push(q);
        });
    }

    // 渲染试卷
    function renderExamByType(){
        let box = document.getElementById('examContainer');
        box.innerHTML = '';
        for(let type in typeConfig){
            let data = typeConfig[type];
            if(data.list.length>0){
                box.innerHTML += `<div class="type-title">${data.name}</div>`;
                data.list.forEach(q=>{
                    let optHtml='';
                    if(q.type==='judge'){
                        optHtml=`<div class="option"><label><input type="radio" name="q${q.realIndex}" value="对" onchange="updateAnswer(${q.realIndex})"> 对</label></div><div class="option"><label><input type="radio" name="q${q.realIndex}" value="错" onchange="updateAnswer(${q.realIndex})"> 错</label></div>`;
                    }else if(q.type==='blank'){
                        optHtml=`<input type="text" class="blank-input" name="q${q.realIndex}" oninput="updateAnswer(${q.realIndex})" placeholder="请填写答案">`;
                    }else{
                        if(q.optionA)optHtml+=`<div class="option"><label><input type="${q.type==='single'?'radio':'checkbox'}" name="q${q.realIndex}" value="A" onchange="updateAnswer(${q.realIndex})"> A、${q.optionA}</label></div>`;
                        if(q.optionB)optHtml+=`<div class="option"><label><input type="${q.type==='single'?'radio':'checkbox'}" name="q${q.realIndex}" value="B" onchange="updateAnswer(${q.realIndex})"> B、${q.optionB}</label></div>`;
                        if(q.optionC)optHtml+=`<div class="option"><label><input type="${q.type==='single'?'radio':'checkbox'}" name="q${q.realIndex}" value="C" onchange="updateAnswer(${q.realIndex})"> C、${q.optionC}</label></div>`;
                        if(q.optionD)optHtml+=`<div class="option"><label><input type="${q.type==='single'?'radio':'checkbox'}" name="q${q.realIndex}" value="D" onchange="updateAnswer(${q.realIndex})"> D、${q.optionD}</label></div>`;
                    }
                    box.innerHTML+=`<div class="question" id="q${q.realIndex}"><p>${q.realIndex+1}、${q.title}</p>${optHtml}</div>`;
                });
            }
        }
    }

    // 渲染答题卡
    function renderAnswerSheet(){
        let numBox = document.getElementById('questionNums');
        numBox.innerHTML = '';
        for(let i=0;i<questions.length;i++){
            numBox.innerHTML += `<button class="num-btn" onclick="jumpToQuestion(${i})" id="btn${i}">${i+1}</button>`;
        }
        updateProgress();
    }

    // 跳转题目
    function jumpToQuestion(index){
        document.getElementById(`q${index}`).scrollIntoView({behavior:'smooth',block:'start'});
    }

    // 更新答案&答题卡状态
    function updateAnswer(index){
        let val = '';
        let inputs = document.querySelectorAll(`input[name="q${index}"]`);
        if(inputs[0].type==='radio' || inputs[0].type==='checkbox'){
            let arr = [];
            inputs.forEach(i=>{if(i.checked)arr.push(i.value);});
            val = arr.join(',');
        }else{
            val = inputs[0].value.trim();
        }
        userAnswers[index] = val;
        // 标记已答
        document.getElementById(`btn${index}`).classList.add('answered');
        updateProgress();
    }

    // 更新进度条
    function updateProgress(){
        let answered = Object.keys(userAnswers).length;
        let percent = (answered/questions.length)*100;
        document.getElementById('progress').style.width = percent + '%';
    }

    // 倒计时
    function startCountDown(){
        timer=setInterval(()=>{
            time--;
            let m=parseInt(time/60).toString().padStart(2,'0');
            let s=(time%60).toString().padStart(2,'0');
            document.getElementById('time').innerText=`${m}:${s}`;
            if(time<=0){clearInterval(timer);showModal('时间到,自动交卷!');submitExam();}
        },1000);
    }

    // 交卷
    function submitExam(){
        clearInterval(timer);
        let ansArr = [];
        for(let i=0;i<questions.length;i++) ansArr[i] = userAnswers[i]||'';
        sessionStorage.setItem("userAnswers",ansArr.join(';'));
        location.href="result.html";
    }
</script>
</body>
</html>

3.管理员admin.html

点击查看代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>题目管理</title>
    <style>
        *{margin:0;padding:0;box-sizing:border-box;font-family:"Microsoft YaHei"}
        body{background:#fdfbf7}
        .header{text-align:right;padding:15px;background:#fff;border-radius:10px;margin-bottom:15px;}
        .logout{padding:6px 15px;background:linear-gradient(45deg,#c45b65,#e68a9c);color:#fff;border:none;border-radius:6px;cursor:pointer;transition:0.3s;}
        .logout:hover{transform:scale(1.05);}
        .container{width:1100px;margin:0 auto;padding:30px;background:rgba(255,255,255,.95);border-radius:15px;box-shadow:0 0 15px rgba(0,0,0,.1);border:2px solid #e6c6d8}
        .title{text-align:center;color:#a86a8c;margin-bottom:25px;font-size:28px}
        .form-box{border:1px solid #e6c6d8;padding:25px;margin-bottom:25px;border-radius:10px;background:#fff8fc}
        .form-box input,.form-box select,.form-box textarea{width:100%;margin:10px 0;padding:10px;border:1px solid #ddd;border-radius:6px;font-size:14px}
        button{padding:10px 20px;margin:8px;cursor:pointer;border:none;border-radius:6px;font-size:15px;font-weight:bold;transition:.2s;}
        .save-btn{background:linear-gradient(45deg,#a86a8c,#d488b2);color:white;}
        .clear-btn{background:linear-gradient(45deg,#6a9ca8,#88c4d4);color:white;}
        .del-btn{background:linear-gradient(45deg,#c45b65,#e68a9c);color:white;}
        .edit-btn{background:linear-gradient(45deg,#e6a87e,#f2c9a6);color:white;}
        button:hover{transform:scale(1.05);}
        table{width:100%;border-collapse:collapse;margin-top:20px;background:white;border-radius:8px;overflow:hidden}
        table,th,td{border:1px solid #e6c6d8;padding:12px;text-align:center}
        th{background:#f0d9e5;color:#a86a8c;font-size:16px}
        .hide{display:none}
        .modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.3);display:flex;justify-content:center;align-items:center;z-index:999;opacity:0;visibility:hidden;transition:0.3s;}
        .modal.show{opacity:1;visibility:visible;}
        .modal-box{width:320px;background:#fff;border-radius:12px;padding:25px;text-align:center;border:2px solid #e6c6d8;box-shadow:0 5px 15px rgba(0,0,0,0.1);}
        .modal-title{font-size:18px;color:#a86a8c;margin-bottom:15px;}
        .modal-btns{display:flex;gap:10px;justify-content:center;margin-top:15px;}
        .modal-btn{padding:8px 25px;background:linear-gradient(45deg,#a86a8c,#d488b2);color:#fff;border:none;border-radius:6px;cursor:pointer;}
        .modal-btn.cancel{background:#ccc;color:#666;}
    </style>
</head>
<body>
<div class="header">
    <button class="logout" onclick="logout()">退出登录</button>
</div>
<div class="container">
    <h1 class="title"> 管理员 - 题目管理 </h1>
    <div class="form-box">
        <input type="hidden" id="qId">
        <select id="qType" onchange="toggleOptions()">
            <option value="single">单选题</option>
            <option value="multiple">多选题</option>
            <option value="judge">判断题</option>
            <option value="blank">填空题</option>
        </select>
        <input type="text" id="qTitle" placeholder="请输入题干">
        <div id="optionBox">
            <input type="text" id="qOptionA" placeholder="选项A">
            <input type="text" id="qOptionB" placeholder="选项B">
            <input type="text" id="qOptionC" placeholder="选项C">
            <input type="text" id="qOptionD" placeholder="选项D">
        </div>
        <input type="text" id="qAnswer" placeholder="正确答案">
        <textarea id="qAnalysis" placeholder="题目解析" rows="3"></textarea>
        <button class="save-btn" onclick="saveQuestion()">保存题目</button>
        <button class="clear-btn" onclick="clearForm()">清空表单</button>
    </div>
    <table>
        <thead><tr><th>ID</th><th>题型</th><th>题干</th><th>操作</th></tr></thead>
        <tbody id="questionList"></tbody>
    </table>
</div>
<div class="modal" id="modal">
    <div class="modal-box">
        <div class="modalTitle" id="modalTitle"></div>
        <div class="modal-btns" id="modalBtns"></div>
    </div>
</div>
<script>
    if(localStorage.getItem('userRole') !== 'admin'){
        showModal('无权限,请使用管理员账号登录!');
        setTimeout(()=>{location.href='login.html'},1500);
    }

    // 3套408考研试卷(每套25题,满分100)
    const examPaper1 = [
        {type:"single",title:"快速排序平均复杂度与稳定性",optionA:"O(n²)稳定",optionB:"O(nlogn)不稳定",optionC:"O(nlogn)稳定",optionD:"O(n)不稳定",answer:"B",analysis:"快排平均O(nlogn),不稳定"},
        {type:"single",title:"Cache解决什么问题",optionA:"容量",optionB:"速度不匹配",optionC:"外存慢",optionD:"指令少",answer:"B",analysis:"匹配CPU与主存速度"},
        {type:"single",title:"FCFS调度特点",optionA:"抢占式",optionB:"非抢占式",optionC:"时间片",optionD:"优先级",answer:"C",analysis:"FCFS是非抢占式调度"},
        {type:"single",title:"HTTP所属层次",optionA:"应用层",optionB:"传输层",optionC:"网络层",optionD:"链路层",answer:"A",analysis:"HTTP是应用层协议"},
        {type:"single",title:"栈的特点",optionA:"FIFO",optionB:"FILO",optionC:"随机",optionD:"无序",answer:"B",analysis:"栈先进后出"},
        {type:"single",title:"不属于运算器的是",optionA:"ALU",optionB:"PC",optionC:"ACC",optionD:"FR",answer:"B",analysis:"PC属于控制器"},
        {type:"single",title:"死锁必要条件不含",optionA:"互斥",optionB:"剥夺",optionC:"请求保持",optionD:"环路",answer:"B",analysis:"死锁无剥夺条件"},
        {type:"single",title:"192.168.x.x属于",optionA:"A类",optionB:"B类",optionC:"C类",optionD:"D类",answer:"C",analysis:"C类IP地址"},
        {type:"single",title:"满二叉树节点数",optionA:"2^h-1",optionB:"2^h",optionC:"h²",optionD:"2h",answer:"A",analysis:"公式2^h-1"},
        {type:"single",title:"指令操作码作用",optionA:"操作类型",optionB:"地址",optionC:"长度",optionD:"格式",answer:"A",analysis:"操作码表征操作类型"},
        {type:"judge",title:"TCP是可靠协议",answer:"对",analysis:"TCP面向连接可靠传输"},
        {type:"judge",title:"虚拟内存扩大物理内存",answer:"错",analysis:"虚拟内存是逻辑扩充"},
        {type:"judge",title:"二叉排序树中序有序",answer:"对",analysis:"中序遍历递增有序"},
        {type:"judge",title:"指令周期≥机器周期",answer:"对",analysis:"指令周期包含机器周期"},
        {type:"judge",title:"路由器在链路层",answer:"错",analysis:"路由器在网络层"},
        {type:"multiple",title:"OS基本功能",optionA:"进程",optionB:"内存",optionC:"文件",optionD:"设备",answer:"A,B,C,D",analysis:"OS四大功能"},
        {type:"multiple",title:"线性结构",optionA:"栈",optionB:"队列",optionC:"链表",optionD:"二叉树",answer:"A,B,C",analysis:"二叉树非线性"},
        {type:"multiple",title:"传输层协议",optionA:"TCP",optionB:"UDP",optionC:"IP",optionD:"ICMP",answer:"A,B",analysis:"TCP/UDP是传输层"},
        {type:"multiple",title:"CPU组成",optionA:"运算器",optionB:"控制器",optionC:"主存",optionD:"Cache",answer:"A,B",analysis:"CPU=运算器+控制器"},
        {type:"multiple",title:"易失性存储",optionA:"RAM",optionB:"ROM",optionC:"Cache",optionD:"硬盘",answer:"A,C",analysis:"断电丢失数据"},
        {type:"blank",title:"进程三态:运行、就绪、__",answer:"阻塞",analysis:"进程三态之一"},
        {type:"blank",title:"BFS借助__实现",answer:"队列",analysis:"广度优先用队列"},
        {type:"blank",title:"MAC地址__比特",answer:"48",analysis:"6字节=48比特"},
        {type:"blank",title:"计算机存储核心__",answer:"存储器",analysis:"存储数据指令"},
        {type:"blank",title:"进程唯一标志__",answer:"PCB",analysis:"PCB是进程标识"}
    ];
    const examPaper2 = [
        {type:"single",title:"归并排序复杂度",optionA:"O(nlogn)稳定",optionB:"O(nlogn)不稳定",optionC:"O(n²)稳定",optionD:"O(n)稳定",answer:"A",analysis:"归并排序稳定且O(nlogn)"},
        {type:"single",title:"虚拟内存技术依托",optionA:"外存",optionB:"寄存器",optionC:"Cache",optionD:"主存",answer:"A",analysis:"虚拟内存依托硬盘外存"},
        {type:"single",title:"时间片轮转属于",optionA:"抢占式",optionB:"非抢占式",optionC:"静态",optionD:"实时",answer:"A",analysis:"时间片轮转是抢占式"},
        {type:"single",title:"UDP所属层次",optionA:"传输层",optionB:"应用层",optionC:"网络层",optionD:"物理层",answer:"A",analysis:"UDP是传输层协议"},
        {type:"single",title:"队列特点",optionA:"FIFO",optionB:"FILO",optionC:"随机",optionD:"无序",answer:"A",analysis:"队列先进先出"},
        {type:"single",title:"属于控制器的是",optionA:"PC",optionB:"ALU",optionC:"ACC",optionD:"MDR",answer:"A",analysis:"PC是程序计数器属于控制器"},
        {type:"single",title:"OPT页面置换算法",optionA:"最优",optionB:"先进先出",optionC:"最近最少",optionD:"时钟",answer:"A",analysis:"OPT是最优置换算法"},
        {type:"single",title:"IP协议作用",optionA:"寻址路由",optionB:"可靠传输",optionC:"数据加密",optionD:"流量控制",answer:"A",analysis:"IP负责网络寻址"},
        {type:"single",title:"二叉树深度计算",optionA:"根到叶最长路径",optionB:"总节点数",optionC:"叶子数",optionD:"度",answer:"A",analysis:"深度是根到叶最长路径"},
        {type:"single",title:"补码运算优点",optionA:"简化减法",optionB:"提高速度",optionC:"扩大容量",optionD:"减少指令",answer:"A",analysis:"补码可将减法变加法"},
        {type:"judge",title:"UDP是可靠协议",answer:"错",analysis:"UDP无连接不可靠"},
        {type:"judge",title:"进程是资源分配单位",answer:"对",analysis:"进程是资源分配基本单位"},
        {type:"judge",title:"哈希表查找O(1)",answer:"对",analysis:"哈希查找时间复杂度O(1)"},
        {type:"judge",title:"DMA方式不需要CPU",answer:"对",analysis:"DMA直接内存访问不占用CPU"},
        {type:"judge",title:"交换机在网络层",answer:"错",analysis:"交换机在数据链路层"},
        {type:"multiple",title:"数据结构存储结构",optionA:"顺序",optionB:"链式",optionC:"索引",optionD:"散列",answer:"A,B,C,D",analysis:"四大存储结构"},
        {type:"multiple",title:"死锁处理方法",optionA:"预防",optionB:"避免",optionC:"检测",optionD:"解除",answer:"A,B,C,D",analysis:"死锁四种处理方式"},
        {type:"multiple",title:"网络层设备",optionA:"路由器",optionB:"三层交换机",optionC:"网桥",optionD:"集线器",answer:"A,B",analysis:"路由器、三层交换机在网络层"},
        {type:"multiple",title:"寄存器类型",optionA:"通用",optionB:"指令",optionC:"地址",optionD:"程序",answer:"A,B,C",analysis:"常见寄存器分类"},
        {type:"multiple",title:"文件物理结构",optionA:"连续",optionB:"链接",optionC:"索引",optionD:"散列",answer:"A,B,C",analysis:"文件三种物理结构"},
        {type:"blank",title:"线程是__调度单位",answer:"独立",analysis:"线程是独立调度单位"},
        {type:"blank",title:"DFS借助__实现",answer:"栈",analysis:"深度优先用栈"},
        {type:"blank",title:"IPv4地址__比特",answer:"32",analysis:"IPv4为32位地址"},
        {type:"blank",title:"ALU中文全称__",answer:"算术逻辑单元",analysis:"运算器核心部件"},
        {type:"blank",title:"临界区是__代码段",answer:"访问共享资源",analysis:"临界区是访问共享资源代码"}
    ];
    const examPaper3 = [
        {type:"single",title:"直接插入排序稳定性",optionA:"稳定",optionB:"不稳定",optionC:"视情况",optionD:"未知",answer:"A",analysis:"直接插入排序是稳定排序"},
        {type:"single",title:"CPU中指令寄存器存放",optionA:"指令",optionB:"数据",optionC:"地址",optionD:"状态",answer:"A",analysis:"指令寄存器存放当前指令"},
        {type:"single",title:"银行家算法用于",optionA:"死锁避免",optionB:"死锁预防",optionC:"死锁检测",optionD:"死锁解除",answer:"A",analysis:"银行家算法避免死锁"},
        {type:"single",title:"DNS作用",optionA:"域名解析",optionB:"文件传输",optionC:"远程登录",optionD:"邮件传输",answer:"A",analysis:"DNS将域名转IP"},
        {type:"single",title:"哈希冲突解决",optionA:"链地址法",optionB:"顺序存储",optionC:"链式存储",optionD:"索引",answer:"A",analysis:"链地址法解决哈希冲突"},
        {type:"single",title:"主存容量单位",optionA:"字节",optionB:"位",optionC:"字",optionD:"块",answer:"A",analysis:"存储容量基本单位字节"},
        {type:"single",title:"LRU置换算法",optionA:"最近最少使用",optionB:"先进先出",optionC:"最优",optionD:"时钟",answer:"A",analysis:"LRU淘汰最近最少使用页面"},
        {type:"single",title:"TCP三次握手目的",optionA:"建立连接",optionB:"断开连接",optionC:"流量控制",optionD:"拥塞控制",answer:"A",analysis:"三次握手建立TCP连接"},
        {type:"single",title:"线性表顺序存储优点",optionA:"随机存取",optionB:"插入方便",optionC:"删除方便",optionD:"空间利用率高",answer:"A",analysis:"顺序表支持随机存取"},
        {type:"single",title:"操作系统管理对象",optionA:"计算机资源",optionB:"应用程序",optionC:"用户数据",optionD:"硬件驱动",answer:"A",analysis:"OS管理计算机软硬件资源"},
        {type:"judge",title:"线程拥有独立资源",answer:"错",analysis:"线程共享进程资源"},
        {type:"judge",title:"顺序表支持随机访问",answer:"对",analysis:"顺序存储可随机存取"},
        {type:"judge",title:"ICMP属于网络层",answer:"对",analysis:"ICMP是网络层协议"},
        {type:"judge",title:"中断处理需要CPU",answer:"对",analysis:"中断响应需要CPU处理"},
        {type:"judge",title:"索引文件存取快",answer:"对",analysis:"索引文件支持快速查找"},
        {type:"multiple",title:"排序算法稳定性",optionA:"插入",optionB:"冒泡",optionC:"归并",optionD:"快速",answer:"A,B,C",analysis:"快排不稳定,其余稳定"},
        {type:"multiple",title:"计组五大部件",optionA:"运算器",optionB:"控制器",optionC:"存储器",optionD:"输入输出",answer:"A,B,C,D",analysis:"计算机五大组成部件"},
        {type:"multiple",title:"OS调度算法",optionA:"FCFS",optionB:"SJF",optionC:"RR",optionD:"Priority",answer:"A,B,C,D",analysis:"常见四种调度算法"},
        {type:"multiple",title:"网络拓扑结构",optionA:"星型",optionB:"总线型",optionC:"环型",optionD:"树型",answer:"A,B,C,D",analysis:"常见网络拓扑"},
        {type:"multiple",title:"存储层次",optionA:"寄存器",optionB:"Cache",optionC:"主存",optionD:"外存",answer:"A,B,C,D",analysis:"计算机存储层次结构"},
        {type:"blank",title:"死锁四个条件__",answer:"互斥",analysis:"死锁首要条件互斥"},
        {type:"blank",title:"TCP四次挥手__",answer:"断开连接",analysis:"四次挥手断开TCP连接"},
        {type:"blank",title:"顺序存储寻址方式__",answer:"随机",analysis:"顺序表随机寻址"},
        {type:"blank",title:"控制器核心部件__",optionA:"CU",answer:"CU",analysis:"控制单元CU是控制器核心"},
        {type:"blank",title:"文件逻辑结构__",answer:"记录式",analysis:"文件逻辑结构分流式和记录式"}
    ];

    // 学生随机抽取一套试卷
    const papers = [examPaper1, examPaper2, examPaper3];
    const randomPaper = papers[Math.floor(Math.random() * papers.length)];
    localStorage.setItem('examQuestions', JSON.stringify(randomPaper));

    let questions = JSON.parse(localStorage.getItem('examQuestions')) || [];
    window.onload = renderList;

    function showModal(title,showCancel=false,confirmCallback=null){
        document.getElementById('modalTitle').innerText = title;
        let btns = `<button class="modal-btn" onclick="closeModal()">确定</button>`;
        if(showCancel){
            btns = `<button class="modal-btn cancel" onclick="closeModal()">取消</button><button class="modal-btn" onclick="${confirmCallback};closeModal()">确认</button>`;
        }
        document.getElementById('modalBtns').innerHTML = btns;
        document.getElementById('modal').classList.add('show');
    }
    function closeModal(){ document.getElementById('modal').classList.remove('show'); }
    function logout(){ showModal('确定退出?',true,'confirmLogout()'); }
    function confirmLogout(){ localStorage.removeItem('userRole'); showModal('退出成功!'); setTimeout(()=>{location.href='login.html'},1000); }
    function toggleOptions(){ document.getElementById('optionBox').classList.toggle('hide',document.getElementById('qType').value==='blank'); }
    function renderList(){
        let list=document.getElementById('questionList');
        list.innerHTML='';
        let typeMap={single:'单选',multiple:'多选',judge:'判断',blank:'填空'};
        questions.forEach((q,index)=>{
            list.innerHTML+=`<tr><td>${index+1}</td><td>${typeMap[q.type]}</td><td>${q.title}</td><td><button class="edit-btn" onclick="editQuestion(${index})">编辑</button><button class="del-btn" onclick="showModal('确定删除?',true,'deleteQuestion(${index})')">删除</button></td></tr>`;
        });
    }
    function saveQuestion(){
        let title = document.getElementById('qTitle').value;
        if(!title){ showModal('请输入题干!'); return; }
        let q={
            type:document.getElementById('qType').value,title:title,
            optionA:document.getElementById('qOptionA').value,optionB:document.getElementById('qOptionB').value,
            optionC:document.getElementById('qOptionC').value,optionD:document.getElementById('qOptionD').value,
            answer:document.getElementById('qAnswer').value,analysis:document.getElementById('qAnalysis').value
        };
        let id=document.getElementById('qId').value;
        id?questions[id]=q:questions.push(q);
        localStorage.setItem('examQuestions',JSON.stringify(questions));
        showModal('保存成功!');clearForm();renderList();
    }
    function editQuestion(index){
        let q=questions[index];
        document.getElementById('qId').value=index;
        document.getElementById('qType').value=q.type;
        document.getElementById('qTitle').value=q.title;
        document.getElementById('qOptionA').value=q.optionA;
        document.getElementById('qOptionB').value=q.optionB;
        document.getElementById('qOptionC').value=q.optionC;
        document.getElementById('qOptionD').value=q.optionD;
        document.getElementById('qAnswer').value=q.answer;
        document.getElementById('qAnalysis').value=q.analysis;
        toggleOptions();
    }
    function deleteQuestion(index){
        questions.splice(index,1);
        localStorage.setItem('examQuestions',JSON.stringify(questions));
        showModal('删除成功!');renderList();
    }
    function clearForm(){
        document.getElementById('qId').value='';
        document.getElementById('qTitle').value='';
        document.getElementById('qOptionA').value='';
        document.getElementById('qOptionB').value='';
        document.getElementById('qOptionC').value='';
        document.getElementById('qOptionD').value='';
        document.getElementById('qAnswer').value='';
        document.getElementById('qAnalysis').value='';
        toggleOptions();
    }
</script>
</body>
</html>

4.登录 login.html 点击查看代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>系统登录</title>
    <style>
        *{margin:0;padding:0;box-sizing:border-box;font-family:"Microsoft YaHei"}
        body{
            background:#fdfbf7;
            display:flex;
            justify-content:center;
            align-items:center;
            height:100vh;
            overflow:hidden;
            position:relative;
        }
        /* 樱花画布 */
        #sakura{
            position:fixed;
            top:0;
            left:0;
            width:100%;
            height:100%;
            pointer-events:none;
            z-index:1;
        }
        .login-box{
            width:400px;
            background:rgba(255,255,255,0.95);
            padding:40px;
            border-radius:15px;
            box-shadow:0 0 20px rgba(168,106,140,0.2);
            border:2px solid #e6c6d8;
            position:relative;
            z-index:2;
            animation: glow 2s ease-in-out infinite alternate;
        }
        @keyframes glow {
            from{box-shadow:0 0 20px rgba(212,136,178,0.2);}
            to{box-shadow:0 0 35px rgba(212,136,178,0.35);}
        }
        .title{
            text-align:center;
            color:#a86a8c;
            margin-bottom:30px;
            font-size:24px;
        }
        .form-item{margin-bottom:20px;}
        .form-item label{
            display:block;
            margin-bottom:8px;
            color:#666;
        }
        .form-item input,.form-item select{
            width:100%;
            padding:12px;
            border:1px solid #ddd;
            border-radius:8px;
            transition:0.3s;
        }
        .form-item input:focus{
            border-color:#e6c6d8;
            box-shadow:0 0 8px rgba(212,136,178,0.2);
            outline:none;
        }
        .login-btn{
            width:100%;
            padding:12px;
            background:linear-gradient(45deg,#a86a8c,#d488b2);
            color:#fff;
            border:none;
            border-radius:8px;
            font-size:16px;
            cursor:pointer;
            margin-top:10px;
            transition:0.3s;
        }
        .login-btn:hover{transform:scale(1.03);}
        .tips{
            text-align:center;
            margin-top:15px;
            color:#999;
            font-size:14px;
        }

        /* 美化弹窗 */
        .modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.3);display:flex;justify-content:center;align-items:center;z-index:999;opacity:0;visibility:hidden;transition:0.3s;}
        .modal.show{opacity:1;visibility:visible;}
        .modal-box{width:320px;background:#fff;border-radius:12px;padding:25px;text-align:center;border:2px solid #e6c6d8;box-shadow:0 5px 15px rgba(0,0,0,0.1);}
        .modal-title{font-size:18px;color:#a86a8c;margin-bottom:15px;}
        .modal-btn{padding:8px 25px;background:linear-gradient(45deg,#a86a8c,#d488b2);color:#fff;border:none;border-radius:6px;margin-top:15px;cursor:pointer;}
    </style>
</head>
<body>
<!-- 樱花画布 -->
<canvas id="sakura"></canvas>

<div class="login-box">
    <h2 class="title"> 在线考试系统登录 </h2>
    <div class="form-item">
        <label>角色</label>
        <select id="role">
            <option value="admin">管理员</option>
            <option value="student">学生</option>
        </select>
    </div>
    <div class="form-item">
        <label>账号</label>
        <input type="text" id="username" placeholder="请输入账号">
    </div>
    <div class="form-item">
        <label>密码</label>
        <input type="password" id="password" placeholder="请输入密码">
    </div>
    <button class="login-btn" onclick="login()">登录</button>
    <div class="tips">管理员:admin/123456  学生:student/123456</div>
</div>

<!-- 美化弹窗 -->
<div class="modal" id="modal">
    <div class="modal-box">
        <div class="modal-title" id="modalTitle"></div>
        <button class="modal-btn" onclick="closeModal()">确定</button>
    </div>
</div>

<script>
    // 樱花飘落动效
    const canvas = document.getElementById('sakura');
    const ctx = canvas.getContext('2d');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    class Sakura {
        constructor(){
            this.x = Math.random() * canvas.width;
            this.y = Math.random() * -canvas.height;
            this.r = Math.random() * 10 + 5;
            this.speed = Math.random() * 1 + 0.5;
            this.wind = Math.random() * 1 - 0.5;
            this.color = '#e6c6d8';
        }
        draw(){
            ctx.beginPath();
            ctx.arc(this.x,this.y,this.r,0,Math.PI*2);
            ctx.fillStyle = this.color;
            ctx.fill();
        }
        update(){
            this.y += this.speed;
            this.x += this.wind;
            if(this.y > canvas.height){
                this.y = -10;
                this.x = Math.random() * canvas.width;
            }
        }
    }

    const sakuras = Array(50).fill().map(()=>new Sakura());
    function animate(){
        ctx.clearRect(0,0,canvas.width,canvas.height);
        sakuras.forEach(s=>{s.draw();s.update();});
        requestAnimationFrame(animate);
    }
    animate();

    window.addEventListener('resize',()=>{
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
    });

    // 弹窗控制
    function showModal(title){
        document.getElementById('modalTitle').innerText = title;
        document.getElementById('modal').classList.add('show');
    }
    function closeModal(){
        document.getElementById('modal').classList.remove('show');
    }

    function login(){
        let role = document.getElementById('role').value;
        let username = document.getElementById('username').value;
        let password = document.getElementById('password').value;

        if(!username || !password){
            showModal('请输入账号密码!');
            return;
        }

        if(role === 'admin' && username === 'admin' && password === '123456'){
            localStorage.setItem('userRole','admin');
            showModal('管理员登录成功!');
            setTimeout(()=>{location.href='admin.html'},1500);
        }
        else if(role === 'student' && username === 'student' && password === '123456'){
            localStorage.setItem('userRole','student');
            showModal('学生登录成功!');
            setTimeout(()=>{location.href='exam.html'},1500);
        }
        else{
            showModal('账号或密码错误!');
        }
    }
</script>
</body>
</html>

5.考试结果 result.html 点击查看代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>考试成绩</title>
    <style>
        *{margin:0;padding:0;box-sizing:border-box;font-family:"Microsoft YaHei"}
        body{background:#fdfbf7}
        .header{text-align:right;padding:15px;background:#fff;border-radius:10px;margin-bottom:15px;}
        .logout{padding:6px 15px;background:linear-gradient(45deg,#c45b65,#e68a9c);color:#fff;border:none;border-radius:6px;cursor:pointer;transition:0.3s;}
        .logout:hover{transform:scale(1.05);}
        .container{width:900px;margin:0 auto;padding:30px;background:rgba(255,255,255,.95);border-radius:15px;box-shadow:0 0 15px rgba(0,0,0,.1);border:2px solid #e6c6d8}
        .title{text-align:center;color:#a86a8c;margin-bottom:20px}
        .score{font-size:26px;color:#27ae60;margin:20px 0;text-align:center;font-weight:bold}

        /* 正确题目样式 */
        .correct-item{
            margin:15px 0;
            padding:20px;
            border:1px solid #27ae60;
            border-radius:10px;
            background:#f8fff8;
        }
        /* 错误题目样式 */
        .wrong-item{
            margin:15px 0;
            padding:20px;
            border:1px solid #c45b65;
            border-radius:10px;
            background:#fff8fc;
        }
        .analysis{
            margin-top:10px;
            padding:10px;
            background:#f9f9f9;
            color:#666;
            border-radius:6px;
        }
        .correct-text{
            color:#27ae60;
            font-weight:bold;
        }
        .wrong-text{
            color:#c45b65;
            font-weight:bold;
        }
        .section-title{
            color:#a86a8c;
            margin:25px 0 15px 0;
            padding-bottom:5px;
            border-bottom:2px solid #e6c6d8;
            font-size:20px;
            font-weight:bold;
        }
        .modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.3);display:flex;justify-content:center;align-items:center;z-index:999;opacity:0;visibility:hidden;transition:0.3s;}
        .modal.show{opacity:1;visibility:visible;}
        .modal-box{width:320px;background:#fff;border-radius:12px;padding:25px;text-align:center;border:2px solid #e6c6d8;box-shadow:0 5px 15px rgba(0,0,0,.1);}
        .modal-title{font-size:18px;color:#a86a8c;margin-bottom:15px;}
        .modal-btn{padding:8px 25px;background:linear-gradient(45deg,#a86a8c,#d488b2);color:#fff;border:none;border-radius:6px;cursor:pointer;margin-top:15px;}
        .btn-back{padding:12px 30px;background:linear-gradient(45deg,#a86a8c,#d488b2);color:#fff;border:none;border-radius:8px;cursor:pointer;display:block;margin:20px auto;}
    </style>
</head>
<body>
<div class="header">
    <button class="logout" onclick="logout()">退出登录</button>
</div>
<div class="container">
    <h1 class="title"> 考试成绩报告 </h1>
    <div class="score">您的得分:<span id="finalScore">0</span>/100</div>
    <div id="allQuestionsList"></div>
    <button class="btn-back" onclick="location.href='login.html'">返回登录</button>
</div>
<div class="modal" id="modal">
    <div class="modal-box">
        <div class="modal-title" id="modalTitle"></div>
        <button class="modal-btn" onclick="closeModal()">确定</button>
    </div>
</div>
<script>
    // 权限校验
    if(!localStorage.getItem('userRole') || localStorage.getItem('userRole') !== 'student'){
        showModal('无权限访问成绩页面!');
        setTimeout(()=>{location.href='login.html'},1500);
    }

    // 弹窗函数
    function showModal(title){
        document.getElementById('modalTitle').innerText = title;
        document.getElementById('modal').classList.add('show');
    }
    function closeModal(){
        document.getElementById('modal').classList.remove('show');
    }

    // 退出登录
    function logout(){
        localStorage.removeItem('userRole');
        showModal('退出成功!');
        setTimeout(()=>{location.href='login.html'},1000);
    }

    // 核心数据
    let questions = JSON.parse(localStorage.getItem('examQuestions')) || [];
    let userAnswersStr = sessionStorage.getItem("userAnswers");
    let userAnswers = userAnswersStr ? userAnswersStr.split(';') : [];
    let totalScore = 0;

    // 按题型分组所有题目
    let allByType = {
        single: [],
        multiple: [],
        judge: [],
        blank: []
    };

    // 分值配置
    const scoreConfig = {
        single: 4,
        multiple: 6,
        judge: 2,
        blank: 4
    };

    // 题型标题
    const typeNames = {
        single: '一、单选题(每题4分)',
        multiple: '二、多选题(每题6分)',
        judge: '三、判断题(每题2分)',
        blank: '四、填空题(每题4分)'
    };

    window.onload = () => {
        if (questions.length === 0) {
            showModal('暂无题目数据!');
            return;
        }
        // 批改所有题目并分组
        gradeAllQuestions();
        // 渲染全部题目
        renderAllQuestions();
    };

    // 批改所有题目(正确+错误)
    function gradeAllQuestions() {
        questions.forEach((q, index) => {
            let userAnswer = userAnswers[index] || '未作答';
            let isCorrect = false;

            // 判断对错
            switch(q.type) {
                case 'single':
                case 'judge':
                    isCorrect = userAnswer === q.answer;
                    break;
                case 'multiple':
                    let userArr = (userAnswer || '').split(',').filter(i => i).sort();
                    let correctArr = q.answer.split(',').sort();
                    isCorrect = JSON.stringify(userArr) === JSON.stringify(correctArr);
                    break;
                case 'blank':
                    isCorrect = userAnswer.trim().toLowerCase() === q.answer.trim().toLowerCase();
                    break;
            }

            // 计算总分
            if (isCorrect) {
                totalScore += scoreConfig[q.type];
            }

            // 加入对应题型分组
            allByType[q.type].push({
                idx: index + 1,
                question: q,
                userAns: userAnswer,
                isCorrect: isCorrect
            });
        });
    }

    // 渲染所有题目(正确+错误)
    function renderAllQuestions() {
        document.getElementById('finalScore').textContent = totalScore;
        let container = document.getElementById('allQuestionsList');
        let html = '';

        // 遍历所有题型
        for (let type in allByType) {
            let qList = allByType[type];
            if (qList.length === 0) continue;

            // 题型标题
            html += `<h3 class="section-title">${typeNames[type]}</h3>`;

            // 渲染该题型所有题目
            qList.forEach(item => {
                let q = item.question;
                let point = scoreConfig[type];
                let isCorr = item.isCorrect;
                let userAns = item.userAns;
                let corrAns = q.answer;

                // 正确/错误样式
                let itemClass = isCorr ? 'correct-item' : 'wrong-item';
                let tipText = isCorr ? '<span class="correct-text"> 回答正确</span>' : '<span class="wrong-text"> 回答错误</span>';

                html += `
                    <div class="${itemClass}">
                        <p><strong>第${item.idx}题(${point}分):${q.title}</strong></p>
                        <p>🧩 你的答案:${userAns}</p>
                        <p> 正确答案:${corrAns}</p>
                        <p>${tipText}</p>
                        ${!isCorr ? `<div class="analysis"> 解析:${q.analysis}</div>` : ''}
                    </div>
                `;
            });
        }

        container.innerHTML = html;
    }
</script>
</body>
</html>

6.功能逻辑实现 QuestionServlet.java 点击查看代码

package com.exam.servlet;

import com.exam.entity.Question;
import com.exam.util.DBUtil;
import com.google.gson.Gson;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@WebServlet("/question")
public class QuestionServlet extends HttpServlet {
    private final Gson gson = new Gson();

    // 统一处理跨域、编码
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setContentType("application/json;charset=UTF-8");
        request.setCharacterEncoding("UTF-8");
        super.service(request, response);
    }

    // GET请求:获取题目列表
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String action = request.getParameter("action");
        if ("list".equals(action)) {
            getQuestionList(response);
        }
    }

    // POST请求:增删改查+判分
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String action = request.getParameter("action");
        switch (action) {
            case "add": addQuestion(request, response); break;
            case "update": updateQuestion(request, response); break;
            case "delete": deleteQuestion(request, response); break;
            case "correct": correctExam(request, response); break;
            default: response.getWriter().write("error: invalid action");
        }
    }

    // 1. 获取所有题目
    private void getQuestionList(HttpServletResponse response) throws IOException {
        List<Question> list = new ArrayList<>();
        Connection conn = DBUtil.getConnection();
        String sql = "SELECT * FROM question";
        try {
            PreparedStatement pstmt = conn.prepareStatement(sql);
            ResultSet rs = pstmt.executeQuery();
            while (rs.next()) {
                Question q = new Question();
                q.setId(rs.getInt("id"));
                q.setType(rs.getString("type"));
                q.setTitle(rs.getString("title"));
                q.setOptionA(rs.getString("option_a"));
                q.setOptionB(rs.getString("option_b"));
                q.setOptionC(rs.getString("option_c"));
                q.setOptionD(rs.getString("option_d"));
                q.setAnswer(rs.getString("answer"));
                q.setAnalysis(rs.getString("analysis"));
                list.add(q);
            }
            response.getWriter().write(gson.toJson(list));
        } catch (Exception e) {
            e.printStackTrace();
            response.getWriter().write("error: " + e.getMessage());
        } finally {
            DBUtil.close(conn, null, null);
        }
    }

    // 2. 新增题目
    private void addQuestion(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String type = request.getParameter("type");
        String title = request.getParameter("title");
        String optionA = request.getParameter("optionA");
        String optionB = request.getParameter("optionB");
        String optionC = request.getParameter("optionC");
        String optionD = request.getParameter("optionD");
        String answer = request.getParameter("answer");
        String analysis = request.getParameter("analysis");

        Connection conn = DBUtil.getConnection();
        String sql = "INSERT INTO question(type,title,option_a,option_b,option_c,option_d,answer,analysis) VALUES(?,?,?,?,?,?,?,?)";
        try {
            PreparedStatement pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, type);
            pstmt.setString(2, title);
            pstmt.setString(3, optionA);
            pstmt.setString(4, optionB);
            pstmt.setString(5, optionC);
            pstmt.setString(6, optionD);
            pstmt.setString(7, answer);
            pstmt.setString(8, analysis);
            pstmt.executeUpdate();
            response.getWriter().write("success");
        } catch (Exception e) {
            e.printStackTrace();
            response.getWriter().write("fail: " + e.getMessage());
        } finally {
            DBUtil.close(conn, null, null);
        }
    }

    // 3. 修改题目
    private void updateQuestion(HttpServletRequest request, HttpServletResponse response) throws IOException {
        Integer id = Integer.parseInt(request.getParameter("id"));
        String type = request.getParameter("type");
        String title = request.getParameter("title");
        String optionA = request.getParameter("optionA");
        String optionB = request.getParameter("optionB");
        String optionC = request.getParameter("optionC");
        String optionD = request.getParameter("optionD");
        String answer = request.getParameter("answer");
        String analysis = request.getParameter("analysis");

        Connection conn = DBUtil.getConnection();
        String sql = "UPDATE question SET type=?,title=?,option_a=?,option_b=?,option_c=?,option_d=?,answer=?,analysis=? WHERE id=?";
        try {
            PreparedStatement pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, type);
            pstmt.setString(2, title);
            pstmt.setString(3, optionA);
            pstmt.setString(4, optionB);
            pstmt.setString(5, optionC);
            pstmt.setString(6, optionD);
            pstmt.setString(7, answer);
            pstmt.setString(8, analysis);
            pstmt.setInt(9, id);
            pstmt.executeUpdate();
            response.getWriter().write("success");
        } catch (Exception e) {
            e.printStackTrace();
            response.getWriter().write("fail: " + e.getMessage());
        } finally {
            DBUtil.close(conn, null, null);
        }
    }

    // 4. 删除题目
    private void deleteQuestion(HttpServletRequest request, HttpServletResponse response) throws IOException {
        Integer id = Integer.parseInt(request.getParameter("id"));
        Connection conn = DBUtil.getConnection();
        String sql = "DELETE FROM question WHERE id=?";
        try {
            PreparedStatement pstmt = conn.prepareStatement(sql);
            pstmt.setInt(1, id);
            pstmt.executeUpdate();
            response.getWriter().write("success");
        } catch (Exception e) {
            e.printStackTrace();
            response.getWriter().write("fail: " + e.getMessage());
        } finally {
            DBUtil.close(conn, null, null);
        }
    }

    // 5. 自动判分(已修复map未定义bug)
    private void correctExam(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String userAnswers = request.getParameter("userAnswers");
        List<Question> questions = getQuestionList();
        int score = 0;
        List<Question> wrongList = new ArrayList<>();

        // 修复:初始化map
        Map<String, Object> resultMap = new HashMap<>();

        String[] answerArr = userAnswers.split(";");
        for (int i = 0; i < questions.size(); i++) {
            Question q = questions.get(i);
            String userAns = i < answerArr.length ? answerArr[i] : "";
            if (userAns.equals(q.getAnswer())) {
                score += 100 / questions.size();
            } else {
                wrongList.add(q);
            }
        }

        resultMap.put("score", score);
        resultMap.put("wrongList", wrongList);
        response.getWriter().write(gson.toJson(resultMap));
    }

    // 内部工具:获取题目列表
    private List<Question> getQuestionList() {
        List<Question> list = new ArrayList<>();
        Connection conn = DBUtil.getConnection();
        try {
            PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM question");
            ResultSet rs = pstmt.executeQuery();
            while (rs.next()) {
                Question q = new Question();
                q.setId(rs.getInt("id"));
                q.setType(rs.getString("type"));
                q.setTitle(rs.getString("title"));
                q.setAnswer(rs.getString("answer"));
                q.setAnalysis(rs.getString("analysis"));
                list.add(q);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(conn, null, null);
        }
        return list;
    }
}

文章摘自:https://www.cnblogs.com/2452925yu/p/19890225