当前位置: 首页 > news >正文

篮球口袋教练 HarmonyOS 学习应用(07):个人页成就与学习统计

一、统计卡片不能从页面临时变量取数

个人页同时展示连续学习、累计时长、课程完成、练习和测验等信息。若每张卡片各自维护一个计数器,返回页面、重启应用或在其他页面新增记录后,数字很容易彼此矛盾。这里把个人页视为统计结果的消费者,而不是新的事实来源。

export class AchievementService { static checkAll(): string[] { const unlocked: string[] = []; // --- streak --- const streak: number = ProgressService.streak(); if (streak >= 3) unlocked.push('streak_3'); if (streak >= 7) unlocked.push('streak_7'); if (streak >= 14) unlocked.push('streak_14'); if (streak >= 30) unlocked.push('streak_30'); // --- study --- const studied: string[] = AppStorage.get<string[]>(StorageKeys.STUDIED) ?? []; if (studied.length >= 1) unlocked.push('study_1'); if (studied.length >= 5) unlocked.push('study_5'); if (studied.length >= 10) unlocked.push('study_10'); const seconds: number = AppStorage.get<number>(StorageKeys.LEARN_SECONDS) ?? 0; if (seconds >= 3600) unlocked.push('time_1h'); if (seconds >= 18000) unlocked.push('time_5h'); // --- practice --- const logs: PracticeLog[] = AppStorage.get<PracticeLog[]>(StorageKeys.PRACTICE_LOGS) ?? []; if (logs.length >= 1) unlocked.push('practice_1'); if (logs.length >= 10) unlocked.push('practice_10'); if (logs.length >= 30) unlocked.push('practice_30'); for (const log of logs) { if (log.rating >= 5) { unlocked.push('practice_star'); break; } } // --- note --- const notes: NoteItem[] = AppStorage.get<NoteItem[]>(StorageKeys.NOTES) ?? []; if (notes.length >= 1) unlocked.push('note_1'); if (notes.length >= 5) unlocked.push('note_5'); // --- explore --- const favs: string[] = AppStorage.get<string[]>(StorageKeys.FAVORITES) ?? []; if (favs.length >= 1) unlocked.push('fav_1'); if (favs.length >= 10) unlocked.push('fav_10'); // --- quiz ---

二、成就条件怎样复用已持久化的学习事实

AchievementService 会把 ProgressService 的连续学习天数、已学习课程集合、PracticeLogService 的日志、笔记集合以及 QuizService 的结果放到同一次条件检查中。它返回的是已解锁成就 id;页面再用该结果筛选徽章,不把“显示过的徽章”反向当作用户数据。

事实唯一来源页面职责
正常结果模型或服务的回读渲染可观察状态
边界结果明确的缺项或失败分支说明原因并保留入口
重进结果持久化或模型再次解析不复用旧页面变量
} static totalSeconds(): number { return AppStorage.get<number>(StorageKeys.LEARN_SECONDS) ?? 0; } // 连续学习天数(从今天往前数连续有记录的天数) static streak(): number { const days: string[] = AppStorage.get<string[]>(StorageKeys.LEARN_DAYS) ?? []; if (days.length === 0) return 0; const sorted: string[] = days.slice().sort(); const today: string = dayKey(); // 如果今天没有学习记录,从昨天开始算 let cur: Date = new Date(); if (sorted.indexOf(today) < 0) { cur.setDate(cur.getDate() - 1); } let count: number = 0; while (true) { const key: string = dayKey(cur); if (sorted.indexOf(key) >= 0) { count++; cur.setDate(cur.getDate() - 1); } else { break; } } return count; } }

三、个人页与成就页为何必须各自回读

累计学习秒数和连续天数都来自持久化键。记录学习行为后,服务先写入 AppStorage 并同步 Preferences;个人页下次显示时重新读取,因此统计不会依赖此前页面是否仍在内存。这个取舍增加了回读成本,却避免了跨页面共享可变数组。

@StorageLink('user_nick') nick: string = ''; @State achieveCount: number = 0; aboutToAppear(): void { this.achieveCount = AchievementService.checkAll().length; } build() { Column() { Row() { Text('我的') .fontSize(AppText.fsTitle) .fontWeight(AppText.fwSemi) .fontColor(this.palette.textPrimary) .layoutWeight(1); } .width('100%').height(AppSizes.topBar) .padding({ left: AppSizes.s4, right: AppSizes.s4 }); Scroll() { Column({ space: AppSizes.s4 }) { this.userCard(); this.statsCard(); this.menuList(); Column().height(AppSizes.s8); } .width('100%') .padding({ left: AppSizes.s4, right: AppSizes.s4 }); }

四、错误统计最容易发生在什么地方

成就阈值的边界必须可解释。例如连续学习三天、完成五个课程、完成一次测验分别是不同事实,不能用总点击次数替代。若某项写入失败,页面应显示未满足条件,而不是预先点亮徽章;否则用户会看到无法在重启后复现的“成功”。

风险不能采用的做法当前链路的处理
数据不存在用默认对象继续渲染停在可解释的空或失败状态
页面重进复用上一次内存字段重新查询模型或服务
重复动作追加同一业务事实通过稳定 id 或进入标记收敛
export class QuizService { static submitResult(moduleId: string, score: number, total: number): void { const results: QuizResult[] = QuizService.allResults(); results.push({ moduleId, score, total, ts: Date.now() } as QuizResult); AppStorage.setOrCreate<string>(QUIZ_KEY, JSON.stringify(results)); } static allResults(): QuizResult[] { const raw: string = AppStorage.get<string>(QUIZ_KEY) ?? '[]'; try { return JSON.parse(raw) as QuizResult[]; } catch (e) { return []; } } static bestScore(moduleId: string): number { const results: QuizResult[] = QuizService.allResults(); let best: number = 0; for (const r of results) { if (r.moduleId === moduleId && r.score > best) { best = r.score; } } return best; } static hasCompleted(moduleId: string): boolean { return QuizService.allResults().some(r => r.moduleId === moduleId); }

五、可观察的验收动作

验收时先完成一项课程或写入一条练习日志,再进入个人页与成就页分别回读。随后强制退出并重新打开,比较累计时长、连续天数和解锁数量是否保持一致;最后切换测验模块,确认某一模块成绩不会改写另一模块的统计。

验收阶段操作应观察到的结果
建立事实完成一次与主题直接对应的动作服务或模型能回读该事实
页面回读进入目标页或结果页标题、数据和状态相互一致
重进验证返回、重启或切换模块后再次进入结果不依赖上一页残留

六、取舍与后续维护

当前实现把业务事实集中到模型与服务,页面只负责呈现和触发操作。这样做会多出类型、查询和回读代码,却能避免同一份统计或记录在多个页面分叉。需要扩展新课程、新资源或新题目时,应先补齐稳定标识与服务合同,再补页面入口。

ArkTS 的响应式状态机制可参考 HarmonyOS ArkTS 状态管理文档。本文的代码和验收围绕同一个原则:可见结果必须能回到对应模型、服务或持久化事实,不以邻近页面和控件文字代替主题结果。

七、围绕事实来源继续演进

篮球口袋教练 HarmonyOS 学习应用(07):个人页成就与学习统计 关注的不是给页面再增加一层显示逻辑,而是让“统计卡片不能从页面临时变量取数”始终能回到唯一的业务事实。模型字段、服务返回值和页面展示应当保持单向关系:事实先发生,页面随后回读;页面不保存第二份能够被误认为真实数据的临时副本。

处理“成就条件怎样复用已持久化的学习事实”时,新增条件必须先归属到课程模型、资源服务、日志事实或测验结果之一。把条件散落在多个组件里,短期看起来省事,后续就会出现入口不同、重进不同、统计不同的问题。把规则放回稳定入口,页面只根据结果改变可见状态,能让同一结论在不同入口被复查。

“个人页与成就页为何必须各自回读”说明当前实现选择了页面重建后重新查询,而不是把对象和统计快照长期塞进路由参数。读取会增加少量成本,却避免课程改名、资源替换、日志编辑或题库切换后继续显示旧内容。学习应用的结果需要能解释,不能只追求一次渲染恰好看起来正确。

异常分支也应保留语义:成就阈值的边界必须可解释。例如连续学习三天、完成五个课程、完成一次测验分别是不同事实,不能用总点击次数替代。若某项写入失败,页面应显示未满足条件,而不是预先点亮徽章;否则用户会看到无法在重启后复现的“成功”。 这要求页面区分数据不存在、动作未完成和资源不可用,并提供返回选择、补充输入、重新下载或再次答题等对应操作;不能把不同原因压缩成同一条模糊的成功或失败提示。

最后执行“可观察的验收动作”时,至少观察一次主题动作、一次目标页回读和一次重进或切换后的回读。三次观察都指向相同模型或服务结果,才说明实现链路没有依赖旧页面残留。

面向维护的检查可以沿三个问题展开:输入对象的稳定标识是什么,哪个服务或模型决定结果,页面如何把该结果显示出来。把连续学习、课程进度、练习日志和测验结果汇成成就事实,个人页只消费服务回读,避免把展示字段当成统计源。 这三个问题分别对应数据合同、调用边界和可见反馈;只要其中一层被绕过,新增入口就可能产生和原入口不同的事实。

对失败路径也要保留同样的追踪能力。先让服务返回清晰的不可用、缺项或未完成状态,再由页面提供与原因匹配的操作。这样日志、课程、资源和题目发生变化时,回归测试能够看到具体的分歧点,而不是只能面对一张没有信息的空白页面。

这种组织不会承诺所有后续需求都零成本。新增字段需要同步更新模型、服务与验收;但它把变更范围限制在可枚举的位置,避免把同一判断复制到首页、详情、个人页和结果页后再分别修补。

http://www.jsqmd.com/news/1289174/

相关文章:

  • 【单片机毕业设计】基于嵌入式技术的温湿度监测与智能执行机构设计 基于多传感器的智能柜体自动管控硬件系统实现(012001)
  • ARM --- SPI通信协议简介,IMX6ULL使用SPI与ADXL345传感器进行数据通信
  • 2026广州代理记账机构靠谱合规排行|中小微企业服务商甄选权威指南+FAQ问答 - 互联网科技品牌测评
  • 2026年常州钟楼区系统门窗实力公司解析 - 优企名品
  • 2026年制造业节能型大型风扇源头厂家:韵蓝环境设备(安徽)有限公司 - 优企名品
  • PDF导航革命:3分钟为你的PDF文档添加智能书签目录
  • 3步搞定:如何轻松从Steam游戏页面批量提取完整数据
  • 猫抓插件:三步搞定网页资源下载,开启媒体嗅探新体验!
  • 合肥南亚理工学校|电话是多少?逐个拆解班型的适合人群 - hflgzz
  • 中小型企业财务软件选型效果实测指南
  • 精选!2026十大企业AI知识库管理系统开发服务商汇总
  • 告别熬夜做PPT✨Paperxie隐藏王牌!一键生成满分学术答辩PPT
  • AI预测“被告人是否上诉”准确率达89.3%,但92%律所尚未部署——这3个合规接入接口正在关闭
  • 暑期学习:自学java第十四天
  • 2026成都区别墅地下室漏水维修服务商推荐 10 家:正规实力机构盘点,选型避坑指南 - 鼎万建筑修缮
  • 终极Win11系统优化指南:4步深度定制你的Windows 11性能与隐私
  • 2026 天津南开区卫生间漏水维修机构大盘点:7 家正规靠谱服务商实力解析,附选型避坑全指南及注意事项 - 鼎万建筑修缮
  • 企业AI搭建:从零到落地的关键步骤
  • NBM5100A与PIC32MX675F512L的低功耗物联网电源管理方案
  • MarkdownTextView扩展开发实战:轻松实现自定义语法高亮
  • 推荐靠谱的环境试验箱销售厂家:首发 - 品牌推广大师
  • Steam游戏DLC解锁终极指南:SmokeAPI完整使用教程
  • 潍坊正规鸢都充电程序售卖租用
  • 突破性Windows网络性能测试方案:iperf3完整指南与下载安装
  • 2026年三轮车托运价格表:整车寄运费多少钱?不拆电池怎么算? - 快递物流资讯
  • 2026北京二手翡翠回收,如何避免被低价收购 - 一日一测评
  • 2026 玄奘路敦煌戈壁徒步|零基础新手完整筹备避坑指南
  • Xshell配色方案终极指南:250+主题打造高效开发环境
  • 如何用OCR4all处理早期印刷书籍?LAREX布局分析工具实战指南
  • 2026年07月 制造业过滤耗材领域:广东佳美滤清器有限公司的体系化能力剖析 - 优企名品