一、专项内容为何先落在课程模型乒乓球课程不是只靠标题排列的海报集合。发球、接发、相持和战术训练既需要面向用户的标题也需要稳定的 id、模块归属、子分类和顺序。把这些字段收敛到课程模型分类页、搜索页和详情页才会对同一门课给出相同结论。export interface Course { id: string; moduleId: string; subCategory: string; subCategoryLabel: string; title: string; desc: string; thumbKey: string; videoSrc?: string; durationSec: number; order: number; intro: Block[]; keyPoints: Block[]; mistakes: Block[]; training: Block[]; keywords: string[]; externalVideoRefs?: ExternalVideoRef[]; } export interface SubCategory { id: string; label: string; } export class ExternalVideoRef { platform: string ; collectionTitle: string ; sectionTitle: string ; bvid: string ; cid: number 0; page: number 0; part: string ; url: string ; durationSec: number 0; }二、模块与子分类如何限制列表范围coursesByModule 先按 moduleId 过滤再按 order 排序分类页在此基础上叠加 subCategoryId。这样“全部”与某一专项不是两份手写列表新增课程只需补齐模型字段。页面不应通过标题包含某个词来猜测它属于发球还是接发。事实唯一来源页面职责正常结果模型或服务的回读渲染可观察状态边界结果明确的缺项或失败分支说明原因并保留入口重进结果持久化或模型再次解析不复用旧页面变量} export function coursesByModule(moduleId: string): Course[] { return COURSES.filter(c c.moduleId moduleId).sort((a, b) a.order - b.order); } export function findCourse(id: string): Course | undefined { return COURSES.find(c c.id id); } export function nextCourse(id: string): Course | undefined { const cur: Course | undefined findCourse(id); if (cur undefined) return undefined; const list: Course[] coursesByModule(cur.moduleId); const idx: number list.findIndex(c c.id id); return idx 0 idx list.length - 1 ? list[idx 1] : undefined; } export function prevCourse(id: string): Course | undefined { const cur: Course | undefined findCourse(id); if (cur undefined) return undefined; const list: Course[] coursesByModule(cur.moduleId); const idx: number list.findIndex(c c.id id); return idx 0 ? list[idx - 1] : undefined; }三、详情页怎样避免标题和课程事实脱节详情页从路由参数取得 courseId 后重新查找课程并据此构造标题、章节和视频入口。这个做法让路由只携带稳定标识页面重建时能重取最新课程而不是复用上一张卡片遗留的文案或图片。.backgroundColor(this.palette.bg); } private filtered(): Course[] { const all: Course[] coursesByModule(this.moduleId); if (this.subCategoryId all) return all; return all.filter(c c.subCategory this.subCategoryId); } }四、数据缺项时应该停止在哪里模型缺项要显式处理。未知 moduleId、找不到 courseId 或没有子分类时页面应返回空状态或禁用无效入口而不是用列表第一项作为默认课程。默认回退会让用户进入错误专项也会让后续学习记录归属错误。风险不能采用的做法当前链路的处理数据不存在用默认对象继续渲染停在可解释的空或失败状态页面重进复用上一次内存字段重新查询模型或服务重复动作追加同一业务事实通过稳定 id 或进入标记收敛import { AppSizes, AppText, ColorPalette, LightPalette, CategoryTabs, TabItem, BlockRenderer, PlaceholderImage, AppToast, TopBar, Routes, DetailParams, PlayerParams, Course, Block, findCourse, nextCourse, prevCourse, coursesByModule, FavoriteService, ProgressService, formatDuration, courseCover, courseVideoSrc, CourseVideoService } from library1; const TAB_INTRO: string intro; const TAB_KEY: string key; const TAB_MISTAKE: string mistake; const TAB_TRAIN: string train; const TXT_DETAIL: string \u8bfe\u7a0b\u8be6\u60c5; const TXT_NOT_FOUND: string \u8bfe\u7a0b\u4e0d\u5b58\u5728; const TXT_SHARE: string \u5206\u4eab; const TXT_SAVED: string \u5df2\u6536\u85cf; const TXT_SAVE: string \u6536\u85cf; const TXT_COPY_OK: string \u5df2\u590d\u5236\u5230\u526a\u8d34\u677f; const TXT_SAVE_OK: string \u5df2\u52a0\u5165\u6536\u85cf; const TXT_UNSAVE_OK: string \u5df2\u53d6\u6d88\u6536\u85cf; const TXT_PLAY_LOCAL: string \u70b9\u51fb\u64ad\u653e; const TXT_NO_VIDEO_PLACEHOLDER: string \u672c\u8bfe\u7a0b\u4ee5\u56fe\u6587\u5b66\u4e60\u4e3a\u4e3b\uff0c\u6682\u65e0\u914d\u5957\u89c6\u9891; const TXT_DOWNLOAD_LOCAL: string \u4e0b\u8f7d\u5230\u672c\u5730; const TXT_DOWNLOADED_LOCAL: string \u5df2\u4e0b\u8f7d\u5230\u672c\u5730; const TXT_DOWNLOADING_LOCAL: string \u6b63\u5728\u4fdd\u5b58; const TXT_DOWNLOAD_OK: string \u5df2\u4fdd\u5b58\u5230\u672c\u5730\u5e76\u540c\u6b65\u5230\u76f8\u518c; const TXT_DOWNLOAD_LOCAL_OK: string \u5df2\u4fdd\u5b58\u5230\u672c\u5730;五、从首页到详情的验收路径验收从首页选择一个模块开始依次切换子分类、打开一门课程并返回列表。需要观察模块标题、课程数量、选中分类与详情标题是否都来自同一 id再用一个不存在的 id 验证页面不会展示无关课程。验收阶段操作应观察到的结果建立事实完成一次与主题直接对应的动作服务或模型能回读该事实页面回读进入目标页或结果页标题、数据和状态相互一致重进验证返回、重启或切换模块后再次进入结果不依赖上一页残留六、取舍与后续维护当前实现把业务事实集中到模型与服务页面只负责呈现和触发操作。这样做会多出类型、查询和回读代码却能避免同一份统计或记录在多个页面分叉。需要扩展新课程、新资源或新题目时应先补齐稳定标识与服务合同再补页面入口。ArkTS 的响应式状态机制可参考 HarmonyOS ArkTS 状态管理文档。本文的代码和验收围绕同一个原则可见结果必须能回到对应模型、服务或持久化事实不以邻近页面和控件文字代替主题结果。七、围绕事实来源继续演进乒乓球口袋教练 HarmonyOS 学习应用01动作课程模型与专项内容组织 关注的不是给页面再增加一层显示逻辑而是让“专项内容为何先落在课程模型”始终能回到唯一的业务事实。模型字段、服务返回值和页面展示应当保持单向关系事实先发生页面随后回读页面不保存第二份能够被误认为真实数据的临时副本。处理“模块与子分类如何限制列表范围”时新增条件必须先归属到课程模型、资源服务、日志事实或测验结果之一。把条件散落在多个组件里短期看起来省事后续就会出现入口不同、重进不同、统计不同的问题。把规则放回稳定入口页面只根据结果改变可见状态能让同一结论在不同入口被复查。“详情页怎样避免标题和课程事实脱节”说明当前实现选择了页面重建后重新查询而不是把对象和统计快照长期塞进路由参数。读取会增加少量成本却避免课程改名、资源替换、日志编辑或题库切换后继续显示旧内容。学习应用的结果需要能解释不能只追求一次渲染恰好看起来正确。异常分支也应保留语义模型缺项要显式处理。未知 moduleId、找不到 courseId 或没有子分类时页面应返回空状态或禁用无效入口而不是用列表第一项作为默认课程。默认回退会让用户进入错误专项也会让后续学习记录归属错误。 这要求页面区分数据不存在、动作未完成和资源不可用并提供返回选择、补充输入、重新下载或再次答题等对应操作不能把不同原因压缩成同一条模糊的成功或失败提示。最后执行“从首页到详情的验收路径”时至少观察一次主题动作、一次目标页回读和一次重进或切换后的回读。三次观察都指向相同模型或服务结果才说明实现链路没有依赖旧页面残留。 最后执行“从首页到详情的验收路径”时至少观察一次主题动作、一次目标页回读和一次重进或切换后的回读。三次观察都指向相同模型或服务结果才说明实现链路没有依赖旧页面残留。面向维护的检查可以沿三个问题展开输入对象的稳定标识是什么哪个服务或模型决定结果页面如何把该结果显示出来。用模块、子分类、课程顺序与课程 id 组织发球、接发、相持和战术内容让首页、分类页与详情页共享同一份课程事实。 这三个问题分别对应数据合同、调用边界和可见反馈只要其中一层被绕过新增入口就可能产生和原入口不同的事实。对失败路径也要保留同样的追踪能力。先让服务返回清晰的不可用、缺项或未完成状态再由页面提供与原因匹配的操作。这样日志、课程、资源和题目发生变化时回归测试能够看到具体的分歧点而不是只能面对一张没有信息的空白页面。这种组织不会承诺所有后续需求都零成本。新增字段需要同步更新模型、服务与验收但它把变更范围限制在可枚举的位置避免把同一判断复制到首页、详情、个人页和结果页后再分别修补。