news 2026/9/12 11:15:37

Web-Dev-For-Beginners 银行应用实战:基于状态管理实现 “Add Transaction“ 交易录入对话框

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Web-Dev-For-Beginners 银行应用实战:基于状态管理实现 “Add Transaction“ 交易录入对话框

Web-Dev-For-Beginners 银行应用实战:基于状态管理实现 "Add Transaction" 交易录入对话框

【免费下载链接】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)」第 4 节《状态管理(State Management)》的课后作业为核心,完整讲解如何在已有集中式状态管理与 localStorage 持久化系统之上,为仪表盘(Dashboard)实现一个专业、可无障碍访问的"添加交易(Add Transaction)"对话框。读完本文,你将掌握模态对话框的两种实现路径(独立页面 vs 弹窗)、完整的数据校验与错误处理、与 Express API 的对接方式,以及如何让新交易在不刷新页面的情况下立即出现在仪表盘上——所有内容均有本仓库源码(7-bank-project/solution/)与 API 服务(7-bank-project/api/)作为事实依据。


1. 任务背景:本作业在前四节课程中的定位

本作业将前四节银行应用课程所学的内容串联起来,它们分别是:

课程小节核心知识点与本作业的关系
1. 模板与路由HTML<template>模板、SPA 路由对话框 UI 以模板形式挂在 Dashboard 模板内部
2. 表单处理表单校验、FormData、提交拦截交易表单的收集、校验与提交逻辑
3. 数据获取fetch/异步请求、账户数据加载提交后刷新账户数据、错误处理
4. 状态管理集中式 state、Object.freeze、localStorage 持久化新交易通过updateState()写入状态并持久化

作业的完成目标非常明确:在不破坏已有状态管理系统的前提下,让用户能够自助录入交易。也就是说,你要新增的功能必须「长在」现有的updateState()/state/ 路由体系之上,而不是另起炉灶。

前置条件:请先确保已完成 7-bank-project/3-data 的数据获取课程,并按 7-bank-project/api/README.md 启动 API 服务。验证方式:在终端执行curl http://localhost:5000/api,返回Bank API v1.0.0即表示服务正常(API 默认监听 5000 端口,与主应用所在端口相互独立,请勿关闭)。


2. Step 1:添加交易按钮(Add Transaction Button)

作业要求先在仪表盘页面上放置一个用户易于发现和访问的「Add Transaction」按钮,并给出四项硬性要求:

  1. 放置在仪表盘的逻辑合理位置(通常位于交易列表标题区右侧);
  2. 使用清晰、面向操作的按钮文案;
  3. 按钮样式与现有 UI 设计保持一致;
  4. 按钮支持键盘访问(原生<button>天然满足)。

本仓库的解决方案在 7-bank-project/solution/index.html 的 Dashboard 模板中给出了标准做法——把按钮放在交易列表标题h2旁边:

<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>

设计要点拆解:

  • 按钮语义:使用<button type="button">而非<a><div>,保证 Tab 键可聚焦、回车/空格可触发;
  • 点击行为onclick="addTransaction()"直接调用对话框的打开函数(这是作业 Option B「模态对话框」的入口);
  • 样式一致性:复用全局.btn .btn-primary类,与登录页、注册页按钮同源;
  • 图标辅助<i>图标均带aria-hidden="true",避免屏幕阅读器朗读装饰性图标。

从 7-bank-project/solution/app.js 可以看到,addTransaction()做的事很简单——给对话框加show类、重置表单并把日期默认设为今天、把焦点移到第一个字段、挂载关闭事件:

function addTransaction() { const dialog = qs('transactionDialog'); if (!dialog) return; dialog.classList.add('show'); // Reset form and set today 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); }

3. Step 2:对话框的两种实现路径(推荐模态弹窗)

作业给出了两条技术路线,并要求二选一:

  • Option A:独立页面(Separate Page)——为交易表单新建一个 HTML 模板,在路由系统中新增一条路由,并实现表单页与仪表盘之间的往返导航;
  • Option B:模态对话框(Modal Dialog,推荐)——使用 JavaScript 在不离开仪表盘的前提下显示/隐藏对话框,可用 HTML 的hidden属性 或 CSS 类实现,并做好焦点管理。

方案 B 之所以被推荐,是因为它不触发页面切换,天然适合与 SPA 路由 + 集中式状态配合,交互也更连贯。本仓库解决方案完整实现了 Option B,其结构分为 HTML 标记、CSS 显隐与 JS 控制三部分。

3.1 HTML:对话框标记(挂在 Dashboard 模板内部)

7-bank-project/solution/index.html 中,对话框section使用hidden属性作为默认隐藏手段,同时用 ARIA 属性描述对话框身份:

<section id="transactionDialog" class="dialog" hidden> <div class="dialog-backdrop">.dialog { display: none; /* 默认隐藏 */ position: fixed; inset: 0; /* 覆盖整个视口 */ overflow: auto; background-color: rgba(0,0,0,.45); /* 半透明遮罩 */ justify-content: center; align-items: flex-start; padding: var(--space-sm); z-index: 1000; } .dialog.show { display: flex; } /* 打开时切换为 flex 居中 */

.dialog-content还配有弹出动画(animation: dialogIn .25s ease-out both),从透明 + 上移 14px 过渡到正常状态,让开合手感更顺滑。注意:.dialog.show { display: flex }写在.dialog之后,由于两者特异性相同、后者胜出,从而正确覆盖默认的display: none

3.3 JS:打开与关闭的控制流

关闭对话框的完整逻辑(7-bank-project/solution/app.js)包含三种触发源:点遮罩(data-dismiss元素)、按 Escape 键、点 Cancel 按钮,且关闭后要把焦点还给触发按钮

function onDialogDismissClick(e) { if (e.target?.hasAttribute?.('data-dismiss')) { cancelTransaction(); } } 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(); // 焦点返回触发按钮 }

关于焦点陷阱(focus trap):作业要求「打开对话框时把焦点困在对话框内」。仓库解决方案采用「打开时聚焦第一个字段 + 关闭时返还焦点」的简化实现。如果你希望严格实现焦点陷阱,需要在keydown中监听 Tab 键,将焦点限制在.dialog-content内可聚焦元素(inputbutton)之间循环——这是本作业可以自行强化的进阶点。


4. Step 3:无障碍实现(Accessibility)

作业明确要求对话框满足模态对话框的无障碍标准,分键盘导航与屏幕阅读器两部分:

键盘导航:

  • 支持 Escape 键关闭对话框(解决方案已实现,见上方onDialogKeydown);
  • 打开时焦点被困在对话框内(解决方案采用聚焦首字段的简化做法);
  • 关闭后焦点返回触发按钮(已实现:cancelTransaction()opener.focus())。

屏幕阅读器支持:

  • 添加恰当的 ARIA 角色与标签——解决方案使用role="dialog"+aria-modal="true"+aria-labelledby="txDialogTitle"(标题)+aria-describedby="txDialogDesc"(说明文字);
  • 播报打开/关闭状态——表单错误区#transactionError使用了role="alert"+aria-live="polite",错误出现时屏幕阅读器会主动朗读;
  • 提供清晰的字段标签与错误信息——每个input上方都有<label for="...">,错误信息通过setFormError()写入并focus()到错误节点。

在 7-bank-project/solution/index.html 中,登录/注册表单同样沿用这一套 ARIA 模式(如#loginError#registerError均为role="alert"),说明这是整个应用的统一无障碍约定,新增对话框应保持一致。


5. Step 4:交易表单的创建与校验

5.1 表单字段设计

作业要求表单收集三个必填字段:

字段类型说明
Date(日期)<input type="date">交易发生日期,默认今天
Description(描述/对象)<input type="text" maxlength="50">交易用途说明
Amount(金额)<input type="number" step="any">正值表示收入(credit),负值表示支出(debit)

解决方案对应的 HTML(7-bank-project/solution/index.html):

<label for="date">Date</label> <div class="field"> <input id="date" name="date" type="date" required> </div> <label for="object">Object</label> <div class="field"> <input id="object" name="object" type="text" maxlength="50" required> </div> <label for="amount">Amount (negative for debit)</label> <div class="field"> <input id="amount" name="amount" type="number" value="0" step="any" inputmode="decimal" required> </div>

这里有几个值得注意的细节:

  • type="number"+step="any"允许任意精度(包括小数);
  • inputmode="decimal"在移动端弹出带小数点的数字键盘;
  • 金额标签直接写明「negative for debit」,在 UI 层就引导用户用负数表示支出;
  • 表单位于<template id="dashboard">内部,由 SPA 路由克隆注入,表单的name属性与FormData直接对应,便于序列化。

5.2 提交前的输入校验

作业要求「提交前校验用户输入,并对非法数据给出清晰错误信息」。解决方案在 7-bank-project/solution/app.js 的confirmTransaction()中做了双重校验

async function confirmTransaction() { const form = qs('transactionForm'); if (!form) return; // 客户端内联校验 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; } clearFormError('transactionError'); const jsonData = JSON.stringify(Object.fromEntries(new FormData(form))); const data = await createTransaction(state.account.user, jsonData); // ...后续状态更新 }

配套的错误显示工具函数:

function setFormError(id, message) { updateElement(id, message); const el = qs(id); if (el) el.focus(); // 把焦点移到错误提示,方便读屏用户 } function clearFormError(id) { const el = qs(id); if (el) el.textContent = ''; }

校验逻辑值得学习的两点:

  1. 数量有限但精准:金额用Number.isFinite()判断是否为有效数字,描述用trim()后判断非空——与后端 7-bank-project/api/server.js 中Amount must be a valid numberObject is required的校验口径一致;
  2. 错误即焦点setFormError()不仅写文案,还把焦点移到错误节点,配合role="alert"实现「错误即时播报」。

6. Step 5:API 集成——对接交易录入端点

6.1 端点规格(来自 7-bank-project/api/README.md)

方法路由说明
POST/api/accounts/:user/transactions为指定账户添加交易
GET/api/accounts/:user获取指定账户全部数据
DELETE/api/accounts/:user/transactions/:id删除指定交易

请求体 JSON 格式示例:

{ "date": "2020-07-23T18:25:43.511Z", "object": "Bought a book", "amount": -20 }

6.2 服务端行为细节(7-bank-project/api/server.js)

阅读服务端源码,可以确认交易接口的完整行为,这对客户端错误处理至关重要:

  • 必填参数dateobjectamount三者缺一即返回400 { error: 'Missing parameters' }
  • 金额校验amount无法转换为数字时返回400 { error: 'Amount must be a number' }
  • 账户校验/api/accounts/:user不存在时返回404 { error: 'User does not exist' }
  • 幂等去重:服务端用 MD5(date + object + amount)生成交易 ID,重复交易返回409 { error: 'Transaction already exists' }
  • 成功响应:返回201与包含iddateobjectamount的完整交易对象,同时服务端会自动更新账户余额account.balance += transaction.amount)。

⚠️ 注意:API 的 server.js 明确注明数据仅存于内存(in-memory),服务停止即丢失;同时该 API 是为教学预先构建好的,不属于本次作业的修改范围。

6.3 客户端集成:从表单 JSON 到错误处理

解决方案中,createTransaction(user, jsonData)是一个对 API 的封装(在 7-bank-project/solution/app.js 中为本地 mock 实现,模拟了同样的错误语义:Malformed transaction dataAmount must be a valid numberObject is requiredAccount not found),而confirmTransaction()的集成流程是:

  1. Object.fromEntries(new FormData(form))收集表单数据;
  2. JSON.stringify序列化后交给createTransaction(state.account.user, jsonData)
  3. 若返回data.error,用setFormError()展示错误并终止流程;
  4. 成功则进入 Step 6 的状态更新环节。

对真实 API 的对接,作业要求「适当处理网络错误」——即try/catch包裹fetch调用,对网络中断、超时给出用户可读的提示(例如「无法连接服务器,请检查 API 是否启动」),这也是本作业中服务端源码没有覆盖、需要你自行完善的客户端逻辑。


7. Step 6:状态管理集成——交易立即可见

这是整个作业的收尾关键:新增交易后,仪表盘必须不刷新页面就立即显示最新交易,并保持状态一致性。

7.1 更新本地状态(7-bank-project/solution/app.js)

// Update local state const newAccount = { ...state.account, balance: (Number(state.account.balance) || 0) + data.amount, transactions: [...(state.account.transactions || []), data] }; updateState('account', newAccount); // Close dialog and update view cancelTransaction(); updateDashboard();

这段代码充分体现了前四节课程沉淀的不可变状态更新模式

  • 用展开运算符...state.account复制旧状态,再覆盖balancetransactions绝不直接修改state.account
  • balance的累加方式与服务端一致(原余额 + 交易金额),保证前后端口径统一;
  • 通过唯一的updateState()写入新状态,自动触发 localStorage 持久化。

7.2 updateState 与持久化(来自第 4 节课程核心)

回顾 7-bank-project/4-state-management/README.md 建立的updateState()模式,它正是本作业状态集成的基石:

function updateState(property, newData) { state = Object.freeze({ ...state, [property]: newData }); localStorage.setItem(storageKey, JSON.stringify(state.account)); }
  • 冻结:每次更新都生成新对象并Object.freeze(),防止意外篡改;
  • 自动持久化:更新即写localStorage(key 为savedAccount),这就是「新交易刷新后依然存在」的底层保证;
  • 集中入口:所有状态变更(登录、注册、登出、刷新数据、添加交易)都走这一个函数,方便调试与审计。

7.3 界面刷新与数据新鲜度

confirmTransaction()updateState()之后调用updateDashboard(),后者根据state.account重新渲染余额与交易表格。由于状态是响应式的数据源、UI 是状态的投影,交易会立即出现,无需整页刷新。

同时,第 4 节课程引入的「刷新即取新数据」模式(routes['/dashboard']init: refresh)保证了每次进入仪表盘都会从服务端拉取最新账户数据:

async function updateAccountData() { const account = state.account; if (!account) return logout(); const data = await getAccount(account.user); if (data.error) return logout(); updateState('account', data); } async function refresh() { await updateAccountData(); updateDashboard(); }

因此整个作业完成后,应用的交易数据流是:表单提交 → API 创建 → 本地状态更新(含余额与交易数组)→ localStorage 持久化 → updateDashboard 即时重绘。此后刷新页面或重新进入仪表盘,还会从服务端同步最新数据,形成闭环。


8. 测试你的实现

8.1 功能测试

  1. 验证「Add Transaction」按钮清晰可见且可访问;
  2. 测试对话框能正常打开与关闭(三种关闭途径:Escape、遮罩/Cancel、提交成功);
  3. 确认表单对所有必填字段生效(清空描述、输入非数字金额应看到错误提示);
  4. 检查成功添加的交易立即出现在仪表盘交易列表中,且余额正确变化;
  5. 确保非法数据(如重复提交同一笔交易)与网络问题(未启动 API)都有明确的错误反馈。

8.2 无障碍测试

  1. 仅使用键盘完成「打开对话框 → 填写表单 → 提交 → 关闭」的完整流程;
  2. 使用屏幕阅读器验证对话框标题、描述、字段标签与错误信息的播报;
  3. 验证焦点管理:打开时进入表单、关闭时返回触发按钮;
  4. 检查所有表单元素都有恰当的<label>

可用的手动验证命令:启动 API 后,模拟一笔来自其他来源的交易以验证数据新鲜度:

curl --request POST \ --header "Content-Type: application/json" \ --data "{ \"date\": \"2020-07-24\", \"object\": \"Bought book\", \"amount\": -20 }" \ http://localhost:5000/api/accounts/test/transactions

9. 评估标准(Rubric)

作业给出了三档评估维度,可用于自评:

维度优秀(Exemplary)合格(Adequate)待改进(Needs Improvement)
功能性功能完美,遵循课程全部最佳实践,用户体验优秀功能正确,但个别最佳实践缺失或有轻微可用性问题功能部分可用或存在显著可用性问题
代码质量组织良好、遵循既有模式、错误处理完善、与状态管理无缝集成可运行但组织欠佳或与现有代码模式不一致结构性缺陷或未与现有模式集成
无障碍完整键盘导航、读屏兼容、遵循 WCAG、焦点管理出色基本无障碍已实现,但缺少部分键盘/读屏细节几乎没有无障碍考虑
用户体验直观、精致的界面,反馈清晰,交互流畅体验良好,反馈或视觉有可改进之处界面混乱、缺少用户反馈

对照仓库解决方案:它在「功能」「代码质量」「用户体验」上均达到优秀档,无障碍部分实现了 Escape 关闭、焦点返还、ARIA 与错误播报,但焦点陷阱采用了简化实现,可作为自行加强的方向。


10. 进阶挑战(可选)

作业在基础需求之外,提供了两条进阶路线,全部可选:

增强功能:

  • 为交易添加分类(餐饮、交通、娱乐等);
  • 实现带实时反馈的输入校验(如输入即校验、防抖提示);
  • 为高级用户提供键盘快捷键;
  • 增加交易编辑与删除能力。

高级集成:

  • 为新添加的交易实现撤销(undo)功能;
  • 支持从 CSV 文件批量导入交易;
  • 实现交易搜索与过滤;
  • 实现数据导出。

这些进阶功能可以结合 7-bank-project/4-state-management/README.md 末尾的「Copilot Agent 挑战」(带 undo/redo 的完整状态历史系统)一起练习——状态历史数组配合不可变更新模式,正是实现撤销/重做的基础。


结语

通过本作业,你在一个真实的银行应用中完成了一条完整的功能开发链路:入口按钮 → 模态对话框 → 无障碍表单 → API 对接 → 集中式状态更新 → 即时 UI 刷新。其核心方法论——不可变状态、单一更新入口、自动持久化、刷新保持新鲜度——正是 Redux、Vuex、Zustand 等主流状态管理库的底层思想。仓库中的完整实现(7-bank-project/solution/app.js、7-bank-project/solution/index.html、7-bank-project/solution/styles.css)与 API 服务(7-bank-project/api/server.js)可作为你动手实现时的对照参考,但强烈建议先独立完成,再对照源码查漏补缺。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 11:15:28

牡丹芍药春季抹芽疏蕾技巧与养护要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 11:15:14

动态检索增强生成(RAG)技术解析与应用实践

1. 论文核心价值解析&#xff1a;动态检索增强生成的技术突破QR3AG这篇论文提出了一种创新性的动态检索增强生成框架&#xff0c;其核心价值在于解决了传统RAG&#xff08;检索增强生成&#xff09;系统中三个关键痛点&#xff1a;第一&#xff0c;传统静态检索机制存在"检…

作者头像 李华
网站建设 2026/9/12 11:15:07

Firecracker 如何配置 vhost-user 块设备并对接外部存储后端

Firecracker 如何配置 vhost-user 块设备并对接外部存储后端 【免费下载链接】firecracker Secure and fast microVMs for serverless computing. 项目地址: https://gitcode.com/GitHub_Trending/fi/firecracker 如果你的块设备 IO 需要在 Virtio 队列处理同层实现自定…

作者头像 李华
网站建设 2026/9/12 11:11:06

Android 16图形系统架构解析:从绘制到显示全链路拆解

做图形系统相关开发这些年&#xff0c;被问得最多的问题其实就一个&#xff1a;屏幕上的一张UI&#xff0c;到底是怎么从App里的代码变成像素的&#xff1f;尤其是到了Android 16这个版本&#xff0c;图形栈涉及的东西更多了&#xff0c;HDR、可变刷新率、多窗口、屏幕折叠形态…

作者头像 李华