主题
记账本项目实战(二)
本页对应 记账本项目实战,共 7 段视频,约 1 小时 1 分。
学习目标
- 能够完成项目环境搭建并配置路径别名,通过实际导入模块验证别名路径可正常解析。
- 能够基于接口返回的账单列表渲染单日账单,并通过映射表将账单类型英文标识转换为中文显示。
- 能够使用状态控制单日账单列表的展开与收起,并同步切换箭头方向与列表显隐。
- 能够封装账单类型图标组件,根据传入的账单类型动态渲染对应图标。
- 能够搭建新增账单页面结构,并实现支出/收入切换时按钮激活状态与账单列表数据的联动更新。
- 能够收集新增账单表单数据并完成提交,包括金额正负处理、账单类型选择、日期选择与格式化显示,以及提交后列表数据更新。
知识点
本节目标与边界说明
本节只做一件事:用 CRA 创建项目,并安装项目后续开发所需的基础依赖。需要先说明:本节笔记没有提供图片,也没有给出可保留的代码片段;转写稿讲的是月度账单列表、展开收起、图标组件、新增账单等业务功能,与环境搭建不是同一主题,不能硬套到本节。因此下面只围绕笔记中的依赖清单展开,不编造转写稿里不存在的环境搭建步骤。
为什么先做环境搭建
环境搭建是后续所有功能的地基。没有统一的项目脚手架和依赖,后面写 Redux、路由、时间格式化、类名拼接、移动端组件、接口请求时,就会出现“代码能看懂但跑不起来”的问题。这里的思路不是一次性把所有高级配置做完,而是先保证项目最小可运行,再按职责安装基础包。做法是:先用 CRA 创建 React 项目,再进入项目目录,按笔记清单一次性安装依赖,最后启动项目验证。
使用 CRA 创建项目
CRA 的作用是快速生成一个可运行的 React 项目结构,省去手动配置构建工具的成本。可以这样执行:
bash
npx create-react-app my-app
cd my-app这里的 my-app 是示例项目名,实际可按项目命名。创建完成后先不要急着写业务代码,先运行:
bash
npm start能正常打开页面,说明脚手架可用。易错点:命令要在目标父目录执行;进入项目目录后再安装依赖;不要把依赖装到项目外层目录,否则 package.json 不会记录到当前项目。
安装基础依赖清单
本节笔记要求安装以下基础包:
bash
npm install @reduxjs/toolkit react-redux react-router-dom dayjs classnames antd-mobile axios这条命令对应六类能力:Redux 状态管理、路由、时间处理、类名处理、移动端组件库、请求插件。安装完成后检查 package.json 的 dependencies 是否出现这些包名。易错点:包名要逐字核对,尤其是 @reduxjs/toolkit 带作用域和斜杠,react-router-dom 不是 react-router,antd-mobile 不是 antd,classnames 是复数。
Redux 状态管理:@reduxjs/toolkit 与 react-redux
为什么需要:项目后续会有跨组件共享状态和异步请求状态,单靠组件内部 state 不好维护。怎么想:@reduxjs/toolkit 负责创建 store、slice、reducer 和异步 action;react-redux 负责把 store 接入 React 组件。怎么做:先安装两个包,后续再创建 store 并用 Provider 包裹应用。易错点:只装 @reduxjs/toolkit 不装 react-redux,React 组件无法方便地读取和派发;不要把两个包名混成一个。
路由:react-router-dom
为什么需要:项目会有页面级切换,例如月度账单、新增账单等。怎么想:Web 端 React 路由使用 react-router-dom,它提供路由容器、路由匹配和跳转能力。怎么做:先安装,后续在入口处配置路由。易错点:不要安装成 react-router 后直接照 Web 示例写;包名要写完整 react-router-dom。
时间处理:dayjs
为什么需要:账单日期、时间戳展示和格式化会反复出现。怎么想:用轻量时间库统一处理,避免手写补零和格式拼接。怎么做:安装后在需要格式化的组件中引入使用。易错点:不要和原生 Date 混用到难以排查;格式化模板要按实际展示需求写。
类名处理:classnames
为什么需要:条件类名、激活态类名在组件中很常见,字符串拼接容易漏空格或写出多余空格。怎么想:用 classnames 根据条件生成类名。怎么做:安装后在组件中引入,把条件对象或数组传进去。易错点:布尔条件与类名字符串要分清;不要手写一长串三元表达式导致可读性下降。
移动端组件库:antd-mobile
为什么需要:项目是移动端界面,需要按钮、弹窗、表单、选择器等基础组件。怎么想:使用 antd-mobile 提供移动端组件,而不是桌面端 antd。怎么做:安装后按需引入组件。易错点:不要把 antd-mobile 和 antd 混装混用;组件样式和交互要按移动端文档使用。
请求插件:axios
为什么需要:项目需要和后端接口通信,例如获取账单列表、提交新增账单。怎么想:用 axios 统一发请求,后续可封装请求模块。怎么做:安装后在需要请求的地方引入,或先建请求封装文件。易错点:不要把请求地址写死在多个组件里;安装后先确认包已进入 dependencies。
安装后的验证与易错点汇总
安装完成后建议做三件事:第一,查看 package.json 中依赖是否齐全;第二,运行 npm start 确认项目能启动;第三,在入口文件临时引入一个依赖做编译验证,确认无包名错误后删除临时代码。常见易错点包括:在错误目录安装、包名拼写错误、把移动端组件库装成桌面端库、只装 Redux 核心包却漏装 react-redux、把路由包名写错。环境搭建不追求复杂,先把“能启动、依赖齐全、包名正确”这三件事做稳,后续业务代码才有可靠基础。
先核对:转写稿与本节标题不匹配
本节笔记标题是「配置别名路径」,但提供的转写稿共 7 个视频,内容全部围绕「月度账单」与「新增账单」功能展开,没有出现“别名路径”“alias”“vite.config”“webpack 配置”“@ 路径映射”等相关讲解。具体对应关系如下:
- 视频1:月度账单——单日账单列表渲染显示;
- 视频2:月度账单——点击切换账单展开和收起;
- 视频3:月度账单——账单类型图标组件封装;
- 视频4:新增账单——功能熟悉和结构搭建;
- 视频5:新增账单——支出和收入功能实现;
- 视频6:新增账单——新增表单实现;
- 视频7:新增账单——收尾优化。
因此,无法依据这份转写稿扩写出「配置别名路径」的教程正文。若强行补写 resolve.alias、jsconfig.json 的 paths、@ 指向 src 等内容,就会脱离给定材料,属于编造,不符合“不得编造”的要求。
转写稿中唯一与“路径”有关的片段
视频3 封装图标组件时,提到图标地址的拼接思路:
“前面这个基地址呢可以不变,只更改后面这个地址的名字就可以了……把这个 type 咱给它拼接到这个 food 的位置。”
这里的“路径”指的是阿里云图标 URL 中最后一段文件名的替换,用于根据账单类型显示不同图标;它解决的是“不同类型渲染不同图标”的问题,并不是项目工程里的模块别名路径配置。两者不是同一个知识点。
建议补充的材料
如果要按「配置别名路径」扩写,需要补充对应视频的转写稿,至少应包含以下信息,才能做到“为什么需要 → 怎么想 → 怎么做 → 易错点”的完整推导:
- 当前项目使用的是什么构建工具,例如 Vite 还是 Webpack;
- 配置写在哪个文件里,例如
vite.config.js还是webpack.config.js; - 别名符号是什么,例如
@是否指向src; - 是否有配套的编辑器提示配置,例如
jsconfig.json或tsconfig.json; - 讲师的调试过程:配置后如何重启服务、如何验证
@/xxx能正常解析、报错时如何排查; - 讲师强调的易错点,例如路径末尾是否带
/、是否只改配置未重启、是否与已有别名冲突等。
当前可确认的结论
在现有材料下,只能确认转写稿讲的是账单列表渲染、展开收起、图标组件封装以及新增账单的完整流程;不能据此写出「配置别名路径」的教程正文。请提供与本节标题对应的转写稿,我再按你要求的小标题分段、保留图片与代码、补充推导和调试过程,扩写成 1000–1800 字的教程正文。
本节范围与目标

本节笔记只记录“背景知识”中的两项配置:一是 Webpack 的路径解析配置,把 @/ 解析为 src/;二是 VSCode 的路径联想配置,让编辑器在输入 @/ 时自动联想出 src/ 下的子级目录。转写稿主要记录月度账单列表、展开收起、图标组件封装、新增账单等业务实现,并未包含本节配置的逐步推导和调试录像。因此下面只依据笔记扩写,不把账单业务代码混入背景配置;其中“固定前缀不变、只替换后续路径名”的思路,可类比转写稿中图标地址“前面基地址不变,只改后面名字”的处理方式,用来帮助理解 @/ 的作用。
知识点一:Webpack 路径解析配置(@/ → src/)
为什么需要: 项目文件层级变深后,相对路径会出现大量 ../../,既难读又容易在移动文件后失效。把 @/ 固定映射到 src/,可以让导入路径更稳定、更短,也便于团队统一写法。
怎么想: 这里要区分“编辑器认识”和“构建工具认识”。Webpack 负责真正打包时的模块解析,所以必须在 Webpack 配置中告诉它:遇到以 @/ 开头的路径,不要当成 npm 包,而是去项目 src/ 下找。思路是配置 resolve.alias,把 @ 指向 src 的绝对路径。
怎么做: 在 Webpack 配置的 resolve.alias 中增加别名,示意如下,具体字段以项目实际配置文件为准:
js
// webpack 配置示意
const path = require('path')
module.exports = {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
}配置后,import xxx from '@/components/Icon' 应被解析到 src/components/Icon。如果项目使用 Vue CLI、Create React App 等封装工具,要找到其允许扩展 Webpack 配置的入口,而不是随意新建配置文件。
易错点: 一是只配了 VSCode 没配 Webpack,编辑器不报错但构建失败;二是只配了 Webpack 没配 VSCode,能构建但编辑器提示找不到模块;三是别名指向了相对路径或错误目录,导致解析到项目根目录之外;四是 @ 与 @/ 写法不统一,团队内要约定固定写法。
知识点二:VSCode 路径联想配置
为什么需要: Webpack 配置只保证“能打包”,但 VSCode 的自动补全、跳转和类型提示还需要编辑器自己理解路径映射。否则输入 @/ 时没有联想,开发体验差,也容易手写错目录名。
怎么想: VSCode 通常通过项目根目录的 jsconfig.json 或 tsconfig.json 的 compilerOptions.paths 理解路径别名。关键是让编辑器知道 @/* 对应 src/*,并且 baseUrl 要设置正确。它和 Webpack alias 是“两套系统、同一套映射”,必须保持一致。
怎么做: 在项目根目录配置示意如下,按项目实际使用 JS 或 TS 选择文件:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}保存后,在代码中输入 @/ 时,VSCode 应能联想 src/ 下的子级目录。若没有立即生效,可重启 VSCode 或检查文件是否在项目根目录、是否被工作区正确加载。
易错点: 一是 paths 写成 "@": ["src"] 还是 "@/*": ["src/*"] 要与实际导入写法匹配;二是 baseUrl 写错导致映射基准不对;三是项目已有 tsconfig.json 却又新建 jsconfig.json,两者冲突;四是 Webpack 的 alias 与 VSCode 的 paths 不一致,出现“编辑器能跳、构建报错”或相反情况。
联调与验证顺序
先配 Webpack,保证构建解析正确;再配 VSCode,保证输入联想和跳转正确。验证时不要只看编辑器是否联想,还要实际运行构建,确认 @/ 导入能被打包。转写稿没有给出本节配置的运行结果,因此这里不虚构“已成功”的结论,只给出可执行的检查顺序:输入 @/ 看联想是否出现;打开导入文件看能否跳转;执行构建看是否报模块找不到;若失败,优先核对 alias 与 paths 是否一一对应。这样能把“编辑器体验”和“构建解析”分开排查,避免把两类问题混在一起。
代码示例
示例一:单日账单列表渲染 + 账单类型中英文适配
jsx
// ① src/contants/index.js —— 中英文映射表
const useForList = [
{ type: 'food', name: '餐饮' },
{ type: 'drinks', name: '酒水饮料' },
{ type: 'dessert', name: '甜品零食' },
{ type: 'shopping', name: '购物' },
{ type: 'salary', name: '工资' },
{ type: 'traffic', name: '出行交通' }
]
// 用 reduce 把数组累加成对象:{ food: '餐饮', drinks: '酒水饮料', ... }
export const itemType = useForList.reduce((acc, item) => {
acc[item.type] = item.name
return acc
}, {})
// ② src/pages/Month/components/Day/index.jsx —— 单日账单列表
import dayjs from 'dayjs'
import { itemType } from '@/contants'
import './index.scss'
export default function Day({ date, billList = [] }) {
return (
<div className="dailyBill">
<div className="header">
<span className="date">{dayjs(date).format('DD')}</span>
<span className="month">{dayjs(date).format('YYYY/MM')}</span>
</div>
<div className="billList">
{billList.map((item) => (
<div className="bill" key={item.id}>
<div className="detail">
{/* 核心:用接口返回的英文 useFor 当 key,取出中文 */}
<p className="useFor">{itemType[item.useFor]}</p>
<p className="time">{dayjs(item.date).format('MM-DD HH:mm')}</p>
</div>
<div className="money">{item.money.toFixed(2)}</div>
</div>
))}
</div>
</div>
)
}关键行与易错点
reduce的初始值必须传{},否则第一个元素会被当成累加器,映射表直接错位。itemType[item.useFor]是整段适配的核心,useFor是接口返回的英文(drinks/salary),映射表把它翻译成中文。- 易错点:
billList要给默认值= [],否则父组件数据还没回来时map直接报错白屏。 - 易错点:
key用接口返回的id,不要用index,列表新增/删除时会出现渲染错乱。 - 传进来的
billList应该是已经按天分好组的数据,分组逻辑放在父组件,子组件只负责渲染。
示例二:点击展开/收起 + 账单类型图标组件封装
jsx
// ① src/components/Icon/index.jsx —— 图标组件
// 图标都存在阿里云,前缀固定,只有最后的文件名随 type 变化
const BASE_URL = 'https://xxx.oss-cn-beijing.aliyuncs.com/'
export default function Icon({ type }) {
return <img className="icon" src={`${BASE_URL}${type}.svg`} alt={type} />
}
// ② src/pages/Month/components/Day/index.jsx —— 展开收起 + 使用图标
import { useState } from 'react'
import classNames from 'classnames'
import dayjs from 'dayjs'
import Icon from '@/components/Icon'
import { itemType } from '@/contants'
import './index.scss'
export default function Day({ date, billList = [] }) {
// 默认收起
const [visible, setVisible] = useState(false)
return (
<div className="dailyBill">
<div className="header">
<span className="date">{dayjs(date).format('DD')}</span>
<img
className={classNames('arrow', visible && 'extend')}
onClick={() => setVisible(!visible)}
src="/arrow.png"
alt=""
/>
</div>
{/* 用状态驱动显示隐藏 */}
<div className="billList" style={{ display: visible ? 'block' : 'none' }}>
{billList.map((item) => (
<div className="bill" key={item.id}>
<Icon type={item.useFor} />
<div className="detail">
<p className="useFor">{itemType[item.useFor]}</p>
</div>
<div className="money">{item.money.toFixed(2)}</div>
</div>
))}
</div>
</div>
)
}关键行与易错点
useState(false)决定初始收起,setVisible(!visible)是取反,点一次开、再点一次关。classNames('arrow', visible && 'extend')里visible && 'extend'在false时会返回false,classnames会自动忽略,所以不会出现arrow false这种脏类名。- 图标组件的关键:
src={${BASE_URL}${type}.svg},路径前缀不变,只把type拼到文件名位置,所以传drinks就显示酒杯、传dessert就显示甜甜圈。 - 易错点:模板字符串里
${}不要写成{},也不要漏掉.svg后缀,否则图片 404。 - 易错点:
Icon的 props 名要和调用处一致(组件里叫type,父组件就传type={item.useFor}),命名对不上会渲染出一张undefined.svg。
示例三:新增账单——支出/收入切换 + 表单收集与提交
jsx
// ① src/store/modules/billSlice.js —— 同步 reducer 与异步 action
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'
const billStore = createSlice({
name: 'bill',
initialState: { billList: [] },
reducers: {
// 同步:把新增的一条塞进列表
addBillList(state, action) {
state.billList.push(action.payload)
}
}
})
export const { addBillList } = billStore.actions
// 异步:先发请求,成功后再走同步 reducer
export const addBillListAsync = createAsyncThunk(
'bill/addBillListAsync',
async (payload, { dispatch }) => {
await axios.post('http://localhost:8888/ka', payload)
dispatch(addBillList(payload))
}
)
export default billStore.reducer
// ② src/pages/New/index.jsx —— 新增账单页面
import { useState } from 'react'
import classNames from 'classnames'
import dayjs from 'dayjs'
import { useDispatch } from 'react-redux'
import { addBillListAsync } from '@/store/modules/billSlice'
import { billList } from '@/contants/billList' // { pay: [...], income: [...] }
import Icon from '@/components/Icon'
import './index.scss'
export default function New() {
const [billType, setBillType] = useState('pay') // pay 支出 / income 收入
const [money, setMoney] = useState(0) // 金额,受控
const [useFor, setUseFor] = useState('') // 选中的账单类型
const [date, setDate] = useState(dayjs().valueOf()) // 记账时间
const dispatch = useDispatch()
const saveBill = () => {
const data = {
type: billType,
money: billType === 'pay' ? -money : +money, // 支出为负、收入为正
date,
useFor
}
dispatch(addBillListAsync(data))
}
return (
<div className="keepAccounts">
<div className="header">
<button
className={classNames('pay', { selected: billType === 'pay' })}
onClick={() => setBillType('pay')}
>
支出
</button>
<button
className={classNames('income', { selected: billType === 'income' })}
onClick={() => setBillType('income')}
>
收入
</button>
</div>
<div className="time">{dayjs(date).format('MM月DD日')}</div>
<div className="money">
<input
type="text"
value={money}
onChange={(e) => setMoney(e.target.value)}
placeholder="0.00"
/>
</div>
<div className="type">
{/* 用 billType 当 key 取列表,支出/收入自动切换 */}
{billList[billType].map((item) => (
<div
key={item.type}
className={classNames('item', { selected: useFor === item.type })}
onClick={() => setUseFor(item.type)}
>
<Icon type={item.type} />
<span>{item.name}</span>
</div>
))}
</div>
<button className="save" onClick={saveBill}>
保存
</button>
</div>
)
}关键行与易错点
billType的取值'pay'/'income'不是随便起的,要和后端接口、billList静态数据的 key 完全一致,否则billList[billType]取到undefined直接崩。billList[billType].map(...)一行同时完成"支出/收入列表切换",不用写两套 JSX。selected用classNames的对象写法,条件为 true 时才追加类名,这是按钮高亮和分类项高亮的统一做法。- 金额适配易错点:
money存的是用户输入的正数,提交前才按billType转成-money/+money,不要在onChange里就取负,否则用户切换支出收入时会出错。 addBillList里用state.billList.push(...),这是 RTK 内置 Immer 的写法,直接改就行,不要写成state.billList = [...state.billList, payload]之外的多余拷贝,也别忘了dispatch(addBillList(payload))才会真正入列表。- 易错点:
useFor初始值是空串,如果用户没选分类就点保存,接口会收到useFor: '',实际项目里要做非空校验。
常见错误
别名
@/配完不生效,或编辑器不联想- 现象:终端编译报
Module not found: @/xxx;或代码能跑但 VS Code 输入@/没有任何提示。 - 原因:只配了一边——webpack 的
resolve.alias和编辑器读取的jsconfig.json(compilerOptions.paths)是两套东西,缺一个就瘸腿;也有配完不重启导致缓存旧配置。 - 修法:两处同时配,
@/* → src/*;改完必须重启 dev server,并在 VS Code 里执行一次 "Restart TS Server / Reload Window" 再验证联想。
- 现象:终端编译报
点开一天的账单,所有天一起展开
- 现象:点某一个箭头,列表里每一天的明细同时显示/收起。
- 原因:
useState声明在了父级列表组件里,被map出来的所有单日项共享了同一个visible。 - 修法:把"展开/收起"的状态下沉到每个单日子组件内部,父组件只负责传
billList;若确实要父级统一控制,就把状态改成以date为 key 的对象({ [date]: true })。
图标组件渲染出来是裂图 / 空白
- 现象:
<img>有元素但图片 404,控制台一堆资源加载失败。 - 原因:直接把接口返回的
type拼进 URL,接口给的是drinks、salary这类值,而图标文件名大小写或命名规则不一致;另外type为空(未选中)时会拼出残缺地址。 - 修法:不要裸拼字符串,先过一层映射表
{ drinks: 'Drink', salary: 'Salary' },映射不到时给默认图标兜底;拼接前判空。
- 现象:
中英文适配后页面显示
undefined- 现象:账单类型位置不显示中文,或直接报
Cannot read properties of undefined。 - 原因:
reduce里忘记return(箭头函数写成块体却没返回值),累加器变成undefined;或初始值没给{}。 - 修法:
reduce回调必须显式return,初始值传{};在组件里取值时再加一层兜底map[item.useFor] || item.useFor。
- 现象:账单类型位置不显示中文,或直接报
金额提交上去正负号不对,统计全乱
- 现象:明明是支出,接口收到的是正数;或者收到字符串
"+100"。 - 原因:没根据
billType做正负处理;或用了+value而value来自 input 是字符串,+变成了字符串拼接。 - 修法:先
Number(money)转数值,再按billType === 'pay' ? -n : n处理;同时校验空值/0 不允许提交。
- 现象:明明是支出,接口收到的是正数;或者收到字符串
输入框打字打不进去 / 输入后状态不更新
- 现象:光标闪烁但内容不变,控制台警告 "You provided a
valueprop to a form field without anonChangehandler"。 - 原因:只写了受控的
value,漏了onChange;或onChange里取的是e.target.value之外的东西。 - 修法:
value与onChange成对出现,onChange内setMoney(e.target.value),并在提交时统一做类型转换。
- 现象:光标闪烁但内容不变,控制台警告 "You provided a
新增成功但列表不刷新,要手动刷新页面才看到
- 现象:接口 200,Network 里能看到数据,但页面列表没有新记录。
- 原因:异步 thunk 里只调了接口,没有
dispatch同步 reducer 去更新 store;或 reducer 里直接push到state之外的新数组却没被 Immer 追踪到。 - 修法:thunk 内
await请求成功后立刻dispatch(addBill(payload));RTK 里放心写state.list.push(...),别整体重新赋值成state = ...。
时间选择器确定后弹层不关 / 显示的还是今天
- 现象:点了确定,弹窗还挂着;或者选完 4 月 3 日,输入区仍写"今天"。
- 原因:
visible状态没在onConfirm里置回false;显示区直接写死了dayjs()而不是格式化用户选中的时间戳。 - 修法:
onConfirm中先setDate(value)、再setVisible(false);展示用dayjs(date).format('MM月DD日')绑定同一份状态。
练习
- 单日汇总:在单日账单列表头部加一个"当日支出合计 / 收入合计",用
reduce按type分组累加。要求:只统计pay与income两类,金额取绝对值展示。 - 图标组件健壮化:把现在"字符串拼 URL"的写法改成映射表 + 默认兜底图标,并在
type为空时不渲染<img>(渲染占位)。思考:如果后端新增了类型而前端没更新映射表,用户会看到什么?如何用propTypes/默认参数降低风险? - 表单校验与提交反馈:给新增账单加上三条校验——金额必填且大于 0、必须选中类型、日期必选;提交成功后清空表单并给出成功提示(
antd-mobile的Toast),失败时提示错误并保留已填内容。 - 日期可选范围:给时间选择器加
max={new Date()}限制不可选未来日期;并额外做一件事——把选中日期持久化到 Redux,使"切到收入/支出"时不会丢失已选日期。 - 展开状态记忆:将展开/收起状态改为"默认展开当天,其他收起",并实现点击箭头时同时切换箭头方向的图标类名。思考:如果要记住用户上次展开的是哪一天(刷新后仍保留),状态应该放在哪里?
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 月度账单-列表区域-单日账单列表渲染显示 | 04:47 |
| 2 | 月度账单-列表区域-点击切换账单展开和收起 | 06:01 |
| 3 | 月度账单-账单类型图标组件封装 | 07:42 |
| 4 | 新增账单-功能熟悉和结构搭建 | 04:23 |
| 5 | 新增账单-支出和收入功能实现 | 09:47 |
| 6 | 新增账单-新增表单实现 | 17:30 |
| 7 | 新增账单-收尾优化 | 11:24 |
小结
- 别名配置是"编译期 + 编辑器"双轨制,任何一边缺失都会表现为"代码能跑但开发体验崩",配置类问题优先怀疑缓存与重启。
- 列表类 UI 的状态粒度决定 bug 形态:状态放在哪一层,就决定了"一项操作影响一片还是只影响一项",展开收起、选中高亮都遵循这个规律。
- 组件封装的价值在于"把变化点外置为 props"——图标组件的变化点只是 URL 尾部的类型名,因此
type就是它唯一该关心的入参。 - 凡是"英文 key → 中文文案/资源地址"的适配,都要经过映射层,而不是在渲染处硬拼;映射层必须考虑 key 缺失的兜底分支。
- 数据驱动视图的固定套路是:先有 state → 再绑定切换事件 → 最后用 state 适配样式和内容,三步顺序不能颠倒,否则会写出"手动操作 DOM"的返工代码。
- 提交表单的难点不在
dispatch,而在"字段名、类型、正负号、时间格式"是否与后端契约严格一致,这类对齐问题应在console.log阶段先验证完再往下写。 - 异步 action 的完整链路是"组件收集 → thunk 请求 → dispatch 同步 reducer → store 更新 → 视图刷新",任何一环断开,表现都是"接口成功但页面不动"。
- 收尾工作(点击激活、时间选择、时间格式化、弹层关闭)永远比核心逻辑更耗时,功能排期时应把这类"细节优化"单独留出时间。