纯前端实现软考中级·软件设计师备考学习网站

纯前端实现软考中级·软件设计师备考学习网站(知识点 / 训练 / 模考 / 错题本 / 进度追踪,附完整源码)

> 一篇实战向的项目复盘:用纯 HTML + CSS + 原生 JS,零依赖、零构建、双击即可运行,实现一个功能完整的软考备考学习平台。包含六大模块、用户登录与 localStorage 数据持久化。文末附完整源码结构与关键代码。

一、前言

准备软考中级·软件设计师考试时,市面上的刷题工具要么需要联网注册,要么广告多、错题本不好用,知识点与题目往往割裂。于是萌生自己做一个的想法:

把知识点梳理、考点训练、模拟考试、错题巩固、进度追踪整合到一个页面;

纯前端、零依赖,双击 `index.html` 就能跑,离线可用;

用 `localStorage` 实现**多账户数据隔离,每个人的错题本、进度互不干扰;

题目基于公开大纲与高频考点原创编写,规避真题版权问题,同时保留备考训练价值。

本文记录整个实现过程与踩过的坑,适合想做练手项目的前端同学参考。

二、技术选型与设计思路

核心设计原则:数据与逻辑分离。题目、知识点、组卷配置作为独立数据文件挂到 `window`,业务逻辑层(store/ui/auth/app)只消费数据,便于后续扩充题库而不动逻辑代码。

三、项目结构

复制代码
clin/
├── index.html                       # 入口,双击即可在浏览器打开
├── css/
│   └── style.css                    # 完整响应式样式(桌面/平板/手机)
└── js/
    ├── data/
    │   ├── knowledge.js             # 12章知识点数据(约110个考点要点)
    │   ├── questions.js             # 55道原创模拟题(分章节/考点/难度)
    │   └── examPapers.js             # 3套模拟卷配置 + 高频考点 + 组卷函数
    ├── store.js                     # localStorage数据层(账户/进度/错题/模考)
    ├── ui.js                        # 通用UI(toast/modal/题卡渲染/计时器)
    ├── auth.js                      # 注册/登录视图与校验
    └── app.js                       # hash路由 + 六大模块视图渲染

`index.html` 用最朴素的方式按依赖顺序引入脚本:

html 复制代码
```html
<script src="js/data/knowledge.js"></script>
<script src="js/data/questions.js"></script>
<script src="js/data/examPapers.js"></script>
<script src="js/store.js"></script>
<script src="js/ui.js"></script>
<script src="js/auth.js"></script>
<script src="js/app.js"></script>
<script>document.addEventListener('DOMContentLoaded', function(){ App.start(); });</script>
```

四、数据层设计:localStorage 多账户持久化

备考数据天然按用户隔离------张三的错题本不能混进李四的进度。我用 键名加账户后缀 的方式分区存储:

sde_users:所有账户 `{ 用户名: { pwdHash, displayName, createdAt } }`

sde_session:当前登录用户名

sde_data_<用户名>:该用户的全部学习数据(progress / practiced / wrongBook / examRecords)

每个用户的数据结构:

javascript 复制代码
```js
function emptyData() {
  return {
    progress: {},        // { chapterId: { read, mastered, lastVisit } }
    practiced: {},       // { qid: { count, correct, level, lastTs } }
    wrongBook: [],       // [{ qid, chapter, topic, wrongCount, lastWrong }]
    examRecords: [],     // [{ paperId, title, total, correct, score, pass, details }]
    createdAt: Date.now(),
    lastActive: Date.now()
  };
}
```

注册逻辑(含用户名格式与密码长度校验):

javascript 复制代码
```js
register: function (username, password, displayName) {
  username = (username || '').trim();
  if (!username || !password) return { ok: false, msg: '用户名和密码不能为空' };
  if (!/^[\w@.-]{3,20}$/.test(username)) return { ok: false, msg: '用户名仅允许字母数字下划线及.@-,3-20位' };
  if (password.length < 4) return { ok: false, msg: '密码至少4位' };
  var users = getUsers();
  if (users[username]) return { ok: false, msg: '该用户名已存在' };
  users[username] = { pwdHash: hash(password), displayName: displayName || username, createdAt: Date.now() };
  setUsers(users);
  localStorage.setItem(K_SESSION, username);
  writeJSON(dataKey(username), emptyData());
  return { ok: true, msg: '注册成功' };
}
```

> 说明:密码用了一个简单的字符串哈希(非加密),仅适用于单机学习场景,生产环境务必换用真正的加密与服务端存储。

五、核心模块实现

5.1 知识点系统梳理(按大纲章节分类)

数据按官方大纲组织成 12 章,每章含若干考点要点:

javascript 复制代码
```js
window.KNOWLEDGE = [
  {
    id: 'ch01',
    title: '第1章 计算机组成与体系结构',
    summary: '数据表示、运算器与控制器、存储体系、总线与I/O、指令系统与流水线。',
    points: [
      { k: '数据的表示', v: '原码/反码/补码/移码...计算机中整数一律用补码表示...' },
      { k: '流水线技术', v: '流水线周期=最长子段时间。n条指令k级流水线时间=(k+n-1)×周期...' },
      // ...
    ]
  },
  // ch02 ~ ch12 ...
];
```

进入章节详情时自动标记"已读",并可拖动滑块设置掌握度,数据实时回写:

javascript 复制代码
```js
markChapterRead: function (chapterId) {
  var d = getData();
  d.progress[chapterId] = d.progress[chapterId] || { read: false, mastered: 0 };
  d.progress[chapterId].read = true;
  d.progress[chapterId].lastVisit = Date.now();
  saveData(null, d);
}
```

5.2 考点强化训练(难度分级 + 错题自动入库)

支持按章节 / 难度(★入门 / ★★中等 / ★★★进阶)/ 题型 / 题量筛选,洗牌后逐题作答。关键是**答错自动入错题本、答对自动移出**:

javascript 复制代码
```js
recordPractice: function (qid, chapter, topic, isCorrect, level) {
  var d = getData();
  var p = d.practiced[qid] || { count: 0, correct: 0, level: level };
  p.count++; if (isCorrect) p.correct++; p.level = level; p.lastTs = Date.now();
  d.practiced[qid] = p;

  if (!isCorrect) {
    var found = d.wrongBook.filter(function (w) { return w.qid === qid; })[0];
    if (found) { found.wrongCount++; found.lastWrong = Date.now(); }
    else d.wrongBook.push({ qid: qid, chapter: chapter, topic: topic, wrongCount: 1, lastWrong: Date.now() });
  } else {
    d.wrongBook = d.wrongBook.filter(function (w) { return w.qid !== qid; }); // 答对移出
  }
  saveData(null, d);
}
```

判分兼容单选 / 多选(多选需选项集合完全一致才算对):

javascript 复制代码
```js
checkAnswer: function (q, chosen) {
  var ans = q.answer instanceof Array ? q.answer.slice().sort() : [q.answer];
  var c = chosen.slice().sort();
  if (c.length !== ans.length) return false;
  for (var i = 0; i < c.length; i++) if (c[i] !== ans[i]) return false;
  return true;
}
```

5.3 真题模拟考试(计时 + 评分 + 题号跳转)

模考是最复杂的模块。组卷在运行时按配置从题库抽取并洗牌,避免数据冗余:

javascript 复制代码
```js
window.buildExamQuestions = function (paper) {
  var pool = (window.QUESTIONS || []).slice();
  var qs;
  if (paper.mode === 'all')      qs = pool.filter(function (q) { return q.type === 'single'; });
  else if (paper.mode === 'levels')   qs = pool.filter(function (q) { return paper.levels.indexOf(q.level) >= 0; });
  else if (paper.mode === 'chapters') qs = pool.filter(function (q) { return paper.chapters.indexOf(q.chapter) >= 0; });
  // 洗牌
  for (var i = qs.length - 1; i > 0; i--) {
    var j = Math.floor(Math.random() * (i + 1));
    var t = qs[i]; qs[i] = qs[j]; qs[j] = t;
  }
  return qs;
};
```

计时器是一个独立小工具,倒计时到 0 自动交卷:

javascript 复制代码
```js
Timer: function (seconds, onTick, onEnd) {
  var left = seconds, t = null, running = false;
  function fmt(s) { var m = Math.floor(s / 60), r = s % 60; return (m<10?'0':'')+m+':'+(r<10?'0':'')+r; }
  return {
    start: function () {
      if (running) return; running = true;
      t = setInterval(function () {
        left--; onTick && onTick(left, fmt(left));
        if (left <= 0) { clearInterval(t); running = false; onEnd && onEnd(); }
      }, 1000);
    },
    stop: function () { clearInterval(t); running = false; },
    fmt: fmt
  };
}
```

交卷判分并生成成绩记录(含每题作答明细,供后续回顾):

javascript 复制代码
```js
function submitExam(timeup) {
  var s = examState;
  s.timer && s.timer.stop();
  var correct = 0, details = [];
  s.qs.forEach(function (q) {
    var chosen = s.answers[q.id] || [];
    var ok = UI.checkAnswer(q, chosen);
    if (ok) correct++;
    details.push({ qid: q.id, chosen: chosen, correct: ok });
  });
  var total = s.qs.length;
  var score = Math.round(correct * 100 / total);
  var pass = (score / 100) >= s.paper.passLine;
  SDEStore.recordExam({
    id: 'exam_' + Date.now(), paperId: s.paper.id, title: s.paper.title,
    total: total, correct: correct, score: score, pass: pass,
    date: Date.now(), durationSec: Math.floor((Date.now() - s.start) / 1000), details: details
  });
  showExamResult(rec);
}
```

模考界面还做了**题号网格跳转面板**(已作答高亮、当前题高亮),方便快速定位未作答题目。

5.4 错题本管理

错题本读取时把题目数据与错题记录合并渲染:

javascript 复制代码
```js
getWrongQuestions: function () {
  var d = getData();
  var map = {};
  (window.QUESTIONS || []).forEach(function (q) { map[q.id] = q; });
  return d.wrongBook.map(function (w) {
    var q = map[w.qid];
    return q ? Object.assign({}, q, { _wrong: w }) : null;
  }).filter(Boolean);
}
```

重做答对后自动移出错题本,支持单题移出与一键清空。

5.5 学习进度追踪

进度页聚合多维度数据:章节阅读进度、掌握度、各章节练习正确率、模考成绩趋势。趋势用纯 CSS 柱状图呈现:

javascript 复制代码
```html
<div class="trend-chart">
  <div class="trend-col"><div class="trend-bar" style="height:80%"></div><span>80</span></div>
  ...
</div>
```

5.6 用户注册登录

登录卡片用 tab 切换登录/注册,表单校验失败用 toast 提示,成功后顶栏显示 `Hi, 昵称`。游客可浏览知识点,访问训练/模考/错题/进度时自动弹出登录卡片。

六、UI/UX 与响应式

配色用 CSS 变量统一管理,蓝色系主色符合学习平台信任感:

javascript 复制代码
```css
:root{
  --pri:#2563eb; --pri-d:#1d4ed8; --pri-l:#dbeafe;
  --suc:#16a34a; --dgr:#dc2626; --warn:#f59e0b;
  --bg:#f4f6fb; --card:#fff; --ink:#1f2937; --mut:#6b7280;
  --bd:#e5e7eb; --r:10px;
}
```

响应式断点适配三档:

javascript 复制代码
```css
@media (max-width:960px){ .stats-grid{grid-template-columns:repeat(2,1fr)} .dashboard-grid{grid-template-columns:1fr} }
@media (max-width:720px){
  .topbar-inner{flex-wrap:wrap;height:auto}
  .nav{order:3;width:100%;overflow-x:auto;flex-wrap:nowrap}  /* 手机导航横向滚动 */
  .nav-link{white-space:nowrap;padding:6px 10px}
}
```

题卡交互细节:选项点击高亮、提交后正确答案绿色、错误选项红色、解析可折叠展开,难度用星级 + 章节/题型/考点三标签直观区分。

七、关键踩坑:跨视图事件监听冲突

开发中遇到一个隐蔽 bug:**从"训练"页跳到"模考"页后,模考的"下一题"按钮点不动了。排查过程

最初各模块用 `document.addEventListener('click', function nav(){...})` 绑定翻页逻辑,并在命中后 `removeEventListener`。但问题是:

  1. 练习页绑了 `document` 级 `nav`,匹配 `data-act="next"`;

  2. 用户没走"下一题"正常出口,而是点了顶部导航直接跳到模考页;

  3. 旧的练习 `nav` 监听器**没被清除**,仍挂在 `document` 上;

  4. 在模考点"下一题",事件先命中旧练习 `nav`,它去操作 `practiceSession`(此时已为旧数据或 null),**抛异常**;

  5. 异常中断了事件传播,新的模考 `nav` 监听器收不到事件 → 按钮看起来"没反应"。

修复方案

引入**视图级监听器管理**:所有模块的临时点击监听改绑到主内容容器 `main` 上,并在路由切换时统一清理:

javascript 复制代码
```js
var _viewListeners = [];
function onMainClick(handler) {
  var fn = function (e) { try { handler(e); } catch (err) { console.error(err); UI.toast('操作异常: ' + err.message, 'error'); } };
  main.addEventListener('click', fn);
  _viewListeners.push(fn);
  return fn;
}
function clearViewListeners() {
  _viewListeners.forEach(function (fn) { main.removeEventListener('click', fn); });
  _viewListeners = [];
}

function route() {
  clearViewListeners();   // 切视图前先清理,避免跨视图残留
  // ... 解析 hash、渲染对应视图
}
```

各模块把原来的 `document.addEventListener('click', function nav(){...})` 统一替换为 `onMainClick(function nav(){...})`,并去掉手动的 `removeEventListener`(切视图时统一清理)。`onMainClick` 还包了 `try/catch`,即使某个处理器抛错也不会中断后续逻辑。

修复后重新用浏览器自动化回归验证,"下一题 / 上一题 / 题号跳转"全部正常。

> 经验:**全局事件委托一定要有清晰的生命周期管理**。用容器级监听 + 切视图清理,比满屏 `removeEventListener` 省心且不易漏。

八、验证效果

用本地静态服务器 + 浏览器自动化做了两轮回归,验证结果:

7 个 JS 文件全部通过 `node --check` 语法校验。

九、总结与扩展

本项目用纯前端技术栈实现了一个功能闭合的备考学习平台,核心收获:

  1. 零依赖的工程组织:数据/逻辑/视图分层,`<script src>` 顺序加载,file 协议直接跑,适合做学习类小工具的分发;

2.localStorage 多账户隔离:键名加后缀分区,简单有效;

  1. hash 路由 + 视图级事件管理:解决了多视图监听器残留的隐蔽 bug;

  2. 原创题库的备考价值:基于公开大纲与高频考点编写,既合规又能复现考点、题型、难度。

可继续扩展的方向

接入 IndexedDB 支持更大题库与图片题;

题库改为 JSON 异步加载(需 http 服务,或用 `fetch` + blob);

增加每日打卡、学习日历、薄弱章节智能推荐;

后端化:用 Node + 数据库替换 localStorage,实现真正的多端同步。


相关推荐
kyriewen1 小时前
我扒了 npm 三年数据:AI 没把依赖变胖,是把包变多了
前端·人工智能·程序员
smartpi_ai1 小时前
51 单片机写好了,大模型语音怎么接?17T 全驱动的现成方案、双板串口分工的边界与画板前的三项硬件确认
单片机·嵌入式硬件·学习
liqingdi4371 小时前
[SWPUCTF 2023 秋季新生赛]Easy SMC
服务器·前端·javascript
用户87359865635871 小时前
为什么 greedy 也可能出不同 token,MoE 为什么更严重
前端
Gizzap_Tech1 小时前
多语言官网如何兼顾统一与本地化?从页面生成到语言切换
前端
IT_陈寒1 小时前
Java缓存踩坑记:明明设置了过期时间,怎么还是读到脏数据?
前端·人工智能·后端
靠coding维持生计的potato1 小时前
使用 unified diff 为 pnpm 依赖编写可复现补丁
前端
Initialize-le2 小时前
systemd 服务起不来?五步排查法
前端·chrome
OpenTiny社区2 小时前
多会话 + 流式输出怎么落地?TinyRobot Kit 对话数据层深度解析
前端·vue.js