001project_wildgrowth/ios/WildGrowth/COMPLETION_迭代_完整代码.md

289 lines
11 KiB
Markdown
Raw Normal View History

2026-02-11 15:26:03 +08:00
# 完成页迭代 — 完整代码(仅交付,禁止自动应用)
以下为审查修正后的完整代码。**关键修正**:底部「回到我的内容」改为 `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<NavigationPath>?` 保持可选NoteTreeView / NoteListView 不传时仍不 push 完成页。
- `isLastNode`, `courseTitle` 等保持不变。
### 2.2 替换「加载成功」分支:去掉占位页,仅用左滑手势 push
**原逻辑**(约 146186 行):
`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(...)` 调用。