答案之书 序列3题库编辑不要边输边落盘用一次性提交守住数据完整性题库编辑页看起来只是几个 TextInput但它是答案之书里最容易写坏数据的入口。用户可能正在用输入法组词可能删掉中间一条答案可能改名后又返回也可能连续点保存。如果每一次 onChange 都落盘Preferences 里会留下大量半成品页面下次打开时读到的已经不是“用户确认保存的题库”而是“用户编辑过程中的某一帧”。这篇文章会把问题拆成四个可落地的点把编辑态和持久态分开只有用户确认时才提交。用稳定行 key 避免 ArkUI 动态输入行复用错位。保存路径由 Service 补全实体和更新时间页面只提交 payload。处理默认题库只读、重复点击、返回脏数据提示这些实际场景。1. 边输边保存会把输入过程当结果在移动端TextInput 的 onChange 不是业务提交事件。输入法组合、删除、粘贴、多行调整都会触发变化。如果这些变化直接写 Preferences用户只是临时删掉一条答案应用却已经把它当成正式题库。答案之书把页面编辑态留在State保存按钮才进入 Service。interfaceEditableAnswer{key:string;text:string;}StateprivatedeckName:string;Stateprivateanswers:EditableAnswer[][];Stateprivatesaving:booleanfalse;privateoriginalSignature:string;这几个字段属于页面临时状态。它们可以频繁变化但在onSave之前不代表持久数据。2. 加载时生成原始签名编辑页需要知道用户是否真的改过内容。这里不需要引入复杂 diff只要把载入时的名称和答案文本生成签名保存按钮根据签名变化决定是否可点。这个办法简单但对本地题库编辑足够稳定。privatemakeSignature():string{constanswerText:stringthis.answers.map((item:EditableAnswer):stringitem.text.trim()).join(\n);return${this.deckName.trim()}\n---\n${answerText};}privateisDirty():boolean{returnthis.makeSignature()!this.originalSignature;}签名只比较领域有效值而不是比较输入框里的每个临时空格。这样可以减少无意义保存也能避免列表无故刷新。3. 默认题库只读要页面和 Service 双保险答案之书的默认题库来自 rawfile 种子用户可以抽取和收藏但不应该被编辑覆盖。UI 禁用只是体验层真正的保护必须在保存入口和 Service 中都存在否则后续新增入口时很容易绕过。privatecanSave():boolean{if(this.saving||this.builtIn){returnfalse;}if(!this.deckName.trim()){returnfalse;}returnthis.validAnswerCount()MIN_ANSWERS_PER_DECKthis.isDirty();}if(existing?.builtIn){thrownewError(默认题库不能修改);}页面层防误触Service 层防绕过。涉及内置内容、权限、备份这类边界时不要只信 UI 状态。4. 动态答案行必须有稳定 key答案行可以新增、删除、排序。ForEach如果用 index 做 key删除中间项后 ArkUI 可能复用原来的 TextInput 节点表现为文本错位或光标状态混乱。答案之书给每一行分配本地 key已有答案用原 id新建答案用本地序号。privateaddAnswer():void{this.nextLocalKey1;constnext:EditableAnswer[]this.answers.slice();next.push({key:new_${this.nextLocalKey},text:});this.answersnext;}ForEach(this.answers,(item:EditableAnswer,index:number){this.answerEditor(item,index);},(item:EditableAnswer):stringitem.key)这里的 key 表达的是输入行身份不是数组位置。只要用户还在编辑同一行组件就不应该因为前面删了一行而复用错对象。5. 页面只提交 SaveDeckPayload编辑页不应该拼完整 Deck。createdAt、updatedAt、builtIn、colorKey都是业务规则不是输入框状态。页面提交的 payload 越薄保存逻辑越容易复用到导入页和新建页。constpayload:SaveDeckPayload{id:this.deckId,name:this.deckName,answers:this.answers.map((item:EditableAnswer):stringitem.text)};awaitDeckService.save(payload);payload 是用户意图Deck 是领域实体。把两者分开后面做导入、恢复和迁移时才不会让页面承担领域补全。6. Service 保存时补齐实体Service 的保存动作要做完整实体合成。已有题库保留创建时间和颜色新题库生成 id 和默认色答案行重新 trim、去空、分配稳定 id。这个阶段也是写入前最后一道业务边界。constnow:numberDate.now();constdeck:Deck{id:payload.id||newId(deck),name:trimmedName,builtIn:existing?.builtIn??false,colorKey:existing?.colorKey??nextDeckColorKey(),answers:cleaned.map((text:string,index:number):Answer({id:existing?.answers[index]?.id??newId(answer),text,createdAt:existing?.answers[index]?.createdAt??now})),createdAt:existing?.createdAt??now,updatedAt:now};实体补全放在 ServiceRepository 就可以保持纯粹保存。页面不用知道颜色策略导入页也不用复制同一段规则。7. 提交锁解决本地连点不要过度设计本地单页面保存不一定需要幂等表但必须挡住连点。答案之书用页面saving做第一道锁Service 校验做第二道边界。只要保存路径不是跨设备或跨网络这个复杂度是合适的。privateasynconSave():Promisevoid{if(!this.canSave()){return;}this.savingtrue;try{awaitDeckService.save(this.createPayload());promptAction.showToast({message:题库已保存});this.pathStack.pop();}finally{this.savingfalse;}}锁放页面上是为了防连点规则放 Service 是为了防错误数据。两层职责不同不要互相替代。8. 返回前处理未保存改动题库编辑页如果直接返回用户可能丢掉一整组答案。轻量应用不一定要做复杂草稿但至少要在 dirty 状态下给出确认。对于答案之书这个确认比自动保存更适合因为题库内容有明确的提交动作。privaterequestBack():void{if(!this.isDirty()){this.pathStack.pop();return;}AlertDialog.show({title:放弃本次编辑,message:未保存的题库修改不会写入本地数据。,primaryButton:{value:继续编辑,action:(){}},secondaryButton:{value:放弃,action:()this.pathStack.pop()}});}这里选择确认而不是自动保存是因为编辑过程不等于用户确认。特别是题库内容误保存的成本比多点一次确认更高。9. 验证与排障验证题库编辑页时要覆盖输入过程和提交边界。只验证“输入两条答案后保存成功”是不够的还要删中间项、连续保存、编辑默认题库、返回未保存、保存后重启应用。排查顺序 1. 保存按钮是否由 canSave 控制 2. ForEach key 是否使用 EditableAnswer.key 3. DeckService.save 是否统一 trim、去空、限制数量 4. 保存成功后 LastDeckUpdateAt 是否变化 5. 返回列表后 LazyForEach key 是否包含 updatedAt如果保存后列表仍显示旧数据优先看刷新信号和列表 key而不是先改 TextInput。验证清单清应用数据后从冷启动进入确认默认数据、页面状态和日志分支符合预期。对本文涉及的写路径准备正常、空值、重复、越界四类输入确认错误停在 Service 或 Repository。页面返回、重新进入、切换题库、收藏、历史或删除后确认对应刷新信号触发重新读取。修改资源或模块归属后重新构建确认 HAP、HAR、HSP 的依赖方向没有反转。涉及真机体验、备份恢复、发布素材的内容单独记录是否已经在设备或平台侧验证。常见问题与处理现象先看哪里处理方式删除中间答案后文本错位ForEach 是否用 index 做 key给每行生成稳定 key默认题库被改坏Service 是否判断 builtInUI 禁用之外保存入口也要拒绝保存后列表还是旧名称updatedAt 和刷新信号保存后更新 LastDeckUpdateAt列表 key 带 updatedAt用户返回丢内容dirty 判断是否存在返回前给出放弃确认小结题库编辑的核心不是 TextInput而是把编辑过程和提交结果分开。页面保存临时态Service 生成可信实体Repository 负责落盘这条线清楚后数据完整性才稳。