ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【中国方言题库|04】HarmonyOS ArkTS 粤语分库实战:处理繁简文本、读音与练习入口

【中国方言题库|04】HarmonyOS ArkTS 粤语分库实战:处理繁简文本、读音与练习入口

【中国方言题库|04】HarmonyOS ArkTS 粤语分库实战:处理繁简文本、读音与练习入口

证据边界:本文静态复核YueBankPage.ets、BankDetailPage.ets、BankCard.ets、MockBanks.ets与路由清单。没有执行构建、模拟器或真机验证;显式读音模型、章节标注和状态枚举属于建议实现。

入口、文本与数据责任图

粤语专题入口只固定题库身份,共享详情容器负责页面能力,数据装配层再按b_yue选择题目。三张图分别说明主题、执行流和责任边界。

一、先从真实入口确认地区身份

YueBankPage.ets是一个薄入口:

import { BankDetailContent } from './BankDetailPage'
@Entry @Component struct YueBankPage { build() { Column() { BankDetailContent({ fixedBankId: 'b_yue' }) } .width('100%') .height('100%') } }

它只负责把b_yue注入共享详情组件。题库简介、文化提示、进度、章节列表、随机练习和模拟考试均由BankDetailContent实现。

本文的唯一标记是:粤语读音必须有可核验的音频来源。代码中出现audioHint、播放按钮或 TTS API,都不能自动证明输出的是标准粤语,更不能证明是真人录音。

二、b_yue是整条链路的统一主键

粤语题库目录如下:

{ id: 'b_yue', regionId: 'yue', name: '粤语题库', cover: $r('app.media.img_bank_cover_yue'), totalCount: 0, accuracy: 0, hot: 95, chapters: YUE_CHAPTERS }

章节包括:

const YUE_CHAPTERS: Chapter[] = makeChapters('yue', [ '基础发音', '九声六调', '日常对话', '茶餐厅词汇', '老广俗语', '香港流行语' ])

这里要区分:

  • 地区 ID:yue
  • 题库 ID:b_yue
  • 章节 ID 前缀:yue_c1到yue_c6

分库入口和练习页传递的是题库 ID;章节模式还会传章节 ID。本地进度也按bankId + chapterId查询。任何一层混用yue和b_yue,都会出现详情为空或进度不更新。

三、粤语文字不是简单的“繁体中文”

题库包含:

唔该 冇见 食咗饭未 好攰 边个 做乜 返屋企 钟意

这些内容既包含传统字形,也包含粤语书写中的常用方言字。它们不能被普通简繁转换库无差别处理。例如“冇”“咗”“乜”不是把普通话简体文本机械转为繁体就能得到的。

当前源码没有发现自动繁简转换服务。题干、选项、解析和audioHint都按本地数据原样保存、原样渲染。这一事实可以准确描述为:

应用保留题库中录入的粤语字形,不在运行时自动改写。

不能描述为:

应用已实现完整的简繁双向转换。

如果产品需要简体解释与粤语原文并存,应把两者建成不同字段,而不是运行时替换字符。

四、为粤语文本建立类型化模型

当前RawQuestion使用:

interface RawQuestion { type: string stem: string options: string[] answer: number analysis: string example?: string audioHint?: string }

它可以承载题目,但无法明确区分“粤语原文”“普通话释义”“粤拼”和“显示字形策略”。更适合粤语内容的模型可以是:

interface YueExpression { id: string traditionalText: string simplifiedExplanation: string jyutping?: string meaning: string exampleTraditional?: string exampleExplanation?: string audioResource?: Resource audioSourceNote?: string }

这里不把粤语原文命名为泛化的traditional后就自动推导简体。simplifiedExplanation是编辑审核后的普通话解释;jyutping是独立读音标注;音频资源与来源说明也是独立字段。

五、字体与 Unicode 显示要在真机验证

ArkUIText能显示 Unicode 字符,但最终可见效果还取决于系统字体是否包含对应字形。测试应覆盖:

  • “唔、冇、咗、攰、啲、嚟”等字符是否缺字。
  • 粤拼数字或声调标记是否被截断。
  • 大字体下原文和释义能否换行。
  • 粗体是否仍有可用字形。
  • phone、tablet、2in1 是否使用一致字体回退。

不要为了一个缺字字符把整套大型字体打进包。应先确认系统字体覆盖,再评估是否需要小范围字体资源及其授权。

Text(expression.traditionalText) .fontSize(Sizes.TITLE_FONT) .fontWeight(FontWeight.Bold) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis })

若原文承担核心学习信息,最好不要强制单行省略。可以允许两行,并让卡片高度随内容增长。

六、当前题库中的audioHint只是文本

大量原始题目包含:

{ type: 'audio', stem: '听音理解:“得唔得呀?”是在问?', audioHint: '得唔得呀', options: ['行不行', '去哪里', '吃什么', '几点钟'], answer: 0, analysis: '“得唔得”就是可不可以、行不行。' }

audioHint的类型是string,内容是待朗读文本,不是音频文件、URI 或媒体资源。仅凭这个字段无法证明发音者、口音、地区或录制质量。

更关键的是,当前目录构建会把所有audio题重分类为vocab:

const reclassified: RawQuestion[] = source.map((r: RawQuestion) => { if (r.type === 'audio') { return { type: 'vocab', stem: r.stem, options: r.options, answer: r.answer, analysis: r.analysis, example: r.example, audioHint: r.audioHint } as RawQuestion } return r })

注释写明“听音题题型已下线”。因此,这些题在当前缓存中是词汇题,不能因为仍保留audioHint就说“粤语听音题已上线”。

七、PracticePage 的 TTS 也不能证明粤语读音

PracticePage创建文本转语音引擎时使用:

this.ttsEngine = await textToSpeech.createEngine({ language: 'zh-CN', person: 0, online: 0 })

失败后回退到:

this.ttsEngine = await textToSpeech.createEngine({ language: 'zh-CN', person: 0, online: 1 })

这说明:

  • 语言参数是zh-CN。
  • 优先本地引擎。
  • 失败后可能使用在线引擎。
  • 模块声明ohos.permission.INTERNET与在线回退一致。

但它没有指定或验证粤语音色。即便引擎能朗读“唔该”,输出也可能按普通话字音处理。商店文案不应写“标准粤语发音”“真人粤语朗读”。

此外,PracticePage 的播放 UI只在question.type === 'audio'时出现,而目录已经把audio改成vocab。因此当前普通题链路中,播放分支可能根本不会触发。

八、真正的粤语读音需要独立资源契约

若要上线粤语读音,建议使用明确资源:

interface PronunciationAsset { id: string expressionId: string locale: 'yue-Hant' region?: 'HK' | 'GZ' speakerType: 'human' | 'licensed-engine' resource: Resource durationMs: number sourceNote: string reviewedAt: string }

播放前按表达 ID 查找资源:

private canPlay(entry: YueExpression): boolean { return entry.audioResource !== undefined }

没有资源时隐藏播放按钮,只显示粤拼或文本读音;不使用普通话 TTS 伪装成粤语。若采用系统支持的粤语 TTS,也要根据官方 API 的实际 locale 与设备支持矩阵实现,并在不可用时给出降级状态。

九、繁简展示应是内容策略,不是全局替换

一个清晰的卡片可以这样组织:

粤语原文:你食咗饭未? 粤拼:nei5 sik6 zo2 faan6 mei6 普通话释义:你吃饭了吗? 例句语境:日常问候

这不是“把整页切换为繁体”,而是保留学习对象并提供解释。若增加繁简偏好,可以只控制解释文本的资源版本,不能修改题库答案所依赖的粤语原文。

错误做法包括:

  • 将“咗”替换成普通话“了”后仍称为粤语原文。
  • 对题干和答案分别转换,导致匹配失效。
  • 用字符替换表生成粤拼。
  • 把香港与广州语音差异压成唯一“标准答案”。

十、分库详情的文化 Profile 已经落地

BankDetailContent为b_yue提供:

{ subtitle: '从茶餐厅到地铁口,练最常见的粤语表达', intro: '粤语题库覆盖基础发音、日常对话和城市常见短句,适合想先听懂、再敢开口的人。', cultureNote: '内容会把早茶、通勤、港式流行语和礼貌表达放进同一套练习里,实用度很高。', focusTags: ['听辨节奏', '礼貌表达', '生活口语'], sceneTags: ['早茶点单', '通勤借过', '街头问路'] }

这些内容真实展示在详情页,但“基础发音”“听辨节奏”是学习主题,不等于当前已有可验证音频。技术文章要把“题库覆盖的知识点”和“应用提供的播放能力”分开。

十一、章节、随机与考试入口保持同一bankId

粤语分库可进入:

{ bankId: 'b_yue', chapterId, mode: 'chapter' } { bankId: 'b_yue', mode: 'random' } { bankId: 'b_yue', mode: 'exam' }

PracticePage 应以b_yue查找题库题目,不依赖上一页标题或当前 Tab。参数校验包括:

  • chapter 模式必须有属于粤语题库的章节 ID。
  • random 模式只能从粤语题目抽取。
  • exam 模式按粤语真实可用题量出卷。
  • 无效题库或空题集不直接进入结果页。

如果用户从收藏或搜索进入粤语题目,也应携带同一题库 ID,使进度写回正确分区。

十二、本地进度不能被文本变体拆成两份

学习记录应使用稳定题目 ID,而不是题干文本作为 key。否则编辑人员把“你食咗饭未?”改成另一字形后,旧进度会丢失或重复。

当前题目 ID由:

id: `${bankId}_q${i + 1}`

生成。它比题干稳定,但依赖数组顺序;在中间插入题目可能让后续 ID整体变化。长期维护可为每道题提供人工稳定 ID,例如:

id: 'yue_daily_have_you_eaten_001'

文本、粤拼、释义和例句可以更新,学习记录仍归到同一道题。

十三、去重不能只依赖题干trim()

当前目录按题干去重:

const key = r.stem.trim() if (seen.has(key)) continue seen.add(key)

粤语文本中,直角引号、弯引号、全半角标点、空格和繁简差异都可能让语义相同题目绕过去重。可以建立内容规范化,仅用于去重 key,不修改展示文本:

function normalizeForDedup(text: string): string { return text .normalize('NFKC') .replace(/[“”"]/g, '') .replace(/\s+/g, '') .trim() }

但不要自动做简繁转换去重,因为不同字形可能对应有意的教学差异。疑似重复应交给内容审核确认。

十四、Unicode 安全不等于语义安全

ArkTS 字符串能够保存粤语字符,但以下操作仍需谨慎:

  • 不用substring()按 UTF-16 单元截断用户可见文本。
  • 搜索时明确是否区分繁简与异体字。
  • 高亮匹配要保持原始索引。
  • 导出和日志使用 UTF-8。
  • JSON 与资源文件保持一致编码。

如果实现搜索别名,可以为每个表达维护searchAliases:

searchAliases: [ '食咗饭未', '吃饭了吗', 'nei5 sik6 zo2 faan6 mei6' ]

显示仍使用原文,搜索可以匹配解释或粤拼。

十五、音频播放的状态机必须完整

真实音频能力至少包含:

idle -> preparing -> playing -> completed -> error

还要处理:

  • 连续点击同一个按钮。
  • 播放一个词时切换另一个词。
  • 页面退到后台。
  • 音频焦点被其他应用占用。
  • 资源不存在或损坏。
  • 在线引擎不可用。

按钮不能只显示一个永远不变的播放图标。加载、播放和错误状态要让用户看得懂;离开页面时停止并释放资源。

十六、多设备布局复用共享详情能力

YueBankPage根容器满宽满高,BankDetailContent根据断点和实际宽度切换:

  • 常规窗口:单列滚动。
  • lg且宽度不小于 700vp:双栏。

底部随机练习与模拟考试按钮使用系统导航避让高度。若后续新增粤语词条音频区,应避免在左右两栏同时创建播放器;可以把播放器服务保持为页面级单实例。

2in1 还应支持鼠标悬停、键盘焦点与空格播放。音频按钮的触控/点击区域不能只有 20vp 图标本身。

十七、隐私与权限说明要对应在线回退

模块声明ohos.permission.INTERNET。如果语音功能使用在线 TTS 回退,隐私与审核备注应准确说明:

  • 核心题库与学习记录保存在本地。
  • 若本地语音引擎不可用,系统语音能力可能尝试在线服务。
  • 当前是否实际启用粤语播放,要以题型和 locale 支持为准。
  • 应用不需要麦克风权限,因为这是语音输出而非录音。

不能同时写“完全离线”和保留在线引擎回退。如果产品决定使用纯本地真实音频资源,可以删除不必要的在线回退与网络权限,再重新构建验证。

十八、粤语分库测试矩阵

入口与参数

  • YueBankPage 固定展示b_yue。
  • 外部传入其他 bankId 不覆盖固定 ID。
  • 路由清单包含pages/YueBankPage。
  • 三种练习模式均保持b_yue。

文本

  • 粤语原文不被自动改写。
  • “唔、冇、咗、攰、啲、嚟”真机无缺字。
  • 普通话释义与粤语原文分字段。
  • 大字体和窄屏不截断核心文本。
  • 搜索别名不改变原文。

音频

  • audioHint不被当作音频资源。
  • 重分类后的 vocab 题不显示虚假听音按钮。
  • 只有真实资源或已验证粤语 locale 才允许播放。
  • 无资源时隐藏按钮。
  • 播放失败有状态反馈。

数据

  • 粤语进度不串到其他题库。
  • 文本编辑后稳定题目 ID不变。
  • 重复题目经规范化检查。
  • 题量来自最终去重、重分类后的集合。

十九、可以宣传与不能宣传的边界

可以准确描述:

  • 粤语分库拥有独立入口并固定b_yue。
  • 本地题库包含粤语字形、普通话解释和文化语境。
  • 页面展示学习进度和六个章节。
  • 支持章节、随机和考试入口。
  • 共享详情适配 phone、tablet、2in1。

当前不能直接描述:

  • 已实现简繁双向转换。
  • 已提供标准粤语或真人粤语读音。
  • 所有“听音题”当前都可播放。
  • audioHint是录音文件。
  • zh-CNTTS 输出已经过粤语发音验证。

二十、总结

粤语分库的工程难点不在十几行入口代码,而在内容语义不能被技术名词掩盖。YueBankPage真实完成了固定地区身份和共享详情复用;题库真实保存了粤语字形、释义和文化场景;练习链路真实支持章节、随机和考试。但当前没有运行时简繁转换,历史听音题被重分类为词汇题,audioHint只是文本,zh-CNTTS 也不能证明粤语读音。

要把“繁简文本与读音”做成可复核能力,应使用结构化粤语原文、普通话解释、粤拼与真实音频资源,建立来源和播放状态,再经过真机字形、发音和多设备验证。保持这条事实边界,才既保护用户体验,也避免上架材料夸大功能。


繁简文本不是一次全局替换

粤语题库里的字形、词义、书写习惯和显示语言是不同维度。把所有繁体字机械转换为简体,可能改变题目要考查的原始写法;完全不转换,又可能让只使用简体界面的用户难以理解。更稳的方式是保留题目原文,并为需要解释的内容增加独立字段,例如规范释义、普通话说明或可选的简体辅助文本。

转换规则不应散落在 ArkUI 页面。页面只决定显示哪一种字段,文本服务负责规范化与缺失回退,题库数据仍保存可追溯原文。这样调整显示策略时不会改写题目本体,也不会让同一道题在列表、练习和结果页出现不同字形。

测试时需要准备三类样本:繁简字形相同、字形不同但语义一致、粤语惯用字不能直接按普通话替换。每类样本都要核对题干、选项、答案与解析,不能只比较标题。若转换后的选项发生重复,还要阻止该题进入练习,因为用户将无法区分正确答案。

读音提示必须与音频能力分开

当前数据模型中出现的读音或audioHint可以作为文字提示,但字段名称不等于已经具备可播放音频。没有真实音频文件、播放组件、资源授权与设备验证时,文章和界面都不应宣传听力训练。文字读音还要明确采用的标注体系,并允许某些题目没有提示。

建议把展示能力写成显式模型,而不是用非空字符串推断播放器存在:

interfacePronunciationHint {text?:stringscheme?:'jyutping'|'custom'audioResource?:Resourcereviewed:boolean}functioncanPlayAudio(hint?:PronunciationHint):boolean{returnhint?.audioResource!==undefined}

这是建议实现,不是当前源码已经存在的接口。它把文本提示、标注体系、人工校对和音频资源拆开。页面可在只有文字时显示“读音提示”,只有资源真实存在时才渲染播放按钮,从数据结构上避免能力夸大。

用反例检查粤语内容是否串库

第一组反例是从粤语入口携带其他题库 ID。由于fixedBankId应优先生效,页面仍应展示b_yue。第二组是传入不存在的 ID:数据层不应静默回退到四川话或其他题库,而应返回题库不存在。第三组是完成粤语章节后进入其他地区,进度数字不应变化。

还要交叉核对页面标题、章节 ID、题目来源和持久化键。只看“粤语题库”标题并不足以证明内容正确;如果RAW_MAP键写错,标题可以正确而题目已经串区。进度键至少包含 bankId,章节记录还应包含 chapterId,不能使用可能变更的显示名称作为主键。

六章分配要能解释内容归属

源码声明基础发音、九声六调、日常对话、茶餐厅词汇、老广俗语和香港流行语六个章节。若题目只是按数组序号取模分配,六章都能获得数据,却不保证题意与章节标题一致;数组顺序变化还会让同一道题转移章节。

长期方案是让原始题目显式携带稳定 chapterId,并在装配阶段校验该章节属于b_yue。迁移前先生成每章题量、无效章节、重复题干和跨库引用报告,再由熟悉粤语的人审核语义。关键词脚本能发现结构问题,不能替代语言判断。

题目内容本身也需要来源与校对记录。技术架构只能保证数据按 ID 隔离,不能自动证明读音、用字和文化解释准确。涉及地域差异的说法,应注明适用语境,避免把某一地区或某一代际的表达概括为全部粤语使用者。

页面状态与练习入口一起收敛

题库不存在、题库存在但无题、某章节为空是三种状态。前者只保留返回;空题库隐藏随机练习和考试;空章节仅禁用对应入口。加载阶段也应阻止重复点击,避免在数据尚未就绪时打开空练习页。

typeYueBankState='loading'|'notFound'|'empty'|'ready'privatecanStart(questionCount:number):boolean{returnthis.pageState==='ready'&&questionCount>0}

状态枚举同样属于建议改造。它让渲染提示和按钮能力来自同一判定,避免多个布尔值互相矛盾。共享详情容器负责通用状态,粤语包装页继续只负责固定身份,职责边界不会因为补空态而倒退。

发布前验证清单

检查项验证方法预期结果
固定入口携带错误 bankId 进入 YueBankPage仍使用 b_yue
繁简文本抽查题干、选项、答案和解析原文可追溯,辅助文本一致
读音边界检查只有 hint、没有音频的题目不出现播放能力
数据隔离对比粤语与其他题库内容标题、章节、题目不串库
进度隔离完成粤语章节后打开其他题库其他题库进度不变
空态测试无题库、空题库和空章节提示与可用按钮匹配
适配长文本、小窗、横屏和深色模式文本不遮挡,入口可达

本轮仅完成静态源码复核,没有运行 hvigor 构建、模拟器或真机测试。表中的动态结论需要在目标环境执行后确认,不能从代码存在直接表述为已经通过。

真机复核还应覆盖连续进入与返回、快速重复点击、应用重启后的进度恢复,以及繁体系统语言和简体系统语言下的显示差异。若页面增加复制、朗读或分享功能,需要重新检查文本来源、权限、无障碍描述和隐私材料,不能默认沿用当前结论。每次题库内容更新后,应抽样比较缓存前后题量和稳定题目 ID,确认修订没有误删答案、打乱章节或覆盖已有进度。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进