【口算王|06】HarmonyOS ArkTS 题库详情实战:组织年级、题型与练习入口 题库详情页不是一张“题库介绍海报”。它要把题库元数据、本地生成题目、学习进度、章节进度和三种练习入口放在同一页面中还要保证手机、小窗和平板看到的是同一份业务状态。页面上的“250 题”“已答 42 题”“正确率 86%”都必须能回到真实数据源。本文基于口算王项目D:\huawei\one16-11的真实源码复核BankDetailPage.ets、MockBanks.ets、MathModels.ets、UserDataManager.ets与PracticePage.ets。项目包名com.jiaweikang.one16用作本文唯一核验标记。当前实现已经覆盖一年级到六年级的题库目录每个题库包含六个章节题目由本地算法生成详情页展示封面、简介、训练重点、章节进度并提供章节练习、随机练习与限时挑战。需要特别注意的是五六年级的页面文案提到小数、分数和百分数但当前通用生成器实际仍主要产生整数加减乘除、混合运算与应用题。文章会把这种“目录文案与题目能力的一致性”作为发布复查重点不会把尚未实现的题型写成已有能力。一、详情页连接了五类数据BankDetailContent同时消费路由或固定参数提供的bankIdMockBanks返回的Bank与ChapterAppStorage中的BankProgressAppStorage中的ChapterProgress当前断点、安全区与页面宽度。这意味着页面不是静态展示组件。它既是目录浏览页也是训练入口和学习状态汇总页。从工程边界看应先确定题库身份再读取目录与进度最后派生展示模型。路由、题库、进度任一环节缺失页面都要有可解释的降级状态。二、Bank 与 Chapter 的真实模型题库模型定义在公共模型层export interface Chapter { id: string index: number title: string total: number finished: number done: boolean } export interface Bank { id: string regionId: string name: string cover: Resource totalCount: number accuracy: number hot: number chapters: Chapter[] }这里的finished、done和accuracy看起来像运行时进度但当前页面并不直接信任这些字段。它通过UserDataManager查询本地进度再计算完成度和正确率。这是正确方向目录定义负责“有什么”用户数据负责“做了多少”。不过模型仍有冗余。Chapter.finished、Chapter.done与ChapterProgress表达重复概念后续维护时容易出现两个来源不同步。更清晰的做法是让目录模型保持只读export interface ChapterDefinition { id: string index: number title: string total: number } export interface ChapterViewState { definition: ChapterDefinition finished: number correct: number ratio: number done: boolean }页面消费合并后的ChapterViewState不再猜测哪个字段更新过。三、六个年级如何组织MockBanks.ets真实声明了六个区域export const REGIONS: Region[] [ { id: grade1, name: 一年级, shortName: 一, ... }, { id: grade2, name: 二年级, shortName: 二, ... }, { id: grade3, name: 三年级, shortName: 三, ... }, { id: grade4, name: 四年级, shortName: 四, ... }, { id: grade5, name: 五年级, shortName: 五, ... }, { id: grade6, name: 六年级, shortName: 六, ... } ]每个区域对应一个题库如b_grade1、b_grade6。题库与区域之间通过regionId关联而不是依靠显示名称匹配。这使标题文案可以调整不会破坏路由和进度主键。年级不是题型。页面用年级组织入口而题目内部使用add、sub、mul、div、mixed、word、speed七种类型。后续做题型筛选时应通过Question.type不能从章节中文标题猜类型。四、章节总数不是写死的展示数字初始化时章节的total都是 0。getBankById()会先调用syncCatalogCounts()export function getBankById(id: string): Bank | undefined { syncCatalogCounts() return BANKS.find((bank: Bank) bank.id id) }同步函数遍历真实生成的题目计算题库总数和每章题数for (const bank of BANKS) { const questions getQuestions(bank.id) bank.totalCount questions.length for (const chapter of bank.chapters) { const chapterTotal questions.filter( (question: Question) question.chapterId chapter.id ).length chapter.total chapterTotal chapter.finished 0 chapter.done false } }当前每个题库目标生成 250 道题章节 ID 按序分配所以六章题量大致均衡。详情页展示的“共 N 题”来自同步后的bank.totalCount不是单独维护的营销数字。五、同步目录时直接修改全局对象的边界syncCatalogCounts()会原地修改全局BANKS和Chapter对象。当前单机数据规模很小功能上可行但有三个工程边界每次getBankById()都遍历所有题库和题目全局可变对象让测试用例之间可能互相影响目录定义与派生统计混在一起。可以缓存不可变目录快照let catalogReady: boolean false function ensureCatalogCounts(): void { if (catalogReady) return for (const bank of BANKS) { const questions getQuestions(bank.id) bank.totalCount questions.length for (const chapter of bank.chapters) { chapter.total questions.filter( (item: Question) item.chapterId chapter.id ).length } } catalogReady true }更进一步可在构建阶段生成题库清单 JSON让运行时只读减少首次进入详情页的工作量。六、题库身份支持两种来源页面既可以作为独立路由页也可以嵌入其他产品布局。fixedBankId优先于路由参数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) } }这个设计让同一内容组件可复用但要明确优先级只要fixedBankId非空路由参数就不会生效。对于未知bankIdgetBankById()返回undefined页面展示“未找到题库”空态。这是当前真实存在的降级能力。仍建议给空态增加返回动作避免用户只能依靠系统返回手势。七、题库级进度如何派生页面从BankProgress[]查找当前题库private bankFinished(): number { const progress UserDataManager.getProgress( this.progressList, this.bankId() ) return progress ? progress.finished : 0 }完成度计算会限制到 1private bankProgressRatio(): number { if (!this.bank || this.bank.totalCount 0) return 0 return Math.min( this.bankFinished() / this.bank.totalCount, 1 ) }这个上限处理很重要因为当前updateProgress()按每次练习累加已答题数。用户重复练习同一题后finished可以超过题库唯一题目数。页面把进度条限制为 100%但旁边文字仍可能显示320/250。这里需要先决定业务口径如果finished表示累计作答次数就不应作为“题库完成度”分子如果表示已覆盖的唯一题数就要按questionId去重如果产品需要两者应分别命名“累计作答”和“题目覆盖率”。仅用Math.min()隐藏进度条溢出并没有解决统计语义。八、首次进入时的正确率回退页面正确率实现为private bankAccuracy(): number { const progress UserDataManager.getProgress( this.progressList, this.bankId() ) if (!progress || progress.finished 0) { return this.bank ? this.bank.accuracy : 0 } return progress.correct / progress.finished }当前BANKS初始accuracy都是 0因此未练习时展示 0%。这可能让用户误以为已经答错而不是尚无数据。可以让返回值表达缺失状态private bankAccuracyText(): string { const progress UserDataManager.getProgress( this.progressList, this.bankId() ) if (!progress || progress.finished 0) return -- const ratio Math.min( 1, Math.max(0, progress.correct / progress.finished) ) return ${Math.round(ratio * 100)}% }“暂无数据”与“正确率 0%”是不同事实详情页应该区分。九、章节进度与完成状态章节进度使用(bankId, chapterId)作为复合键private chapterFinished(chapter: Chapter): number { const progress UserDataManager.getChapterProgress( this.chapterProgressList, this.bankId(), chapter.id ) return progress ? progress.finished : 0 } private chapterDone(chapter: Chapter): boolean { return chapter.total 0 this.chapterFinished(chapter) chapter.total }章节卡根据进度显示“开始”“继续”或“完成”。这个状态机来自真实数据finished 0开始0 finished total继续finished total完成。但ChapterProgress.finished同样按练习次数累加。如果一章有 42 道题用户重复完成两组 20 题即使题目重叠也可能被标为完成。要表达真实覆盖率需要保存已完成题目 ID 集合或位图。十、三个练习入口的路由契约详情页提供三类入口。章节练习router.pushUrl({ url: pages/PracticePage, params: { bankId: this.bank.id, chapterId: chapter.id, mode: chapter } })随机练习router.pushUrl({ url: pages/PracticePage, params: { bankId: this.bank!.id, mode: random } })限时挑战router.pushUrl({ url: pages/PracticePage, params: { bankId: this.bank!.id, mode: exam } })同一个PracticePage通过mode和可选的chapterId决定题目来源。这种路由契约简单清晰但字符串模式容易拼写错误。可以收敛为常量export class PracticeMode { static readonly CHAPTER: string chapter static readonly RANDOM: string random static readonly EXAM: string exam }所有入口、参数模型和页面判断共用同一组值。十一、题库文案与题目生成能力必须一致详情页为每个年级准备了专属介绍。例如五年级文案包含“小数、分数和单位换算”六年级包含“分数、百分数和比例”。但当前buildQuestions()的七个槽位实际生成整数加法非负整数减法表内乘法整除题整数混合运算简单加法应用题整数限时题。grade只影响数字范围和少量常数并没有生成小数、分数、百分数或比例。章节标题与focusTags是展示文案不代表题型已经实现。这是上架审核和技术文章都必须守住的边界。修复有两条路径在真实题型实现前把五六年级文案改为当前可支持的整数综合练习新增 decimal、fraction、percent、ratio 题目模型、生成器、格式化与解析再保留现有文案。不能只改标题或标签来制造功能完整的印象。十二、题型与章节目前是轮转分配生成题目时章节 ID 由题目序号取模得到chapterId: ${regionId}_c${(i % 6) 1}题型则由另一个七槽位轮转决定。因为 6 和 7 不同每个章节会混合多种题型而不是“第一章只含加法、第二章只含减法”。getQuestionsByChapter()只按chapterId过滤export function getQuestionsByChapter( bankId: string, chapterId: string ): Question[] { return getQuestions(bankId).filter( (question: Question) question.chapterId chapterId ) }因此章节中文标题与真实题目集合也需要复查。理想设计是在生成时根据章节定义选择题型和参数而不是仅按序号轮转。十三、宽屏布局是当前真实能力页面通过断点和实际宽度共同决定宽屏private useWideLayout(): boolean { return this.currentBp lg this.pageWidth 700 }页面根容器监听面积变化.onAreaChange((oldArea: Area, newArea: Area) { const width Number(newArea.width) if (width 0) { this.pageWidth width } })宽屏时左侧 38% 展示封面、汇总和简介右侧展示训练重点与章节列表窄屏则使用单列滚动。这是面向 HarmonyOS 多设备和窗口变化的真实适配不是只按设备名分支。需要验证临界宽度附近是否频繁切换、38%左栏是否能容纳长标题以及 2in1 缩放时底部操作区是否一直可达。十四、底部操作区如何避开系统导航页面固定显示随机练习和限时挑战按钮底部间距取private bottomSafePadding(): number { return Math.max( Sizes.BOTTOM_NAV_MIN_PADDING, this.getUIContext().px2vp( this.navigationIndicatorHeightPx ) ) }这保证按钮至少保留项目定义的最小安全距离同时适配真实导航指示区高度。主内容使用Scroll底部操作不随内容滚走常用训练入口始终可达。横屏小窗仍要检查两个文本按钮是否过窄。必要时可在极窄宽度下改为纵向按钮或只保留图标加短标签但不能让文字溢出。十五、建议构造统一的 BankDetailViewState当前多个 Builder 会重复调用bankFinished()、bankAccuracy()、profile()和章节查询。数据量很小但统一展示状态更利于测试export interface BankDetailViewState { bankId: string title: string subtitle: string totalCount: number answeredCount: number accuracyText: string progressRatio: number chapters: ChapterViewState[] focusTags: string[] sceneTags: string[] }构建流程可以是校验bankId读取不可变题库定义读取题库与章节进度归一化计数与比例生成BankDetailViewStateArkUI Builder 只渲染。页面不再散落业务判断单元测试也能直接验证“未练习正确率显示--”“重复练习不冒充覆盖率”等规则。十六、性能优化应先看数据规模每个题库 250 题共六个题库约 1500 题。syncCatalogCounts()在获取任一题库时扫描全部题库章节统计又对每章执行一次filter()。当前规模通常可接受但这是可避免的重复工作。优化顺序建议给getQuestions()保留现有缓存目录统计只初始化一次单次遍历同时累计题库、章节和题型数量题库定义与用户进度分开只有测量发现瓶颈后再考虑更复杂索引。不要为了 1500 条本地对象引入网络数据库或沉重状态框架。简单缓存和单次归并已经足够。十七、验证矩阵场景预期合法 bankId展示对应年级题库未知 bankId展示“未找到题库”无路由参数安全空态不崩溃首次进入已答 0正确率显示无数据更合理有题库进度完成度和正确率按同一记录计算累计作答超过题量进度条不溢出同时明确累计口径章节无进度按钮显示“开始”章节部分进度按钮显示“继续”章节达到总题量显示“完成”点击章节携带 bankId、chapterId、chapter 模式点击随机练习携带 bankId、random 模式点击限时挑战携带 bankId、exam 模式宽度低于 700单列滚动lg 且宽度至少 700双栏布局深浅色切换文本、封面遮罩、标签和按钮可读还应抽样核对每个年级的章节标题与真实题目不仅看题量还要检查题型、数字范围、答案格式和解析内容。十八、常见问题与排查现象可能原因检查位置题库总数为 0未执行目录统计getBankById()章节题数都为 0chapterId 不匹配syncCatalogCounts()完成度长期 100%finished 是累计次数进度口径正确率显示 NaNfinished 为 0 或数据损坏bankAccuracy()点击章节进入空页章节 ID 与题目 ID 不一致getQuestionsByChapter()五年级没有小数题文案超过生成器能力buildQuestions()平板仍是单列断点或 pageWidth 未达条件useWideLayout()底部按钮被遮挡安全区高度未初始化navigationIndicatorHeightPx十九、发布前检查清单六个年级、六个题库和章节 ID 一一对应题库总数来自真实生成题目章节总数来自chapterId过滤结果题型标签与Question.type一致首次进入不把“无数据”误写成“0%能力”累计作答次数与唯一题目覆盖率分开章节完成规则符合产品口径三种练习入口参数可被PracticePage正确识别未知题库有空态和返回路径五六年级文案不超过当前题目生成能力手机、小窗、平板和 2in1 布局可达长标题、标签和章节名不溢出底部按钮避开系统导航区深浅色下封面遮罩和文本对比度合格本地生成、离线可用等声明与真实权限和代码一致。二十、总结口算王的题库详情页已经把年级目录、题库元数据、本地题量、题库进度、章节进度和三类训练入口组织成一个可用界面并通过断点与实际宽度提供单列、双栏布局。它的核心价值不是卡片数量而是把“题库定义”和“用户训练状态”合并为可操作入口。继续提升时要守住三条线目录文案必须与真实题目生成器一致累计作答不能直接冒充唯一题目覆盖率所有路由入口必须共用稳定的 bankId、chapterId 和 mode 契约。把这些数据边界明确后页面才能在 HarmonyOS 5.0 以上设备上稳定适配也能让文章、应用介绍和上架材料中的每个能力声明都可从源码复核。