英语纠错页看起来只是一个输入框加一个结果列表,但真正写到 HarmonyOS 5.0+ ArkTS 页面里,容易出现几个实际问题:用户输入长句后页面挤压,示例句点击后旧结果还留在页面上,空输入时没有明确反馈,规则匹配到了"已经正确"的表达也被当成错误展示,结果卡片把错误片段、修改建议和解释混在一起,最后用户看不出到底该改哪里、为什么改。对学习工具来说,纠错页不是"显示一段答案",而是要把原句、建议和解释层级展示清楚。
本文基于「句匠」真实源码复核,包名标记为 com.jiaweikang.one18。核心文件是 entry/src/main/ets/pages/AICorrectPage.ets。需要先说明边界:当前页面标题里写了"AI 纠错练习",但源码实现是本地规则版,使用 RegExp 规则匹配中式英语、拼写、介词、时态、冠词和第三人称单数等高频错误;本文不会把它写成云端大模型、联网推理或服务端纠错能力。module.json5 中虽然声明了 ohos.permission.INTERNET,但本文所讨论的 AICorrectPage 纠错结果来自本地 RULES 数组。

1. 页面要解决的问题:纠错不是一个字符串,而是一组结构化建议
如果纠错结果只返回一段文本,例如"这里应该改成 like very much",用户需要自己找原句中的位置,也看不出规则类型。当前源码没有这样做,而是定义了 CorrectionItem:
ts
interface CorrectionItem {
rule: string
before: string
after: string
reason: string
}
这个结构把纠错建议拆成四层。rule 是错误类型,例如副词搭配、固定表达、介词搭配、时态错误;before 是原句中匹配到的错误片段;after 是建议替换片段;reason 是面向学习者的解释。页面渲染时就能分别展示标签、删除线、绿色建议和解释文本。
这种结构比直接拼接字符串更稳。后续如果要把纠错结果保存到学习记录、加入错题本、做分类型统计,或者给每条建议加"我已掌握"状态,只需要扩展 CorrectionItem,不需要重新解析一段自然语言结果。
2. 规则层:RulePattern 把匹配和构造结果绑定在一起
源码里第二个关键接口是 RulePattern:
ts
interface RulePattern {
pattern: RegExp
build: (m: RegExpMatchArray) => CorrectionItem
}
它的设计目标很明确:pattern 只负责发现问题,build 负责把匹配结果转换成结构化建议。例如 very like 这类中式英语问题,规则写成:
ts
{
pattern: /\bvery\s+like\b/i,
build: (m: RegExpMatchArray) => {
return {
rule: '副词搭配',
before: m[0],
after: 'like ... very much',
reason: 'very 不能直接修饰一般动词,常用 very much 后置。'
} as CorrectionItem
}
}
这里的工程价值不是正则本身,而是"发现问题"和"解释问题"放在同一条规则里。这样页面不用知道 very like 为什么错,也不用根据规则名再查一次说明。每条规则都自带展示所需的信息,ResultCard 只负责渲染。
当前规则覆盖的类型包括:
| 规则方向 | 示例 |
|---|---|
| 中式英语 | I very like the book. |
| 固定表达 | I no idea. |
| 主谓一致 | She go to school every day. |
| 介词搭配 | interested on、good in、depend of |
| 高频拼写 | recieve、tomorow、seperate |
| 时态错误 | I yesterday go、since 2020 I live |
| 冠词使用 | a honest、an university |
这些都是本地规则能复核的内容,不能被扩大描述为"覆盖所有语法错误"。
3. 状态层:input、results、analyzed 三个状态各司其职
AICorrectPage 的页面状态很少,但职责清晰:
ts
@State input: string = ''
@State results: CorrectionItem[] = []
@State analyzed: boolean = false
@StorageLink('navigationIndicatorHeightPx') navigationIndicatorHeightPx: number = 0
input 是用户正在编辑的原句;results 是分析后的结构化结果;analyzed 决定是否展示结果卡片。这里最关键的是 analyzed。如果只靠 results.length 判断是否展示结果,空输入、无错误和未分析三种状态会混在一起:它们的结果数组都可能是空的,但用户含义完全不同。
当前源码用 analyzed 把这三种状态拆开:
| 状态 | input |
results |
analyzed |
页面含义 |
|---|---|---|---|---|
| 初始 | 空 | 空 | false | 不显示结果 |
| 空输入后点击分析 | 空 | 空 | true | 显示分析后的空结果 |
| 输入但规则未命中 | 非空 | 空 | true | 显示未发现明显错误 |
| 输入且命中规则 | 非空 | 非空 | true | 显示建议列表 |
| 输入变化后 | 变化中 | 旧值清空或隐藏 | false | 等待重新分析 |
这个设计能避免一个常见 UI 问题:用户改了输入句子,页面仍显示上一句的纠错建议。源码在 TextArea.onChange 中把 analyzed 重置为 false,就是为了让结果和当前输入保持一致。
4. 输入卡片:TextArea 承载原句,按钮只做两件事
输入区域使用 TextArea,而不是 TextInput。这符合英语句子纠错的场景,因为用户可能粘贴长句、复合句或多个短句。源码中设置了占位文本、字体颜色、背景色、圆角、padding、高度和宽度:
ts
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 })
这段代码有两个发布级细节。第一,输入框颜色使用 Colors.INPUT_*,不是系统默认颜色,能降低深色模式下输入文字不可读的风险。第二,输入变化只更新 input 和 analyzed,不立刻执行分析,避免每次输入都遍历规则并造成结果跳动。
按钮区只有"清空"和"开始纠错"两个动作。清空时同步重置输入、结果和分析状态:
ts
.onClick(() => {
this.input = ''
this.results = []
this.analyzed = false
})
开始纠错只调用 analyze()。这种按钮职责很清楚,测试时也容易覆盖:清空后不应显示旧结果;输入变化后旧结果隐藏;点击分析后才展示当前句子的结果。

5. analyze:空输入、规则遍历和结果过滤
页面核心方法是 analyze():
ts
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
}
这里有三个值得保留的工程判断。
第一,空输入不是异常。用户点击"开始纠错"但没有输入内容时,页面把 results 置空,并把 analyzed 设为 true。这样 UI 可以明确进入"已分析但没有建议"的状态,而不是保持无反馈。
第二,规则遍历是本地同步逻辑。当前 RULES 数量有限,适合在页面方法里直接遍历。后续如果规则数量变大,或者要加入网络纠错,就应该把规则引擎抽到 service 层,但当前源码还没有这个复杂度。
第三,只保留 before !== after 的建议。冠词规则里有些情况会返回"已经正确"的表达,如果不做过滤,页面会显示没有实际改动的建议。这个过滤让结果列表只包含真正需要用户关注的变化。
6. 示例句:fillExample 重置状态,避免示例和旧结果混在一起
示例卡片提供五条句子:
ts
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:
ts
private fillExample(s: string): void {
this.input = s
this.analyzed = false
this.results = []
}
这一步很关键。示例句本质上是快捷输入,不是自动分析。如果用户刚分析过上一句,再点一个示例,旧结果必须清空,否则用户会看到"输入已经变了,建议还是上一句"的错位状态。
示例卡片也用了单行省略:
ts
Text(s)
.layoutWeight(1)
.fontSize(Sizes.BODY_FONT)
.fontColor(Colors.TEXT_SECONDARY)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
这保证较长示例不会挤掉右侧"试用"按钮。对手机、小窗和 2in1 窄窗口来说,这类文本约束是页面稳定性的基础。
7. ResultCard:先区分无建议,再渲染建议列表
结果卡片只有在 analyzed 为 true 时显示。进入卡片后,再根据 results.length 分支。没有建议时,页面展示"没有发现明显错误"的提示,并说明当前本地规则库覆盖范围有限:
ts
if (this.results.length === 0) {
Column({ space: 6 }) {
Text('恭喜,没有发现明显错误')
Text('当前 AI 助教使用本地规则库,覆盖高频中式英语、拼写、介词、时态、冠词、第三人称单数等常见错误。如果句子较短或类型未覆盖,也可能不会给出建议。')
}
}
这段文案很重要,因为它没有夸大能力。规则没有命中,不等于句子绝对正确。页面明确告诉用户"类型未覆盖也可能不会给出建议",这比直接写"句子完全正确"更符合本地规则版的边界。
有建议时,页面用 ForEach 渲染每条 CorrectionItem:
ts
ForEach(this.results, (r: CorrectionItem, i: number) => {
this.CorrectionItemView(r, i + 1)
}, (r: CorrectionItem, i: number) => `c_${i}_${r.before}`)
key 使用序号加 before,能区分同一结果列表中的不同建议。虽然当前 analyze() 对每条规则只取一次 text.match,不会列出同一规则的多个命中,但这个 key 至少避免纯 index 在内容变化时完全失去业务含义。

8. CorrectionItemView:错误、建议、解释三层分开
每条纠错建议由 CorrectionItemView 渲染。它先显示规则标签,再显示错误片段、建议片段和解释原因。核心结构可以简化为:
ts
Text(`#${idx} ${r.rule}`)
Text(r.before).decoration({ type: TextDecorationType.LineThrough })
Text(r.after).fontWeight(FontWeight.Bold)
Text(r.reason).lineHeight(18)
这几个视觉选择服务于同一个目标:让用户按顺序读懂"错在哪里、改成什么、为什么"。before 使用删除线和错误色;after 使用成功色和加粗;reason 使用较小字体和固定行高,承担学习解释而不是抢占视觉焦点。
如果把这三层放在一段普通文本里,用户很难快速定位。分层展示后,每条建议都像一个小型学习卡片,尤其适合手机屏幕上的连续阅读。
这里还要注意 maxLines(2)。错误片段和建议片段最多两行,能避免某条长建议撑爆整个卡片。解释文本则允许用 lineHeight(18) 保持阅读节奏。
9. 本地规则版的边界:不要把页面写成云端纠错
当前 AICorrectPage 有"AI 助教"的产品表达,但源码层是本地规则库。这个边界要在技术文章和发布材料中讲清楚。
| 能力 | 当前源码是否支撑 | 说明 |
|---|---|---|
| 本地高频语法纠错 | 支撑 | RULES 中有 RegExp 和解释 |
| 拼写纠错 | 支撑部分高频词 | 如 receive、tomorrow、environment |
| 介词搭配 | 支撑部分固定搭配 | 如 interested in、good at |
| 任意长文润色 | 不支撑 | 没有通用模型或长文本重写 |
| 云端 AI 纠错 | 未在此页体现 | 页面逻辑没有请求服务端 |
| 多语言翻译 | 不支撑 | 当前只围绕英文错误建议 |
module.json5 里声明了 ohos.permission.INTERNET,这说明应用级别存在联网权限配置。但就本页源码而言,AICorrectPage 没有发起 HTTP、WebView 或外部模型调用。写文章时必须把"应用权限声明"和"当前页面实现"分开,不能因为页面标题含 AI 就虚构云端推理链路,也不能因为应用声明 INTERNET 就把本地规则页写成在线服务。
10. 验证清单:输入、示例、空态和多结果都要测
围绕当前源码,纠错页至少要覆盖下面这些测试:
| 测试项 | 操作 | 预期 |
|---|---|---|
| 空输入 | 不输入直接点开始纠错 | 显示已分析状态,不崩溃 |
| 示例填充 | 点击 I very like the book. |
输入框更新,旧结果清空 |
| 单条命中 | 点击分析 | 显示副词搭配建议 |
| 多条命中 | 输入 I very like it and recieve it tomorow. |
显示多条建议 |
| 无命中 | 输入规则未覆盖句子 | 显示本地规则覆盖范围提示 |
| 清空 | 点击清空 | 输入、结果和 analyzed 全部重置 |
| 长句 | 粘贴较长英文句 | TextArea 可滚动或保持布局,不遮挡按钮 |
| 底部安全区 | 手势导航设备 | 页面底部 Blank 使用 bottomSafePadding() 留出空间 |
这些测试都能从源码推导,不需要伪造线上接口结果。尤其是"无命中"场景,应该确认文案没有误导用户认为句子绝对正确。
11. 常见问题和修正方式
问题一:输入变化后旧建议仍显示。 检查 TextArea.onChange 是否把 analyzed 设为 false。如果只改 input,结果卡片会继续展示旧建议。
问题二:示例句填入后立即显示上一句结果。 检查 fillExample 是否同步清空 results。示例填充不是分析动作,应该等待用户点击"开始纠错"。
问题三:规则返回了没有变化的建议。 检查 analyze() 中的 item.before !== item.after 过滤。冠词规则这类可能返回"已正确"的场景,不应进入建议列表。
问题四:错误、建议和解释混在一起不好读。 把 CorrectionItemView 拆成规则标签、错误片段、建议片段和解释文本四个视觉层级,错误用删除线,建议用成功色,解释使用较小字号。
问题五:产品文案夸大为在线 AI。 以源码为准。当前页面是本地规则版,可以说"本地规则纠错""AI 助教式体验",但不能说"云端大模型实时改写"或"覆盖所有语法错误"。
12. 小结:清晰的纠错页来自结构化结果,而不是更长的提示词
「句匠」当前 AICorrectPage 的关键点不是算法复杂,而是页面边界清楚:TextArea 负责原句输入,RULES 负责本地规则匹配,analyze() 负责空输入、遍历和过滤,ResultCard 负责区分无建议和建议列表,CorrectionItemView 负责把错误、建议和解释分层展示。
这种写法适合 HarmonyOS 学习类应用的第一版纠错页。它不会伪装成万能纠错系统,但能把高频错误讲清楚,让用户知道"哪里错、改成什么、为什么改"。后续如果要接入真正的云端纠错或模型能力,也应该保留 CorrectionItem 这种结构化输出,再把网络请求、错误处理、隐私说明和权限声明补齐。
本篇文章的封面使用当前文章独立的 media/cover.png,同一软件合集继续复用应用级 collection-cover.png;合集封面保持统一,合集内每篇文章封面保持不同,便于跨平台发布和回读核验。
部分内容由AI辅助生成。