【单机离线版】大学考试题库复习工具:前端离线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.stem、F.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 后始终保持字母排序,避免CBA和ABC判为不匹配 - 已提交题目通过
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);
}
});
设计要点:
-
打开题库 ≠ 恢复进度 :通过
initBank(skipLoadProgress)参数区分:- 页面刷新自动恢复时
initBank()→ 调用loadLastProgress()恢复进度 - 用户手动打开题库时
initBank(true)→ 跳过恢复,提供干净状态
- 页面刷新自动恢复时
-
重新作答保留历史记录 :重要设计决策 ------
reloadAll()只清空内存状态(fen_shu/if_answer/userAnswers),不清除 localStorage 中的历史答题记录。这样用户既能重新开始答题,又能随时查看历史记录。这条设计是通过 Playwright 测试验证后才最终确定的。 -
关闭题库时清除恢复标记:
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""参考答案"等)
七、可扩展的优化方向
- 深色模式 :CSS Variables 主题系统已为此做好铺垫,只需新增
[data-theme="dark"]变量覆盖 - Service Worker + PWA:可封装为 PWA 离线应用,支持手机安装
- 多种导出格式:答题记录支持导出为 Excel/PDF
- Web Worker:大题库解析放到 Worker 线程,避免主线程阻塞
- IndexedDB 替代 localStorage:突破 5MB 存储上限,支持更大题库
八、总结
本文分享了纯 HTML 单文件题库复习工具的完整实现方案,核心亮点包括:
- SheetJS 浏览器端 Excel 解析 :无需后端,离线解析
.xls/.xlsx - 智能字段映射算法:自动识别表头变体,兼容不同格式
- localStorage 多层数据架构:题库 → 答题记录 → 恢复标记,清晰分层
- 完整的答题状态机:未答/已答未提交/已提交正确/已提交错误四种状态流转
- 错题统计与分析:遍历历史记录统计出错频率
- 可选的本地 AI 集成:通过 Ollama 实现离线 AI 题目解析
- Playwright 自动化测试:67 条测试用例覆盖 14 个模块,包含回归验证
工具全程无需联网,数据完全存储在用户浏览器本地,安全可靠。如果你也在备考,或者对前端离线应用开发感兴趣,欢迎参考实现。
软件下载地址:https://download.csdn.net/download/qq616491978/92928098
项目技术栈 :HTML5 + CSS3 + JavaScript + SheetJS + Playwright
开源协议 :代码可自由参考使用,SheetJS 采用 Apache 2.0 协议可商用
运行方式 :浏览器直接打开
题库复习工具.html即可