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