Web-Dev-For-Beginners 银行应用实战:实现「添加交易」对话框并接入状态管理
【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners
本指南围绕 Web-Dev-For-Beginners 课程中银行应用(7-bank-project)第四课的课后作业展开:为已完成状态管理与数据持久化的应用补齐「添加交易」(Add Transaction)功能。你将学到如何设计可访问的模态对话框、构造交易表单、对接后端 API,并让新增交易通过既有状态管理系统即时刷新仪表盘。读完本文,你可以独立完成从按钮、对话框、表单到 API 与状态层整合的完整功能闭环,并掌握可复用的 Web 无障碍与前端架构实践。
作业背景与学习目标
银行应用此刻已经具备集中式状态管理(state对象 +updateState()受控更新)、localStorage 持久化以及数据刷新机制,但还缺少真实银行应用的关键能力:让用户自己录入交易。本作业将四个银行课的核心知识点串成一条完整链路——HTML 模板、表单处理、API 集成与状态管理(详见 第四课讲义 与前三课内容)。
完成本作业后,你将能够:
- 创建一个用户友好的数据录入对话框界面;
- 实现支持键盘与屏幕阅读器的可访问表单设计;
- 整合新功能与既有状态管理系统;
- 实践API 通信与错误处理;
- 应用现代 Web 开发模式解决真实场景需求。
前置条件:环境与 API 就绪检查
作业建立在第四课之前完成的代码之上,动手前请确认环境就绪:
- 完成 数据获取课程,应用能正常加载并展示账户数据;
- 安装 Node.js 用于运行后端 API;
- 参照 服务器 API 说明 在本地启动 API:进入
7-bank-project/api目录执行npm install,随后npm start,服务监听5000端口(注意与主应用所在终端并存,不要关闭)。
用一条命令验证 API 是否正常运行:
curl http://localhost:5000/api # -> 应返回 "Bank API v1.0.0"这条命令会向本地 API 发送 GET 请求、测试连接,若一切正常则返回 API 版本信息。仓库自带的 server.js 中,GET /路由正是返回pkg.description + ' v' + pkg.version,即响应内容的来源。
Step 1:在仪表盘放置「添加交易」按钮
在仪表盘页面上创建一个用户容易发现、方便使用的按钮,要求:
- 位置合理:放在仪表盘的逻辑位置(如交易列表标题区);
- 文案清晰:使用面向操作的按钮文本;
- 风格一致:样式与现有 UI 设计统一;
- 键盘可达:确保按钮可被键盘聚焦操作。
参考实现中,按钮被放在交易列表标题栏右侧,使用内联onclick调起对话框。对应的 HTML 位于 solution/index.html:
<div class="transactions-title"> <h2 id="transactions-description">Transactions</h2> <button class="btn btn-primary" type="button" onclick="addTransaction()"> <i class="fa-solid fa-plus" aria-hidden="true"></i> Add transaction </button> </div>注意图标使用aria-hidden="true",避免屏幕阅读器朗读装饰性图标;按钮本身是原生<button>,天然支持键盘 Tab 聚焦与 Enter/Space 触发。
Step 2:对话框实现——独立页面还是模态框
作业提供两种实现路径,推荐后者:
方案 A:独立页面
- 为交易表单创建新的 HTML 模板;
- 在路由系统中新增一条路由;
- 实现进入/返回表单页的导航。
方案 B:模态对话框(推荐)
- 用 JavaScript 控制对话框的显示/隐藏,不离开仪表盘页面;
- 使用 HTML 的
hidden属性 或 CSS 类实现显隐; - 通过正确的焦点管理营造流畅的用户体验。
参考实现采用方案 B:对话框默认带hidden属性,配合.dialog样式(display: none),通过addTransaction()添加.show类(display: flex)来展示。核心结构见 solution/index.html:
<section id="transactionDialog" class="dialog" hidden> <div class="dialog-backdrop">function addTransaction() { const dialog = qs('transactionDialog'); if (!dialog) return; dialog.classList.add('show'); const form = qs('transactionForm'); if (form) { form.reset(); form.date.valueAsDate = new Date(); // 日期默认今天 form.date.focus(); // 焦点移到第一个字段 } const backdrop = dialog.querySelector('[data-dismiss]') || dialog; backdrop.addEventListener('click', onDialogDismissClick); dialog.addEventListener('keydown', onDialogKeydown); } function onDialogKeydown(e) { if (e.key === 'Escape') { cancelTransaction(); } } function cancelTransaction() { const dialog = qs('transactionDialog'); if (!dialog) return; dialog.classList.remove('show'); dialog.removeEventListener('keydown', onDialogKeydown); const opener = document.querySelector('button[onclick="addTransaction()"]'); if (opener) opener.focus(); // 焦点归还触发按钮 }屏幕阅读器支持
- 为对话框添加合适的 ARIA 角色与标签:
role="dialog"、aria-modal="true"、aria-labelledby(指向标题)与aria-describedby(指向说明文字); - 向读屏软件通告对话框的开/关状态;
- 为表单字段提供清晰的标签与错误信息。
表单错误区使用role="alert"+aria-live="polite"组合(见 solution/index.html),错误写入时读屏会自动播报;错误设置函数setFormError()还会将焦点移到错误元素上,方便键盘用户立刻定位问题:
function setFormError(id, message) { updateElement(id, message); const el = qs(id); if (el) el.focus(); }Step 4:交易表单的创建与校验
表单需要收集三类必填数据:
| 字段 | 含义 | 输入规则 |
|---|---|---|
| 日期(Date) | 交易发生时间 | <input type="date" required>,默认今天 |
| 描述(Object) | 交易用途 | 文本,maxlength="50",必填 |
| 金额(Amount) | 交易金额 | 正数为收入(credit),负数为支出(debit),step="any"支持小数 |
参考表单结构(solution/index.html):
<form id="transactionForm" action="javascript:void(0)" novalidate> <label for="date">Date</label> <input id="date" name="date" type="date" required> <label for="object">Object</label> <input id="object" name="object" type="text" maxlength="50" required> <label for="amount">Amount (negative for debit)</label> <input id="amount" name="amount" type="number" value="0" step="any" inputmode="decimal" required> <div id="transactionError" class="error" role="alert" aria-live="polite"></div> ... </form>表单特性要求:提交前校验用户输入、对非法数据给出明确错误信息、提供有助益的占位文本与标签、样式与现有设计一致。novalidate关闭浏览器原生气泡校验,改由 JS 统一内联校验,保证错误信息可控且能被读屏正确播报。
confirmTransaction()(solution/app.js)中的校验逻辑与后端保持同一套规则——金额必须是有限数值、描述不可为空:
const amountVal = Number(form.amount.value); const objectVal = String(form.object.value || '').trim(); if (!Number.isFinite(amountVal)) { setFormError('transactionError', 'Amount must be a valid number'); return; } if (!objectVal) { setFormError('transactionError', 'Object is required'); return; }Step 5:API 集成——端点、数据格式与错误处理
作业要求对接后端 API,正确端点与数据格式以 服务器 API 文档 为准:
| 方法 | 路由 | 说明 |
|---|---|---|
| GET | /api/ | 获取服务器信息 |
| POST | /api/accounts/ | 创建账户,如{ user: 'Yohan', description: 'My budget', currency: 'EUR', balance: 100 } |
| GET | /api/accounts/:user | 获取指定账户全部数据 |
| DELETE | /api/accounts/:user | 删除账户 |
| POST | /api/accounts/:user/transactions | 添加交易,如{ date: '2020-07-23T18:25:43.511Z', object: 'Bought a book', amount: -20 } |
| DELETE | /api/accounts/:user/transactions/:id | 删除指定交易 |
本作业使用的端点即POST /api/accounts/:user/transactions,请求体 JSON 字段为date、object、amount。后端实现(server.js)的细节可以帮助你理解预期行为:
- 账户不存在返回
404 { error: 'User does not exist' }; - 缺少
date/object/amount任一字段返回400 { error: 'Missing parameters' }; - 金额不是有效数字返回
400 { error: 'Amount must be a number' }; - 服务端用 md5 对
date + object + amount生成交易 ID,重复内容返回409 { error: 'Transaction already exists' }; - 成功时以
201返回新建的交易对象{ id, date, object, amount },并同步更新账户余额。
注意:仓库中的 server.js 将数据保存在内存中(含预置的
test账户),重启即丢失,仅用于教学演示。
前端把表单数据序列化为 JSON 并提交(solution/app.js):
const jsonData = JSON.stringify(Object.fromEntries(new FormData(form))); const data = await createTransaction(state.account.user, jsonData); if (data.error) { setFormError('transactionError', data.error); return; }错误处理要点:成功/失败消息都要呈现给用户;对网络错误要优雅降级(如提示后允许重试);服务端返回的error字段应直接映射到表单错误区。
Step 6:状态管理集成——新交易立即上屏
这是把整个功能与既有架构缝合的关键一步,要求:
- 成功添加交易后刷新账户数据;
- 仪表盘显示无需整页刷新即可更新;
- 新交易立即出现在交易列表;
- 全程保持状态一致性。
参考实现没有重新拉取全量数据,而是基于当前state.account以不可变更新方式本地合成新账户状态(solution/app.js):
const newAccount = { ...state.account, balance: (Number(state.account.balance) || 0) + data.amount, transactions: [...(state.account.transactions || []), data] }; updateState('account', newAccount); cancelTransaction(); updateDashboard();关键点在于复用第四课建立的受控更新入口updateState()(solution/app.js):
function updateState(property, newData) { state = Object.freeze({ ...state, [property]: newData }); localStorage.setItem(storageKey, JSON.stringify(state.account)); }由于所有状态变更都收敛到这一个函数,持久化只需要一行代码:写入 localStorage 自动完成,updateDashboard()重读state.account后立即把余额与最新交易渲染上屏(solution/app.js),交易按日期倒序排列,负数行加debit、正数行加credit样式类。
如果希望更贴近「从服务器拿最新数据」的模式,第四课还提供了updateAccountData()+refresh()机制,并挂到路由init上(见 第四课讲义 的 Data Refresh 小节),可作为本作业的进阶选择——在提交成功后调用refresh()以服务器返回为准更新仪表盘。
预期结果
完成六个步骤后,应用应具备一个外观与行为都专业、可用的「添加交易」功能。下方是仓库提供的示例效果图:
测试你的实现
功能测试
- 验证「添加交易」按钮清晰可见且可访问;
- 测试对话框能正确打开与关闭;
- 确认所有必填字段的校验都生效;
- 检查成功提交的交易能立即出现在仪表盘;
- 确保非法数据与网络问题的错误处理都正常工作。
无障碍测试
- 仅用键盘走完整个流程;
- 用屏幕阅读器验证读屏通告是否正确;
- 验证焦点管理正常(打开时聚焦表单、关闭后回到触发按钮);
- 检查所有表单元素都有合适的标签。
评价标准
| 标准 | 优秀 | 合格 | 待改进 |
|---|---|---|---|
| 功能性 | 添加交易功能完美运行,用户体验出色,遵循课程全部最佳实践 | 功能正确但可能未遵循部分最佳实践或有轻微可用性问题 | 功能部分可用或有明显可用性问题 |
| 代码质量 | 组织良好,遵循既定模式,错误处理正确,与既有状态管理无缝整合 | 可运行但存在组织问题或与既有代码风格不一致 | 结构性缺陷或与既有模式整合不佳 |
| 可访问性 | 完整键盘导航支持、读屏兼容,遵循 WCAG 指南,焦点管理出色 | 基础无障碍已实现但缺少部分键盘或读屏特性 | 几乎没有或完全没有考虑无障碍 |
| 用户体验 | 直观精致的界面,反馈清晰,交互流畅,外观专业 | 体验良好,但反馈或视觉设计有小幅提升空间 | 体验差,界面令人困惑或缺少用户反馈 |
附加挑战(可选)
完成基础要求后,可尝试以下增强:
功能增强
- 增加交易分类(餐饮、交通、娱乐等);
- 实现带实时反馈的输入校验;
- 为熟练用户提供键盘快捷键;
- 增加交易编辑与删除能力。
进阶集成
- 实现最近添加交易的撤销(undo)功能;
- 支持从 CSV 文件批量导入交易;
- 提供交易搜索与筛选;
- 实现数据导出。
这些可选功能将帮助你练习更进阶的 Web 开发概念,并让银行应用更加完整。若想进一步练习状态管理,第四课讲义的 GitHub Copilot Agent Challenge 还提供了带撤销/重做与历史上限的状态管理挑战,可作为本作业的自然延伸。
【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考