# 完成页迭代 — 完整代码(仅交付,禁止自动应用) 以下为审查修正后的完整代码。**关键修正**:底部「回到我的内容」改为 `navStore.switchToGrowthTab()`,不再使用 `navigationPath = NavigationPath()`,以保证从任意 Tab 进入都能回到技能页 Tab。 --- ## 1. CompletionView.swift(整文件替换) ```swift import SwiftUI // MARK: - 🏆 课程完结页 (Gesture Flow Edition) // 交互:左滑进入(上级控制),右滑返回(系统原生),底部点击回技能页 Tab struct CompletionView: View { let courseId: String let courseTitle: String? let completedLessonCount: Int @EnvironmentObject private var navStore: NavigationStore @Environment(\.dismiss) private var dismiss @State private var isSealed = false @State private var breathingOpacity: Double = 0.3 @State private var contentOpacity: Double = 0 var body: some View { ZStack { Color.bgPaper.ignoresSafeArea() VStack(spacing: 0) { Spacer().frame(height: 60) Spacer() ZStack { Circle() .strokeBorder( isSealed ? AnyShapeStyle( LinearGradient( colors: [Color.brandVital, Color.cyberIris], startPoint: .topLeading, endPoint: .bottomTrailing ) ) : AnyShapeStyle(Color.inkSecondary.opacity(0.3)), style: StrokeStyle(lineWidth: isSealed ? 4 : 1, dash: isSealed ? [] : [5, 5]) ) .frame(width: 220, height: 220) .shadow( color: isSealed ? Color.brandVital.opacity(0.4) : .clear, radius: 20, x: 0, y: 0 ) .scaleEffect(isSealed ? 1.0 : 0.95) .opacity(isSealed ? 1.0 : breathingOpacity) .animation(.easeInOut(duration: 0.5), value: isSealed) if isSealed { VStack(spacing: 8) { Text("已完成的") .font(.system(size: 16, weight: .medium)) .foregroundColor(.inkSecondary) Text("第 \(completedLessonCount) 节") .font(.system(size: 36, weight: .bold, design: .serif)) .foregroundStyle( LinearGradient( colors: [Color.brandVital, Color.cyberIris], startPoint: .topLeading, endPoint: .bottomTrailing ) ) } .transition(.scale(scale: 0.5).combined(with: .opacity)) } else { Text("完") .font(.system(size: 80, weight: .light, design: .serif)) .foregroundColor(.inkPrimary.opacity(0.2)) .opacity(breathingOpacity) } } .contentShape(Circle()) .onTapGesture { triggerInteraction() } Spacer() Button { handleReturnToRoot() } label: { HStack(spacing: 4) { Text("回到我的内容") .font(.system(size: 15, weight: .medium)) Image(systemName: "arrow.right") .font(.system(size: 12)) } .foregroundColor(.brandVital) .padding(.vertical, 12) .padding(.horizontal, 32) .background(Capsule().fill(Color.brandVital.opacity(0.05))) } .opacity(contentOpacity) .padding(.bottom, 80) } } .toolbar(.hidden, for: .navigationBar) .modifier(SwipeBackEnablerModifier()) .onAppear { startBreathing() } } private func startBreathing() { withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { breathingOpacity = 0.8 } } private func triggerInteraction() { guard !isSealed else { return } let generator = UIImpactFeedbackGenerator(style: .heavy) generator.impactOccurred() withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) { isSealed = true } withAnimation(.easeOut.delay(0.5)) { contentOpacity = 1.0 } } /// 回到技能页 Tab 根目录(与当前「继续学习」一致) private func handleReturnToRoot() { navStore.switchToGrowthTab() } } // MARK: - 侧滑返回:隐藏导航栏时仍可右滑 pop struct SwipeBackEnablerModifier: ViewModifier { func body(content: Content) -> some View { content.background(SwipeBackEnabler()) } } private struct SwipeBackEnabler: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { UIViewController() } func updateUIViewController(_ uiViewController: UIViewController, context: Context) { DispatchQueue.main.async { if let nc = uiViewController.navigationController { nc.interactivePopGestureRecognizer?.delegate = nil nc.interactivePopGestureRecognizer?.isEnabled = true } } } } ``` --- ## 2. VerticalScreenPlayerView.swift(仅改动部分) ### 2.1 保持现有 init 与属性 - `navigationPath: Binding?` 保持可选,NoteTreeView / NoteListView 不传时仍不 push 完成页。 - `isLastNode`, `courseTitle` 等保持不变。 ### 2.2 替换「加载成功」分支:去掉占位页,仅用左滑手势 push **原逻辑**(约 146–186 行): `TabView` 内 `ForEach(allCourseNodes)` + `CompletionPlaceholderPage().tag("wg://completion")`,外加 `.onChange(of: currentNodeId)` 在 `newId == "wg://completion"` 时 append completion。 **新逻辑**: - `TabView` 内**仅** `ForEach(allCourseNodes)`,每页在最后一节上挂 `LastPageSwipeModifier`,左滑时调用 `triggerCompletionNavigation`。 - **删除** `CompletionPlaceholderPage` 及其 tag、**删除** `.onChange(of: currentNodeId)` 中与 `"wg://completion"` 相关的逻辑。 - 新增 `@State private var isNavigatingToCompletion = false`(防抖),以及 `triggerCompletionNavigation()`、`LastPageSwipeModifier`。 **替换后的内容区代码**(直接替换原 `else if !allCourseNodes.isEmpty { ... }` 整块): ```swift } else if !allCourseNodes.isEmpty { TabView(selection: $currentNodeId) { ForEach(allCourseNodes, id: \.id) { node in let isFirst = isFirstNodeInChapter(nodeId: node.id) let chapterTitle = getChapterTitle(for: node.id) LessonPageView( courseId: courseId, nodeId: node.id, currentGlobalNodeId: $currentNodeId, initialScrollIndex: node.id == initialNodeId ? initialScrollIndex : nil, headerConfig: HeaderConfig( showChapterTitle: isFirst, chapterTitle: chapterTitle ), courseTitle: courseTitle, navigationPath: navigationPath ) .tag(node.id) .modifier(LastPageSwipeModifier( isLastPage: node.id == allCourseNodes.last?.id, onSwipeLeft: triggerCompletionNavigation )) } } .tabViewStyle(.page(indexDisplayMode: .never)) .ignoresSafeArea(.container, edges: .bottom) } ``` ### 2.3 新增状态与函数(放在 Logic Helpers 区域) ```swift @State private var isNavigatingToCompletion = false private func triggerCompletionNavigation() { guard !isNavigatingToCompletion, let path = navigationPath else { return } isNavigatingToCompletion = true let generator = UIImpactFeedbackGenerator(style: .medium) generator.impactOccurred() let count = UserManager.shared.studyStats.lessons path.wrappedValue.append(CourseNavigation.completion( courseId: courseId, courseTitle: courseTitle, completedLessonCount: count )) DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { isNavigatingToCompletion = false } } ``` ### 2.4 删除 onAppear 中「从完成页返回切回最后一节」逻辑 **删除**: ```swift if currentNodeId == "wg://completion", let lastId = allCourseNodes.last?.id { currentNodeId = lastId } ``` (去掉占位页后不再存在 `"wg://completion"` 选中的情况。) ### 2.5 删除 CompletionPlaceholderPage,新增 LastPageSwipeModifier **删除** `CompletionPlaceholderPage` 结构体。 **在** `// MARK: - 📄 单页课程视图` **之前** 新增: ```swift // MARK: - 最后一页左滑:仅在最后一节挂载,左滑 push 完成页 private struct LastPageSwipeModifier: ViewModifier { let isLastPage: Bool let onSwipeLeft: () -> Void func body(content: Content) -> some View { content .simultaneousGesture( DragGesture(minimumDistance: 20, coordinateSpace: .local) .onEnded { value in guard isLastPage else { return } if value.translation.width < -60, abs(value.translation.width) > abs(value.translation.height) { onSwipeLeft() } } ) } } ``` --- ## 3. 调用方(GrowthView / ProfileView / DiscoveryView) **无需改动**。CompletionView 仍为三参:`courseId`, `courseTitle`, `completedLessonCount`,依赖 `@EnvironmentObject navStore`,底部用 `switchToGrowthTab()` 回到技能页 Tab。 --- ## 4. 小结 - **CompletionView**:无顶部返回、无打字机;赛博印章交互;右滑依赖 `SwipeBackEnablerModifier`;底部「回到我的内容」= `navStore.switchToGrowthTab()`。 - **VerticalScreenPlayerView**:去掉占位页与 `onChange` 完成页逻辑;最后一节左滑用 `LastPageSwipeModifier` + `triggerCompletionNavigation` push 完成页;保留可选 `navigationPath`,笔记流不变。 - **不修改** GrowthView / ProfileView / DiscoveryView 的 `CompletionView(...)` 调用。