现场编程 个人记账本
| 作业所属课程 | 班级的链接 |
|---|---|
| 作业要求 | https://edu.cnblogs.com/campus/fzu/2026-01SoftwareEngineeringandSoftwareEngineeringPractice/homework/16781 |
| 作业的目标 | 开发一个个人记账本,用户可以对自己平时的收入和支出情况进行简单的记录,查看和统计 |
| 团队名称 | 门诊排队 |
| 团队成员名字-学号 | 102401431-丘南星、102401422-陈嘉俊、102401426-薛强龙、102402128-李韩远、102402129-梁洪睿、102402137-翁齐文、102402131-马天鸿、102402130-林晓捷、102401425-薛昌建、102401415-林豪、102401433-肖志渊、102401428-魏怡健 |
一、组员职责分工
我们团队共 12 人,为了高效协作,将项目拆分为“骨架与界面组”、“核心逻辑组”和“特色功能与工具组”三个大组。
| 姓名 | 负责组别 | 负责文件/模块 | 具体工作 |
|---|---|---|---|
| 李韩远 | 核心逻辑组 | chart.js |
用 ECharts(CDN引入),把分类数据渲染成漂亮的饼图。 |
| 薛昌建 | 骨架与界面组 | style.css |
编写全局样式,搞定全局颜色、排版、表格、按钮样式。 |
| 林豪 | 骨架与界面组 | form.html |
编写表单区块,包含日期选择、金额输入、类别下拉框、备注输入、提交按钮。 |
| 肖志渊 | 骨架与界面组 | list.html |
编写列表区块,用表格搭好数据展示区(日期、金额、类别、备注、操作删除按钮)。 |
| 梁洪睿 | 核心逻辑组 | storage.js |
封装 saveData() 和 getData(),使用浏览器的 localStorage 存取 JSON 数据。 |
| 翁齐文 | 核心逻辑组 | form.js |
监听提交按钮,获取用户输入,校验金额,调用 saveData() 保存数据。 |
| 马天鸿 | 核心逻辑组 | list.js |
读取数据动态渲染 HTML 表格,并实现点击“删除”按钮移除对应记录。 |
| 林晓捷 | 核心逻辑组 | stats.js |
编写算法算出:总收入、总支出、总结余,并动态更新到页面。 |
| 丘南星 | 骨架与界面组 | index.html |
编写主骨架,把所有人的 <div> 和 <script> 标签拼装起来,负责最终 Git 合并。 |
| 陈嘉俊 | 特色功能与工具组 | export.js |
编写导出按钮,点击后把当前的所有账本数据导出为 data.json 文件下载到本地。 |
| 魏怡健 | 特色功能与工具组 | filter.js |
编写“按类别筛选”的逻辑,用户选“饮食”,列表就只显示饮食的账。 |
| 薛强龙 | 特色功能与工具组 | utils.js |
编写工具函数(如日期格式化、防抖函数),并协助大家解决代码冲突。 |
二、程序运行环境
- 操作系统:Windows 11
- 浏览器:Edge
- 运行方式:直接双击打开
index.html(纯前端,无需安装依赖、无需后端服务) - 数据存储:浏览器
localStorage,刷新页面后数据保留 - 开发工具:[VS Code / 记事本 / 其他]
- Git 仓库:https://github.com/LinkWalker/Account-Book
三、软件运行截图和视频
主界面展示

记账表单展示

记录表展示

筛选功能展示

饼状图统计功能展示

删除功能展示

视频链接

(请使用微信扫码)
四、软件实现的关键代码
原生 JS + localStorage,8 个 IIFE 模块靠自定义事件通信,无框架、无构建。
1. 数据流:唯一写入口 + 事件广播
form.js ──saveData──▶ localStorage ──getData──▶ list / stats / chart / export
// index.html 末尾:唯一的粘合代码,模块之间从不直接调用
document.addEventListener('ledger:change', e =>
document.dispatchEvent(new CustomEvent('records:changed', { detail: e.detail })));
document.addEventListener('DOMContentLoaded', () =>
window.LedgerFilter.init(window.getData, window.renderList));
2. 金额全程整数分
// utils.js:元文本 → 整数分,纯整数运算,避开 0.1+0.2 的浮点误差
function moneyToCents(value) {
var text = String(value).trim();
// 只接受非负、最多两位小数,天然拒绝 1e3 / -5 / 1.005
if (!/^\d+(?:\.\d{1,2})?$/.test(text)) throw new RangeError('金额非法');
var p = text.split('.');
return Number(p[0]) * 100 + Number((p[1] || '').padEnd(2, '0')); // '12.3' → 1230
}
// 分 → 元字符串,展示统一走这里,负数(负结余)也正确
function centsToMoney(c) {
var a = Math.abs(c);
return (c < 0 ? '-' : '') + Math.floor(a / 100) + '.' + String(a % 100).padStart(2, '0');
}
记录:{ id, type:'income'|'expense', date:'YYYY-MM-DD', amount:整数分, category, note, createdAt },仅导出时 /100 换成元。
3. 存储层:错误必须可感知
// storage.js:返回布尔值,失败原因挂在函数属性 lastError 上
function saveData(records) {
saveData.lastError = null;
try {
if (!Array.isArray(records)) throw new TypeError('账本数据必须是数组');
localStorage.setItem('accountBookData', JSON.stringify(records)); // 整体覆盖
return true;
} catch (error) { // 空间不足 / 隐私模式 / 循环引用
saveData.lastError = error.message;
return false; // 绝不谎报成功
}
}
// getData() 读失败同样返回 [],靠 lastError 区分「空账本」与「数据损坏」
// form.js:读失败就不写,避免把用户原始数据洗掉
var list = LedgerStorage.getData();
if (LedgerStorage.getData.lastError) throw new Error('读取账本失败,未覆盖原数据');
list.push(record);
if (LedgerStorage.saveData(list) !== true) throw new Error(LedgerStorage.saveData.lastError);
4. 录入:三道校验 + 类型↔类别联动
// form.js:日期必须真实存在、金额 > 0、类别必须匹配收支类型
if (cents <= 0) return { ok: false, msg: '金额必须大于 0', field: 'recordAmount' };
var allowed = type === 'income' ? INCOME_CATEGORIES : EXPENSE_CATEGORIES;
if (allowed.indexOf(raw.category) === -1) return { ok: false, msg: '类别与类型不匹配' };
var note = String(raw.note || '').replace(/\s+/g, ' ').trim().slice(0, 30); // 折叠+截断
切换类型时同步 option.disabled,从源头挡住「支出 + 工资」;校验失败只写 #formTip 并把焦点打回出错项,不清空表单、不派发成功事件。
5. 渲染与筛选:一个纯函数给三处共用
// list.js:先套筛选 → 按日期倒序 → 逐行 escapeHtml 后拼 innerHTML
records = LedgerFilter.filterRecords(records); // 增删后的刷新也保留当前筛选
records.slice().sort(...).forEach(r => tbody.appendChild(renderRow(r)));
// filter.js:空字符串代表「不限」,始终返回新数组
return filterByCategory(records, category).filter(r =>
(!date || r.date === date) && (min === '' || r.amount / 100 >= Number(min))); // 分→元再比
统计卡片也调 filterRecords(),列表 / 卡片 / 饼图口径天然一致;chart.js 在 ECharts 缺失时只提示「图表加载失败」,记账不受影响。
6. 设计决策一览
| 关键决策 | 解决的问题 |
|---|---|
| 金额用整数分 | 浮点累加误差,只在展示/导出边界转元 |
布尔返回值 + lastError |
分清「空账本」与「读失败」,防止误覆盖 |
| 自定义事件解耦 | 8 个模块零直接依赖,加视图不改旧代码 |
| 读失败拒绝写入 | 数据损坏时保住原始 localStorage |
统一 escapeHtml / 本地时区日期 |
备注 XSS;toISOString() 凌晨差一天 |
五、三大亮点
① 金额全程整数「分」,一分不差(utils.js + storage.js)
金额文本由正则一次收口,0.10 + 0.20 在整数域里精确等于 0.3;18 元存 1800、展示 18.00、导出 18,三处口径由单元测试钉死。
② 永不静默丢数据(storage.js + form.js)
saveData 返回布尔值、原因记在 lastError;getData 读失败也返回 [],调用方据此拒绝写入,
损坏的 localStorage 不会被新记录覆盖;保存失败时用户输入原样保留,ECharts 挂掉只影响图表。
③ 零耦合、可回归(index.html + tests/)
8 个模块只通过 ledger:change → records:changed → stats:updated 三级事件通信,6 行桥接是唯一粘合点,
加视图不用改旧代码;12 条集成测试用 node:vm 桩环境真跑这 8 个脚本,无需浏览器即可回归。
六、遇到的困难及解决方法
困难 1:改了种子数据但页面不更新
- 困难描述:开发列表模块时,修改代码里的初始数据后刷新页面,表格内容没有变化,一度以为文件没保存。
- 做过哪些尝试:检查文件是否保存成功、强制刷新浏览器、换浏览器打开,均无效。
- 是否解决:已解决。
- 有何收获:根因是
localStorage里已存有旧数据,加载逻辑优先读缓存而没读新种子数据。解决方案是加一个SEED_VERSION版本号,种子数据变更时递增版本号,加载时发现版本不一致就强制覆盖旧缓存。这让我理解了“数据优先级”和“缓存一致性”的问题。
困难 2:组员分开开发,缺乏交流,导致写出来的许多部分不匹配。
- 困难描述:例如消费的类别,有的只有6种,有的有12种之多。
- 做过哪些尝试:交流后决定选择更多的种类。
- 是否解决:已解决。
- 有何收获:组员分开开发需要保持交流,确保彼此的任务间能联系在一起。
困难 3:在汇总各组员成果时,出现部分功能没能正常实现。
- 困难描述:在1.0.0到1.0.2版本中,账目的筛选功能都出现问题只能显示部分,显示收支比例的饼状图消失。
- 做过哪些尝试:向负责对应程序的同学求助,重新编写代码。
- 是否解决:已解决。。
- 有何收获:需要提前规划好项目结构,避免这种不匹配情况的出现。
七、每个组员的团队编程体验
李韩远:负责用 ECharts 画饼图。从看文档到把数据喂给图表,调样式花了挺久。
薛昌建:负责全局 CSS,一开始大家的 HTML 结构五花八门,后来我提取了 CSS 变量统一调样式,终于让页面看着像个正经软件了。
林豪:写表单 HTML 看似简单,但怎么布局让用户填得舒服、报错提示放哪都需要推敲,和写 JS 的兄弟对接口也非常顺畅。
肖志渊:负责搭列表表格的架子。为了配合马天鸿写 JS,我特意把各个标签的 ID 和 Class 起得很清楚,体会到了“好结构能让开发事半功倍”。
梁洪睿:写了底层的 storage.js。为了防止程序崩溃,我在读写 JSON 的时候加了异常兜底,与其他组员交流工作汇报进度,很有参与感。
翁齐文:处理表单提交时踩过页面自动刷新的坑,后来学会了拦截默认行为。加完金额校验后,感觉程序的严谨性大大提升了。
马天鸿:被动态生成的删除按钮坑惨了,后来学会了用“事件委托”,把事件绑在父元素上,彻底搞懂了 DOM 事件冒泡。
林晓捷:算钱不难,难在怎么让总数随着增删记录实时变。这让我第一次体会到了“数据驱动视图”的思想,收获很大。
丘南星:负责最后的代码合并和拼装。看着大家交上来的文件能完美跑通,让我深刻体会到了统一定好接口规范有多重要。
陈嘉俊:用 Blob 对象实现了数据导出下载,以前不知道前端还能直接造文件,这次学到了新招,感觉非常实用。
魏怡健:做筛选时一开始想硬改 DOM,差点和马天鸿的代码打架。后来改成“拿数据 → 过滤 → 交给他重新渲染”,明白了模块解耦的好处。
薛强龙:帮忙写了一些日期工具函数,还客串了“救火队员”解决 Git 冲突。深深感受到统一代码规范能省下好多麻烦。
八、PSP 表
PSP2.1
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 10 | 10 |
| · Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
| Development | 开发 | 145 | 170 |
| · Analysis | · 需求分析 | 10 | 10 |
| · Assigning Job | · 分配工作 | 5 | 10 |
| · Developed separately | · 各自开发 | 100 | 110 |
| · Merge and Test | · 合并工作成果并测试 | 20 | 20 |
| · debug | · 修复bug并迭代 | 10 | 20 |
| 合计 | 155 | 180 |
十、GitHub 仓库地址和 commit 记录
- GitHub 仓库:https://github.com/LinkWalker/Account-Book
- Release 版本:https://github.com/LinkWalker/Account-Book/releases
- Commit 记录:
肖志渊

薛强龙

翁齐文

梁洪睿

丘南星

薛昌建

林豪

魏怡健

林晓捷

陈嘉俊

李韩远

马天鸿


浙公网安备 33010602011771号