现场编程 个人记账本

作业所属课程 班级的链接
作业要求 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

三、软件运行截图和视频

主界面展示

image

记账表单展示

image

记录表展示

image

筛选功能展示

image

饼状图统计功能展示

image

删除功能展示

image

视频链接

a8b398da5e33fe9ea7937bff1d1113b5
(请使用微信扫码)

四、软件实现的关键代码

原生 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 记录

肖志渊

image

薛强龙

image

翁齐文

image

梁洪睿

image

丘南星

image

薛昌建

image

林豪

image

魏怡健

image

林晓捷

image

陈嘉俊

image

李韩远

image

马天鸿

DE6C427AF3A0F38A4FAAF00D8E017BC8

posted @ 2026-11-07 21:10  uzk  阅读(25)  评论(0)    收藏  举报