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

289 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 完成页迭代 — 完整代码(仅交付,禁止自动应用)
以下为审查修正后的完整代码。**关键修正**:底部「回到我的内容」改为 `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(...)` 调用。