摘要:两个地区分库如果分别编写导航、返回、详情和空状态,短期看只是多几十行 ArkTS,长期却会产生行为漂移。“中国方言题库”的闽南语与客家话入口都复用
BankDetailContent,由BankCard统一选择路由,main_pages.json注册页面,TopBar统一返回,题库 ID 再决定目录、Profile、题目与进度。本文基于 HarmonyOS 5.0+ 当前真实源码,复核两条分库链路,重点分析“题库不存在”和“题库存在但暂无内容”为什么不能混成一个空状态。
版本范围与可直接落地的最小改造
本文核验范围明确为HarmonyOS 5.0 及以上版本、Stage 模型、ArkTS/ArkUI Router 页面栈。针对源码中“题库缺失”和“内容为空”尚未分离的问题,可以先在共享详情组件落地一个不改变路由结构的最小改造。
type BankDetailState = 'ready' | 'bankMissing' | 'contentEmpty' private detailState(): BankDetailState { if (this.bank === undefined) return 'bankMissing' if (this.bank.totalCount <= 0) return 'contentEmpty' return 'ready' } private canStartPractice(): boolean { return this.detailState() === 'ready' }渲染时让bankMissing显示“未找到题库”,让contentEmpty显示“暂无可练题目”,并用canStartPractice()禁用随机练习与模拟考试。该改造只触及共享详情状态判断,闽南语与客家话入口、路由清单、卡片映射和现有进度键无需变化。
验收输入:b_minnan / b_hakka / b_unknown 验收状态:正常详情 / 正常详情 / 题库缺失 补充夹具:有效题库 totalCount=0 -> 内容空态且按钮禁用一、两个页面只有地区 ID 不同
MinnanBankPage.ets:
import { BankDetailContent } from './BankDetailPage' @Entry @Component struct MinnanBankPage { build() { Column() { BankDetailContent({ fixedBankId: 'b_minnan' }) } .width('100%') .height('100%') } }HakkaBankPage.ets:
import { BankDetailContent } from './BankDetailPage' @Entry @Component struct HakkaBankPage { build() { Column() { BankDetailContent({ fixedBankId: 'b_hakka' }) } .width('100%') .height('100%') } }两个入口都没有复制标题栏、封面卡片、章节列表、进度条或练习按钮。唯一差别是fixedBankId。
本文的唯一标记是:空状态必须区分路由失配与内容为空。前者是配置或参数问题,后者是有效题库暂时没有可练内容,两者的恢复动作完全不同。
二、完整导航链路有四个环节
从题库卡片进入地区分库,不是调用一次router.pushUrl()就结束。完整链路包括:
BankCard 选择目标页面 -> main_pages.json 注册页面 -> 地区入口注入 fixedBankId -> BankDetailContent 查询题库并渲染其中任一环节不一致,都会形成不同故障:
- 卡片映射错误:进入另一个地区。
- 页面未注册:导航失败。
- 固定 ID 拼错:显示“未找到题库”。
- 目录缺少题库:入口存在但无法加载。
因此,统一导航首先要统一这些环节之间的契约,而不是只让两个页面看起来相似。
三、BankCard是地区路由分发点
当前卡片组件按题库 ID 选择页面:
private detailPageUrl(): string { switch (this.bank.id) { case 'b_minnan': return 'pages/MinnanBankPage' case 'b_hakka': return 'pages/HakkaBankPage' default: return 'pages/BankDetailPage' } }实际源码还包含四川话、粤语、东北话和上海话分支。未知题库会回退到通用详情页。
这个设计的优点是地区卡片可以落到稳定的独立页面;风险是switch、页面文件和路由清单需要同步更新。新增题库时漏改其中一处,编译未必立即指出业务映射错误。
可以通过自动化测试验证:
expect(detailPageUrl('b_minnan')).toBe('pages/MinnanBankPage') expect(detailPageUrl('b_hakka')).toBe('pages/HakkaBankPage')这里是测试思路,不表示项目当前已经接入该测试框架。
四、路由清单必须包含两个入口
main_pages.json当前真实注册:
{ "src": [ "pages/BankDetailPage", "pages/MinnanBankPage", "pages/HakkaBankPage", "pages/PracticePage", "pages/ExamResultPage" ] }这使pages/MinnanBankPage与pages/HakkaBankPage能被 Router 找到。文件存在但未进入清单,不能算可导航页面。
发布前至少做三组检查:
卡片映射 URL == main_pages.json 中的字符串 main_pages.json 条目 == 实际 ets 页面路径 页面 fixedBankId == BANKS 中的题库 ID字符串路径应保持完全一致,包括大小写。开发机文件系统可能容忍大小写差异,构建或设备环境不一定容忍。
五、固定 ID 比外部参数优先
共享详情初始化逻辑为:
aboutToAppear(): void { if (this.fixedBankId.length > 0) { this.bank = getBankById(this.fixedBankId) return } const params = router.getParams() as BankDetailParams | undefined if (params && params.bankId) { this.bank = getBankById(params.bankId) } }闽南语页面始终查询b_minnan,客家话页面始终查询b_hakka。地区专属入口不会被调用方附带的其他bankId覆盖。
这是导航一致性的第二层保护。第一层由BankCard选择页面,第二层由入口固定地区。即使调用方参数残留,详情仍不会串区。
六、题库 ID 串起目录、内容与进度
目录中的两项为:
{ id: 'b_minnan', regionId: 'minnan', name: '闽南语题库', cover: $r('app.media.img_bank_cover_minnan'), hot: 84, chapters: MN_CHAPTERS } { id: 'b_hakka', regionId: 'hakka', name: '客家话题库', cover: $r('app.media.img_bank_cover_hakka'), hot: 80, chapters: HK_CHAPTERS }同一 ID 还用于:
RAW_MAP 选择题目集合 QUESTIONS_CACHE 选择缓存 PracticePage 选择练习题 UserDataManager 读取题库进度 章节进度的 bankId 维度不要把minnan与b_minnan当成可互换别名。前者是地区 ID,后者是题库 ID;章节又使用minnan_c1形式。
七、两套章节配置保持相同结构
闽南语章节:
const MN_CHAPTERS = makeChapters('minnan', [ '基础发音', '日常用语', '海洋词汇', '南洋文化', '闽南俗语', '歌仔戏词' ])客家话章节:
const HK_CHAPTERS = makeChapters('hakka', [ '基础发音', '客家家训', '农耕词汇', '迁徙故事', '土楼生活', '客家俗语' ])两者都由makeChapters()产生六章,详情组件因此能使用同一套ForEach、进度条和“开始/继续/完成”状态。
不过当前题目展开时按索引对六取模分配章节,不代表每道题已经按语义人工标注。章节 UI 一致与内容分类准确是两个问题,文章不能把前者包装成后者。
八、地区特色由 Profile 配置,不由页面复制
闽南语 Profile:
{ subtitle: '从问候到俗语,感受闽南语的生活温度', intro: '闽南语题库兼顾基础发音、常用问候和民间俗语,适合从熟悉的日常场景建立记忆。', cultureNote: '内容会结合歌仔戏、南洋迁徙和民间饮食文化,让词句背后有更完整的来历。', focusTags: ['基础问候', '俗语记忆', '文化理解'], sceneTags: ['家常寒暄', '市场交流', '戏曲文化'] }客家话 Profile:
{ subtitle: '从家族称呼到土楼生活,练出客家话的辨识度', intro: '客家话题库适合从称谓、家训和迁徙文化切入,先抓住有根脉感的表达,再做题型强化。', cultureNote: '会把土楼、祭祖、农耕和客家家风放进练习语境里,让语言和文化一起记住。', focusTags: ['称谓表达', '家风词汇', '文化背景'], sceneTags: ['家族交流', '土楼生活', '节俗礼仪'] }共享页面不等于共享文案。正确做法是把地区差异收敛成数据配置,而不是复制整棵 ArkUI 组件树。
九、真实题目内容也完全分开
闽南语题目包含:
逐家、好势、啥物、拍谢、囡仔、厝边、佗位 中秋博饼、红砖古厝、妈祖信俗、南洋迁徙客家话题目包含:
屋下、阿公、食朝、转屋下、细人仔、硬颈 围屋、晴耕雨读、客家山歌、酿豆腐、历史迁徙它们分别映射到b_minnan与b_hakka:
['b_minnan', MN_QUESTIONS.concat(/* 多组扩展数据 */)] ['b_hakka', HK_QUESTIONS.concat(/* 多组扩展数据 */)]共享详情只读取转换后的Bank与Question,不会把两地区原始数组合并。测试中应抽查题目的bankId,而不是只看页面标题。
十、顶部返回由同一个TopBar实现
详情页统一使用:
TopBar({ title: this.bank ? this.bank.name : '题库详情' })TopBar的返回点击为:
.onClick(() => { router.back() })并通过状态栏避让高度计算顶部空间:
private topSafePadding(): number { return Math.max( 0, this.getUIContext().px2vp(this.topAvoidAreaHeightPx) ) }闽南语与客家话无需分别维护返回图标、触控区域和状态栏内边距。46vp的返回容器也比只让24vp图标响应点击更容易操作。
十一、返回语义依赖正确的入栈方式
地区卡片进入详情通常使用router.pushUrl(),详情页再执行router.back()。这条组合能回到来源页面,并保留来源列表的滚动与状态。
需要避免这些错误组合:
入口使用 replaceUrl,详情却期待回到题库列表 详情返回时硬编码跳到首页,丢失真实来源 练习完成后重复 push 详情,形成多层相同页面 卡片连续点击造成重复入栈当前TopBar采用标准返回,不硬编码目的地。若产品允许从搜索、统计或收藏进入同一分库,这种返回语义更自然。
十二、现有空状态只覆盖“题库不存在”
当getBankById()返回undefined时,共享详情显示:
if (this.bank === undefined) { Column({ space: 12 }) { Image($r('app.media.img_empty_default')) .width(120) .height(120) .objectFit(ImageFit.Contain) .opacity(0.6) Text('未找到题库') } .layoutWeight(1) .width('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) }这是真实存在的空态,适用于:
fixedBankId拼写错误。- 目录删除了对应题库。
- 通用详情页收到未知
bankId。 - 页面与数据版本不一致。
它不是“题目为空”提示。题库对象存在但题量为零时,bank仍然有效,页面会进入正常详情分支。
十三、路由失配与内容为空必须分开
建议把页面状态显式建模:
type DetailState = | 'loading' | 'ready' | 'bankMissing' | 'contentEmpty' | 'loadError'对应提示与动作应不同:
bankMissing:题库入口无效,返回上一页 contentEmpty:题库已存在,暂时没有可练题目 loadError:读取失败,可重试 ready:正常展示详情和练习入口若把所有情况都显示“未找到题库”,用户无法判断是数据尚未上线还是页面坏了;开发日志也失去定位价值。
十四、内容为空时不应保留可点击练习按钮
当前底部动作直接导航:
router.pushUrl({ url: 'pages/PracticePage', params: { bankId: this.bank!.id, mode: 'random' } })如果题库目录存在、最终题量却为零,用户仍可能点击随机练习或模拟考试。更完整的详情状态应根据totalCount控制:
private canStartPractice(): boolean { return this.bank !== undefined && this.bank.totalCount > 0 }然后禁用按钮并显示原因:
该分库暂无可练题目 内容准备完成后即可开始这段是改进建议。当前源码已经有“未找到题库”,但没有独立的“内容为空”详情状态,不能声称两种空态均已完成。
十五、getQuestions()的默认回退需要警惕
题目查询中存在:
const source = RAW_MAP.get(bankId) || SICHUAN_QUESTIONS对于有效的b_minnan和b_hakka,它们都能命中RAW_MAP,不会触发回退。但如果错误 ID 绕过详情进入练习,函数会使用四川话原始题目,再把展开后的bankId标成错误 ID。
这会把“配置错误”伪装成“有题可做”,比明确空态更难发现。更安全的接口应返回空数组或结果对象:
interface QuestionLoadResult { ok: boolean bankId: string questions: Question[] reason?: 'bank-not-found' | 'content-empty' }在可验证内容系统中,宁可明确失败,也不要静默借用另一个地区的数据。
十六、章节空态还需要第三层判断
即使题库总题量大于零,某个章节也可能没有题。当前章节筛选为:
export function getQuestionsByChapter( bankId: string, chapterId: string ): Question[] { return getQuestions(bankId).filter( (question: Question) => question.chapterId === chapterId ) }练习页检测章节结果为空后,会再次读取整个题库。这能避免空白练习页,却可能让用户点“歌仔戏词”后做到了全题库题目。
更清晰的策略是:
章节有题:进入章节练习 章节无题:留在详情并提示“本章暂无题目” 题库无题:显示内容空态,禁用所有练习入口 题库不存在:显示路由/目录失配空态空态越接近错误发生的位置,用户越不容易被错误范围误导。
十七、错误状态要保留返回能力
共享详情即使bank === undefined,顶部仍然存在:
TopBar({ title: this.bank ? this.bank.name : '题库详情' })所以“未找到题库”页面仍有返回按钮。这一点很重要:空状态不能变成无法退出的死路。
还应验证系统返回手势与顶部按钮一致:
点击顶部返回:回到来源页 系统侧滑/返回键:回到来源页 连续进入两个分库后返回:按真实栈顺序退出 空态返回:不重复创建首页对于 2in1 设备,还要验证鼠标点击和键盘导航能触达返回控件。
十八、标题过长与左右占位保持对称
TopBar左右都保留46vp区域,中间标题使用:
Text(this.title) .layoutWeight(1) .textAlign(TextAlign.Center) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis })因此“闽南语题库”和“客家话题库”都能保持视觉居中。右侧没有功能时仍使用Blank().width(46),避免标题因左侧返回按钮而偏移。
如果以后标题增加地区全称,不要通过缩小到不可读字号解决;应保留单行省略,并在正文 Hero 卡片显示完整题库名。
十九、进度状态也要按两个题库隔离
共享详情读取进度时使用当前题库 ID:
const p = UserDataManager.getProgress( this.progressList, this.bankId() )闽南语与客家话分别使用:
b_minnan b_hakka章节进度还组合bankId + chapterId。导航正确但进度键错误,仍会出现“进入闽南语,看到客家话进度”的问题。
回归测试应执行真实流程:
完成一组闽南语题 -> 返回详情 -> 闽南语进度增加 进入客家话详情 -> 客家话进度不随之增加 完成客家话某章 -> 仅对应 hakka_cN 更新二十、两类内容的真实性边界
可以准确描述当前能力:
- 闽南语与客家话均有独立入口。
- 两个入口均复用共享题库详情。
- 卡片组件按题库 ID 选择入口。
- 页面已注册在
main_pages.json。 - 顶部导航统一使用
router.back()。 - 题库不存在时显示“未找到题库”。
- 两套本地题目与 Profile 分别配置。
- 支持章节、随机和考试入口。
当前不应描述:
- 已有“内容为空”的独立详情状态。
- 所有章节都有人工语义标注。
- 所有历史听音题都有真实音频。
- 无效题库 ID 一定返回空数组。
- 方言内容已由权威机构逐条认证。
- 闽南语或客家话内部不存在地区差异。
二十一、导航与空状态测试清单
闽南语入口
BankCard(b_minnan) -> pages/MinnanBankPage 页面注入 b_minnan 标题显示闽南语题库 题目 bankId 均为 b_minnan 返回恢复原列表位置客家话入口
BankCard(b_hakka) -> pages/HakkaBankPage 页面注入 b_hakka 标题显示客家话题库 题目 bankId 均为 b_hakka 返回恢复原列表位置异常路径
未知 bankId 显示“未找到题库” 错误 fixedBankId 不展示其他地区 空态仍可返回 未注册页面在构建/自动化阶段被发现 题量为零时不应进入无意义练习 空章节不应静默扩展为全题库多设备
phone 单列可完整滚动 tablet/2in1 宽屏布局不遮挡返回 状态栏避让正确 底部按钮避开系统导航区域 大字体下标题、空态和按钮不重叠二十二、总结
闽南语与客家话分库展示了一种可持续的地区导航方式:BankCard决定目标路由,main_pages.json注册页面,薄入口固定题库 ID,BankDetailContent统一详情与缺失态,TopBar统一返回,再由题库 ID 贯穿内容、章节和进度。
当前源码已经真实完成“题库不存在”的空状态,却没有把“有效题库暂无内容”和“某章节暂无题目”分别建模;getQuestions()对未知 ID 还会回退到四川话数据。这些边界不影响对现有有效入口的复核,却是继续扩展地区题库时必须优先处理的工程风险。
统一页面并不意味着把所有错误压成一句提示。真正可靠的复用,是共享导航和布局,同时保留足够精确的状态:路由失配、目录缺失、内容为空、章节为空和读取失败都能被识别、解释并安全返回。做到这一点,新增地区才不会让用户体验和排障成本一起失控。
---
AI 辅助声明:本文由 AI 辅助整理,现状结论基于“中国方言题库”当前MinnanBankPage、HakkaBankPage、BankCard、TopBar、BankDetailContent、MockBanks与路由清单真实源码复核;独立内容空态、结果模型和严格校验均作为改进建议呈现,未描述为已上线能力。