news 2026/8/24 12:45:47

【中国方言题库|11】HarmonyOS ArkTS 学习统计实战:计算地区学习进度与收藏数量

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【中国方言题库|11】HarmonyOS ArkTS 学习统计实战:计算地区学习进度与收藏数量

摘要:中国方言题库的学习统计页没有维护一套额外计数器,而是从bankProgressexamHistoryfavoriteRecordswrongRecords四组本地事实数据实时汇总。StatService负责全局答题数、加权正确率、考试次数、收藏数、错题数和已学习题库数,页面再按题库展示完成量与正确率,并在大屏断点下切换为三列布局。本文面向 HarmonyOS 5.0 及以上版本,基于真实 ArkTS 源码复核统计公式、Preferences 到 AppStorage 的数据链、近五次平均分、进度条钳制和多设备布局,同时指出累计答题次数可能超过题库题量等真实语义边界。

一、统计页应消费事实,不应手工同步计数器

统计类页面最常见的错误,是每完成一次动作就同时修改多个计数器:

答题记录 +1 总答题数 +1 题库答题数 +1 今日答题数 +1

任何一步遗漏都会产生漂移。中国方言题库采用更简单的路线:答题流程只更新题库进度、考试历史、收藏和错题等事实集合,统计页按需汇总。

唯一校验标记:统计值从事实集合推导,不再维护第二套手工计数器

二、真实源码分为页面、统计服务和持久化三层

本文复核:

entry/src/main/ets/pages/LearningStatsPage.ets libraryb/src/main/ets/utils/StatService.ets librarya/src/main/ets/utils/UserDataManager.ets entry/src/main/ets/mock/MockBanks.ets entry/src/main/ets/common/components/ProgressBar.ets entry/src/main/ets/entryability/EntryAbility.ets

职责分工是:

UserDataManager -> 读取和持久化本地事实 AppStorage / @StorageLink -> 在页面间同步事实数组 StatService -> 执行纯统计计算 LearningStatsPage -> 按断点渲染概览和题库进度

页面没有直接访问 Preferences,也没有在 Builder 中复制一套持久化逻辑。

三、四组事实数据分别代表什么

页面声明:

@StorageLink('bankProgress') progressList: BankProgress[] = [] @StorageLink('examHistory') examHistory: ExamHistory[] = [] @StorageLink('favoriteRecords') favRecords: FavoriteRecord[] = [] @StorageLink('wrongRecords') wrongRecords: WrongRecord[] = []

四组数据语义不同:

数据代表事实统计用途
BankProgress[]每个题库累计答题与答对量总答题、总正确、题库进度
ExamHistory[]每次考试结算快照考试次数、近五次均分
FavoriteRecord[]当前收藏的问题收藏数量
WrongRecord[]当前仍在错题集的问题错题数量

“收藏数”和“错题数”是当前集合长度,不是历史上累计收藏过或答错过多少次。

四、Preferences 如何进入 AppStorage

应用启动时,EntryAbility.onCreate()调用:

UserDataManager.init(this.context)

UserDataManager从名为dialect_quiz的 Preferences 读取 JSON 字符串,然后初始化:

AppStorage.setOrCreate<FavoriteRecord[]>( 'favoriteRecords', JSON.parse(favStr) as FavoriteRecord[] ) AppStorage.setOrCreate<WrongRecord[]>( 'wrongRecords', JSON.parse(wrongStr) as WrongRecord[] ) AppStorage.setOrCreate<BankProgress[]>( 'bankProgress', JSON.parse(progStr) as BankProgress[] ) AppStorage.setOrCreate<ExamHistory[]>( 'examHistory', JSON.parse(examStr) as ExamHistory[] )

统计页通过@StorageLink消费这些值。数据修改后,相关页面能观察到新数组,不需要再次打开数据库。

五、BankProgress 是累计尝试量,不是去重题目数

模型定义:

export interface BankProgress { bankId: string finished: number correct: number lastChapterId: string updatedAt: string }

更新时执行累加:

finished: old.finished + addFinished correct: old.correct + addCorrect

因此finished表示累计完成的答题次数。用户重复练习同一道题,计数仍会增加。它不是“题库中已经去重掌握的题目数量”。

标题中“学习进度”在当前实现里的准确含义是“累计答题量相对题库题量的展示进度”,不能把它当成严格的知识点完成率。

六、StatService 返回统一 LearningStats

统计结果接口:

export interface LearningStats { totalAnswered: number totalCorrect: number accuracyPercent: number examCount: number favoriteCount: number wrongCount: number studiedBankCount: number }

页面通过:

private stats() { return StatService.summarize( this.progressList, this.examHistory, this.favRecords, this.wrongRecords ) }

获得同一份统计口径。首页、我的页面也复用StatService.summarize(),避免多个页面各算各的。

七、总答题数和总正确数如何计算

实现为:

let totalAnswered = 0 let totalCorrect = 0 for (const p of progressList) { totalAnswered += p.finished totalCorrect += p.correct }

这是一种按题库进度聚合的累计量。如果四川话完成 30 次、粤语完成 20 次:

totalAnswered = 30 + 20 = 50

它不依赖题库总数,也不扫描全部题目,计算复杂度为:

O(progressList.length)

八、全局正确率是加权正确率

公式:

accuracyPercent: totalAnswered > 0 ? Math.round( totalCorrect / totalAnswered * 100 ) : 0

它先汇总所有正确数和答题数,再计算比例。假设:

四川话:1/1,正确率 100% 粤语:50/100,正确率 50%

全局正确率是:

(1 + 50) / (1 + 100) = 50.5% -> 50%

而不是:

(100% + 50%) / 2 = 75%

前者按真实答题样本加权,更符合“所有答题的总体正确率”。

九、零答题保护避免 NaN

totalAnswered === 0时,服务返回0

totalAnswered > 0 ? Math.round(totalCorrect / totalAnswered * 100) : 0

因此新用户看到0%,不会出现NaN%Infinity%。统计服务统一处理后,多个页面无需重复防御。

仍需注意损坏数据,例如correct > finished会产生超过 100% 的正确率。当前 Preferences 数据来自内部更新路径,正常不会出现;如增加导入恢复,应在数据层校验。

十、考试次数为何直接取历史长度

服务返回:

examCount: examHistory.length

每进入一次结果页,UserDataManager.addExamHistory()会追加一条记录。因此页面展示的是本地历史条数。

如果结果页生命周期重复触发导致同一场考试重复写入,考试次数也会被放大。统计服务不会主动去重,因为ExamHistory没有sessionId。要解决,应在写入源头增加幂等键,而不是在统计页猜哪些记录重复。

十一、收藏数量是当前收藏集合长度

统计:

favoriteCount: favorites.length

收藏模型:

export interface FavoriteRecord { questionId: string bankId: string createdAt: string }

toggleFavorite()找到同一questionId时移除,否则在数组头部新增。项目题目 ID 带题库前缀,因此当前集合能表达跨题库收藏。

这不是“收藏操作次数”。用户收藏后取消,数量会回落。

十二、错题数量代表当前待复习项

服务:

wrongCount: wrongs.length

添加错题时先过滤同题,再新增:

const filtered = records.filter( r => r.questionId !== questionId ) const result = [ { questionId, bankId, wrongAt: nowStr() }, ...filtered ]

因此同一道题答错多次仍只占一条。在错题练习中答对后,页面调用removeWrong()删除它。

所以“错题数”更接近当前错题本中的待复习数量,而不是历史错误次数。

十三、已学习题库数的真实口径

服务使用:

studiedBankCount: progressList.length

只要某题库有一条BankProgress就被计入。正常更新路径首次答题才创建记录,因此通常等于有学习行为的题库数。

但它没有检查:

p.finished > 0

也没有验证bankId是否仍存在于BANKS。如果导入了零进度或旧版本残留题库,数量可能偏大。更严格的实现是:

const validBankIds = new Set( BANKS.map(bank => bank.id) ) const studiedBankCount = progressList.filter( p => p.finished > 0 && validBankIds.has(p.bankId) ).length

当前源码尚未采用这种过滤。

十四、近五次平均分依赖历史顺序

recentAvgScore()

static recentAvgScore( examHistory: ExamHistory[], count: number = 5 ): number { if (examHistory.length === 0) return 0 const recent = examHistory.slice( 0, Math.min(count, examHistory.length) ) const sum = recent.reduce( (s, e) => s + e.score, 0 ) return Math.round(sum / recent.length) }

UserDataManager.addExamHistory()把新记录放在数组头部,所以slice(0, 5)确实得到最近五次。

若未来从文件导入历史,必须先保证顺序,或者按finishedAt排序;否则“近五次”只代表数组前五条。

十五、题库正确率如何处理未开始状态

实现:

static bankAccuracy( progressList: BankProgress[], bankId: string ): number { const p = progressList.find( r => r.bankId === bankId ) if (!p || p.finished === 0) return 0 return Math.round( p.correct / p.finished * 100 ) }

没有进度或答题数为零时显示0%。页面同时使用progressLabel()显示“未开始”,因此用户能区分“未开始的 0%”和“已经答题但全错的 0%”吗?

当前两者文案不同:

未开始:进度标签“未开始”,正确率 0% 全错:进度标签“学习中”,正确率 0%

这组组合足以表达差异。

十六、每个题库的已答题数来自 bankId

服务:

static bankFinished( progressList: BankProgress[], bankId: string ): number { const p = progressList.find( r => r.bankId === bankId ) return p ? p.finished : 0 }

页面对BANKS中每个题库调用该方法。题库模型含regionId,当前六个题库分别对应四川、粤语、东北、上海、闽南、客家等地区或方言分支,因此页面按题库展示,也形成了地区学习分布。

但源码标题是“各题库学习进度”,并没有直接按Region聚合多个题库。若未来一个地区下有多个题库,需要额外按regionId汇总。

十七、题库总题量来自真实题目目录

BANKS初始totalCount为 0,随后syncCatalogCounts()遍历实际题目:

for (const bank of BANKS) { const questions = getQuestions(bank.id) bank.totalCount = questions.length }

所以页面分母:

bank.totalCount

来自当前题库真实题目数量,不是手写宣传数字。

十八、累计答题可能超过题库总题量

页面展示:

`${finished}/${bank.totalCount}`

由于finished是累计尝试次数,用户反复练习后可能出现:

320/300

progressLabel()会在:

finished >= bank.totalCount

时显示“已完成”。这在视觉上表示学习量已覆盖总题量阈值,但不能证明 300 道不同题都做过。

若产品需要去重完成度,应保存已答questionId集合,不能从累计次数推导。

十九、进度条会把 ratio 钳制到 0 到 1

页面传入:

ratio: bank.totalCount > 0 ? finished / bank.totalCount : 0

公共ProgressBar再执行:

Math.max( 0, Math.min(1, this.ratio) )

因此即使finished > totalCount,前景宽度最多 100%,不会溢出容器。数字仍会显示真实累计值,不会被静默改写。

二十、统计概览由四个核心指标组成

概览卡显示:

已答题 正确率 考试次数 学习题库

紧凑布局使用两行两列;大屏布局使用一行四列。指标值全部来自StatService.summarize(),没有写死演示数字。

页面每次调用this.stats()都重新汇总。当前数组很小,开销有限;如果历史达到数千条,可在状态变化时生成一次 ViewModel,避免一次构建重复计算。

二十一、数据摘要卡展示收藏、错题和均分

DataSummary()

this.DataCard( `${this.stats().favoriteCount}`, '收藏题目', Colors.ACCENT ) this.DataCard( `${this.stats().wrongCount}`, '错题数', Colors.ERROR ) this.DataCard( `${StatService.recentAvgScore( this.examHistory )}`, '近5次均分', Colors.SUCCESS )

三张卡片使用不同语义色,但标签文字仍明确说明含义,不只依靠颜色区分。长标签通过单行省略保护布局。

二十二、大屏为何同时检查断点和宽度

布局判断:

private useGridLayout(): boolean { return ( this.currentBp === 'lg' && this.pageWidth >= 700 ) }

既要求断点为lg,又要求实际页面宽度至少 700。这样可以避免断点状态滞后或容器嵌套导致页面实际宽度不足时仍强行三列。

pageWidth通过根容器:

.onAreaChange( (oldArea: Area, newArea: Area) => { const width = Number(newArea.width) if (width > 0) { this.pageWidth = width } } )

实时更新,支持窗口缩放。

二十三、三列题库卡片如何适应宽屏

大屏使用:

Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween })

每个题库卡外层:

.width('32%') .margin({ bottom: 10 })

一行三列留出间隙,题库数量超过三时自动换行。紧凑模式则用单列Column

这种适配覆盖 phone、tablet 和 2in1 的基础信息密度变化,没有为每种设备复制页面。

二十四、题库卡的文本溢出保护

题库名称:

.maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis })

摘要卡标签也有同样保护。题库图固定 58x58,右侧内容使用:

.layoutWeight(1)

所以长名称会优先省略,不会把进度数字挤出卡片。宽屏三列下尤其需要这种约束。

二十五、页面为什么全部可滚动

主体放在:

Scroll() { Column({ space: 18 }) { // 概览、摘要、各题库 } } .layoutWeight(1) .edgeEffect(EdgeEffect.Spring)

手机横屏、小窗或系统字体放大时,所有题库仍可通过滚动访问。底部使用Blank().height(20)保留视觉间距。

当前页面没有读取底部导航安全区;它作为普通二级页使用TopBar,但在手势导航和特殊窗口下仍应实机确认最后一张卡是否有足够避让。

二十六、统计服务为何适合单元测试

StatService方法:

  • 不访问 UI;
  • 不读 Context;
  • 不直接访问 Preferences;
  • 输入数组,返回数字或对象;
  • 没有异步回调。

因此可以用简单样本验证:

const progress: BankProgress[] = [ { bankId: 'b_sichuan', finished: 10, correct: 8, lastChapterId: '', updatedAt: '' }, { bankId: 'b_yue', finished: 20, correct: 10, lastChapterId: '', updatedAt: '' } ]

预期:

totalAnswered = 30 totalCorrect = 18 accuracyPercent = 60 studiedBankCount = 2

二十七、近五次均分的边界测试

至少覆盖:

空历史 -> 0 1 次 80 分 -> 80 3 次 80/90/100 -> 90 6 次 -> 只取数组前 5 条 count=0 -> 当前实现会产生 0/0

最后一个边界很重要。方法默认count=5,页面不会传 0;但公共 API 若被其他模块调用count=0recent=[],随后除以零会得到NaN

可以增加:

if (count <= 0) return 0

当前页面路径安全,但服务契约仍有加固空间。

二十八、数据损坏时应在哪一层处理

UserDataManager.init()把所有读取放在一个try/catch中。任意 JSON 解析失败会回退初始化多组空数组。这样能避免启动崩溃,但可能因为一个字段损坏而丢失其他可用统计。

更细粒度的策略是逐键解析:

function parseArray<T>( raw: string, fallback: T[] ): T[] { try { const value = JSON.parse(raw) as T[] return Array.isArray(value) ? value : fallback } catch (_) { return fallback } }

统计页只消费已归一化的数据,不应在 UI 层到处判断 JSON 错误。

二十九、收藏数量与地区数量如何扩展

标题关注“地区学习进度与收藏数量”。当前收藏只展示全局总数。如果要按地区统计收藏,可利用记录中的bankId和题库的regionId

FavoriteRecord.bankId -> Bank.id -> Bank.regionId

再按regionId分组。当前每个主要题库对应一个地区,分组结果会接近按题库统计。

扩展时不要直接从题目中文名称解析地区。应使用稳定 ID 映射,避免文案调整破坏统计。

三十、当前统计不是活跃天数或学习时长

页面没有保存每日学习日志,也没有汇总durationSec为总学习时长。ExamHistory只记录考试用时,普通章节和随机练习的timerSec没有进入统计服务。

因此现有能力不能描述为:

  • 连续学习天数;
  • 今日学习时长;
  • 总学习小时数;
  • 地区掌握度预测;
  • 用户排名。

这些指标都需要新增真实数据模型和更新链路。

三十一、性能上避免在 build 中多次 summarize

OverviewCard()中多次调用:

this.stats().totalAnswered this.stats().accuracyPercent this.stats().examCount this.stats().studiedBankCount

每次都会重新遍历progressList。当前只有少量题库,影响可忽略;但代码可读性和可测试性仍可改善。

可在构建前准备:

const stats = this.stats()

ArkUI Builder 对普通局部语句有约束时,可以改为缓存@StateViewModel,并在事实数组变化时更新。不要为了小数组过早引入复杂响应式框架。

三十二、统计测试矩阵

场景预期
全部数组为空所有统计为 0,页面不崩溃
单题库 10 答 8 对总答题 10,正确率 80%
多题库样本不等使用加权正确率
收藏后取消收藏数回落
同题重复答错错题集合只保留一条
错题模式答对错题数减少
无考试历史近五次均分 0
超过 5 次考试只取最新前 5 条
累计答题超过题量数字真实显示,进度条不溢出
lg 且宽度 >=700三列题库布局
lg 但宽度不足保持单列
窗口缩放页面Width更新并切换布局

三十三、发布前真实性与合规检查

  1. “已答题”明确是累计答题次数;
  2. “学习题库”不等于全部题库完成;
  3. “错题数”是当前错题集,不是历史错误次数;
  4. “收藏题目”是当前收藏集合;
  5. “近5次均分”依赖历史按新到旧排列;
  6. 不宣称学习时长、连续天数、排名或掌握度;
  7. 清除本地数据后统计同步归零;
  8. 深浅色下概览卡与数据卡对比度合格;
  9. 手机、平板、2in1、小窗和横屏布局不截断;
  10. 大字体下题库名称和指标标签不重叠;
  11. Preferences 只保存必要本地学习数据;
  12. 安装、启动、答题、统计更新、重启恢复和卸载流程稳定。

三十四、结语

LearningStatsPage的工程价值不在于显示了多少张卡,而在于数据口径集中:BankProgress保存累计答题事实,ExamHistory保存考试结算,收藏和错题保存当前集合;StatService用纯函数汇总;页面通过@StorageLink响应数据变化,并在大屏条件下切换布局。

同样要承认当前边界:题库“完成量”是累计尝试次数,可能超过真实题量;学习题库数直接取进度数组长度;近五次均分依赖数组顺序;页面没有学习时长、活跃天数或排名。统计可信的前提,不是数字看起来丰富,而是每个数字都能沿着真实记录追溯到计算公式。

---

AI 辅助声明:本文部分内容由 AI 辅助整理,所有功能描述、代码片段、统计口径和工程结论均依据项目真实源码人工复核;未伪造学习时长、用户排名、收藏量或平台数据。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/24 12:41:08

[光学原理与应用-541]:计算机视觉检测激光器腔体污染:系统方案

一、腔体中需要检测的污染类型表格检测对象常见污染 / 缺陷典型尺寸腔镜 / 反射镜表面灰尘颗粒、油污、指纹、镀膜脱落、激光损伤点、划痕μm~mm 级晶体端面颗粒、油污、膜层损伤、微裂纹、崩边μm 级晶体内部 / 焊接面微裂纹、掺杂不均、焊接空洞、铟焊层缺陷μm~ 亚 mm胶接部…

作者头像 李华
网站建设 2026/8/24 12:35:40

RAG系统从Demo到生产:12大核心痛点与实战解决方案

很多开发者都有过这样的经历&#xff1a;用 LangChain 或 LlamaIndex 快速搭建一个 RAG&#xff08;检索增强生成&#xff09;Demo&#xff0c;效果惊艳&#xff0c;感觉“大模型知识库”的智能问答系统已经触手可及。然而&#xff0c;当你信心满满地准备将这套系统部署上线&am…

作者头像 李华
网站建设 2026/8/24 12:34:59

隐马尔可夫方法

隐马尔可夫方法&#xff1a; 几个基本要素&#xff1a;隐状态的数量K&#xff0c;每个状态的概率向量Π&#xff0c;隐状态转移矩阵A&#xff0c;隐状态到观测的转换矩阵&#xff08;即发射矩阵&#xff09;B。模型可被定义为λ [A,B, Π]。 隐马尔可夫就是要在基于模型λ成…

作者头像 李华
网站建设 2026/8/24 12:34:07

(论文速读)Diff2Flow:基于扩散模型对齐的训练流匹配模型

论文题目&#xff1a;Diff2Flow: Training Flow Matching Models via Diffusion Model Alignment&#xff08;Diff2Flow&#xff1a;基于扩散模型对齐的训练流匹配模型&#xff09;会议&#xff1a;CVPR2025摘要&#xff1a;扩散模型通过高保真输出彻底改变了生成性任务&#x…

作者头像 李华
网站建设 2026/8/24 12:33:59

构建企业级AI Agent:LangGraph与MCP协议下的可观测性实践

1. 先搞清楚面试官到底想听什么&#xff1a;从“玩具”到“企业级”的鸿沟面试时被问到“写一个 AI Agent 项目”&#xff0c;如果你只讲通了 LangChain 的链条、调了 OpenAI 的 API&#xff0c;大概率会被认为项目深度不够。现在的面试官&#xff0c;尤其是中高级岗位&#xf…

作者头像 李华
网站建设 2026/8/24 12:31:23

腾讯元宝流程图怎么导出 ?「AI 导出鸭」一键全搞定,告别格式乱

引言 你是否遇到过&#xff1a;在腾讯元宝辛苦画好的流程图&#xff0c;导出后文字乱码、线条错位、高清图变马赛克&#xff1f;团队协作时&#xff0c;流程图无法直接插入PPT或Word&#xff0c;只能截图再修图&#xff1f;更头疼的是&#xff0c;不同浏览器、不同设备导出的效…

作者头像 李华