云计算百科
云计算领域专业知识百科平台

篮球口袋教练 HarmonyOS 学习应用(05):练习记录与体能训练计划

“今天练了什么”只有在能回看、能统计时才有价值。篮球口袋教练将练习记录作为独立数据项保存,练习页负责录入与展示,统计页从同一组记录计算总次数、总时长、平均评分和本周次数。训练计划则作为课程内容提供方法,不与用户实际完成记录混为一谈。

篮球口袋教练 HarmonyOS 学习应用(05):练习记录与体能训练计划运行回读

一、记录模型保存一次训练的事实

练习记录至少包含类型、内容、时长、评分和时间。保存时由服务读取现有列表、追加新记录,再同步更新响应式状态和持久化数据;页面刷新不需要重新拼装不同来源的临时变量。

static add(log: PracticeLog): void {
const logs = PracticeLogService.all()
logs.unshift(log)
AppStorage.setOrCreate(StorageKeys.PRACTICE_LOGS, logs)
PreferencesHelper.get().persist(StorageKeys.PRACTICE_LOGS, logs)
}

数据用途不能替代什么
练习类型与内容 说明训练做了什么 不是课程完成状态
时长 汇总训练投入 不是视频播放时长
满意度 回顾主观感受 不是考试成绩
时间戳 计算本周和历史统计 不是训练计划日期

二、计划内容与实际记录各司其职

体能训练课程给出“一周训练安排”的方法:2 天技术、2 天体能、1 天实战、1 天投篮、1 天恢复。它帮助用户安排节奏,但不会自动生成练习记录。只有用户保存真实训练内容,统计页面才增加次数和时长。

这种区分避免了一个常见误导:用户看过计划,不等于已经完成计划;用户新增一条记录,也不等于完成整周安排。两种数据保留各自的含义,复盘时才不会把建议当成事实。

三、运行回读的记录链路

模拟器中新增并保存了一条“体能训练”记录,内容为“核心力量与折返跑训练”,时长 30 分钟,满意度 3 星。重启应用后,练习记录页回读到总次数 1、总时长 30 分钟、平均评分 3.0、本周 1 次和完整记录卡片。由此可知该记录不是仅存在于当前页面内存。

四、为什么统计应从原始记录计算

总次数、总时长和平均评分都是派生结果。若把它们和原始记录分别保存,删除或修正一条记录时容易发生不一致。更稳妥的方式是保存原始日志,在需要展示时按统一规则计算汇总;空列表则返回清晰的零值和空状态。

const totalMinutes = logs.reduce((sum, item) => sum + item.duration, 0)
const averageRating = logs.length === 0 ? 0 :
logs.reduce((sum, item) => sum + item.rating, 0) / logs.length

练习记录解决的是“已经做过什么”,训练计划解决的是“接下来如何安排”。当这两条线在界面上并列而不混写时,用户才能既看见建议,也看见属于自己的真实训练轨迹。

训练计划与练习事实为什么要分开

训练计划回答的是“建议怎样安排”,练习记录回答的是“今天实际做了什么”。两者的字段和生命周期不同:计划可以被反复阅读,日志必须有时间、时长和主观评分,才能支持后续复盘。把二者分开可以避免课程浏览量被误算成训练量。

服务层先复制当前日志集合,再按新增、更新或删除产生下一份集合并持久化。页面保存后重新读取服务结果,因此练习列表、统计卡片和个人页不会各自保留一个过期数组。编辑同一条日志时只替换对应 id,不应再追加第二条。

}

static add(log: PracticeLog): void {
const next: PracticeLog[] = PracticeLogService.list().slice();
next.unshift(log);
PracticeLogService.save(next);
}

static update(log: PracticeLog): void {
const next: PracticeLog[] = PracticeLogService.list().slice();
const idx: number = next.findIndex(l => l.id === log.id);
if (idx >= 0) {
next[idx] = log;
PracticeLogService.save(next);
}
}

static remove(id: string): void {
const cur: PracticeLog[] = PracticeLogService.list();
const next: PracticeLog[] = cur.filter(l => l.id !== id);
PracticeLogService.save(next);
}

static find(id: string): PracticeLog | undefined {
return PracticeLogService.list().find(l => l.id === id);
}

static genId(): string {
return 'pl_' + Date.now().toString(36) + '_' + Math.random().toString(36).substring(2, 6);
}
}

PracticeLogService 的写入与更新约束

空日志不是错误:列表应给出继续训练入口,统计应返回零值而不是上一轮遗留数字。删除操作同样要作用于持久化集合,随后重读聚合;只隐藏一张卡片会让重启后的记录重新出现。

id: this.logId.length > 0 ? this.logId : PracticeLogService.genId(),
date: this.date,
durationMin: this.durationMin,
type: this.type,
content: this.content,
rating: this.rating,
createdAt: oldLog?.createdAt ?? now,
updatedAt: now
} as PracticeLog;
if (this.logId.length > 0) {
PracticeLogService.update(log);
AppToast.show('已更新');
} else {
PracticeLogService.add(log);
AppToast.show('已保存');
}
router.back();
}

private todayStr(): string {
const d: Date = new Date();
const y: string = d.getFullYear().toString();
const m: string = (d.getMonth() + 1).toString().padStart(2, '0');
const day: string = d.getDate().toString().padStart(2, '0');
return y + '-' + m + '-' + day;
}

private durationOptions(): DurationOpt[] {
return [
{ value: 30, label: '30' } as DurationOpt,
{ value: 60, label: '60' } as DurationOpt,

检查对象事实来源页面应表现
正常路径 计划保留为课程内容,真实训练只由 PracticeLogService 写入、更新和删除;列表与统计从同一日志集合读取。 显示与真实数据一致的结果
边界条件 编辑既有记录、删除记录、空日志和重启恢复不能重复计数或把建议计划混入事实。 不给出假成功,保留可恢复入口
回读验证 新增一条体能训练、编辑时长、重启后回读列表与汇总,再删除该条确认次数和时长同时变化。 跨页面或重进后结果一致

列表和统计如何读取同一份日志

该模型的代价是输入表单必须明确区分课程建议和实际记录,但换来统计口径可解释。读者看到本周次数时,能够追溯到具体日志,而不是猜测它来自哪个课程页面。

this.refreshLogs();
}

private refreshLogs(): void {
this.visibleLogs = PracticeLogService.all();
}
}

interface StarItem {
idx: number;
filled: boolean;
}

interface TypeStat {
type: string;
avg: number;
count: number;
}

上述代码片段来自当前实现的连续调用点:它们分别回答“谁提供事实”“谁消费结果”“异常时在哪里停止”。读者排查同类问题时,应先验证这三个边界,而不是只根据按钮颜色判断业务是否完成。

重启后的练习记录回读

新增一条体能训练、编辑时长、重启后回读列表与汇总,再删除该条确认次数和时长同时变化。

定位这类问题时,第一步应回到实际风险:若把“看过一周计划”当作“已经完成训练”,统计页会制造虚假的完成度;若录入页和列表各存一份数据,编辑和删除也会不同步。。先确认输入是否已经被模型或服务拒绝,再检查页面是否把该结果展示出来;如果先从视觉现象倒推,往往会把偶然残留的控件状态误判成业务完成。

当前实现选择的是:计划保留为课程内容,真实训练只由 PracticeLogService 写入、更新和删除;列表与统计从同一日志集合读取。。这意味着每个页面不必重复实现同一份判断,却也要求任何新增入口都调用相同的服务或模型;绕过该入口虽然能暂时缩短页面代码,却会让后续回读失去一致性。

需要单独保住的边界是:编辑既有记录、删除记录、空日志和重启恢复不能重复计数或把建议计划混入事实。。边界出现时,页面应当保留原因和下一步操作,而不是把错误状态压成空白或成功提示。读者据此可以区分“没有数据”“资源不可用”和“动作尚未完成”。

把这条边界写进文章还有一个维护价值:当课程内容、页面布局或资源形式变化时,验收仍可以围绕同一个事实来源进行,而不必依赖某张旧截图或某个控件曾经显示过的文字。这样得到的结论能被下一次修改复查。

排查顺序应先确认的事实不应采用的替代做法
输入 课程、记录或题目是否有稳定标识 从页面文本推断业务对象
服务 判断或写入是否经过唯一入口 在多个页面复制同一段临时逻辑
回读 重进后是否从持久化或模型得到同一结果 只凭一次按钮变化判定成功

ArkTS 响应式状态的基础机制可参考 HarmonyOS ArkTS 状态管理文档。这里的重点不是堆叠状态字段,而是让页面在每次进入时都从同一业务事实重新得到可见结果。

当需求继续扩展时,应把新增条件加入现有服务或模型的明确入口,并为该条件补充可观察的回读动作。这样课程内容、页面交互和持久化数据仍可沿同一条链路解释,而不会在不同入口形成互相矛盾的结论。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 篮球口袋教练 HarmonyOS 学习应用(05):练习记录与体能训练计划
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!