首页
/ 《Web-Dev-For-Beginners》实战:为银行应用实现「添加交易」对话框并接入既有状态管理系统

《Web-Dev-For-Beginners》实战:为银行应用实现「添加交易」对话框并接入既有状态管理系统

2026-09-08 16:28:49作者:毕习沙Eudora

本篇技术指南围绕 7-bank-project/4-state-management/assignment.md 展开,讲解如何在已有的银行示例应用(Banking App)中,从零实现一个可用的「添加交易」(Add Transaction)对话框:从仪表盘入口按钮、模态框结构与无障碍设计,到表单校验、后端 API 对接,再到与既有状态管理系统(集中式状态、Object.freeze 不可变更新、localStorage 持久化)无缝集成,最终做到不刷新页面即时刷新余额与交易列表。完成本指南后,你将掌握一条从「静态页面」到「可写、可持久化、可无障碍访问」的前端功能闭环的开发路径,并可参照仓库中的参考实现逐一比对验收。

一、任务全景:串起银行应用四条主线的「收官练习」

在前序课程中,你的银行应用已经逐步构建起四块核心能力,而本作业要求你把这四块能力在同一功能上汇合:

课程模块 已具备能力 在本作业中的落点
模板与路由 <template> + 前端路由(SPA 页面切换) 决定「添加交易」用独立路由页面还是仪表盘内的模态框承载
表单处理 表单控件、提交拦截、输入校验与错误提示 收集日期、描述、金额并做提交前校验
数据与 API 通过 fetch 对接后端账户数据 把表单 JSON 提交到交易接口并处理响应
状态管理 集中式 state、受控更新、持久化与数据刷新 新增交易后更新 state.account 并驱动仪表盘重绘

其核心学习目标可归纳为五条:创建易用的数据录入对话框界面实现支持键盘与屏幕阅读器的无障碍表单将新功能接入既有状态管理系统练习 API 通信与错误处理把现代 Web 开发模式应用到真实业务功能上。可见它并非单纯新增一个弹窗,而是要求你以「专业水准」把已有架构串成一个可用的真实功能。

下面以官方仓库的参考实现(7-bank-project/solution/app.js7-bank-project/solution/index.html)为蓝本,按作业步骤逐层展开。

二、第 1 步:在仪表盘放置「添加交易」入口按钮

作业第一步要求为仪表盘添加一个用户容易发现、易于操作的按钮,并提出四个硬性要求:

  • 位置合理:按钮应位于仪表盘逻辑上的显眼区域,通常紧挨交易列表标题;
  • 文案清晰且带动作引导:直接使用 "Add transaction" 之类的动词短语,而非含糊的 "More" 或 "Edit";
  • 视觉风格与既有 UI 一致:复用应用已有的按钮样式体系;
  • 保证键盘可达:它是真实可聚焦的 <button> 元素,而不是不可聚焦的 <div> 或图片。

仓库参考实现中,该按钮位于交易区块标题栏右侧,并复用了主按钮类 btn btn-primary

<!-- 参考 7-bank-project/solution/index.html -->
<div class="transactions-title">
  <h2 id="transactions-description">
    <i class="fa-solid fa-receipt" aria-hidden="true"></i> 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>

技术要点有二:

  1. type="button" 显式声明它不会触发任何表单提交,避免误把整个仪表盘页面当作表单处理;
  2. 触发的是 app.js 中的 addTransaction()——它负责打开对话框并完成焦点迁移,这正是第 3 步无障碍实现的起点。

三、第 2 步:两种对话框实现方案对比

作业给出了两种实现路径,要求你自主选择其一:

方案 A:独立页面(Separate Page)

  • 为交易表单新建一个 HTML 模板;
  • 在既有路由表(routes)中注册新路由;
  • 实现「进入表单页、保存后返回仪表盘」的双向导航。

该方案的优点是语义直接、便于分享 URL;缺点是需要额外的路由注册与状态传递,且每次往返都会打断用户在当前页面的上下文。

方案 B:模态对话框(Modal Dialog,作业推荐方案)

  • 用 JavaScript 在仪表盘内直接控制对话框的显示/隐藏,不离开当前页面;
  • 利用 HTML 全局 hidden 属性或 CSS 类控制显隐;
  • 通过正确的焦点管理营造流畅的用户体验。

仓库参考实现采用方案 B,其显隐机制是「hidden 属性 + CSS 类」的组合:容器元素先以 hidden 隐藏,显示时添加 show 类覆盖默认样式(见 styles.css):

.dialog {
  display: none;            /* 默认隐藏 */
  position: fixed;
  inset: 0;
  z-index: 1000;
}
.dialog.show { display: flex; }   /* 打开时切换 display */

而 JS 侧(app.js)负责在打开时执行一组「入场准备」:表单 reset()、把日期默认设为今天(form.date.valueAsDate = new Date())、并立即把焦点移到首个输入框 form.date.focus(),同时注册点击遮罩与键盘事件监听。

为何建议方案 B?因为「添加交易」属于高频、小体量的录入动作,模态框让用户看到余额与交易列表保持可见,配合第 6 步的即时刷新体验最佳。

四、第 3 步:无障碍(Accessibility)实现——模态对话框达标的关键

作业明确要求对话框满足模态框无障碍标准,可从键盘导航屏幕阅读器两个维度落实。

4.1 键盘导航三要素

  1. 支持 Escape 关闭对话框。参考实现的键盘处理(app.js)监听对话框上的 keydown,命中 Escape 即调用 cancelTransaction()
function onDialogKeydown(e) {
  if (e.key === 'Escape') {
    cancelTransaction();
  }
}
  1. 打开时把焦点"圈禁"在对话框内。本例通过把 keydown 监听器挂在对话框容器上、并由后端 UI 内容只含表单字段来达成焦点不会自然逃逸到背景页面(更严格的做法是拦截 Tab 键循环焦点,可作进阶练习)。

  2. 关闭后把焦点还给触发按钮。这是最常见的被遗漏项——若不做处理,用户关闭弹窗后焦点会掉回 <body>,下一次键盘操作会从页面开头重新开始。参考实现的 cancelTransaction()app.js)在移除 show 类、解绑监听后,主动查询触发按钮并把焦点归还给它:

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();   // 焦点回归触发器
}

4.2 屏幕阅读器支持

对话框容器需要完整的 ARIA 标注。参考实现的 HTML(7-bank-project/solution/index.html)是一个极佳的可对照模板:

<div class="dialog-content"
     role="dialog"
     aria-modal="true"
     aria-labelledby="txDialogTitle"
     aria-describedby="txDialogDesc">
  <h2 id="txDialogTitle" class="text-center">Add transaction</h2>
  <p id="txDialogDesc" class="muted text-center">
    Use negative amount for debit and positive for credit; Esc closes…
  </p>
  <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>
</div>

要点逐一对应作业要求:

  • ARIA 标签与角色role="dialog" 声明模态语义;aria-modal="true" 告知辅助技术背景内容不可交互;aria-labelledby 指向可见标题;aria-describedby 指向辅助说明文字;
  • 打开/关闭状态的播报:内容在打开瞬间进入无障碍树即被感知,且错误容器带 role="alert"aria-live="polite",提交失败时校验消息会被自动读出;
  • 清晰的字段标签与错误消息:每个输入框都有与之配对的 <label for>,错误提示(如 "Amount must be a valid number")写入 #transactionError 并调用 .focus() 让读屏立刻聚焦(见 app.js)。

五、第 4 步:构建收集交易数据的表单

表单是对话框的核心。作业规定三个必填字段

字段 含义 输入类型 业务约束
日期(Date) 交易发生的日期 type="date" 必填;实现中默认填充今天
描述/对象(Description/Object) 这笔交易是什么 type="text"maxlength="50" 必填,如 "Bought book"
金额(Amount) 交易数值 type="number"step="any" 收入为正数、支出为负数(如 -20

术语说明:界面与文档常称第三个字段为"描述(description)",而仓库 API 与参考表单统一使用字段名 object(见上文 HTML name="object"),两者指同一概念,对接时以 JSON 键 object 为准。

表单还要求具备四项特征:

  1. 提交前校验用户输入。参考实现(app.js)在 confirmTransaction() 里做内联校验:金额必须能转成有限数值(Number.isFinite(amountVal)),对象描述不得为空,任一不满足即写入错误区并提前 return,不会发起网络请求;
  2. 对非法数据给出清晰错误消息,如 Amount must be a valid numberObject is required,并让错误区域获得焦点以便读屏播报;
  3. 包含辅助性占位文本与标签——示例中金额标签就写明 "negative for debit",甚至帮助文案里再次提示 Use negative amount for debit and positive for credit
  4. 与既有设计体系一致——按钮复用 btn btn-primary / btn-ghost,输入框、标签、错误提示均来自全局样式表。

另外注意表单使用 novalidate 关闭浏览器原生气泡,改为自己控制校验时机与消息展示,这正是第 5 步做统一错误处理的前提。

六、第 5 步:API 集成——把表单数据送到后端

6.1 先读 API 规格

作业要求先查阅 服务端 API 文档 确认正确的端点与数据格式。结合该文档与后端实现 server.js,新增交易的关键契约如下:

规格
端点 POST /api/accounts/:user/transactions
请求体(JSON) { "date": "2020-10-05", "object": "Bought book", "amount": -20 }
成功响应 201 Created,返回含服务端生成的 id 的完整交易对象
400 缺少 date/object/amount,或 amount 非法(Missing parameters / Amount must be a number
404 账户不存在(User does not exist
409 重复交易——后端以 md5(date + object + amount) 生成 id 并查重(Transaction already exists

后端行为需留意两点:交易会被 push 进 account.transactions,同时余额自动累加account.balance += transaction.amount),因此前端只需以「追加后余额与交易」刷新本地账户对象即可保持一致。此外 server.js 的数据全部存于内存(含内置的 test 账户),服务重启即清空,属教学环境预期。

仓库还提供了可直接在 VS Code REST Client 中发送的示例请求(7-bank-project/api/api.http):

POST http://localhost:5000/api/accounts/sinedied/transactions
Content-Type: application/json

{
  "date": "2020-07-24",
  "object": "Bought book",
  "amount": -20
}

本地启动方式:在 7-bank-project/api 目录执行 npm install 后运行 npm start,服务默认监听 5000 端口(与前端应用所在的 3000 端口并存,勿关闭)。

6.2 前端提交链路

参考实现的提交逻辑把「序列化 → 请求 → 错误处理」浓缩为几行(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;
}
  • 构造 JSONnew FormData(form) 依表单控件的 name(date/object/amount)收集数据,Object.fromEntries 转为对象,JSON.stringify 序列化后发送——不需要手写键值映射,也避免了遗漏新字段;
  • 错误处理:任何失败(校验、账户不存在、网络中断等)统一通过 setFormError 写入表单错误区并聚焦,实现"一处统一提示";
  • 成功/失败反馈:成功后进入第 6 步的状态更新流程,失败则停留在对话框内让用户原地修正。

教学版前端用一个内存 mock 模块模拟了异步 API(solution/app.js 中的 createTransaction),它对真实网络环境的意义在于:请求与 UI 解耦,后续把 serverUrl 指向真实后端时,对话框层代码无需改动。

七、第 6 步:状态管理集成——不刷新页面即时更新仪表盘

这是整个作业的"点睛"步骤。作业要求:添加成功后刷新账户数据、不刷新页面更新仪表盘、新交易立即可见、全程保持状态一致。其实现基础是课程第 4 部分打造的集中式不可变状态(详见 7-bank-project/4-state-management/README.md):所有变更都必须经由唯一的 updateState(),每次变更产生一个用 Object.freeze() 锁定的新状态对象,并自动把当前账户写入 localStorage

// 参考实现(solution/app.js)
let state = Object.freeze({ account: null });

function updateState(property, newData) {
  state = Object.freeze({ ...state, [property]: newData });
  localStorage.setItem(storageKey, JSON.stringify(state.account));
}

「添加交易」与状态系统的对接因此在 confirmTransaction() 中完成(app.js):

// 用响应数据构造新的账户对象(余额累加、交易追加),再整体更新状态
const newAccount = {
  ...state.account,
  balance: (Number(state.account.balance) || 0) + data.amount,
  transactions: [...(state.account.transactions || []), data]
};
updateState('account', newAccount);

// 关闭对话框并重绘仪表盘
cancelTransaction();
updateDashboard();

这一步体现三条原则:

  1. 不直接改旧对象:新对象基于展开运算符派生,旧状态在更新前保持完整,天然支持未来做撤销(undo)等历史追溯功能;
  2. 单一更新入口:余额、交易列表的变更与 localStorage 持久化都由 updateState() 一次完成,不存在"改了页面忘了存盘"的窗口期;
  3. 重绘与数据分离updateDashboard()app.js)只负责"读 state.account → 渲染",它把交易按日期倒序排列,负数为支出、正数为收入分别加 debit/credit 样式类;由于状态是唯一数据源,任何写入路径(本作业新增的对话框、其它标签页的 storage 事件等)都能得到一致的呈现。

整个数据流向可概括为下图所示的闭环:用户动作 → 事件处理 → 受控状态更新 → 视图重绘,这正是集中式状态管理解决"多个函数各改各的数据"问题的关键架构。

Action、State、View 三者构成的状态数据流闭环示意图

八、实现效果与验收对照

完成六个步骤后,你的银行应用将具备一个观感与行为都接近专业产品的完整功能:点击仪表盘上的 Add transaction 弹出模态框,录入日期、对象与金额(负数表示支出),确认后对话框关闭、余额与交易列表立刻刷新,全程无需重载页面。

银行应用「添加交易」对话框运行效果:含 Date、Object、Amount 字段与 Cancel/OK 按钮

功能测试清单

  1. 确认「Add transaction」按钮清晰可见且可访问;
  2. 验证对话框能够正确打开与关闭(含 Cancel、遮罩点击、Escape 三种关闭路径);
  3. 确认表单校验覆盖全部必填字段并展示明确错误;
  4. 验证添加成功后新交易立即出现在仪表盘交易列表中;
  5. 确认对非法数据与网络异常都能给出恰当的失败处理。

无障碍测试清单

  1. 仅用键盘完整走通「打开 → 录入 → 提交/取消」全流程;
  2. 用屏幕阅读器验证开合播报与错误提示能被正确朗读;
  3. 验证焦点管理(打开进对话框、Escape/取消后回到触发按钮);
  4. 检查所有表单元素都带正确的 <label>

九、评估标准速览

作业附带的评估矩阵可作为自检清单,也可作为评审他人实现的标尺:

维度 优秀(Exemplary) 合格(Adequate) 待改进(Needs Improvement)
功能性 功能无瑕疵、体验出色,遵守课程全部最佳实践 功能正确但个别最佳实践未落实或有轻微可用性问题 功能部分可用或存在明显可用性缺陷
代码质量 组织清晰、遵循既有模式、含错误处理、与状态管理无缝衔接 可用但组织欠佳或与既有代码风格不一致 结构性缺陷明显、未与既有模式良好集成
无障碍 完整键盘导航、读屏兼容、符合 WCAG 且焦点管理出色 有基础无障碍但缺失部分键盘/读屏能力 几乎没有考虑无障碍
用户体验 界面直觉化、打磨到位、反馈清晰、交互流畅、观感专业 体验尚可但反馈或视觉尚有提升空间 界面令人困惑或缺少用户反馈

十、进阶挑战(可选)

在基础功能完成后,作业还给出两组加分方向,用于练习更高级的 Web 开发概念:

  • 增强功能:为交易增加分类(餐饮、交通、娱乐等);实现带实时反馈的输入校验;为熟练用户提供键盘快捷键;支持交易的编辑与删除。
  • 高级集成:为最近添加的交易实现 Undo(撤销);支持从 CSV 批量导入;实现交易搜索与筛选;增加数据导出能力。

值得注意的是,课程主线状态管理部分(7-bank-project/4-state-management/README.md 的进阶挑战)恰好要求为状态系统加入「历史数组 + 撤销/重做」,与这里「Undo 最近添加交易」的挑战互相呼应——若两者结合,你实际上是在把对话框写入路径与状态历史快照打通,这已经触及生产级应用常见的时间旅行调试(time-travel)思路。

小结

本文从「任务全景」出发,按六步实施路径拆解了「添加交易」对话框的完整实现:入口按钮与模态框结构、可对照 ARIA 标准的无障碍细节、三字段表单与提交前校验、以上下文无关的 JSON 对接后端交易接口,最终经由 updateState() 把新交易并入集中式状态并驱动仪表盘即时重绘。官方参考实现全部收于 7-bank-project/solution(HTML 模板见 index.html、逻辑见 app.js、样式见 styles.css),可随时逐行对照学习。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390