【单机离线版】大学考试题库复习工具:前端离线Excel解析 + localStorage持久化 + Playwright

【单机离线版】大学考试题库复习工具:前端离线Excel解析 + localStorage持久化 + Playwright

摘要:本文分享一款使用CodeBuddy重写+测试,实现的纯HTML单文件实现的题库复习工具,深入解析如何利用 SheetJS 实现浏览器端 Excel 解析、基于 localStorage 的多题库数据持久化方案、智能字段映射算法、以及使用 Playwright 构建 60+ 测试用例的完整自动化测试体系。所有数据完全离线存储在浏览器本地,无需后端服务,开箱即用。


软件下载地址:https://download.csdn.net/download/qq616491978/92928098

一、项目背景与设计理念

在日常学习和备考中,我经常需要从 Excel 题库中随机刷题。市面上的刷题工具要么需要注册登录,要么数据存储在服务端让人不放心。于是我决定自己动手,打造一个纯HTML单文件的题库复习工具。

核心设计目标

目标 说明
无依赖 纯 HTML/CSS/JS,浏览器打开即用,不依赖任何后端服务
离线优先 Excel 解析本地化,数据存储在浏览器 localStorage
多题型支持 单选、多选、判断题三种题型全覆盖
完整闭环 导入 → 答题 → 提交/评分 → 错题统计 → 历史回溯
可测试 配套 Playwright 自动化测试,覆盖 60+ 测试用例

技术选型

  • SheetJS(xlsx) :用于浏览器端解析 .xls / .xlsx 文件,采用 Apache 2.0 开源协议,可商用
  • localStorage:浏览器本地键值存储,实现多题库管理、答题记录持久化、页面刷新自动恢复
  • CSS Variables:主题色系统,便于后续扩展深色模式
  • Ollama API:可选的 AI 分析功能,连接本地大模型解析题目

二、整体架构

整个项目是单文件 HTML,结构清晰,自上而下分为:

复制代码
📁 题库复习工具.html
├── <style>    全局样式与 CSS Variables 主题系统
├── <body>
│   ├── #toolbar          顶部工具栏
│   ├── #main_container   主容器(Flex 布局)
│   │   ├── #welcome_page    欢迎页(含格式说明 + 导入区)
│   │   ├── #left_panel      答题区(题干 + 选项 + 操作按钮)
│   │   ├── #right_panel     题号导航面板(280px 侧边栏)
│   │   └── #ai_panel        AI分析面板(350px,可收起)
│   ├── #custom_modal     通用弹窗
│   ├── #history_modal    历史记录/打开题库弹窗
│   └── #toast            全局消息提示
└── <script>  全部业务逻辑(约 900 行)

Flex 三栏布局,左右分屏:

复制代码
┌──────────────────────────────────────────────────────┐
│  toolbar:导入 | 打开题库 | 关闭 | 提交 | 重新作答 | ...  │
├───────────────────────────┬──────────┬───────────────┤
│                           │          │               │
│   welcome_page /          │ 导航面板  │  AI面板       │
│   left_panel(答题区)     │  题号网格  │  (可收起)     │
│                           │          │               │
└───────────────────────────┴──────────┴───────────────┘

三、核心技术实现详解

3.1 Excel 离线解析 ------ SheetJS 集成

传统做法是将 Excel 上传到后端解析,但我们通过 SheetJS 在浏览器中直接读取文件二进制流并解析。

本地化引用

首先将 SheetJS 从 CDN 迁移到本地 lib/ 目录,实现完全离线运行:

html 复制代码
<!-- 原始 CDN 引用 -->
<script src="https://cdn.sheetjs.com/xlsx-0.20.1/package/dist/xlsx.full.min.js"></script>

<!-- 改为本地引用,完全离线 -->
<script src="lib/xlsx.full.min.js"></script>
解析流程
javascript 复制代码
function handleFileImport(event) {
    const file = event.target.files[0];
    const reader = new FileReader();
    reader.onload = function(e) {
        // 1. 读取二进制数组
        const data = new Uint8Array(e.target.result);
        // 2. SheetJS 解析 Excel
        const workbook = XLSX.read(data, { type: 'array' });
        const sheetName = workbook.SheetNames[0];
        const sheet = workbook.Sheets[sheetName];
        // 3. 转为 JSON 行数据(header:1 保留表头)
        const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' });

        if (rows.length < 2) {
            showModal('导入失败', 'Excel文件至少需要包含表头和一行数据。');
            return;
        }

        // 4. 第一行为表头,后续为数据行
        const headers = rows[0].map(h => String(h).trim());
        const result = {};
        for (let i = 1; i < rows.length; i++) {
            const rowData = {};
            for (let j = 0; j < headers.length; j++) {
                rowData[headers[j]] = rows[i][j] !== undefined ? rows[i][j] : '';
            }
            const rowId = String(rows[i][0] !== undefined ? rows[i][0] : i);
            result[rowId] = rowData;
        }

        // 5. 保存到 localStorage 并初始化
        ti_ku = result;
        currentBankId = file.name.replace(/\.(xls|xlsx)$/i, '') 
            + '-' + new Date().toISOString().slice(0, 19).replace(/:/g, '-');
        saveBankToLocal(currentBankId, ti_ku);
        initBank(true);
        showToast('题库导入成功!共 ' + Object.keys(ti_ku).length + ' 道题');
    };
    reader.readAsArrayBuffer(file);
}

关键设计点

  • 使用 XLSX.utils.sheet_to_json 配合 header: 1 将整个工作表转为二维数组,第一行为表头
  • 题库 ID 由文件名 + 时间戳组成,确保每次导入都生成唯一标识,支持重复导入同一文件
  • 解析完成后直接写入 localStorage 持久化

3.2 智能字段自动检测算法

Excel 的表头字段名可能存在变体(如"试题""题干""题目"),手工规定字段名太死板。我设计了一个启发式检测算法:

javascript 复制代码
function detectFields(keys) {
    F = { stem: null, type: null, ans: null, remark: null,
          optA: null, optB: null, optC: null,
          optD: null, optE: null, optF: null };

    for (let k of keys) {
        let ks = String(k).trim();

        // 题干字段:支持"试题""题干""题目""问题"及包含关键词
        if (!F.stem && (ks === "试题" || ks === "题干" || ks === "题目" 
                     || ks.includes("试题") || ks.includes("题干")))
            F.stem = k;

        // 题型字段:支持"题型""题目类型""类型"
        if (!F.type && (ks === "题型" || ks.includes("题型") || ks.includes("类型")))
            F.type = k;

        // 答案字段
        if (!F.ans && (ks === "答案" || ks === "参考答案" || ks === "正确答案"))
            F.ans = k;

        // 选项字段:支持"答案A"/"选项A"等
        if (!F.optA && (ks === "答案A" || ks.toUpperCase() === "A")) F.optA = k;
        if (!F.optB && (ks === "答案B" || ks.toUpperCase() === "B")) F.optB = k;
        // ... C/D/E/F 同理
    }

    // 兜底:仍未找到题干,取第一个非保留字段
    if (!F.stem) {
        let reserved = new Set([F.type, F.ans, F.remark, ...]);
        for (let k of keys) {
            if (!reserved.has(k)) { F.stem = k; break; }
        }
    }
}

设计思路

  • 按优先级匹配:先精确匹配,再模糊匹配(includes),最后兜底取第一个非保留字段
  • 大小写不敏感处理选项字段(ku = ks.toUpperCase()
  • 字段映射结果全局存储到 F 对象中,后续所有渲染和逻辑统一通过 F.stemF.ans 等访问

3.3 多选题 Toggle 交互机制

单选题和判断题的行为是互斥选择,多选题则需要支持勾选/取消勾选。核心逻辑在 selectOption 中统一处理:

javascript 复制代码
function selectOption(opt) {
    if (!user_choose_number || !ti_ku) return;
    let idx = ti_mu_keys.indexOf(user_choose_number);
    if (idx !== -1 && if_answer[idx] === 1) return; // 已提交不可修改

    let current = userAnswers[user_choose_number] || '';

    if (isMultiChoice(user_choose_number)) {
        // 多选模式:Toggle
        if (current.includes(opt)) {
            current = current.replace(opt, '');       // 取消选择
        } else {
            let letters = current.split('').filter(c => c >= 'A' && c <= 'F');
            letters.push(opt);
            letters.sort();                           // 字母排序保持一致性
            current = letters.join('');
        }
    } else {
        // 单选模式:直接替换
        current = opt;
    }

    // 存储用户答案
    if (current === '') {
        delete userAnswers[user_choose_number];
    } else {
        userAnswers[user_choose_number] = current;
    }

    // 刷新 UI 样式
    ['A','B','C','D','E','F'].forEach(o => {
        let el = $('opt_' + o);
        if (el) el.classList.toggle('selected', current.includes(o));
    });
}

关键技巧

  • 多选题答案以字母串存储(如 "ABC"),toggle 后始终保持字母排序,避免 CBAABC 判为不匹配
  • 已提交题目通过 if_answer[idx] === 1 加锁,防止修改
  • 取消全部选择时删除 key(delete userAnswers[xh]),而非保留空串,利于统计判断

扩展阅读:上面的处理方式是典型的状态管理前置于 UI 渲染 模式 ------ 先更新数据(userAnswers),再根据数据驱动 UI 更新。这比直接操作 DOM 类名更可靠,也是 React/Vue 等框架的核心思想。


3.4 localStorage 多题库数据持久化方案

这是整个项目中最复杂的部分。需要管理的数据包括:

数据 Key格式 说明
题库列表 quiz_bank_list JSON数组,存储所有题库ID
题库数据 quiz_banks JSON对象,key→题库内容
答题记录 quiz_records_{bankId} 每个题库独立的记录数组,最多50条
恢复标记 quiz_last_bank 最后一次打开的题库ID
题库数据层
javascript 复制代码
function saveBankToLocal(id, data) {
    const banks = JSON.parse(localStorage.getItem('quiz_banks') || '{}');
    banks[id] = data;
    localStorage.setItem('quiz_banks', JSON.stringify(banks));
    
    const bankList = JSON.parse(localStorage.getItem('quiz_bank_list') || '[]');
    if (!bankList.includes(id)) bankList.push(id);
    localStorage.setItem('quiz_bank_list', JSON.stringify(bankList));
}
答题记录层
javascript 复制代码
function saveRecordToLocal(bankId, record) {
    const key = 'quiz_records_' + bankId;
    const records = JSON.parse(localStorage.getItem(key) || '[]');
    records.push({
        time: new Date().toISOString(),
        data: record
    });
    // 最多保留50条,防止存储溢出
    if (records.length > 50) records.splice(0, records.length - 50);
    localStorage.setItem(key, JSON.stringify(records));
}
页面刷新自动恢复
javascript 复制代码
// 初始化时检查
(function() {
    let lastBank = localStorage.getItem('quiz_last_bank');
    if (lastBank) {
        let data = loadBankFromLocal(lastBank);
        if (data) {
            ti_ku = data;
            currentBankId = lastBank;
            initBank();  // initBank() 会调用 loadLastProgress() 恢复答题进度
        }
    }
})();

// 关闭页面时保存标记
window.addEventListener('beforeunload', function() {
    if (currentBankId) {
        localStorage.setItem('quiz_last_bank', currentBankId);
    }
});

设计要点

  1. 打开题库 ≠ 恢复进度 :通过 initBank(skipLoadProgress) 参数区分:

    • 页面刷新自动恢复时 initBank() → 调用 loadLastProgress() 恢复进度
    • 用户手动打开题库时 initBank(true) → 跳过恢复,提供干净状态
  2. 重新作答保留历史记录 :重要设计决策 ------ reloadAll() 只清空内存状态(fen_shu/if_answer/userAnswers),不清除 localStorage 中的历史答题记录。这样用户既能重新开始答题,又能随时查看历史记录。这条设计是通过 Playwright 测试验证后才最终确定的。

  3. 关闭题库时清除恢复标记

javascript 复制代码
function closeBank() {
    // ... 状态重置
    localStorage.removeItem('quiz_last_bank'); // 防止刷新后自动恢复
}

3.5 评分系统与状态机

答题状态通过三个并行数组管理:

javascript 复制代码
let fen_shu = [];      // 得分:1=对, 0=错, -1=未答
let if_answer = [];    // 是否提交:1=已提交, 0=未提交
let userAnswers = {};  // 用户答案对象:{ 题号: "A" 或 "AB" }

评分逻辑核心 ------ 字母排序后比较,避免 ABC vs CBA 的误判:

javascript 复制代码
function _gradeAll() {
    for (let i = 0; i < ti_mu_keys.length; i++) {
        let xh = ti_mu_keys[i];
        let q = ti_ku[xh];
        let realAns = String(q[F.ans] || '').trim();

        if (userAnswers[xh]) {
            if_answer[i] = 1;
            let userLetters = String(userAnswers[xh]).toUpperCase()
                .split('').filter(c => c >= 'A' && c <= 'F').sort().join('');
            let realLetters = realAns.toUpperCase()
                .split('').filter(c => c >= 'A' && c <= 'F').sort().join('');
            fen_shu[i] = (userLetters === realLetters) ? 1 : 0;
        } else {
            if_answer[i] = 0;
            fen_shu[i] = -1;
        }
    }
}

提交后的视觉反馈 ------ 正确选项标绿,错误选项标红,已判分题目锁定:

css 复制代码
.option-item.correct-mark {
    border-color: #52c41a;
    background: #f6ffed;
}
.option-item.wrong-mark {
    border-color: #ff4d4f;
    background: #fff1f0;
}
.option-item.locked {
    cursor: default;
    opacity: 0.8;
}

3.6 错题分析与统计

错题分析是一个很有实用价值的功能。它遍历当前题库的所有历史答题记录,统计每道题的出错次数:

javascript 复制代码
function icrAnalyze() {
    const records = getRecordsForBank(currentBankId);
    if (records.length === 0) {
        showModal('提示', '未找到历史答题记录,请至少提交一次答案。');
        return;
    }

    // 统计错题
    let errorCount = {};
    records.forEach(rec => {
        let data = rec.data;
        for (let i = 0; i < data.fen_shu.length; i++) {
            if (data.if_answer[i] === 1 && data.fen_shu[i] === 0) {
                // 答过但答错 → 计入错题
                let xh = ti_mu_keys[i];
                errorCount[xh] = (errorCount[xh] || 0) + 1;
            }
        }
    });

    // 按出错次数降序排列
    let errorItems = Object.entries(errorCount)
        .sort((a, b) => b[1] - a[1]);

    isIcrMode = true;
    icrList = errorItems.map(item => parseInt(item[0]));

    // 在导航面板标记错题并显示错误次数徽标
    for (let [xh, cnt] of errorItems) {
        let div = document.getElementById('nav_' + xh);
        div.classList.add('wrong');
        let badge = div.querySelector('.err-badge');
        badge.innerText = cnt;
        badge.style.display = 'flex';
    }

    // 自动跳转到第一道错题
    chooseNumber(String(icrList[0]));
}

错题模式下,导航面板会锁定非错题号,只能浏览错题:

javascript 复制代码
// 题号点击事件中
d.onclick = function() {
    if (isIcrMode && !icrList.includes(parseInt(ti_mu_keys[i]))) return;
    user_choose_number = ti_mu_keys[i];
    chooseNumber(ti_mu_keys[i]);
};

错题徽标的 CSS 实现 ------ 绝对定位在题号右上角,带白色阴影隔离:

css 复制代码
.err-badge {
    position: absolute;
    top: -7px; right: -7px;
    background: var(--danger);
    color: #fff;
    font-size: 10px;
    min-width: 16px; height: 16px;
    border-radius: 8px;
    box-shadow: 0 0 0 2px #fff;  /* 白色阴影隔离背景 */
}

3.7 键盘快捷键系统

通过全局键盘事件监听实现,并在输入框聚焦时自动跳过,避免与正常输入冲突:

javascript 复制代码
document.addEventListener('keydown', function(e) {
    if (!ti_ku) return;                                          // 无题库不响应
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;

    switch(e.key) {
        case 'ArrowLeft':  prevQuestion();     break;           // ← 上一题
        case 'ArrowRight': nextQuestion();     break;           // → 下一题
        case 'a': case 'A': selectOption('A'); break;           // A键选A
        case 'b': case 'B': selectOption('B'); break;
        case 'c': case 'C': selectOption('C'); break;
        case 'd': case 'D': selectOption('D'); break;
        case 'e': case 'E': selectOption('E'); break;
        case 'f': case 'F': selectOption('F'); break;
        case 'Enter': doSubmit(); break;                        // 回车提交
        case 's': case 'S':
            if (e.ctrlKey) { e.preventDefault(); saveProgress(); }
            break;                                              // Ctrl+S保存
    }
});

3.8 AI 分析功能(Ollama 本地大模型集成)

作为可选的增强功能,集成了 Ollama 本地大模型 API。用户只需安装 Ollama 并拉取模型(如 deepseek-r1:1.5b),即可调用本地大模型分析当前题目:

javascript 复制代码
async function aiAnalyze() {
    // 构建题目 Prompt
    let prompt = '以下是当前的题目信息:\n' + questionText + '\n\n' +
        '请作为一名专业的导师,根据上述信息,解释为什么选择该参考答案。\n' +
        '注意:千万不要自己去推断新的答案,必须严格以提供的【参考答案】为准!';

    let modelName = localStorage.getItem('ollama_model') || 'deepseek-r1:1.5b';
    let response = await fetch('http://127.0.0.1:11434/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            model: modelName,
            messages: [{ role: 'user', content: prompt }],
            stream: false,
            options: {
                num_ctx: $('cb_think').checked ? 4096 : 1024,  // 思考模式上下文
                num_predict: isDetailed ? 1000 : 500            // 详细模式Token数
            }
        })
    });

    let data = await response.json();
    output.innerHTML = data.message.content;
}

安全提醒 :此功能仅在本地网络运行,API 调用 127.0.0.1:11434,不会将数据传输到外部服务器。


四、CSS 架构亮点

4.1 CSS Variables 主题系统

使用 CSS 自定义属性定义完整的主题色彩系统,修改 :root 变量即可实现主题切换:

css 复制代码
:root {
    --primary: #1890ff;
    --primary-hover: #40a9ff;
    --primary-light: #e6f7ff;
    --success: #52c41a;
    --danger: #ff4d4f;
    --warning: #fa8c16;
    --bg: #f0f2f5;
    --card-bg: #ffffff;
    --text: #333333;
    --shadow: 0 1px 4px rgba(0,0,0,0.05);
    --radius: 8px;
}

4.2 题号导航状态体系

题号按钮有 5 种状态,通过 CSS 类名组合表达:

状态 CSS 类 外观 含义
默认 number_div 灰色背景 未操作
当前 + active 蓝色实心 正在查看
已答未提交 + answered 蓝色边框 + 圆点 已选择但未提交
答对 + correct 绿色背景 提交且正确
答错 + wrong 红色背景 提交但错误

每种状态都是 number_div 基础类的叠加,CSS 实现:

css 复制代码
.number_div { background: #f5f5f5; border: 1px solid #e8e8e8; }
.number_div.active { background: var(--primary); color: #fff; }
.number_div.answered { background: var(--primary-light); color: var(--primary); }
.number_div.correct { background: var(--success-bg); color: var(--success); }
.number_div.wrong { background: var(--danger-bg); color: var(--danger); }

/* 已回答但未提交的圆点标记 */
.number_div.answered::after {
    content: '';
    position: absolute;
    top: 2px; right: 2px;
    width: 6px; height: 6px;
    background: var(--primary);
    border-radius: 50%;
}
.number_div.correct::after,
.number_div.wrong::after { display: none; }  /* 已提交时隐藏圆点 */

4.3 Toast 消息组件

纯 CSS 实现的 Toast 消息,无侵入、不阻塞交互:

css 复制代码
#toast {
    position: fixed;
    top: 56px;
    left: 50%;
    transform: translateX(-50%);
    background: #333;
    color: #fff;
    padding: 8px 24px;
    border-radius: 20px;
    z-index: 10000;
    opacity: 0;
    transition: opacity 0.3s;
    pointer-events: none;
}
#toast.show { opacity: 1; }
javascript 复制代码
function showToast(msg) {
    let t = $('toast');
    t.textContent = msg;
    t.classList.add('show');
    clearTimeout(t._timer);
    t._timer = setTimeout(() => t.classList.remove('show'), 2000);
}

五、Playwright 自动化测试体系

5.1 为什么选择 Playwright

项目包含了 67 条结构化的测试用例,使用 Playwright 实现自动化。选择 Playwright 而非其他框架的原因:

  • 原生支持多浏览器(Chromium / Firefox / WebKit)
  • 自动等待机制,无需手动 sleep
  • 强大的 page.evaluate(),可以直接在浏览器上下文中执行 JS,验证 localStorage 状态
  • page.locator() API 语法简洁,支持 CSS/XPath/文本/正则多种选择器

5.2 测试架构设计

复制代码
tests/
├── 01-import.spec.js          # Excel 导入测试(5条)
├── 02-open-close.spec.js      # 打开/关闭题库测试(6条)
├── 03-answer.spec.js          # 答题交互测试(7条)
├── 04-submit.spec.js          # 提交与评分测试(6条)
├── 05-reload-save.spec.js     # 重新作答与保存进度(4条)
├── 06-show-answer.spec.js     # 显示答案测试(3条)
├── 07-navigation.spec.js      # 题目导航测试(4条)
├── 08-history.spec.js         # 历史记录测试(5条)
├── 09-error-analysis.spec.js  # 错题分析测试(5条)
├── 10-bank-manage.spec.js     # 题库管理测试(4条)
├── 11-ai-panel.spec.js        # AI面板测试(4条)
├── 12-shortcuts.spec.js       # 快捷键测试(6条)
├── 13-modal.spec.js           # 弹窗交互测试(2条)
└── 14-edge-cases.spec.js      # 边界与异常测试(6条)

5.3 localStorage 断言技巧

Playwright 的 page.evaluate() 可以在测试中直接访问浏览器端 JS 变量和 localStorage,这是测试这类纯前端项目最核心的手段:

javascript 复制代码
// 验证 localStorage 中记录被保留
const recordsKept = await page.evaluate(() => {
    const list = JSON.parse(localStorage.getItem('quiz_bank_list') || '[]');
    if (list.length === 0) return false;
    const id = list[0];
    const records = JSON.parse(localStorage.getItem('quiz_records_' + id) || '[]');
    return records.length > 0;
});
expect(recordsKept).toBe(true);

// 验证内存状态被清空
const stateCleared = await page.evaluate(() => {
    return fen_shu.every(f => f === -1) &&
           if_answer.every(f => f === 0) &&
           Object.keys(userAnswers).length === 0;
});
expect(stateCleared).toBe(true);

5.4 弹窗按钮串扰问题的回归测试

这是一个典型的回归测试案例。当"历史记录"弹窗和"打开题库"弹窗复用同一 DOM 结构时,容易发生底部按钮串扰:

javascript 复制代码
test('TC-045 历史记录弹窗与打开题库弹窗互不干扰', async ({ page }) => {
    // 1. 打开历史记录,确认有"关闭"+"清空全部记录"两个按钮
    await page.locator('.tb-btn', { hasText: '历史记录' }).click();
    const footerButtons1 = await page.locator('#history_modal .modal-footer button').count();
    expect(footerButtons1).toBe(2);

    // 2. 关闭
    await page.locator('#history_modal .modal-btn', { hasText: '关 闭' }).click();

    // 3. 打开题库列表,确认仅有"关闭"按钮
    await page.locator('.tb-btn', { hasText: '打开题库' }).click();
    const footerButtons2 = await page.locator('#history_modal .modal-footer button').count();
    expect(footerButtons2).toBe(1);

    // 4. 关闭后再次打开历史记录,按钮应恢复
    await page.locator('#history_modal .modal-btn', { hasText: '关 闭' }).click();
    await page.locator('.tb-btn', { hasText: '历史记录' }).click();
    const footerButtons3 = await page.locator('#history_modal .modal-footer button').count();
    expect(footerButtons3).toBe(2);
});

5.5 测试命令

bash 复制代码
# 运行全部测试
npx playwright test

# 有头模式(可视化调试)
npx playwright test --headed

# 查看 HTML 报告
npx playwright show-report

六、Excel 题库格式规范

为了让题目能被正确解析,Excel 文件需按以下格式准备:

序号 题型 试题 答案A 答案B 答案C 答案D 答案E 答案F 答案 备注
1 单选题 1GB等于多少MB? 1000 1024 124 1240 B
2 多选题 Word的功能包括? 文字录入 排版 绘画 动画 ABCD
3 判断题 U盘是外存储器。 正确 错误 A

规则总结

  • 单选题 :答案填单个字母,如 B
  • 多选题 :答案填字母组合,如 ABCD(不区分顺序)
  • 判断题 :正确填 A,错误填 B
  • 空值选项:没有的选项列留空即可,系统自动隐藏
  • 表头:支持变体识别(如"题干""选项A""参考答案"等)

七、可扩展的优化方向

  1. 深色模式 :CSS Variables 主题系统已为此做好铺垫,只需新增 [data-theme="dark"] 变量覆盖
  2. Service Worker + PWA:可封装为 PWA 离线应用,支持手机安装
  3. 多种导出格式:答题记录支持导出为 Excel/PDF
  4. Web Worker:大题库解析放到 Worker 线程,避免主线程阻塞
  5. IndexedDB 替代 localStorage:突破 5MB 存储上限,支持更大题库

八、总结

本文分享了纯 HTML 单文件题库复习工具的完整实现方案,核心亮点包括:

  1. SheetJS 浏览器端 Excel 解析 :无需后端,离线解析 .xls/.xlsx
  2. 智能字段映射算法:自动识别表头变体,兼容不同格式
  3. localStorage 多层数据架构:题库 → 答题记录 → 恢复标记,清晰分层
  4. 完整的答题状态机:未答/已答未提交/已提交正确/已提交错误四种状态流转
  5. 错题统计与分析:遍历历史记录统计出错频率
  6. 可选的本地 AI 集成:通过 Ollama 实现离线 AI 题目解析
  7. Playwright 自动化测试:67 条测试用例覆盖 14 个模块,包含回归验证

工具全程无需联网,数据完全存储在用户浏览器本地,安全可靠。如果你也在备考,或者对前端离线应用开发感兴趣,欢迎参考实现。


软件下载地址:https://download.csdn.net/download/qq616491978/92928098

项目技术栈 :HTML5 + CSS3 + JavaScript + SheetJS + Playwright

开源协议 :代码可自由参考使用,SheetJS 采用 Apache 2.0 协议可商用

运行方式 :浏览器直接打开 题库复习工具.html 即可

相关推荐
daols881 小时前
vxe-table 实现数据分组统计与表尾合计
前端·javascript·vue.js·vxe-table
向日的葵0061 小时前
Vue 函数定义、事件绑定与列表渲染精讲
前端·javascript·vue.js
神奇的代码在哪里1 小时前
【单机离线版】excel转json软件,纯HTML+JS零依赖实现Excel转JSON工具,一个index.html搞定所有转换!
html·json·excel·excel转json·xlsx转json·xls转json
神秘代码行者1 小时前
pnpm zip命令详解
前端·npm·pnpm
创实信息1 小时前
从安装到首次运行:GitHub Copilot CLI 新手完整上手指南
github·copilot·ai编程·ai助手
lpd_lt1 小时前
AI生成Spring Boot + Vue 3 + MySQL + MyBatis-Plus的项目实战
java·spring boot·vue·ai编程
Xpower 171 小时前
Codex 桌面端更新后 Chrome 插件和 Computer Use 不可用,怎么排查和修复
前端·人工智能·chrome·python·学习
lolo大魔王2 小时前
Gin 框架响应格式与 HTML 模板渲染完整实战教程
前端·html·gin
llz_1124 小时前
web-第二次课后作业
前端·后端·web