第 15 课:编辑任务、弹窗复用和单条记录更新
第 15 课编辑任务、弹窗复用和单条记录更新这一课我们把任务页继续从“能看、能筛、能排、能分页”推进到能编辑一条已有任务能把旧数据回填进表单能只更新目标记录而不是整表重载能复用同一个弹窗组件同时支持“创建”和“编辑”这一步非常关键。因为很多初学者第一次做“编辑功能”时容易陷入两个常见误区复制一份几乎相同的创建弹窗再单独做一个编辑弹窗保存后直接重新请求整张列表不去思考“局部更新”和“整表重载”的区别所以这一课的核心问题是当页面已经有一张任务表时编辑单条记录到底应该怎样组织状态、复用表单组件并把更新控制在最小范围内先讲结论这一课最重要的 3 句话是创建任务和编辑任务往往共享的是同一套表单结构不应该轻易复制两份组件。编辑单条记录更适合先做本地数组中的目标项替换再同步写回服务层而不是默认整表重载。页面级 composable 负责“当前在编辑谁”和“更新哪一条记录”弹窗组件只负责“显示表单和提交草稿”。你只要先把这 3 句记住这一课的主线就不会乱。这一课我们改了什么这一轮更新了src/types/task.tssrc/services/taskService.tssrc/composables/useTasksPage.tssrc/components/tasks/TaskCreateDialog.vuesrc/components/tasks/TaskTable.vuesrc/views/TasksView.vuesrc/components/tasks/TaskPageHeader.vuesrc/composables/__tests__/useTasksPage.spec.tse2e/app.spec.tsdocs/README.mddocs/15-editing-and-local-updates.md从这份改动清单你可以看出“编辑任务”绝对不只是多一个按钮。它同时会牵涉类型设计页面级状态单条数据更新表单回填组件复用单元测试端到端测试这说明你现在正在进入一个更真实的前端工程阶段。为什么“编辑任务”不是再做一个新弹窗这一课里我们没有新建一个和创建弹窗几乎一样的TaskEditDialog.vue。而是继续使用TaskCreateDialog.vue只是把它升级成了一个更通用的“任务编辑器弹窗”。现在这个组件通过mode区分两种工作方式createedit同时还新增了initialTask它专门用来在编辑模式下把旧任务数据回填进表单。这一步很值得你学。因为在真实项目里创建编辑通常共享的是同一套字段标题负责人截止日期状态优先级描述也就是说它们的差别往往不在“表单结构”而在初始值不同标题文案不同主按钮文案不同提交后执行的动作不同所以你要建立一个判断标准如果两个界面结构几乎一样先想“怎么复用同一个组件”而不是马上复制一份。这一课里弹窗组件是怎么复用的现在 TaskCreateDialog.vue 增加了几组关键能力。1.mode它决定当前弹窗是在创建模式编辑模式基于这个模式组件会自动切换弹窗标题提示文案主按钮文案描述字段的默认兜底文案这说明什么说明组件复用不是“硬把所有逻辑塞一起”而是把公共结构留下再把变化点抽成可配置参数。2.initialTask它是编辑模式最关键的输入。因为编辑不是从空表单开始而是要把原来那条任务的值先展示出来。所以组件内部新增了fillTaskForm()它负责如果传入了旧任务就把旧任务回填到表单如果没传旧任务就恢复到默认创建表单这一层抽象非常好因为“回填旧数据”和“重置默认状态”都统一走这一个入口。3.watch(props.visible)这一步是很多新手第一次做编辑表单最容易漏掉的。为什么因为创建弹窗通常只需要在关闭后清空表单编辑弹窗则需要在每次打开时重新根据当前目标任务回填数据所以现在组件会在弹窗打开时创建模式回填空白默认值编辑模式回填initialTask这说明你开始学会区分关闭后的清理和打开时的初始化这是表单组件里非常重要的思维。为什么“当前正在编辑谁”要放在页面级 composable这一课里useTasksPage.ts 新增了这些状态editDialogVisibleeditingTaskIdeditingTaskeditingTaskDraft这样设计的原因很清楚编辑任务不是弹窗组件一个人的事情它会同时影响任务表格里点的是哪一行弹窗要回填哪条数据提交后要替换数组中的哪一项所以这里真正的核心状态是当前到底在编辑哪一条任务这显然属于页面级状态而不是弹窗内部状态。如果把它藏在弹窗里你很快就会失去和表格、任务数组之间的清晰连接。所以你要记住和“列表中哪一条记录被选中/被编辑”有关的状态通常都更适合放在页面级 composable。editingTaskId为什么比直接存整条对象更稳这一课里我们记录的是editingTaskId然后再用它去推导editingTaskeditingTaskDraft为什么不直接把整个任务对象塞进ref因为id更稳定、更轻。这样做的好处是1. 页面里只有一个“真正的任务源”当前完整任务对象仍然统一来自tasks而不是在别处又复制一份对象快照。2. 推导关系更清晰我们先知道“正在编辑哪条任务”然后再从主任务数组里把它找出来。3. 更不容易出现对象引用过期问题如果任务数组后面发生了局部更新基于id重新推导能更容易拿到最新值。所以这一课也在强化一个很重要的思维页面里尽量只维护最小必要状态其他值优先通过推导得到。为什么编辑不是“整表重载”这是这一课最核心的教学点。在 useTasksPage.ts 里这次新增了updateTask()它做的事情不是提交后重新调用loadTasks()而是找到当前被编辑任务在本地数组中的索引构造一条新的完整任务对象用splice直接替换那一项再把更新后的任务写回模拟服务也就是说这次编辑走的是单条记录更新而不是整表重载为什么这次更适合单条记录更新因为当前操作本质上是我只改了一条任务所以页面上真正变化的也只有这一条。如果你每次编辑后都重新请求整张列表虽然也能工作但会带来几个问题1. 状态成本更高你可能重新进入loadingsuccesserror这一整套请求流程。2. 用户感知更重表格可能整体闪一下甚至出现刷新提示。3. 逻辑上不够精确明明只是改一条记录却把整页当成一次全量刷新来处理。所以你要记住如果你已经知道目标记录是哪一条而且本地就有这条数据优先考虑局部更新。什么时候才更适合整表重载这并不代表整表重载永远不好。整表重载更适合这些情况后端更新后会重新计算很多附加字段当前页数据依赖复杂聚合结果本地很难准确推导最终状态你更信任后端返回的最新完整列表所以你以后要学会区分单条记录更新适合目标明确改动范围小本地可精确替换整表重载适合影响范围大服务端会做复杂再计算本地不容易保证最终一致性这正是这一课标题里“单条记录更新”和“整表重载”的本质区别。为什么服务层也要补updateTaskRecord这一课里taskService.ts 新增了updateTaskRecord()它会在模拟服务的内存数据库里根据id找到那条任务直接替换那一条记录这样做的意义是页面层不只是“看起来改了”而是把这次修改同步写回到了模拟数据源里。所以后面如果再重新加载列表这条修改不会丢失。这一步也很像真实项目里的分层思路页面层决定什么时候更新、更新谁服务层负责把更新写回数据源为什么表格组件只负责抛出edit这一课里TaskTable.vue 新增了编辑按钮edit事件它并没有自己决定打开什么弹窗用哪个表单更新哪条数据它只做了一件事把当前这一行任务抛给父组件这就说明表格组件的职责仍然很清晰它是列表展示组件它提供操作出口真正的业务动作仍由页面级逻辑决定这就是很典型的展示组件 事件出口思维。这一课里页面层是怎么串起整条编辑流程的在 TasksView.vue 里现在编辑流程被串成了这样表格点击“编辑”TaskTable把当前行任务抛给父层页面层调用openEditDialog(task)useTasksPage记录editingTaskId编辑弹窗打开并根据editingTaskDraft回填表单点击“保存修改”后页面层调用updateTask()本地目标记录被替换模拟服务里的同一条记录也被同步更新这条链路非常适合你拿来练习复述。因为它能帮助你彻底区分哪一步是组件事件哪一步是页面状态哪一步是服务层写入为什么更新成功后要清空editingTaskId这一课里更新完成后会顺手把editingTaskId重置成null。原因很简单编辑流程已经结束了旧的编辑目标不应该继续残留。如果不清空很容易出现下一次打开弹窗时还带着旧目标页面逻辑误以为当前还在编辑所以这一步本质上是在做编辑会话结束后的状态清理这和创建弹窗关闭后重置表单本质上是同一种状态卫生习惯。为什么编辑后不主动回到第一页这一课和“创建任务”有一个重要区别。创建任务后我们会主动回到第一页因为新任务被插到了数组最前面。但编辑任务后我们没有强行回第一页。为什么因为编辑通常只是修改当前记录内容用户仍然应该停留在原来的上下文里。这一步非常值得你注意因为它说明不同动作不一定共享同一套交互后处理。创建和编辑都操作了任务但创建更像“插入一条新记录”编辑更像“在原位置更新一条旧记录”。所以后续交互也不该完全一样。这一课为什么还要补单元测试这次我们在 useTasksPage.spec.ts 里新增了一条关键测试updates a task in place and closes edit dialog它验证了这些点编辑弹窗是否真的打开当前编辑草稿是否正确回填旧标题调用更新后本地任务数组里的目标记录是否被替换模拟服务更新函数是否真的被调用编辑弹窗是否关闭editingTaskDraft是否被清空这条测试非常有价值因为它不是只测“有没有变”。它测的是一整条局部更新流程有没有闭环。为什么这次还要补端到端测试这一轮在 app.spec.ts 里我们还新增了一个真实交互测试登录进入任务页点击编辑修改标题保存验证表格里出现新标题这条测试的意义在于单元测试告诉你页面级逻辑有没有写对端到端测试则告诉你真实按钮真实弹窗真实输入框真实表格更新这一整套 UI 流程到底能不能跑通。这就是为什么前端工程里通常既需要单元测试也需要 e2e。真实项目里编辑功能最容易犯的 10 个错误1. 为创建和编辑各复制一份几乎相同的弹窗组件这样后面维护会越来越痛苦。2. 编辑弹窗打开时不回填旧数据用户会看到一张空表单不知道自己在改什么。3. 把“当前正在编辑谁”放在弹窗内部这样页面层和列表层很难保持清晰关联。4. 明明只改一条记录却每次都整表重载能跑但不够精确也不够轻。5. 只改本地数组不写回服务层一旦重新加载修改就丢了。6. 更新成功后不清理编辑目标下一次打开容易残留旧状态。7. 让表格组件自己负责打开弹窗和更新数据这样展示层和业务层会快速耦合在一起。8. 编辑和创建共用表单时不把变化点抽成配置最终会把组件内部写成一堆难懂的条件分支。9. 只测提交结果不测回填流程很多编辑 bug 恰恰出在“打开弹窗时旧数据没带对”。10. 不区分“创建后应该跳转”和“编辑后应该停留”不同动作往往需要不同的交互后处理。你现在应该能回答的 10 个问题为什么创建和编辑通常适合复用同一个表单弹窗mode和initialTask分别解决了什么问题为什么编辑流程里“当前正在编辑谁”更适合放在页面级 composable为什么这次记录的是editingTaskId而不是直接保存整条任务对象为什么编辑任务更适合先做本地单条记录替换而不是默认整表重载单条记录更新和整表重载分别更适合什么场景为什么服务层也要补updateTaskRecord()为什么表格组件只负责抛出edit事件而不直接处理编辑逻辑为什么编辑完成后要清空editingTaskId为什么创建成功后常常回第一页而编辑成功后通常不需要这一课的动手练习练习 1打开 useTasksPage.ts自己按顺序口述一遍openEditDialog()editingTaskIdeditingTaskDraftupdateTask()这 4 个点分别负责什么。目标把编辑功能背后的页面级状态链讲清楚。练习 2打开 TaskCreateDialog.vue自己回答mode改变了哪些界面表现initialTask为什么只在编辑模式下有意义为什么这里要在弹窗打开时回填表单目标训练你识别“组件复用时哪些是公共结构哪些是可配置变化点”。练习 3试着自己继续扩展一个“编辑后联动”的小需求比如编辑任务状态后如果当前筛选不再匹配这条任务会怎样变化编辑截止日期后如果当前列表正在按日期排序这条任务会不会自动换位置目标训练你开始从“单个功能”过渡到“功能之间如何联动”的思维。这节课的复习结论把这一课压缩成 9 句话创建和编辑大多共享同一套表单结构所以优先考虑复用同一个弹窗组件。mode用来区分工作方式initialTask用来在编辑模式下回填旧数据。“当前正在编辑谁”属于页面级状态不应该藏在弹窗内部。用editingTaskId再去推导editingTaskDraft通常比直接保存整条对象更稳。编辑单条记录时如果目标明确且本地已有数据优先考虑局部更新。局部更新的核心是替换本地数组中的目标项再同步写回服务层。表格组件只负责抛出edit事件真正的编辑流程应由页面层统一编排。创建和编辑虽然都操作任务但交互后处理不一定一样不能机械复用。当功能开始涉及“表格 弹窗 服务层 测试”四层协同时工程意识会比写单个按钮更重要。