【句匠|07】HarmonyOS ArkTS 纠错结果实战:把语法、搭配与地道表达分层呈现
英语纠错页面最怕两种误解。第一种是把“AI 纠错”写成一个黑盒用户输入句子后只看到一段笼统建议不知道到底错在哪里。第二种是把所有结果堆成纯文本语法、拼写、介词搭配、时态和冠词混在一起用户很难把错误片段、建议表达和原因对应起来。句匠项目里的AICorrectPage.ets走的是另一条更可复核的路线当前页面使用本地规则库不上传用户句子也不调用云端大模型。它把每个命中的规则构造成CorrectionItem再在 UI 上拆成“错误类型、原始片段、建议表达、解释原因”四层。本文基于真实源码D:\huawei\one18-11\entry\src\main\ets\pages\AICorrectPage.ets同时参考考试结果页ExamResultPage.ets的错题入口边界复盘 HarmonyOS 5.0 ArkTS 页面如何把纠错结果做成可阅读、可解释、可验证的结构化卡片。正文唯一复核标记com.jiaweikang.one18。本文只讨论源码可复核能力本地RULES、CorrectionItem、analyze()、fillExample()、ResultCard()、CorrectionItemView()、空结果提示和考试结果页的错题解析入口。它不声称当前版本实现了云端 AI 判分、长文语义润色、真实语料库检索或服务端数据上传。1. 先把“纠错结果”定义成结构而不是一段字符串在句匠源码里纠错结果的最小单元是CorrectionItem。它不是一个简单的message字段而是把用户真正需要看的四个信息拆开interface CorrectionItem { rule: string before: string after: string reason: string }这四个字段对应页面上的四个层次字段页面含义用户能得到什么rule错误类型知道这是拼写、介词、时态还是固定表达问题before原始错误片段能定位自己句子里哪一段被命中after建议表达能直接拿到可替换的写法reason解释原因明白为什么要这样改这比“你的句子有语法错误请改正”更适合学习场景。学习类应用不只是给答案还要让用户知道错误属于哪个知识点。源码后续的 UI 也围绕这个结构展开而不是在展示层临时切字符串。2. 本地规则由RegExp build组成规则定义使用RulePattern。每条规则包含一个正则和一个构建函数命中后由build返回CorrectionItem。interface RulePattern { pattern: RegExp build: (m: RegExpMatchArray) CorrectionItem } const RULES: RulePattern[] [ { pattern: /\bvery\slike\b/i, build: (m: RegExpMatchArray) { return { rule: 副词搭配, before: m[0], after: like ... very much, reason: very 不能直接修饰一般动词常用 very much 后置。 } as CorrectionItem } } ]这个设计有两个实际好处。第一识别逻辑和结果文案放在同一条规则里后续维护时不会出现“命中了介词错误却展示成拼写错误”的错位。第二build可以使用正则捕获组生成动态建议例如第三人称单数、过去式或since句型。当前源码里的规则覆盖高频中式英语、固定表达、动词单复数、第三人称单数、介词搭配、拼写、时态和冠词等类型。它的边界也很清楚这是本地规则库不是云端大模型。长句语义、上下文语气和文章级润色不是这段源码已经实现的能力。3.analyze()是结果生成边界用户点击“开始纠错”后页面调用analyze()。这个函数只做四件事读取输入、处理空输入、遍历规则、写入结果状态。State input: string State results: CorrectionItem[] [] State analyzed: boolean false private analyze(): void { const text this.input const items: CorrectionItem[] [] if (text.trim().length 0) { this.results [] this.analyzed true return } for (const r of RULES) { const m text.match(r.pattern) if (m) { const item r.build(m) if (item.before ! item.after) items.push(item) } } this.results items this.analyzed true }这里有一个容易忽略的细节if (item.before ! item.after) items.push(item)。源码中有些规则会识别冠词场景但如果原句已经正确就不会塞进结果列表。这样可以避免“命中了一条规则却提示用户改成同样内容”的噪音。从状态角度看results表示本次分析得到的建议列表analyzed表示用户已经触发过分析。两者分开很重要空输入或无命中时results.length都是 0但analyzed能告诉页面是否应该展示结果区域。如果没有analyzed页面很难区分“还没分析”和“分析后没有发现明显错误”。4. 输入区只维护草稿和触发动作InputCard()负责输入句子、清空输入和触发分析。它没有直接渲染纠错结果也没有把规则写在输入区域里。TextArea({ placeholder: e.g. I very like the book and I want to recieve it tomorow., text: this.input }) .placeholderColor(Colors.INPUT_PLACEHOLDER) .fontColor(Colors.INPUT_TEXT) .fontSize(Sizes.BODY_FONT) .backgroundColor(Colors.BACKGROUND_ALT) .borderRadius(12) .padding(12) .height(120) .width(100%) .onChange((v: string) { this.input v this.analyzed false })onChange里把analyzed置回false这个处理很实用。用户改了输入旧结果就不应该继续被当成当前句子的结论。否则页面会出现输入已经变化、结果仍然对应旧句子的错觉。按钮区的逻辑也很直接Text(清空) .onClick(() { this.input this.results [] this.analyzed false }) Text(开始纠错) .onClick(() { this.analyze() })清空按钮同时清理输入、结果和分析状态开始纠错只进入analyze()。这个边界避免了按钮之间互相修改对方职责。5. 示例句入口服务于可复现不是装饰纠错页面提供了几条示例句this.ExampleRow(I very like the book.) this.ExampleRow(She go to school every day.) this.ExampleRow(I am interested on music.) this.ExampleRow(I will recieve it tomorow.) this.ExampleRow(I yesterday go to park.)点击示例后调用fillExample()private fillExample(s: string): void { this.input s this.analyzed false this.results [] }示例句的价值不只是让页面更丰富而是给用户和测试人员一个稳定复现路径。比如I very like the book.应该命中副词搭配She go to school every day.应该命中第三人称单数I will recieve it tomorow.应该命中拼写错误。每条示例都应该能落到某类规则上。如果后续继续加规则建议同步维护示例句。规则没有示例很容易没人发现它已经失效示例没有对应规则则会让用户误以为纠错功能不可用。6. 结果区先判断“无建议”再渲染列表ResultCard()在analyzed为真时出现。它先展示结果数量然后按results.length分成空结果和建议列表两种路径。if (this.results.length 0) { Column({ space: 6 }) { Text(恭喜没有发现明显错误) .fontSize(Sizes.BODY_FONT) .fontWeight(FontWeight.Medium) .fontColor(Colors.SUCCESS) Text(当前 AI 助教使用本地规则库覆盖高频中式英语、拼写、介词、时态、冠词、第三人称单数等常见错误。如果句子较短或类型未覆盖也可能不会给出建议。) .fontSize(Sizes.SMALL_FONT) .fontColor(Colors.TEXT_HINT) } } else { ForEach(this.results, (r: CorrectionItem, i: number) { this.CorrectionItemView(r, i 1) }, (r: CorrectionItem, i: number) c_${i}_${r.before}) }空结果文案有一个重要边界它没有说“句子完全正确”而是说“没有发现明显错误”并说明当前使用的是本地规则库。这个表述更符合源码事实。因为正则规则没有命中不代表英语句子在语义、语气、长句结构上完全没有问题。ForEach的 key 使用i和r.before对当前本地列表足够稳定。若后续结果允许删除、排序或合并建议引入更稳定的id例如规则名加命中起止位置。7. 单条结果卡片把错误、建议、原因分开CorrectionItemView()是文章标题里“分层呈现”的核心。它先展示规则名再展示错误片段、建议表达和解释原因。Builder CorrectionItemView(r: CorrectionItem, idx: number) { Column({ space: 8 }) { Row() { Text(#${idx} ${r.rule}) .fontSize(Sizes.SMALL_FONT) .fontColor(Color.White) .padding({ left: 8, right: 8, top: 3, bottom: 3 }) .borderRadius(8) .backgroundColor(Colors.PRIMARY) } Row({ space: 8 }) { Text(错误) .fontSize(Sizes.SMALL_FONT) .fontColor(Colors.ERROR) Text(r.before) .layoutWeight(1) .fontSize(Sizes.BODY_FONT) .fontColor(Colors.ERROR) .decoration({ type: TextDecorationType.LineThrough }) .maxLines(2) } Row({ space: 8 }) { Text(建议) .fontSize(Sizes.SMALL_FONT) .fontColor(Colors.SUCCESS) Text(r.after) .layoutWeight(1) .fontSize(Sizes.BODY_FONT) .fontWeight(FontWeight.Bold) .fontColor(Colors.SUCCESS) .maxLines(2) } Text(r.reason) .fontSize(Sizes.SMALL_FONT) .fontColor(Colors.TEXT_SECONDARY) .lineHeight(18) .width(100%) } }这段 UI 的可读性来自三个处理处理效果错误片段加删除线用户能直观看到被替换的内容建议表达加粗并使用成功色结果重点明确不需要在长段落里找答案原因单独成段解释和替换文本分离适合学习场景同时layoutWeight(1)和maxLines(2)能减少长文本挤压标签的问题。纠错结果经常包含英文片段如果没有宽度约束容易在小屏上把“错误”“建议”标签挤出可视区域。8. 规则库覆盖的是高频错误不是所有英语问题源码注释明确写的是“简单本地规则检测一些高频中式英语 / 拼写 / 介词错误”。规则示例包括{ pattern: /\binterested\s(on|at|of)\b/i, build: (m: RegExpMatchArray) { return { rule: 介词搭配, before: m[0], after: interested in, reason: be interested in 是固定搭配。 } as CorrectionItem } }{ pattern: /\b(recieve|receeve)\b/i, build: (m: RegExpMatchArray) { return { rule: 高频拼写, before: m[0], after: receive, reason: i before e, except after c。 } as CorrectionItem } }{ pattern: /\bI\syesterday\sgo\b/i, build: (m: RegExpMatchArray) { return { rule: 时态错误, before: m[0], after: I went yesterday, reason: yesterday 提示一般过去时go 改为 went。 } as CorrectionItem } }这些规则适合入门学习、错点提示和常见错误训练。它们不适合被宣传成“完全理解上下文的 AI 语法老师”。正则命中的粒度通常是片段级比如recieve、interested on、I yesterday go不是整篇作文级。工程上要避免过度承诺。文章、应用说明和审核材料都应该讲清楚当前功能是在本地规则库上做纠错练习帮助识别常见错误和展示修改建议。9. 考试结果页的“错题解析”是另一个结果入口本篇主题是纠错结果但brief.json同时指向ExamResultPage.ets原因在于考试结果页也承担了“把结果转成可复盘入口”的职责。考试结果页读取PracticePage传来的考试参数interface ExamResultParams { bankId: string score: number total: number correct: number durationSec: number records: string } aboutToAppear(): void { const params router.getParams() as ExamResultParams | undefined if (params) { this.bankId params.bankId this.score params.score this.total params.total this.correct params.correct this.durationSec params.durationSec try { this.records JSON.parse(params.records) as AnswerRecord[] } catch (_) {} } this.rankInfo getRankByScore(this.score) this.examHistory UserDataManager.addExamHistory( this.examHistory, this.bankId, this.score, this.total, this.correct, this.durationSec) }页面底部有两个动作错题解析和再考一次。错题解析会把records原样传回PracticePage并设置mode: wrongAnalysisrouter.pushUrl({ url: pages/PracticePage, params: { bankId: this.bankId, mode: wrongAnalysis, records: JSON.stringify(this.records) } })这说明句匠有两类结果呈现AICorrectPage展示英文句子的本地纠错结果ExamResultPage展示考试分数、答题网格并把错题记录交给练习页做解析。两者都遵守同一个思路结果先结构化再进入展示层。10. 答题网格同样从记录推导颜色考试结果页的答题网格用makeAnswerGrid()构造private makeAnswerGrid(): GridItem[] { const result: GridItem[] [] for (let i 0; i this.total; i) { let state: string empty if (i this.records.length) { state this.records[i].correct ? correct : wrong } result.push({ index: i 1, state }) } return result } private stateColor(state: string): string { if (state correct) return Colors.SUCCESS if (state wrong) return Colors.ERROR return Colors.DIVIDER }这段代码和纠错卡片的思路一致UI 颜色不是手工保存的而是从结构化结果推导出来。CorrectionItem推导错误/建议卡片AnswerRecord推导正确/错误/未答网格。需要注意一个真实边界makeAnswerGrid()按records下标判断答题状态。如果考试中存在跳题、未答后继续答后面的题当前实现会把records顺序当成题号顺序。这个问题在上一篇练习题状态文章已经提到当前源码没有为未答题补齐空记录也没有按questionId对齐网格。本文不能把它描述成完整的考试答题矩阵。11. 可复核的测试用例针对AICorrectPage.ets可以用下面的本地输入做回归输入预期命中预期展示I very like the book.副词搭配beforevery likeafterlike ... very muchShe go to school every day.第三人称单数建议把go改成goesI am interested on music.介词搭配建议interested inI will recieve it tomorow.高频拼写至少命中recieve和tomorow空字符串空输入results[]且analyzedtrue未覆盖句型无明显错误展示本地规则库边界说明验证时重点看三件事输入改变后旧结果是否消失点击开始纠错后结果数量是否正确每条卡片是否同时包含错误片段、建议表达和原因。12. 常见问题与处理建议问题可能原因处理方向输入改了但结果没变onChange没有重置analyzed保留this.analyzed false必要时同步清理results明明命中规则却没有展示item.before item.after被过滤检查规则是否把已正确内容也构造成了结果一个句子里同类错误只报一处当前使用text.match不是全局扫描如果要多处命中改成matchAll或循环执行正则用户以为没结果等于完全正确空结果文案过度承诺保留“没有发现明显错误”和本地规则库说明长英文片段挤压标签文本没有宽度约束使用layoutWeight、maxLines和必要的省略想做云端 AI 润色当前源码没有网络请求需要先设计隐私、权限、接口、失败态和审核材料13. 迁移到其他 HarmonyOS 页面时的做法如果在其他 HarmonyOS 学习应用里做类似功能可以按下面的结构迁移interface ExplainableResult { category: string source: string suggestion: string explanation: string } State input: string State results: ExplainableResult[] [] State analyzed: boolean false private runLocalCheck(): void { const next: ExplainableResult[] [] // 统一在这里调用规则、词典或服务 this.results next this.analyzed true }关键不是字段名完全一致而是保留三条边界第一结果结构要先于 UI。不要让卡片组件自己解析一段长文本。第二空结果要有边界说明。没有命中规则不等于没有任何问题。第三用户输入改变后要让旧结果失效。否则页面会展示过期判断。总结句匠的纠错结果页面没有把“AI”写成不可验证的黑盒。AICorrectPage.ets使用本地规则库把每次命中转换成CorrectionItem再通过ResultCard()和CorrectionItemView()分层展示错误类型、原始片段、建议表达和原因。ExamResultPage.ets则从考试记录生成分数、答题网格和错题解析入口体现了同样的结构化结果思路。对 HarmonyOS ArkTS 学习应用来说这种实现更稳输入状态、分析状态、结果数组和展示卡片各有边界页面能解释自己做了什么也能诚实说明自己没做什么。只要继续保持这个边界后续无论扩展更多本地规则还是接入真正的在线 AI 服务结果层都不会变成难以维护的一段字符串。