
HarmonyOS应用实战-启示散页-04-题库导入不是 split 一下就完把分享协议写成可恢复输入答案之书的题库导入看起来很轻用户粘贴一段文本应用拆成多条答案。但真正上线后输入来源会很乱别人分享的文本带标题复制时多了空行聊天软件把引号和序号混在一起甚至同一条答案重复出现。导入如果只是split(\n)最后保存进本地的不是题库而是一批不可恢复的脏文本。这篇文章会把问题拆成四个可落地的点把导入文本定义成轻量协议而不是临时字符串。导入前预览确认后才进入题库保存链路。用解析结果承载错误行、重复行和有效行。导入仍复用 DeckService避免绕过题库保存规则。1. 分享文本本质上是不可信输入导入入口来自剪贴板或分享不受应用控制。用户可能粘贴 Markdown 列表、带序号的答案、空行、全角标点也可能把题库名和答案混在一起。第一步不是保存而是把输入解析成可解释的结果哪些行有效哪些行被忽略题库名从哪里来。interfaceImportParseResult{title:string;answers:string[];duplicated:string[];ignored:string[];}解析结果要能回到 UI。只有这样用户才知道导入保存了什么没有保存什么。2. 定义最小分享协议轻量工具不需要复杂文件格式但需要约定。答案之书可以接受第一行# 题库名后面每行一条答案也兼容1.、-、•这类常见列表符号。协议越简单越适合用户在聊天里复制。题库周末决策题库 - 先睡一觉再决定 - 让事情自然发生 - 问问真正的原因这个格式的好处是可读、可复制、可手工修正。导入协议不应该只有程序能看懂。如果更偏向 Markdown 风格也可以在解析器里兼容标题行但文章示例建议用题库这种更明确的前缀。它不依赖 Markdown 渲染环境复制到聊天、备忘录或系统分享面板里也不容易被吞掉。导入协议越接近普通文本用户越敢自己改。3. 解析时先规范化每一行导入解析不要急着写业务对象。先做行级规范化去掉列表前缀、trim、过滤空行、控制长度。重复判断也应该基于规范化后的文本避免先等等和先等等被当成两条。functionnormalizeAnswerLine(line:string):string{returnline.replace(/^\s*[-*•]\s/,).replace(/^\s*\d[.)、]\s*/,).trim();}constnormalized:string[]raw.split(/\r?\n/).map(normalizeAnswerLine).filter((line:string):booleanline.length0);规范化是导入质量的核心。它不改变用户意图只去掉传输和复制过程中带来的格式噪声。4. 重复行要报告不要悄悄吞掉用户导入一批答案时重复内容可能是误复制也可能是刻意强调。答案之书更适合默认去重但要在预览里告诉用户哪些行重复。这样既保护题库质量也不给用户制造“保存后怎么少了几条”的困惑。constseen:SetstringnewSet();constanswers:string[][];constduplicated:string[][];for(constlineofnormalized){constkey:stringline.toLocaleLowerCase();if(seen.has(key)){duplicated.push(line);continue;}seen.add(key);answers.push(line);}去重规则要稳定。这里用小写文本作为 key足以覆盖答案之书的短文本场景。5. 导入页先预览不直接保存导入解析成功也不代表应该马上落盘。预览页要展示题库名、有效答案数量、重复行和被忽略行让用户在保存前能修正文本。这个步骤能挡住大部分脏数据。Stateprivatepreview:ImportParseResult|nullnull;privateonParse(raw:string):void{constresult:ImportParseResultImportParser.parse(raw);if(result.answers.lengthMIN_ANSWERS_PER_DECK){promptAction.showToast({message:有效答案太少请补充后再导入});return;}this.previewresult;}预览不是装饰它是导入链路的确认点。用户确认前Preferences 不应该发生变化。6. 保存仍然走 DeckService导入页最容易犯的错是觉得自己已经解析好了于是直接写 Repository。这样会绕过题库名校验、答案数量限制、颜色补全、更新时间和刷新信号。答案之书导入确认后仍走DeckService.save保证所有入口的写入规则一致。privateasyncconfirmImport():Promisevoid{if(!this.preview){return;}awaitDeckService.save({id:,name:this.preview.title||导入题库,answers:this.preview.answers});promptAction.showToast({message:题库已导入});this.pathStack.pop();}导入解析只负责把外部文本变成 payload。能不能保存、怎么补齐实体仍由 Service 决定。7. 错误行要留给用户修不要只给失败如果整段导入失败用户通常不知道该改哪里。解析器应该把过长、空白、格式异常的行收集到 ignored 里预览页可以展示前几条。这样用户能快速修复而不是反复猜。if(line.lengthMAX_ANSWER_LEN){ignored.push(过长${line.slice(0,24)}...);continue;}if(line.startsWith(//)){ignored.push(注释${line});continue;}answers.push(line);导入入口面对的是人写的文本不是严格机器协议。错误信息越具体越能减少用户放弃导入的概率。8. 保存前保留原文和解析摘要导入失败时用户最需要的是回到原文继续改而不是重新粘贴一遍。页面可以保留最近一次原文和解析摘要但不要把它当成正式题库保存。这样用户从预览页返回编辑时仍能看到刚才的文本和错误行。interfaceImportDraftState{rawText:string;parsedAt:number;result?:ImportParseResult;}StateprivateimportDraft:ImportDraftState{rawText:,parsedAt:0};这段状态只属于导入页生命周期。它帮助用户恢复输入但不进入 Preferences真正的本地数据仍然只在用户确认导入后写入。9. 为导出预留同一套格式导入协议最好同时能用于导出。答案之书可以把题库导出为同样的 Markdown 风格文本用户复制到备忘录或聊天里未来再导入时仍能还原。这样本地离线应用也有了轻量备份能力。functionexportDeck(deck:Deck):string{constlines:string[][题库${deck.name}];deck.answers.forEach((answer:Answer):void{lines.push(-${answer.text});});returnlines.join(\n);}导入和导出使用同一套人类可读格式可以降低用户迁移成本也方便发布前解释本地数据备份范围。10. 验证与排障导入验证要准备真实复制场景微信聊天复制、Markdown 列表、带序号清单、重复内容、超长行、只有标题没有答案。每个场景都要确认预览结果和最终保存结果一致。推荐样本 1. # 标题 20 行短答案 2. 1. xxx 编号列表 3. 重复三次同一答案 4. 混入空行、注释、超长行 5. 只有一条有效答案如果预览数量和保存后题库数量不一致优先看导入页是否绕过了 DeckService或 Service 是否又做了不同规则的过滤。验证清单清应用数据后从冷启动进入确认默认数据、页面状态和日志分支符合预期。对本文涉及的写路径准备正常、空值、重复、越界四类输入确认错误停在 Service 或 Repository。页面返回、重新进入、切换题库、收藏、历史或删除后确认对应刷新信号触发重新读取。修改资源或模块归属后重新构建确认 HAP、HAR、HSP 的依赖方向没有反转。涉及真机体验、备份恢复、发布素材的内容单独记录是否已经在设备或平台侧验证。常见问题与处理现象先看哪里处理方式导入后少了几条duplicated 和 ignored 是否展示预览页展示被去重和被忽略的行空题库被保存是否绕过 DeckService.save导入确认只提交 payload 给 Service标题被当成答案第一行 # 协议是否解析标题行单独提取为题库名用户无法修正格式错误信息是否具体记录过长、注释、空行等原因小结导入不是字符串拆分而是外部输入恢复成可信领域数据的过程。先协议、再解析、再预览、最后复用 Service题库导入才不会成为本地数据污染源。