7.6 KiB
完成页导航方案 — 审查报告
审查对象:基于「零副作用 / SSOT / 响应式导航 / 原生手感」的完成页与播放器导航改造方案
审查结论:只做审查,不修改代码;本报告供决策与后续实现参考。
一、方案目标与标准(引用)
| 标准 | 含义 |
|---|---|
| 零副作用 | 不引入「假页面」污染数据源,无 Hack |
| 单一事实来源 (SSOT) | 数据源 allCourseNodes 仅含真实课程章节 |
| 响应式导航 | 用 NavigationPath 堆栈精确控制流向,消除白屏回退 |
| 原生手感 | 用手势识别(非页面索引)触发跳转 |
二、方案要点摘要
-
删除 CompletionPlaceholderPage 的导航职责(或整页删除,视实现而定)
- 若保留:仅作纯视觉占位,无
onAppear等导航逻辑。
- 若保留:仅作纯视觉占位,无
-
VerticalScreenPlayerView
- 在最后一节挂载「边缘左滑手势」修饰符(如
LastPageSwipeModifier)。 - 左滑触发:
navigationPath.append(CourseNavigation.completion(...)),或先设currentNodeId = "wg://completion"再由现有onChange统一 push。 - TabView 仅渲染真实
allCourseNodes,不把「完成」当作一节数据。
- 在最后一节挂载「边缘左滑手势」修饰符(如
-
CompletionView
- 新增
@Binding var navigationPath: NavigationPath。 - 「回到我的内容」/「继续学习」调用
handleReturnToMap():navigationPath.removeLast(2),一次 pop 完成页 + 播放器,直接回地图。
- 新增
-
调用方
- GrowthView / ProfileView / DiscoveryView 的
.completion分支向CompletionView传入对应 path 的Binding(如$navStore.growthPath等)。
- GrowthView / ProfileView / DiscoveryView 的
三、与当前实现的对照
| 维度 | 当前实现 | 方案 |
|---|---|---|
| 数据源 | allCourseNodes 纯净;TabView 多一页 CompletionPlaceholderPage 用 tag "wg://completion",未写入 allCourseNodes |
与当前一致或更进一步:完全移除占位页,仅手势触发 push |
| 进入完成页 | 左滑到占位页 → onChange(of: currentNodeId) → 0.1s 后 append .completion |
最后一节左滑手势 → 直接 append 或先切 tag 再 append,不依赖「多一页」 |
| 完成页返回 | navStore.switchToGrowthTab() + dismiss(),先回播放器再靠系统/逻辑 |
removeLast(2) 穿透回地图,无中间层 |
| 占位页 | 存在,纯视觉 + 父视图 onChange 驱动 push,无占位内 onAppear |
方案 A:保留为纯视觉;方案 B:删除,仅手势 |
结论:方案在「单一事实来源」和「响应式导航」上比当前更彻底;当前已避免在数据源里掺假节点,但仍依赖 TabView 多一页占位。
四、按标准的符合度
4.1 零副作用
- 方案:若不保留占位页,则无「假页」;若保留占位页且仅视觉、无逻辑,则也算零逻辑副作用。
- 当前:占位页无
onAppear导航,副作用已收敛,但 TabView 仍多一个「虚拟页」。 - 符合度:方案完全符合;当前基本符合,方案更干净。
4.2 单一事实来源 (SSOT)
- 方案:
allCourseNodes仅课程章节;完成页由导航栈 + 手势驱动,不进入数据模型。 - 当前:
allCourseNodes已是纯净的flatMap章节节点,未追加 placeholder node。 - 符合度:方案与当前都符合;方案在视图层也不再依赖「多一页」的 tag,SSOT 更纯粹。
4.3 响应式导航
- 方案:
CompletionView通过Binding<NavigationPath>执行removeLast(2),栈由 path 唯一决定,无switchToGrowthTab()+dismiss()的二次操作。 - 当前:依赖
dismiss()回播放器,再从播放器回地图,存在中间层与潜在白屏/闪烁。 - 符合度:方案明显更符合;当前有改进空间。
4.4 原生手感
- 方案:最后一节用
DragGesture(或类似)识别左滑,不依赖「滑到下一 tab 索引」才触发。 - 当前:依赖用户滑到「占位页」才触发,仍与 TabView 索引/选页绑定。
- 符合度:方案更贴近「手势驱动」;当前是「页面索引 + 手势」混合。
五、与既有文档的一致性
5.1 COMPLETION_VIEW_UI_LAYER_SPEC.md
- 说明要求:返回用
dismiss(),不接收或操作NavigationPath。 - 方案:改为接收
Binding<NavigationPath>并removeLast(2),与当前 spec 冲突。 - 建议:若采用方案,需同步更新 COMPLETION_VIEW_UI_LAYER_SPEC:
- 写明「穿透式返回」为可选/推荐实现;
- 接口增加
@Binding var navigationPath: NavigationPath; - 底部按钮行为改为「可调用
removeLast(2)直接回地图」,并注明调用方需传入对应 path 的 Binding。
5.2 COMPLETION_PAGE_MEANING.md
- 当前描述为:最后一节后再左滑一页进入占位页,占位页触发 push。
- 方案:最后一节左滑即触发(或先切 tag 再触发),可完全去掉「多一页」概念。
- 建议:若采用方案,更新 COMPLETION_PAGE_MEANING:
- 「最后一页」仍指课程的最后一节;
- 进入完成页的方式改为「在最后一节左滑(手势)触发」,并注明是否保留占位页。
六、风险与注意点
-
多入口 path
CompletionView 在 Growth / Profile / Discovery 三处被 present,需分别传入growthPath/profilePath/homePath的 Binding;漏传或传错会导致removeLast(2)作用在错误栈上。建议:- 在审查/实现时逐处确认传参;
- 或为 CompletionView 封装「当前栈」来源(例如 Environment 注入当前 path),避免调用方误绑。
-
NavigationPath.count
removeLast(2)前需保证path.count >= 2(完成页 + 播放器)。若从深链或异常入口进入完成页,栈深度可能不足,需保留兜底(如dismiss())。 -
手势与 TabView 滑动冲突
最后一节左滑既会触发自定义手势,也是 TabView 的翻页手势。需通过minimumDistance、coordinateSpace或「边缘优先」等策略区分,避免误触或重复触发。建议在真机多测:最后一节左滑、快速连续左滑、斜滑。 -
从完成页返回后的 Tab 状态
当前实现:从完成页 dismiss 回播放器后,onAppear里若currentNodeId == "wg://completion"会切回allCourseNodes.last?.id。采用方案后,若使用removeLast(2),不会回到播放器,故无需再依赖该逻辑;若仍保留「先 dismiss 再回地图」的入口,需保留或等价处理该状态恢复。
七、审查结论与建议
| 项目 | 结论 |
|---|---|
| 架构与标准 | 方案满足「零副作用、SSOT、响应式导航、原生手感」四项标准,且比当前实现更彻底。 |
| 与现有 spec | 与 COMPLETION_VIEW_UI_LAYER_SPEC 的「不操作 NavigationPath」冲突,需更新 spec。 |
| 实现成本 | 中等:CompletionView 接口与三处调用方改动;VerticalScreenPlayerView 增加手势与可选移除占位页;需回归测试返回与多入口。 |
| 建议 | 若采纳方案: |
- 先更新 COMPLETION_VIEW_UI_LAYER_SPEC 与 COMPLETION_PAGE_MEANING,再改代码;
- CompletionView 保留
path.count >= 2判断与dismiss()兜底; - 手势与 TabView 的冲突在真机验证,必要时加防抖或边缘区域限制;
- 三处 navigationDestination 的
Binding传参做清单检查,避免漏传/错传。 |
报告日期:基于当前代码与所提供方案整理,未对仓库做任何代码修改。