29-表单返回就丢内容-用草稿快照和离开守卫保住输入
第29篇表单返回就丢内容用草稿快照和离开守卫保住输入摘要表单返回就丢内容本质是把输入态当成页面临时变量。用户填了一半跳去选图片、看协议或接电话页面重建后输入全没了。更稳的做法是把表单拆成输入态、草稿快照和提交结果输入变化时更新脏状态关键节点保存草稿返回时用离开守卫确认提交成功后再清理草稿。我见过一个课程发布表单标题、简介、封面都填完后用户进入图片选择页再返回页面重新创建之前输入全部丢了。临时把字段塞进全局变量能救一条路径但切账号、提交失败和重新进入又会出现旧草稿回潮。表单要按草稿链路设计而不是靠页面实例活着。这篇文章解决四个实际问题如何区分页面输入态、草稿态和已提交事实。如何保存草稿快照而不把半成品当正式数据。如何在返回时提示用户避免误丢内容。如何验证重进、提交失败、提交成功和清理路径。先分清三种状态状态说明生命周期输入态当前页面输入框里的值页面内高频变化草稿态可恢复的未提交快照跨页面、可过期提交态已保存到业务数据长期事实表单丢内容通常是输入态没有及时变成草稿态旧草稿回潮则是提交成功后没有清理草稿态。定义草稿快照exportinterfaceCourseFormDraft{draftId:stringtitle:stringsummary:stringcoverUri:stringupdatedAt:number}exportfunctionisEmptyDraft(draft:CourseFormDraft):boolean{returndraft.title.trim().length0draft.summary.trim().length0draft.coverUri.length0}草稿只保存用户输入不保存提交状态、Toast 文案和页面 loading。这样恢复时不会把旧交互状态也带回来。DraftStore 只负责读写草稿exportinterfaceDraftStore{load(draftId:string):PromiseCourseFormDraft|nullsave(draft:CourseFormDraft):Promisevoidremove(draftId:string):Promisevoid}exportclassCourseDraftService{constructor(privatereadonlystore:DraftStore){}asyncsaveDraft(draft:CourseFormDraft):Promisevoid{if(isEmptyDraft(draft)){awaitthis.store.remove(draft.draftId)return}awaitthis.store.save({...draft,updatedAt:Date.now()})}}空草稿直接清理避免用户每次进入都看到没有意义的恢复提示。页面输入变化只更新当前草稿Componentstruct CourseEditorPage{Statedraft:CourseFormDraftcreateEmptyCourseDraft()Statedirty:booleanfalseprivatedraftService:CourseDraftServicecreateCourseDraftService()privateasyncupdateTitle(value:string):Promisevoid{this.draft{...this.draft,title:value}this.dirtytrueawaitthis.draftService.saveDraft(this.draft)}privatehasUnsavedInput():boolean{returnthis.dirty!isEmptyDraft(this.draft)}}高频输入可以加防抖但 owner 不变页面持有输入态DraftService 保存可恢复快照。返回前用离开守卫exporttypeLeaveDecisionleave|stay|save_then_leaveexportclassFormLeaveGuard{decide(dirty:boolean,draft:CourseFormDraft):LeaveDecision{if(!dirty||isEmptyDraft(draft)){returnleave}returnstay}}守卫只做判断不弹窗、不保存。页面根据结果决定是否展示确认弹窗或直接返回。提交成功后清草稿exportclassCoursePublishService{constructor(privatereadonlyrepository:CourseRepository,privatereadonlydraftStore:DraftStore){}asyncpublish(draft:CourseFormDraft):Promisevoid{awaitthis.repository.save({title:draft.title.trim(),summary:draft.summary.trim(),coverUri:draft.coverUri})awaitthis.draftStore.remove(draft.draftId)}}提交失败时不要清草稿。用户修正后还能继续提交这比让用户重新填写更符合真实使用路径。验证清单填一半进入图片选择页返回后内容仍在。关闭应用再打开编辑页能恢复未提交草稿。提交失败后草稿仍在错误可见。提交成功后再次进入不出现旧草稿。空表单返回不弹无意义确认。切账号后不会恢复另一个账号的草稿。草稿要有过期和归属草稿不是永久缓存。一个账号的草稿不能被另一个账号看到太久以前的草稿也不一定适合自动恢复。字段用途ownerId区分账号或本地用户域draftId区分新增、编辑、复制等入口updatedAt判断是否过期source区分手动编辑、导入恢复等来源如果没有 owner切账号后容易恢复旧草稿如果没有 updatedAt半年以前的半成品也可能突然弹出来。恢复草稿前要先判断它是否属于当前用户和当前入口。恢复草稿前先做判断exportinterfaceDraftRestorePolicy{shouldRestore(draft:CourseFormDraft,ownerId:string,now:number):boolean}exportclassDefaultDraftRestorePolicyimplementsDraftRestorePolicy{shouldRestore(draft:CourseFormDraft,ownerId:string,now:number):boolean{constexpirednow-draft.updatedAt7*24*60*60*1000if(expired){returnfalse}returndraft.draftId.startsWith(ownerId)}}恢复策略不要写在 UI 弹窗里。页面可以问“是否有可恢复草稿”但判断草稿是否属于当前入口应该交给策略或 Service。发布前回归路径新增页草稿和编辑页草稿互不覆盖。账号 A 的草稿不会在账号 B 下恢复。过期草稿不自动弹出但可以在草稿箱中手动找回。提交成功后草稿删除提交失败后草稿保留。从图片选择页、协议页、系统后台返回输入内容都不丢。真实复现路径要包括跨页面回来表单丢内容最容易在跨页面回来时出现。测试时不要只填标题然后按返回要覆盖几个真实路径填完标题去选封面图回来后标题还在填完简介去看协议页回来后简介还在输入一半接电话或切后台再回到应用后草稿仍然能恢复。很多页面在普通返回时没问题但跳转图片选择、系统权限页、协议页后会重新创建这才是草稿链路真正要兜住的地方。复现时按这三组入口分别走普通返回编辑页直接按返回键。跨页面返回进入图片选择、协议页后再回来。系统返回切后台、锁屏、恢复应用后继续编辑。草稿恢复也要给用户选择自动恢复草稿不一定总是好事。用户可能只是想新建一条课程却被上次半成品直接填满。更稳的交互是发现可恢复草稿后给出提示用户选择“继续编辑”再恢复选择“丢弃”则清理草稿。这样既不会丢内容也不会让旧草稿打扰新任务。exporttypeDraftRestoreActionrestore|discard|ignoreexportinterfaceDraftRestorePrompt{title:stringupdatedText:stringaction:DraftRestoreAction}这段模型把恢复草稿变成一次明确选择而不是页面启动时偷偷覆盖输入框。交付前验收表验收项通过标准草稿恢复只恢复当前用户、当前入口的未过期草稿离开确认有内容才提示空表单不打扰提交失败草稿保留错误可见提交成功草稿删除再进页面不回潮这张表适合放到提测说明里。表单问题不是单个按钮能证明的必须用跨页面返回和失败重试来确认。小结表单输入不是长期事实但也不能只在页面里表单要把输入态、草稿态和提交态分开。页面负责输入DraftStore 负责可恢复快照提交成功后再清草稿。这样返回、重进、失败重试和成功清理都有明确边界。有内容才提示空表单不打扰 || 提交失败 | 草稿保留错误可见 || 提交成功 | 草稿删除再进页面不回潮 |这张表适合放到提测说明里。表单问题不是单个按钮能证明的必须用跨页面返回和失败重试来确认。小结表单输入不是长期事实但也不能只在页面里表单要把输入态、草稿态和提交态分开。页面负责输入DraftStore 负责可恢复快照提交成功后再清草稿。这样返回、重进、失败重试和成功清理都有明确边界。