From 40ef986c6f55be8b40dc8922818dfa36c191cf8a Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sat, 6 Sep 2025 18:36:13 +0800 Subject: [PATCH 01/18] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E6=97=A0=E7=94=A8=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/View/Blind/AvatarBox.swift | 105 ------ wake/View/Blind/BlindBox.swift | 0 wake/View/Blind/Box.swift | 301 ------------------ wake/View/Blind/Box1.swift | 253 --------------- wake/View/Blind/Box3.swift | 226 ------------- wake/View/Blind/Box4.swift | 140 -------- wake/View/Blind/Box5.swift | 222 ------------- wake/View/Blind/Box6.swift | 250 --------------- .../View/{Blind => Subscribe}/JoinModal.swift | 0 9 files changed, 1497 deletions(-) delete mode 100644 wake/View/Blind/AvatarBox.swift delete mode 100644 wake/View/Blind/BlindBox.swift delete mode 100644 wake/View/Blind/Box.swift delete mode 100644 wake/View/Blind/Box1.swift delete mode 100644 wake/View/Blind/Box3.swift delete mode 100644 wake/View/Blind/Box4.swift delete mode 100644 wake/View/Blind/Box5.swift delete mode 100644 wake/View/Blind/Box6.swift rename wake/View/{Blind => Subscribe}/JoinModal.swift (100%) diff --git a/wake/View/Blind/AvatarBox.swift b/wake/View/Blind/AvatarBox.swift deleted file mode 100644 index de7f62a..0000000 --- a/wake/View/Blind/AvatarBox.swift +++ /dev/null @@ -1,105 +0,0 @@ -import SwiftUI - -struct AvatarBoxView: View { - @Environment(\.dismiss) private var dismiss - @EnvironmentObject private var router: Router - @State private var isAnimating = false - - var body: some View { - ZStack { - // Background color - Color.white - .ignoresSafeArea() - - VStack(spacing: 0) { - // Navigation Bar - HStack { - Button(action: { - dismiss() - }) { - Image(systemName: "chevron.left") - .font(.system(size: 17, weight: .medium)) - .foregroundColor(.black) - .padding() - } - - Spacer() - - Text("动画页面") - .font(.headline) - .foregroundColor(.black) - - Spacer() - - // Invisible spacer to center the title - Color.clear - .frame(width: 44, height: 44) - } - .frame(height: 44) - .background(Color.white) - - Spacer() - - // Animated Content - ZStack { - // Pulsing circle animation - Circle() - .fill(Color.blue.opacity(0.2)) - .frame(width: 200, height: 200) - .scaleEffect(isAnimating ? 1.5 : 1.0) - .opacity(isAnimating ? 0.5 : 1.0) - .animation( - Animation.easeInOut(duration: 1.5) - .repeatForever(autoreverses: true), - value: isAnimating - ) - - // Center icon - Image(systemName: "sparkles") - .font(.system(size: 60)) - .foregroundColor(.blue) - .rotationEffect(.degrees(isAnimating ? 360 : 0)) - .animation( - Animation.linear(duration: 8) - .repeatForever(autoreverses: false), - value: isAnimating - ) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - - Spacer() - - // Bottom Button - Button(action: { - router.navigate(to: .feedbackView) - }) { - Text("Continue") - .font(.headline) - .foregroundColor(.themeTextMessageMain) - .frame(maxWidth: .infinity) - .frame(height: 56) - .background(Color.themePrimary) - .cornerRadius(25) - .padding(.horizontal, 24) - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - .navigationBarBackButtonHidden(true) - .navigationBarHidden(true) - .onAppear { - isAnimating = true - } - } -} - -// MARK: - Preview -struct AvatarBoxView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - AvatarBoxView() - .environmentObject(Router.shared) - } - .navigationViewStyle(StackNavigationViewStyle()) - } -} diff --git a/wake/View/Blind/BlindBox.swift b/wake/View/Blind/BlindBox.swift deleted file mode 100644 index e69de29..0000000 diff --git a/wake/View/Blind/Box.swift b/wake/View/Blind/Box.swift deleted file mode 100644 index 9da0c25..0000000 --- a/wake/View/Blind/Box.swift +++ /dev/null @@ -1,301 +0,0 @@ -import SwiftUI - -struct FilmStripView: View { - @State private var animate = false - // 使用SF Symbols名称数组 - private let symbolNames = [ - "photo.fill", "heart.fill", "star.fill", "bookmark.fill", - "flag.fill", "bell.fill", "tag.fill", "paperplane.fill" - ] - private let targetIndices = [2, 5, 3] // 每条胶片最终停止的位置 - - var body: some View { - ZStack { - Color.black.edgesIgnoringSafeArea(.all) - - // 三条胶片带 - FilmStrip( - symbols: symbolNames, - targetIndex: targetIndices[0], - offset: 0, - stripColor: .red - ) - .rotationEffect(.degrees(5)) - .zIndex(1) - - FilmStrip( - symbols: symbolNames, - targetIndex: targetIndices[1], - offset: 0.3, - stripColor: .blue - ) - .rotationEffect(.degrees(-3)) - .zIndex(2) - - FilmStrip( - symbols: symbolNames, - targetIndex: targetIndices[2], - offset: 0.6, - stripColor: .green - ) - .rotationEffect(.degrees(2)) - .zIndex(3) - } - .onAppear { - withAnimation( - .timingCurve(0.2, 0.1, 0.8, 0.9, duration: 4.0) - ) { - animate = true - } - } - } -} - -// 单个胶片带视图 -struct FilmStrip: View { - let symbols: [String] - let targetIndex: Int - let offset: Double - let stripColor: Color - @State private var animate = false - - var body: some View { - GeometryReader { geometry in - let itemWidth: CGFloat = 100 - let spacing: CGFloat = 8 - let totalWidth = itemWidth * CGFloat(symbols.count) + spacing * CGFloat(symbols.count - 1) - - // 胶片背景 - RoundedRectangle(cornerRadius: 10) - .fill(stripColor.opacity(0.8)) - .frame(height: 160) - .overlay( - // 胶片齿孔 - HStack(spacing: spacing) { - ForEach(0.. CGFloat { - let baseDistance: CGFloat = 1000 - let speedFactor: CGFloat = 1.0 - - return baseDistance * speedFactor * progressCurve() - } - - // 中间正胶卷偏移量计算(向左移动) - private func calculateMiddleOffset() -> CGFloat { - let baseDistance: CGFloat = -1100 - let speedFactor: CGFloat = 1.05 - - return baseDistance * speedFactor * progressCurve() - } - - // 下方倾斜胶卷偏移量计算(向右移动) - private func calculateBottomOffset() -> CGFloat { - let baseDistance: CGFloat = 1000 - let speedFactor: CGFloat = 0.95 - - return baseDistance * speedFactor * progressCurve() - } - - // 动画曲线:先慢后快,最后卡顿 - private func progressCurve() -> CGFloat { - if animationProgress < 0.6 { - // 初期加速阶段 - return easeInQuad(animationProgress / 0.6) * 0.7 - } else if animationProgress < 0.85 { - // 高速移动阶段 - return 0.7 + easeOutQuad((animationProgress - 0.6) / 0.25) * 0.25 - } else { - // 卡顿阶段 - let t = (animationProgress - 0.85) / 0.15 - return 0.95 + t * 0.05 - } - } - - // 缓入曲线 - private func easeInQuad(_ t: CGFloat) -> CGFloat { - return t * t - } - - // 缓出曲线 - private func easeOutQuad(_ t: CGFloat) -> CGFloat { - return t * (2 - t) - } - - // 启动动画序列 - private func startAnimation() { - // 第一阶段:逐渐加速 - withAnimation(.easeIn(duration: 3.5)) { - animationProgress = 0.6 - } - - // 第二阶段:高速移动 - DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { - withAnimation(.linear(duration: 2.5)) { - animationProgress = 0.85 - } - - // 第三阶段:卡顿效果 - DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { - withAnimation(.easeOut(duration: 1.8)) { - animationProgress = 1.0 - isCatching = true - } - - // 卡顿后重合消失,显示目标图片 - DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) { - withAnimation(.easeInOut(duration: 0.7)) { - isDisappearing = true - } - - // 显示重复播放按钮 - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - withAnimation(.easeInOut(duration: 0.3)) { - showReplayButton = true - } - } - } - } - } - } -} - -// 电影胶卷视图组件 -struct FilmReelView1: View { - let images: [String] - - var body: some View { - HStack(spacing: 10) { - ForEach(images.indices, id: \.self) { index in - ZStack { - // 胶卷边框 - RoundedRectangle(cornerRadius: 4) - .stroke(Color.gray, lineWidth: 2) - .background(Color(red: 0.15, green: 0.15, blue: 0.15)) - - // 图片内容 - Rectangle() - .fill( - LinearGradient( - gradient: Gradient(colors: [.blue, .indigo]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .opacity(0.9) - .cornerRadius(2) - .padding(2) - - // 模拟图片文本 - Text("\(images[index])") - .foregroundColor(.white) - .font(.caption2) - } - .frame(width: 90, height: 130) - // 胶卷孔洞 - .overlay( - HStack { - VStack(spacing: 6) { - ForEach(0..<6) { _ in - Circle() - .frame(width: 6, height: 6) - .foregroundColor(.gray) - } - } - Spacer() - VStack(spacing: 6) { - ForEach(0..<6) { _ in - Circle() - .frame(width: 6, height: 6) - .foregroundColor(.gray) - } - } - } - ) - } - } - } -} - -// 预览 -struct ReplayableFilmReelAnimation_Previews: PreviewProvider { - static var previews: some View { - ReplayableFilmReelAnimation() - } -} - \ No newline at end of file diff --git a/wake/View/Blind/Box3.swift b/wake/View/Blind/Box3.swift deleted file mode 100644 index c482278..0000000 --- a/wake/View/Blind/Box3.swift +++ /dev/null @@ -1,226 +0,0 @@ -import SwiftUI - -struct FilmAnimation1: View { - // 设备尺寸 - private let deviceWidth = UIScreen.main.bounds.width - private let deviceHeight = UIScreen.main.bounds.height - - // 动画状态控制 - @State private var animationProgress: CGFloat = 0.0 // 0-1总进度 - @State private var isAnimating: Bool = false - @State private var animationComplete: Bool = false - - // 胶卷数据 - private let reelImages: [[String]] = [ - (0..<150).map { "film1-\($0+1)" }, // 上方胶卷 - (0..<180).map { "film2-\($0+1)" }, // 中间胶卷(垂直) - (0..<150).map { "film3-\($0+1)" } // 下方胶卷 - ] - - // 胶卷参数 - private let frameWidth: CGFloat = 90 - private let frameHeight: CGFloat = 130 - private let frameSpacing: CGFloat = 10 - private let totalDistance: CGFloat = 2000 // 总移动距离 - - // 动画时间参数 - private let accelerationDuration: Double = 5.0 // 加速阶段时长(0-5s) - private let constantSpeedDuration: Double = 6.0 // 匀速+放大阶段时长(5-11s) - private var totalDuration: Double { accelerationDuration + constantSpeedDuration } - private var scaleStartProgress: CGFloat { accelerationDuration / totalDuration } - private let finalScale: CGFloat = 3.0 // 展示完整胶片的缩放比例 - - // 对称布局核心参数(重点调整) - private let symmetricTiltAngle: Double = 8 // 减小倾斜角度,增强对称感 - private let verticalOffset: CGFloat = 140 // 减小垂直距离,靠近中间胶卷 - private let initialMiddleY: CGFloat = 50 // 中间胶卷初始位置上移,缩短与上下距离 - - // 上下胶卷与中间胶卷的初始水平偏移(确保视觉对称) - private let horizontalOffset: CGFloat = 30 - - var body: some View { - ZStack { - // 深色背景 - Color(red: 0.08, green: 0.08, blue: 0.08) - .edgesIgnoringSafeArea(.all) - - // 上方倾斜胶卷(左高右低,与中间距离适中) - FilmReelView3(images: reelImages[0]) - .rotationEffect(Angle(degrees: -symmetricTiltAngle)) - .offset(x: topReelPosition - horizontalOffset, y: -verticalOffset) // 水平微调增强对称 - .opacity(upperLowerOpacity) - .zIndex(1) - - // 下方倾斜胶卷(左低右高,与中间距离适中) - FilmReelView3(images: reelImages[2]) - .rotationEffect(Angle(degrees: symmetricTiltAngle)) - .offset(x: bottomReelPosition + horizontalOffset, y: verticalOffset) // 水平微调增强对称 - .opacity(upperLowerOpacity) - .zIndex(1) - - // 中间胶卷(垂直居中) - FilmReelView3(images: reelImages[1]) - .offset(x: middleReelPosition, y: middleYPosition) - .scaleEffect(currentScale) - .position(centerPosition) - .zIndex(2) - .edgesIgnoringSafeArea(.all) - } - .onAppear { - startAnimation() - } - } - - // MARK: - 动画逻辑 - - private func startAnimation() { - guard !isAnimating && !animationComplete else { return } - isAnimating = true - - withAnimation(Animation.timingCurve(0.2, 0.0, 0.8, 1.0, duration: totalDuration)) { - animationProgress = 1.0 - } - - DispatchQueue.main.asyncAfter(deadline: .now() + totalDuration) { - isAnimating = false - animationComplete = true - } - } - - // MARK: - 动画计算 - - private var currentScale: CGFloat { - guard animationProgress >= scaleStartProgress else { - return 1.0 - } - - let scalePhaseProgress = (animationProgress - scaleStartProgress) / (1.0 - scaleStartProgress) - return 1.0 + (finalScale - 1.0) * scalePhaseProgress - } - - // 中间胶卷Y轴位置(微调至更居中) - private var middleYPosition: CGFloat { - if animationProgress < scaleStartProgress { - return initialMiddleY - (initialMiddleY * (animationProgress / scaleStartProgress)) - } else { - return 0 // 5s后精准居中 - } - } - - private var upperLowerOpacity: Double { - if animationProgress < scaleStartProgress { - return 0.8 - } else { - let fadeProgress = (animationProgress - scaleStartProgress) / (1.0 - scaleStartProgress) - return 0.8 * (1.0 - fadeProgress) - } - } - - private var centerPosition: CGPoint { - CGPoint(x: deviceWidth / 2, y: deviceHeight / 2) - } - - // MARK: - 位置计算(确保对称运动) - - private var motionProgress: CGFloat { - if animationProgress < scaleStartProgress { - let t = animationProgress / scaleStartProgress - return t * t // 加速阶段 - } else { - return 1.0 + (animationProgress - scaleStartProgress) * - (scaleStartProgress / (1.0 - scaleStartProgress)) - } - } - - // 上方胶卷位置(与下方保持对称速度) - private var topReelPosition: CGFloat { - totalDistance * 0.9 * motionProgress - } - - // 中间胶卷位置(主视觉移动) - private var middleReelPosition: CGFloat { - -totalDistance * 1.2 * motionProgress - } - - // 下方胶卷位置(与上方保持对称速度) - private var bottomReelPosition: CGFloat { - totalDistance * 0.9 * motionProgress // 与上方速度完全一致 - } -} - -// MARK: - 胶卷组件 - -struct FilmReelView3: View { - let images: [String] - - var body: some View { - HStack(spacing: 10) { - ForEach(images.indices, id: \.self) { index in - FilmFrameView3(imageName: images[index]) - } - } - } -} - -struct FilmFrameView3: View { - let imageName: String - - var body: some View { - ZStack { - // 胶卷边框 - RoundedRectangle(cornerRadius: 4) - .stroke(Color.gray, lineWidth: 2) - .background(Color(red: 0.15, green: 0.15, blue: 0.15)) - - // 帧内容 - Rectangle() - .fill(gradientColor) - .cornerRadius(2) - .padding(2) - - // 帧标识 - Text(imageName) - .foregroundColor(.white) - .font(.caption2) - } - .frame(width: 90, height: 130) - // 胶卷孔洞 - .overlay( - HStack { - VStack(spacing: 6) { - ForEach(0..<6) { _ in - Circle() - .frame(width: 6, height: 6) - .foregroundColor(.gray) - } - } - Spacer() - VStack(spacing: 6) { - ForEach(0..<6) { _ in - Circle() - .frame(width: 6, height: 6) - .foregroundColor(.gray) - } - } - } - ) - } - - private var gradientColor: LinearGradient { - if imageName.hasPrefix("film1") { - return LinearGradient(gradient: Gradient(colors: [.blue, .indigo]), startPoint: .topLeading, endPoint: .bottomTrailing) - } else if imageName.hasPrefix("film2") { - return LinearGradient(gradient: Gradient(colors: [.yellow, .orange]), startPoint: .topLeading, endPoint: .bottomTrailing) - } else { - return LinearGradient(gradient: Gradient(colors: [.teal, .cyan]), startPoint: .topLeading, endPoint: .bottomTrailing) - } - } -} - -// 预览 -struct FilmAnimation_Previews3: PreviewProvider { - static var previews: some View { - FilmAnimation1() - } -} - \ No newline at end of file diff --git a/wake/View/Blind/Box4.swift b/wake/View/Blind/Box4.swift deleted file mode 100644 index 0ef9004..0000000 --- a/wake/View/Blind/Box4.swift +++ /dev/null @@ -1,140 +0,0 @@ -import SwiftUI - -// MARK: - 主视图:电影胶卷盲盒动效 -struct FilmStripBlindBoxView: View { - @State private var isAnimating = false - @State private var revealCenter = false - - // 三格盲盒内容(使用 SF Symbols 模拟不同“隐藏款”) - let boxContents = ["popcorn", "star", "music.note"] - - var body: some View { - GeometryReader { geometry in - let width = geometry.size.width - - ZStack { - // 左边盲盒胶卷帧 - BlindBoxFrame(symbol: boxContents[0]) - .offset(x: isAnimating ? -width / 4 : -width) - .opacity(isAnimating ? 1 : 0) - - // 中间盲盒胶卷帧(最终放大) - BlindBoxFrame(symbol: boxContents[1]) - .scaleEffect(revealCenter ? 1.6 : 1) - .offset(x: isAnimating ? 0 : width) - .opacity(isAnimating ? 1 : 0) - - // 右边盲盒胶卷帧 - BlindBoxFrame(symbol: boxContents[2]) - .offset(x: isAnimating ? width / 4 : width * 1.5) - .opacity(isAnimating ? 1 : 0) - } - .onAppear { - // 第一阶段:胶卷滑入 - withAnimation(.easeOut(duration: 1.0)) { - isAnimating = true - } - - // 第二阶段:中间帧“开盒”放大 - DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { - withAnimation( - .interpolatingSpring(stiffness: 80, damping: 12).delay(0.3) - ) { - revealCenter = true - } - } - } - } - .frame(height: 140) - .padding() - .background(Color.black.opacity(0.05)) - } -} - -// MARK: - 盲盒胶卷帧:带孔 + 橙色背景 + SF Symbol -struct BlindBoxFrame: View { - let symbol: String - - var body: some View { - ZStack { - // 胶片边框(橙色 + 打孔) - FilmBorder() - - // SF Symbol 作为“盲盒内容” - Image(systemName: symbol) - .resizable() - .scaledToFit() - .foregroundColor(.white.opacity(0.85)) - .frame(width: 60, height: 60) - } - .frame(width: 120, height: 120) - } -} - -// MARK: - 胶片边框:#FFB645 背景 + 打孔 -struct FilmBorder: View { - var body: some View { - Canvas { context, size in - let w = size.width - let h = size.height - - // 背景色:FFB645 - let bgColor = Color(hex: 0xFFB645) - context.fill(Path(CGRect(origin: .zero, size: size)), with: .color(bgColor)) - - // 打孔参数 - let holeRadius: CGFloat = 3.5 - let margin: CGFloat = 12 - let holeYOffset: CGFloat = h * 0.25 - - // 左侧打孔(3个) - for i in 0..<3 { - let y = CGFloat(i + 1) * (h / 4) - context.fill( - Path(ellipseIn: CGRect( - x: margin - holeRadius * 2, - y: y - holeRadius, - width: holeRadius * 2, - height: holeRadius * 2 - )), - with: .color(.black) - ) - } - - // 右侧打孔(3个) - for i in 0..<3 { - let y = CGFloat(i + 1) * (h / 4) - context.fill( - Path(ellipseIn: CGRect( - x: w - margin, - y: y - holeRadius, - width: holeRadius * 2, - height: holeRadius * 2 - )), - with: .color(.black) - ) - } - } - } -} - -// MARK: - Color 扩展:支持 HEX 颜色 -extension Color { - init(hex: UInt) { - self.init( - .sRGB, - red: Double((hex >> 16) & 0xff) / 255, - green: Double((hex >> 8) & 0xff) / 255, - blue: Double(hex & 0xff) / 255, - opacity: 1.0 - ) - } -} - -// MARK: - 预览 -struct FilmStripBlindBoxView_Previews: PreviewProvider { - static var previews: some View { - FilmStripBlindBoxView() - .preferredColorScheme(.dark) - } -} \ No newline at end of file diff --git a/wake/View/Blind/Box5.swift b/wake/View/Blind/Box5.swift deleted file mode 100644 index 93dd160..0000000 --- a/wake/View/Blind/Box5.swift +++ /dev/null @@ -1,222 +0,0 @@ -import SwiftUI - -struct FilmAnimation5: View { - // 设备尺寸 - private let deviceWidth = UIScreen.main.bounds.width - private let deviceHeight = UIScreen.main.bounds.height - - // 动画状态控制 - @State private var animationProgress: CGFloat = 0.0 // 0-1总进度 - @State private var isAnimating: Bool = false - @State private var animationComplete: Bool = false - - // 胶卷数据 - private let reelImages: [[String]] = [ - (0..<150).map { "film1-\($0+1)" }, // 上方倾斜胶卷 - (0..<180).map { "film2-\($0+1)" }, // 中间胶卷 - (0..<150).map { "film3-\($0+1)" } // 下方倾斜胶卷 - ] - - // 胶卷参数 - private let frameWidth: CGFloat = 90 - private let frameHeight: CGFloat = 130 - private let totalDistance: CGFloat = 1800 // 总移动距离 - - // 动画阶段时间参数(核心调整) - private let accelerationDuration: Double = 5.0 // 0-5s加速 - private let constantSpeedDuration: Double = 1.0 // 5-6s匀速移动 - private let scaleDuration: Double = 2.0 // 6-8s共同放大 - private var totalDuration: Double { accelerationDuration + constantSpeedDuration + scaleDuration } - - // 各阶段进度阈值 - private var accelerationEnd: CGFloat { accelerationDuration / totalDuration } - private var constantSpeedEnd: CGFloat { (accelerationDuration + constantSpeedDuration) / totalDuration } - - // 对称倾斜参数 - private let symmetricTiltAngle: Double = 10 // 上下胶卷对称倾斜角度 - private let verticalOffset: CGFloat = 120 // 上下胶卷垂直距离(对称) - private let finalScale: CGFloat = 4.0 // 最终放大倍数 - - var body: some View { - ZStack { - // 深色背景 - Color(red: 0.08, green: 0.08, blue: 0.08) - .edgesIgnoringSafeArea(.all) - - // 上方倾斜胶卷(向右移动) - FilmReelView5(images: reelImages[0]) - .rotationEffect(Angle(degrees: -symmetricTiltAngle)) - .offset(x: topReelPosition, y: -verticalOffset) - .scaleEffect(currentScale) - .opacity(upperLowerOpacity) - .zIndex(2) - - // 下方倾斜胶卷(向右移动) - FilmReelView5(images: reelImages[2]) - .rotationEffect(Angle(degrees: symmetricTiltAngle)) - .offset(x: bottomReelPosition, y: verticalOffset) - .scaleEffect(currentScale) - .opacity(upperLowerOpacity) - .zIndex(2) - - // 中间胶卷(向左移动,最终保留) - FilmReelView5(images: reelImages[1]) - .offset(x: middleReelPosition, y: 0) - .scaleEffect(currentScale) - .opacity(1.0) // 始终不透明 - .zIndex(1) - .edgesIgnoringSafeArea(.all) - } - .onAppear { - startAnimation() - } - } - - // MARK: - 动画逻辑 - - private func startAnimation() { - guard !isAnimating && !animationComplete else { return } - isAnimating = true - - // 分阶段动画曲线:先加速后匀速 - withAnimation(Animation.timingCurve(0.3, 0.0, 0.7, 1.0, duration: totalDuration)) { - animationProgress = 1.0 - } - - // 动画结束标记 - DispatchQueue.main.asyncAfter(deadline: .now() + totalDuration) { - isAnimating = false - animationComplete = true - } - } - - // MARK: - 动画计算 - - // 共同放大比例(6s后开始放大) - private var currentScale: CGFloat { - guard animationProgress >= constantSpeedEnd else { - return 1.0 // 前6s保持原尺寸 - } - - // 放大阶段相对进度(0-1) - let scalePhaseProgress = (animationProgress - constantSpeedEnd) / (1.0 - constantSpeedEnd) - return 1.0 + (finalScale - 1.0) * scalePhaseProgress - } - - // 上下胶卷透明度(放大阶段逐渐隐藏) - private var upperLowerOpacity: Double { - guard animationProgress >= constantSpeedEnd else { - return 0.8 // 前6s保持可见 - } - - // 放大阶段同步淡出 - let fadeProgress = (animationProgress - constantSpeedEnd) / (1.0 - constantSpeedEnd) - return 0.8 * (1.0 - fadeProgress) - } - - // MARK: - 移动速度控制(确保匀速阶段速度一致) - - private var motionProgress: CGFloat { - if animationProgress < accelerationEnd { - // 0-5s加速阶段:二次方曲线加速 - let t = animationProgress / accelerationEnd - return t * t - } else { - // 5s后匀速阶段:保持最大速度 - return 1.0 + (animationProgress - accelerationEnd) * - (accelerationEnd / (1.0 - accelerationEnd)) - } - } - - // 上方胶卷位置(向右移动) - private var topReelPosition: CGFloat { - totalDistance * 0.8 * motionProgress - } - - // 中间胶卷位置(向左移动) - private var middleReelPosition: CGFloat { - -totalDistance * 0.8 * motionProgress // 与上下胶卷速度大小相同,方向相反 - } - - // 下方胶卷位置(向右移动) - private var bottomReelPosition: CGFloat { - totalDistance * 0.8 * motionProgress // 与上方胶卷速度完全一致,保持对称 - } -} - -// MARK: - 胶卷组件 - -struct FilmReelView5: View { - let images: [String] - - var body: some View { - HStack(spacing: 10) { - ForEach(images.indices, id: \.self) { index in - FilmFrameView5(imageName: images[index]) - } - } - } -} - -struct FilmFrameView5: View { - let imageName: String - - var body: some View { - ZStack { - // 胶卷边框 - RoundedRectangle(cornerRadius: 4) - .stroke(Color.gray, lineWidth: 2) - .background(Color(red: 0.15, green: 0.15, blue: 0.15)) - - // 帧内容 - Rectangle() - .fill(gradientColor) - .cornerRadius(2) - .padding(2) - - // 帧标识 - Text(imageName) - .foregroundColor(.white) - .font(.caption2) - } - .frame(width: 90, height: 130) - // 胶卷孔洞 - .overlay( - HStack { - VStack(spacing: 6) { - ForEach(0..<6) { _ in - Circle() - .frame(width: 6, height: 6) - .foregroundColor(.gray) - } - } - Spacer() - VStack(spacing: 6) { - ForEach(0..<6) { _ in - Circle() - .frame(width: 6, height: 6) - .foregroundColor(.gray) - } - } - } - ) - } - - private var gradientColor: LinearGradient { - if imageName.hasPrefix("film1") { - return LinearGradient(gradient: Gradient(colors: [.blue, .indigo]), startPoint: .topLeading, endPoint: .bottomTrailing) - } else if imageName.hasPrefix("film2") { - return LinearGradient(gradient: Gradient(colors: [.yellow, .orange]), startPoint: .topLeading, endPoint: .bottomTrailing) - } else { - return LinearGradient(gradient: Gradient(colors: [.teal, .cyan]), startPoint: .topLeading, endPoint: .bottomTrailing) - } - } -} - -// 预览 -struct FilmAnimation_Previews5: PreviewProvider { - static var previews: some View { - FilmAnimation5() - } -} - \ No newline at end of file diff --git a/wake/View/Blind/Box6.swift b/wake/View/Blind/Box6.swift deleted file mode 100644 index 5ba1d07..0000000 --- a/wake/View/Blind/Box6.swift +++ /dev/null @@ -1,250 +0,0 @@ -import SwiftUI - -struct FilmAnimation: View { - // 设备尺寸 - private let deviceWidth = UIScreen.main.bounds.width - private let deviceHeight = UIScreen.main.bounds.height - - // 动画状态控制 - @State private var animationProgress: CGFloat = 0.0 // 0-1总进度 - @State private var isAnimating: Bool = false - @State private var animationComplete: Bool = false - - // 胶卷数据 - private let reelImages: [[String]] = [ - (0..<300).map { "film1-\($0+1)" }, // 上方胶卷 - (0..<350).map { "film2-\($0+1)" }, // 中间胶卷 - (0..<300).map { "film3-\($0+1)" } // 下方胶卷 - ] - - // 胶卷参数 - private let frameWidth: CGFloat = 90 - private let frameHeight: CGFloat = 130 - private let frameSpacing: CGFloat = 12 - - // 动画阶段时间参数 - private let accelerationDuration: Double = 5.0 // 0-5s加速 - private let constantSpeedDuration: Double = 1.0 // 5-6s匀速 - private let scaleStartDuration: Double = 1.0 // 6-7s共同放大 - private let scaleFinishDuration: Double = 1.0 // 7-8s仅中间胶卷放大 - private var totalDuration: Double { - accelerationDuration + constantSpeedDuration + scaleStartDuration + scaleFinishDuration - } - - // 各阶段进度阈值 - private var accelerationEnd: CGFloat { accelerationDuration / totalDuration } - private var constantSpeedEnd: CGFloat { (accelerationDuration + constantSpeedDuration) / totalDuration } - private var scaleStartEnd: CGFloat { - (accelerationDuration + constantSpeedDuration + scaleStartDuration) / totalDuration - } - - // 布局与运动参数(核心:对称倾斜角度) - private let tiltAngle: Double = 10 // 基础倾斜角度 - private let upperTilt: Double = -10 // 上方胶卷:左高右低(负角度) - private let lowerTilt: Double = 10 // 下方胶卷:左低右高(正角度) - private let verticalSpacing: CGFloat = 200 // 上下胶卷垂直间距 - private let finalScale: CGFloat = 4.5 - - // 移动距离参数 - private let maxTiltedReelMovement: CGFloat = 3500 // 倾斜胶卷最大移动距离 - private let maxMiddleReelMovement: CGFloat = -3000 // 中间胶卷最大移动距离 - - var body: some View { - // 固定背景 - Color(red: 0.08, green: 0.08, blue: 0.08) - .edgesIgnoringSafeArea(.all) - .overlay( - ZStack { - // 上方倾斜胶卷(左高右低,向右移动) - if showTiltedReels { - FilmReelView(images: reelImages[0]) - .rotationEffect(Angle(degrees: upperTilt)) // 左高右低 - .offset(x: upperReelXPosition, y: -verticalSpacing/2) - .scaleEffect(tiltedScale) - .opacity(tiltedOpacity) - .zIndex(1) - } - - // 下方倾斜胶卷(左低右高,向右移动) - if showTiltedReels { - FilmReelView(images: reelImages[2]) - .rotationEffect(Angle(degrees: lowerTilt)) // 左低右高 - .offset(x: lowerReelXPosition, y: verticalSpacing/2) - .scaleEffect(tiltedScale) - .opacity(tiltedOpacity) - .zIndex(1) - } - - // 中间胶卷(垂直,向左移动) - FilmReelView(images: reelImages[1]) - .offset(x: middleReelXPosition, y: 0) - .scaleEffect(middleScale) - .opacity(1.0) - .zIndex(2) - .edgesIgnoringSafeArea(.all) - } - ) - .onAppear { - startAnimation() - } - } - - // MARK: - 动画逻辑 - - private func startAnimation() { - guard !isAnimating && !animationComplete else { return } - isAnimating = true - - withAnimation(Animation.easeInOut(duration: totalDuration)) { - animationProgress = 1.0 - } - - DispatchQueue.main.asyncAfter(deadline: .now() + totalDuration) { - isAnimating = false - animationComplete = true - } - } - - // MARK: - 位置计算(确保向右移动) - - // 上方倾斜胶卷X位置 - private var upperReelXPosition: CGFloat { - let startPosition: CGFloat = -deviceWidth * 1.2 // 左侧屏幕外起始 - return startPosition + (maxTiltedReelMovement * movementProgress) - } - - // 下方倾斜胶卷X位置 - private var lowerReelXPosition: CGFloat { - let startPosition: CGFloat = -deviceWidth * 0.8 // 稍右于上方胶卷起始 - return startPosition + (maxTiltedReelMovement * movementProgress) - } - - // 中间胶卷X位置 - private var middleReelXPosition: CGFloat { - let startPosition: CGFloat = deviceWidth * 0.3 - return startPosition + (maxMiddleReelMovement * movementProgress) - } - - // 移动进度(0-1) - private var movementProgress: CGFloat { - if animationProgress < constantSpeedEnd { - return animationProgress / constantSpeedEnd - } else { - return 1.0 // 6秒后停止移动 - } - } - - // MARK: - 缩放与显示控制 - - // 中间胶卷缩放 - private var middleScale: CGFloat { - guard animationProgress >= constantSpeedEnd else { - return 1.0 - } - - let scalePhaseProgress = (animationProgress - constantSpeedEnd) / (1.0 - constantSpeedEnd) - return 1.0 + (finalScale - 1.0) * scalePhaseProgress - } - - // 倾斜胶卷缩放 - private var tiltedScale: CGFloat { - guard animationProgress >= constantSpeedEnd, animationProgress < scaleStartEnd else { - return 1.0 - } - - let scalePhaseProgress = (animationProgress - constantSpeedEnd) / (scaleStartEnd - constantSpeedEnd) - return 1.0 + (finalScale * 0.6 - 1.0) * scalePhaseProgress - } - - // 倾斜胶卷透明度 - private var tiltedOpacity: Double { - guard animationProgress >= constantSpeedEnd, animationProgress < scaleStartEnd else { - return 0.8 - } - - let fadeProgress = (animationProgress - constantSpeedEnd) / (scaleStartEnd - constantSpeedEnd) - return 0.8 * (1.0 - fadeProgress) - } - - // 控制倾斜胶卷显示 - private var showTiltedReels: Bool { - animationProgress < scaleStartEnd - } -} - -// MARK: - 胶卷组件 - -struct FilmReelView: View { - let images: [String] - - var body: some View { - HStack(spacing: 12) { - ForEach(images.indices, id: \.self) { index in - FilmFrameView(imageName: images[index]) - } - } - } -} - -struct FilmFrameView: View { - let imageName: String - - var body: some View { - ZStack { - // 胶卷边框 - RoundedRectangle(cornerRadius: 4) - .stroke(Color.gray, lineWidth: 2) - .background(Color(red: 0.15, green: 0.15, blue: 0.15)) - - // 帧内容 - Rectangle() - .fill(gradientColor) - .cornerRadius(2) - .padding(2) - - // 帧标识 - Text(imageName) - .foregroundColor(.white) - .font(.caption2) - } - .frame(width: 90, height: 130) - // 胶卷孔洞 - .overlay( - HStack { - VStack(spacing: 6) { - ForEach(0..<6) { _ in - Circle() - .frame(width: 6, height: 6) - .foregroundColor(.gray) - } - } - Spacer() - VStack(spacing: 6) { - ForEach(0..<6) { _ in - Circle() - .frame(width: 6, height: 6) - .foregroundColor(.gray) - } - } - } - ) - } - - private var gradientColor: LinearGradient { - if imageName.hasPrefix("film1") { - return LinearGradient(gradient: Gradient(colors: [.blue, .indigo]), startPoint: .topLeading, endPoint: .bottomTrailing) - } else if imageName.hasPrefix("film2") { - return LinearGradient(gradient: Gradient(colors: [.yellow, .orange]), startPoint: .topLeading, endPoint: .bottomTrailing) - } else { - return LinearGradient(gradient: Gradient(colors: [.purple, .pink]), startPoint: .topLeading, endPoint: .bottomTrailing) - } - } -} - -// 预览 -struct FilmAnimation_Previews: PreviewProvider { - static var previews: some View { - FilmAnimation() - } -} - diff --git a/wake/View/Blind/JoinModal.swift b/wake/View/Subscribe/JoinModal.swift similarity index 100% rename from wake/View/Blind/JoinModal.swift rename to wake/View/Subscribe/JoinModal.swift -- 2.47.2 From 9e965c1e4e177b0aa53fbe0a22fb6c01e705957f Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sat, 6 Sep 2025 18:45:18 +0800 Subject: [PATCH 02/18] =?UTF-8?q?refactor:=20=E8=B0=83=E6=95=B4=E7=9B=B2?= =?UTF-8?q?=E7=9B=92=E8=BF=94=E5=9B=9E=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/ContentView.swift | 37 +++++++++++++++++++++++++++---------- wake/Utils/Router.swift | 3 ++- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/wake/ContentView.swift b/wake/ContentView.swift index b6f24cb..25add79 100644 --- a/wake/ContentView.swift +++ b/wake/ContentView.swift @@ -75,19 +75,36 @@ struct BlindBoxView: View { } // 盲盒列表 struct BlindList: Codable, Identifiable { - let id: Int64 + // API 返回为字符串,这里按字符串处理 + let id: String let boxCode: String - let userId: Int64 + let userId: String let name: String let boxType: String let features: String? - let resultFileId: Int64? + let resultFile: FileInfo? let status: String let workflowInstanceId: String? let videoGenerateTime: String? let createTime: String - let coverFileId: Int64? - let description: String + let coverFile: FileInfo? + let description: String? + + struct FileInfo: Codable { + let id: String + let fileName: String? + let url: String? + // 为了兼容任意元数据结构,这里使用字典的最宽松版本 + // 如果后续需要更强类型,可以引入自定义的 AnyCodable/JSONValue + let metadata: [String: String]? + + enum CodingKeys: String, CodingKey { + case id + case fileName = "file_name" + case url + case metadata + } + } enum CodingKeys: String, CodingKey { case id @@ -96,12 +113,12 @@ struct BlindBoxView: View { case name case boxType = "box_type" case features - case resultFileId = "result_file_id" + case resultFile = "result_file" case status case workflowInstanceId = "workflow_instance_id" case videoGenerateTime = "video_generate_time" case createTime = "create_time" - case coverFileId = "cover_file_id" + case coverFile = "cover_file" case description } } @@ -163,13 +180,13 @@ struct BlindBoxView: View { init(from listItem: BlindList) { self.init( - id: listItem.id, + id: Int64(listItem.id) ?? 0, boxCode: listItem.boxCode, - userId: listItem.userId, + userId: Int64(listItem.userId) ?? 0, name: listItem.name, boxType: listItem.boxType, features: listItem.features, - url: nil, + url: listItem.resultFile?.url, status: listItem.status, workflowInstanceId: listItem.workflowInstanceId, videoGenerateTime: listItem.videoGenerateTime, diff --git a/wake/Utils/Router.swift b/wake/Utils/Router.swift index 6337967..0bd118f 100644 --- a/wake/Utils/Router.swift +++ b/wake/Utils/Router.swift @@ -23,7 +23,8 @@ enum AppRoute: Hashable { case .login: LoginView() case .avatarBox: - AvatarBoxView() + // AvatarBoxView has been removed; route to BlindBoxView as replacement + BlindBoxView(mediaType: .all) case .feedbackView: FeedbackView() case .feedbackDetail(let type): -- 2.47.2 From b07de811a57c1b07001bdebe8d2b4150bba16c75 Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sat, 6 Sep 2025 19:00:09 +0800 Subject: [PATCH 03/18] =?UTF-8?q?refactor:=20=E8=B0=83=E6=95=B4=E7=9B=B2?= =?UTF-8?q?=E7=9B=92=E7=BB=93=E6=9E=84=E4=BD=93=E5=88=B0Models=E7=9B=AE?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/ContentView.swift | 130 +-------------------------------- wake/Models/BlindModels.swift | 131 ++++++++++++++++++++++++++++++++++ wake/Utils/Router.swift | 2 +- 3 files changed, 133 insertions(+), 130 deletions(-) create mode 100644 wake/Models/BlindModels.swift diff --git a/wake/ContentView.swift b/wake/ContentView.swift index 25add79..e37f488 100644 --- a/wake/ContentView.swift +++ b/wake/ContentView.swift @@ -67,135 +67,7 @@ struct AVPlayerController: UIViewControllerRepresentable { } } -struct BlindBoxView: View { - enum BlindBoxMediaType { - case video - case image - case all - } - // 盲盒列表 - struct BlindList: Codable, Identifiable { - // API 返回为字符串,这里按字符串处理 - let id: String - let boxCode: String - let userId: String - let name: String - let boxType: String - let features: String? - let resultFile: FileInfo? - let status: String - let workflowInstanceId: String? - let videoGenerateTime: String? - let createTime: String - let coverFile: FileInfo? - let description: String? - - struct FileInfo: Codable { - let id: String - let fileName: String? - let url: String? - // 为了兼容任意元数据结构,这里使用字典的最宽松版本 - // 如果后续需要更强类型,可以引入自定义的 AnyCodable/JSONValue - let metadata: [String: String]? - - enum CodingKeys: String, CodingKey { - case id - case fileName = "file_name" - case url - case metadata - } - } - - enum CodingKeys: String, CodingKey { - case id - case boxCode = "box_code" - case userId = "user_id" - case name - case boxType = "box_type" - case features - case resultFile = "result_file" - case status - case workflowInstanceId = "workflow_instance_id" - case videoGenerateTime = "video_generate_time" - case createTime = "create_time" - case coverFile = "cover_file" - case description - } - } - // 盲盒数量 - struct BlindCount: Codable { - let availableQuantity: Int - - enum CodingKeys: String, CodingKey { - case availableQuantity = "available_quantity" - } - } - - // MARK: - BlindBox Response Model - - struct BlindBoxData: Codable { - let id: Int64 - let boxCode: String - let userId: Int64 - let name: String - let boxType: String - let features: String? - let url: String? - let status: String - let workflowInstanceId: String? - // 视频生成时间 - let videoGenerateTime: String? - let createTime: String - let description: String? - - enum CodingKeys: String, CodingKey { - case id - case boxCode = "box_code" - case userId = "user_id" - case name - case boxType = "box_type" - case features - case url - case status - case workflowInstanceId = "workflow_instance_id" - case videoGenerateTime = "video_generate_time" - case createTime = "create_time" - case description - } - - init(id: Int64, boxCode: String, userId: Int64, name: String, boxType: String, features: String?, url: String?, status: String, workflowInstanceId: String?, videoGenerateTime: String?, createTime: String, description: String?) { - self.id = id - self.boxCode = boxCode - self.userId = userId - self.name = name - self.boxType = boxType - self.features = features - self.url = url - self.status = status - self.workflowInstanceId = workflowInstanceId - self.videoGenerateTime = videoGenerateTime - self.createTime = createTime - self.description = description - } - - init(from listItem: BlindList) { - self.init( - id: Int64(listItem.id) ?? 0, - boxCode: listItem.boxCode, - userId: Int64(listItem.userId) ?? 0, - name: listItem.name, - boxType: listItem.boxType, - features: listItem.features, - url: listItem.resultFile?.url, - status: listItem.status, - workflowInstanceId: listItem.workflowInstanceId, - videoGenerateTime: listItem.videoGenerateTime, - createTime: listItem.createTime, - description: listItem.description - ) - } - } - +struct BlindBoxView: View { let mediaType: BlindBoxMediaType @State private var showModal = false // 控制用户资料弹窗显示 @State private var showSettings = false // 控制设置页面显示 diff --git a/wake/Models/BlindModels.swift b/wake/Models/BlindModels.swift new file mode 100644 index 0000000..76412f5 --- /dev/null +++ b/wake/Models/BlindModels.swift @@ -0,0 +1,131 @@ +import Foundation + +// MARK: - Blind Box Media Type +enum BlindBoxMediaType { + case video + case image + case all +} + +// MARK: - Blind Box List +struct BlindList: Codable, Identifiable { + // API 返回为字符串,这里按字符串处理 + let id: String + let boxCode: String + let userId: String + let name: String + let boxType: String + let features: String? + let resultFile: FileInfo? + let status: String + let workflowInstanceId: String? + let videoGenerateTime: String? + let createTime: String + let coverFile: FileInfo? + let description: String? + + struct FileInfo: Codable { + let id: String + let fileName: String? + let url: String? + // 为了兼容任意元数据结构,这里使用字典的最宽松版本 + // 如果后续需要更强类型,可以引入自定义的 AnyCodable/JSONValue + let metadata: [String: String]? + + enum CodingKeys: String, CodingKey { + case id + case fileName = "file_name" + case url + case metadata + } + } + + enum CodingKeys: String, CodingKey { + case id + case boxCode = "box_code" + case userId = "user_id" + case name + case boxType = "box_type" + case features + case resultFile = "result_file" + case status + case workflowInstanceId = "workflow_instance_id" + case videoGenerateTime = "video_generate_time" + case createTime = "create_time" + case coverFile = "cover_file" + case description + } +} + +// MARK: - Blind Box Count +struct BlindCount: Codable { + let availableQuantity: Int + + enum CodingKeys: String, CodingKey { + case availableQuantity = "available_quantity" + } +} + +// MARK: - Blind Box Data +struct BlindBoxData: Codable { + let id: Int64 + let boxCode: String + let userId: Int64 + let name: String + let boxType: String + let features: String? + let url: String? + let status: String + let workflowInstanceId: String? + // 视频生成时间 + let videoGenerateTime: String? + let createTime: String + let description: String? + + enum CodingKeys: String, CodingKey { + case id + case boxCode = "box_code" + case userId = "user_id" + case name + case boxType = "box_type" + case features + case url + case status + case workflowInstanceId = "workflow_instance_id" + case videoGenerateTime = "video_generate_time" + case createTime = "create_time" + case description + } + + init(id: Int64, boxCode: String, userId: Int64, name: String, boxType: String, features: String?, url: String?, status: String, workflowInstanceId: String?, videoGenerateTime: String?, createTime: String, description: String?) { + self.id = id + self.boxCode = boxCode + self.userId = userId + self.name = name + self.boxType = boxType + self.features = features + self.url = url + self.status = status + self.workflowInstanceId = workflowInstanceId + self.videoGenerateTime = videoGenerateTime + self.createTime = createTime + self.description = description + } + + init(from listItem: BlindList) { + self.init( + id: Int64(listItem.id) ?? 0, + boxCode: listItem.boxCode, + userId: Int64(listItem.userId) ?? 0, + name: listItem.name, + boxType: listItem.boxType, + features: listItem.features, + url: listItem.resultFile?.url, + status: listItem.status, + workflowInstanceId: listItem.workflowInstanceId, + videoGenerateTime: listItem.videoGenerateTime, + createTime: listItem.createTime, + description: listItem.description + ) + } +} diff --git a/wake/Utils/Router.swift b/wake/Utils/Router.swift index 0bd118f..609229b 100644 --- a/wake/Utils/Router.swift +++ b/wake/Utils/Router.swift @@ -7,7 +7,7 @@ enum AppRoute: Hashable { case feedbackView case feedbackDetail(type: FeedbackView.FeedbackType) case mediaUpload - case blindBox(mediaType: BlindBoxView.BlindBoxMediaType) + case blindBox(mediaType: BlindBoxMediaType) case blindOutcome(media: MediaType, time: String? = nil, description: String? = nil) case memories case subscribe -- 2.47.2 From 3e49ee9489d6a28e345945860c7145de0948107a Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sat, 6 Sep 2025 19:14:34 +0800 Subject: [PATCH 04/18] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E4=BC=9A=E5=91=98=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/Models/MemberProfile.swift | 39 +++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/wake/Models/MemberProfile.swift b/wake/Models/MemberProfile.swift index e69d6a4..b4e0919 100644 --- a/wake/Models/MemberProfile.swift +++ b/wake/Models/MemberProfile.swift @@ -16,6 +16,41 @@ struct MemberProfileResponse: Codable { } } +// MARK: - TitleRanking +struct TitleRanking: Codable { + let displayName: String + let ranking: Int + let value: Int + let materialType: String + let userId: String + let region: String + let userAvatarUrl: String? + let userNickName: String? + + enum CodingKeys: String, CodingKey { + case displayName = "display_name" + case ranking + case value + case materialType = "material_type" + case userId = "user_id" + case region + case userAvatarUrl = "user_avatar_url" + case userNickName = "user_nick_name" + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(displayName, forKey: .displayName) + try container.encode(ranking, forKey: .ranking) + try container.encode(value, forKey: .value) + try container.encode(materialType, forKey: .materialType) + try container.encode(userId, forKey: .userId) + try container.encode(region, forKey: .region) + try container.encodeIfPresent(userAvatarUrl, forKey: .userAvatarUrl) + try container.encodeIfPresent(userNickName, forKey: .userNickName) + } +} + // MARK: - MemberProfile struct MemberProfile: Codable { let materialCounter: MaterialCounter @@ -26,7 +61,7 @@ struct MemberProfile: Codable { let totalPoints: Int let usedBytes: Int let totalBytes: Int - let titleRankings: [String] + let titleRankings: [TitleRanking] let medalInfos: [MedalInfo] let membershipLevel: String let membershipEndAt: String @@ -57,7 +92,7 @@ struct MemberProfile: Codable { totalPoints = try container.decode(Int.self, forKey: .totalPoints) usedBytes = try container.decode(Int.self, forKey: .usedBytes) totalBytes = try container.decode(Int.self, forKey: .totalBytes) - titleRankings = try container.decode([String].self, forKey: .titleRankings) + titleRankings = try container.decode([TitleRanking].self, forKey: .titleRankings) if let medalInfos = try? container.decode([MedalInfo].self, forKey: .medalInfos) { self.medalInfos = medalInfos -- 2.47.2 From 685e694134176e9ae8cc60dd1114396089a1fcd5 Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sat, 6 Sep 2025 22:08:47 +0800 Subject: [PATCH 05/18] =?UTF-8?q?chore:=20=E8=B0=83=E6=95=B4=E7=9B=B2?= =?UTF-8?q?=E7=9B=92=E9=A1=B5=E9=9D=A2=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/{ => View/Blind}/ContentView.swift | 7 ++++++- wake/View/{Owner/UserInfo => OnBoarding}/UserInfo.swift | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) rename wake/{ => View/Blind}/ContentView.swift (99%) rename wake/View/{Owner/UserInfo => OnBoarding}/UserInfo.swift (98%) diff --git a/wake/ContentView.swift b/wake/View/Blind/ContentView.swift similarity index 99% rename from wake/ContentView.swift rename to wake/View/Blind/ContentView.swift index e37f488..e284dcd 100644 --- a/wake/ContentView.swift +++ b/wake/View/Blind/ContentView.swift @@ -108,6 +108,11 @@ struct BlindBoxView: View { init(mediaType: BlindBoxMediaType) { self.mediaType = mediaType + + // 检查盲盒列表,如果不存在First/Second盲盒,则跳转到对应的页面重新触发新手引导 + if mediaType == BlindBoxMediaType.all { + print("获取盲盒列表") + } } // 倒计时 @@ -850,7 +855,7 @@ struct BlindBoxView: View { // MARK: - 预览 #Preview { - BlindBoxView(mediaType: .video) + BlindBoxView(mediaType: .all) } struct TransparentVideoPlayer: UIViewRepresentable { diff --git a/wake/View/Owner/UserInfo/UserInfo.swift b/wake/View/OnBoarding/UserInfo.swift similarity index 98% rename from wake/View/Owner/UserInfo/UserInfo.swift rename to wake/View/OnBoarding/UserInfo.swift index b2b8ab4..0e98060 100644 --- a/wake/View/Owner/UserInfo/UserInfo.swift +++ b/wake/View/OnBoarding/UserInfo.swift @@ -180,6 +180,10 @@ struct UserInfo: View { if let userData = response.data { self.userName = userData.username } + + // 触发盲盒生成 + + // 导航到首页盲盒等待用户开启第一个盲盒 Router.shared.navigate(to: .blindBox(mediaType: .image)) case .failure(let error): @@ -302,4 +306,4 @@ struct UserInfo_Previews: PreviewProvider { static var previews: some View { UserInfo() } -} \ No newline at end of file +} -- 2.47.2 From 8f97b0c544844a96cb017b3b1e6a172cd9fa9748 Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sat, 6 Sep 2025 22:13:22 +0800 Subject: [PATCH 06/18] =?UTF-8?q?feat:=20=E9=85=8D=E7=BD=AEtoken=E7=94=A8?= =?UTF-8?q?=E4=BA=8Epreview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/View/Blind/ContentView.swift | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/wake/View/Blind/ContentView.swift b/wake/View/Blind/ContentView.swift index e284dcd..4d41245 100644 --- a/wake/View/Blind/ContentView.swift +++ b/wake/View/Blind/ContentView.swift @@ -856,15 +856,26 @@ struct BlindBoxView: View { // MARK: - 预览 #Preview { BlindBoxView(mediaType: .all) + .onAppear { + // 仅在Preview中设置模拟令牌(不要在生产代码中使用) + #if DEBUG + if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { + // 设置模拟令牌用于Preview + let previewToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJqdGkiOjczNzAwMTY5NzMzODE1NzA1NjAsImlkZW50aXR5IjoiNzM1MDQzOTY2MzExNjYxOTc3NyIsImV4cCI6MTc1Nzc1Mzc3NH0.tZ8p5sW4KX6HFoJpJN0e4VmJOAGhTrYD2yTwQwilKpufzqOAfXX4vpGYBurgBIcHj2KmXKX2PQMOeeAtvAypDA" + KeychainHelper.saveAccessToken(previewToken) + print("🔑 Preview token set for testing") + } + #endif + } } -struct TransparentVideoPlayer: UIViewRepresentable { - func makeUIView(context: Context) -> UIView { - let view = UIView() - view.backgroundColor = .clear - view.isOpaque = false - return view - } +// struct TransparentVideoPlayer: UIViewRepresentable { +// func makeUIView(context: Context) -> UIView { +// let view = UIView() +// view.backgroundColor = .clear +// view.isOpaque = false +// return view +// } - func updateUIView(_ uiView: UIView, context: Context) {} -} +// func updateUIView(_ uiView: UIView, context: Context) {} +// } -- 2.47.2 From 8fb6d5c6b9e637ec9129f41fcfba7eeac4b620ca Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sat, 6 Sep 2025 22:18:16 +0800 Subject: [PATCH 07/18] =?UTF-8?q?chore:=20=E5=88=A4=E6=96=AD=E7=9B=B2?= =?UTF-8?q?=E7=9B=92=E6=95=B0=E9=87=8F=EF=BC=8C=E7=A1=AE=E5=AE=9A=E6=98=AF?= =?UTF-8?q?=E5=90=A6=E8=BF=9B=E5=85=A5=E6=96=B0=E6=89=8B=E5=BC=95=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/View/Blind/ContentView.swift | 61 +++++++++++++++++-------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/wake/View/Blind/ContentView.swift b/wake/View/Blind/ContentView.swift index 4d41245..20a9558 100644 --- a/wake/View/Blind/ContentView.swift +++ b/wake/View/Blind/ContentView.swift @@ -108,11 +108,6 @@ struct BlindBoxView: View { init(mediaType: BlindBoxMediaType) { self.mediaType = mediaType - - // 检查盲盒列表,如果不存在First/Second盲盒,则跳转到对应的页面重新触发新手引导 - if mediaType == BlindBoxMediaType.all { - print("获取盲盒列表") - } } // 倒计时 @@ -162,6 +157,39 @@ struct BlindBoxView: View { startPolling() case .all: print("Loading all content...") + // 检查盲盒列表,如果不存在First/Second盲盒,则跳转到对应的页面重新触发新手引导 + NetworkService.shared.get( + path: "/blind_boxs/query", + parameters: nil + ) { (result: Result, NetworkError>) in + DispatchQueue.main.async { + switch result { + case .success(let response): + if response.data.count == 0 { + // 跳转到新手引导-First盲盒页面 + print("❌ 没有盲盒,跳转到新手引导-First盲盒页面") + // return + } + if response.data.count == 1 && response.data[0].boxType == "First" { + // 跳转到新手引导-Second盲盒页面 + print("❌ 只有First盲盒,跳转到新手引导-Second盲盒页面") + // return + } + + self.blindList = response.data ?? [] + // 如果列表为空数组 设置盲盒状态为none + if self.blindList.isEmpty { + self.animationPhase = .none + } + print("✅ 成功获取 \(self.blindList.count) 个盲盒") + case .failure(let error): + self.blindList = [] + self.animationPhase = .none + print("❌ 获取盲盒列表失败:", error.localizedDescription) + } + } + } + // 会员信息 NetworkService.shared.get( path: "/membership/personal-center-info", @@ -195,27 +223,6 @@ struct BlindBoxView: View { } } } - // 盲盒列表 - NetworkService.shared.get( - path: "/blind_boxs/query", - parameters: nil - ) { (result: Result, NetworkError>) in - DispatchQueue.main.async { - switch result { - case .success(let response): - self.blindList = response.data ?? [] - // 如果列表为空数组 设置盲盒状态为none - if self.blindList.isEmpty { - self.animationPhase = .none - } - print("✅ 成功获取 \(self.blindList.count) 个盲盒") - case .failure(let error): - self.blindList = [] - self.animationPhase = .none - print("❌ 获取盲盒列表失败:", error.localizedDescription) - } - } - } } } // 轮询接口 @@ -862,7 +869,7 @@ struct BlindBoxView: View { if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { // 设置模拟令牌用于Preview let previewToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJqdGkiOjczNzAwMTY5NzMzODE1NzA1NjAsImlkZW50aXR5IjoiNzM1MDQzOTY2MzExNjYxOTc3NyIsImV4cCI6MTc1Nzc1Mzc3NH0.tZ8p5sW4KX6HFoJpJN0e4VmJOAGhTrYD2yTwQwilKpufzqOAfXX4vpGYBurgBIcHj2KmXKX2PQMOeeAtvAypDA" - KeychainHelper.saveAccessToken(previewToken) + let _ = KeychainHelper.saveAccessToken(previewToken) print("🔑 Preview token set for testing") } #endif -- 2.47.2 From e9cdb82b70114624e3b11dca7394b5dca5abe49c Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sun, 7 Sep 2025 01:06:52 +0800 Subject: [PATCH 08/18] =?UTF-8?q?feat:=20=E8=A7=A6=E5=8F=91=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=AC=AC=E4=B8=80=E4=B8=AA=E7=9B=B2=E7=9B=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/Utils/ApiClient/BlindBoxApi.swift | 111 +++++++++++++++++++++ wake/Utils/MaterialUpload.swift | 66 ++++++++++++ wake/View/OnBoarding/SecondBoxUpload.swift | 0 wake/View/OnBoarding/UserInfo.swift | 36 +++++-- wake/WakeApp.swift | 11 +- 5 files changed, 212 insertions(+), 12 deletions(-) create mode 100644 wake/Utils/ApiClient/BlindBoxApi.swift create mode 100644 wake/Utils/MaterialUpload.swift create mode 100644 wake/View/OnBoarding/SecondBoxUpload.swift diff --git a/wake/Utils/ApiClient/BlindBoxApi.swift b/wake/Utils/ApiClient/BlindBoxApi.swift new file mode 100644 index 0000000..2d13cb4 --- /dev/null +++ b/wake/Utils/ApiClient/BlindBoxApi.swift @@ -0,0 +1,111 @@ +import Foundation + +// MARK: - Generate Blind Box Request Model +// struct GenerateBlindBoxRequest: Codable { +// let boxType: String +// let materialIds: [String] + +// enum CodingKeys: String, CodingKey { +// case boxType = "box_type" +// case materialIds = "material_ids" +// } +// } + +// MARK: - Generate Blind Box Response Model +struct GenerateBlindBoxResponse: Codable { + let code: Int + let data: BlindBoxDataWrapper? + + struct BlindBoxDataWrapper: Codable { + let id: String + let boxCode: String + let userId: String + let name: String + let boxType: String + let features: String? + let resultFile: FileInfo? + let status: String + let workflowInstanceId: String? + let videoGenerateTime: String? + let createTime: String + let coverFile: FileInfo? + let description: String + + // 添加计算属性以获取Int64值 + var idValue: Int64 { Int64(id) ?? 0 } + var userIdValue: Int64 { Int64(userId) ?? 0 } + + struct FileInfo: Codable { + let id: String + let fileName: String? + let url: String? + let metadata: [String: String]? + + enum CodingKeys: String, CodingKey { + case id + case fileName = "file_name" + case url + case metadata + } + } + + enum CodingKeys: String, CodingKey { + case id + case boxCode = "box_code" + case userId = "user_id" + case name + case boxType = "box_type" + case features + case resultFile = "result_file" + case status + case workflowInstanceId = "workflow_instance_id" + case videoGenerateTime = "video_generate_time" + case createTime = "create_time" + case coverFile = "cover_file" + case description + } + } +} + +// MARK: - Blind Box API Client +class BlindBoxApi { + static let shared = BlindBoxApi() + + private init() {} + + /// 生成盲盒 + /// - Parameters: + /// - boxType: 盲盒类型 (如 "First") + /// - materialIds: 素材ID数组 + /// - completion: 完成回调,返回盲盒数据或错误 + func generateBlindBox( + boxType: String, + materialIds: [String], + completion: @escaping (Result) -> Void + ) { + // 将Codable结构体转换为字典 + let parameters: [String: Any] = [ + "box_type": boxType, + "material_ids": materialIds + ] + + NetworkService.shared.postWithToken( + path: "/blind_box/generate", + parameters: parameters, + completion: { (result: Result) in + DispatchQueue.main.async { + switch result { + case .success(let response): + if response.code == 0 { + completion(.success(response.data)) + } else { + completion(.failure(NetworkError.serverError("服务器返回错误码: \(response.code)"))) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + ) + } +} \ No newline at end of file diff --git a/wake/Utils/MaterialUpload.swift b/wake/Utils/MaterialUpload.swift new file mode 100644 index 0000000..be39171 --- /dev/null +++ b/wake/Utils/MaterialUpload.swift @@ -0,0 +1,66 @@ +import Foundation + +// MARK: - 数据模型 +struct MaterialRequest: Codable { + let fileId: String + let previewFileId: String + + enum CodingKeys: String, CodingKey { + case fileId = "file_id" + case previewFileId = "preview_file_id" + } +} + +struct AddMaterialResponse: Codable { + let code: Int + let data: [String]? +} + +// MARK: - 素材上传工具类 +class MaterialUpload { + static let shared = MaterialUpload() + + private init() {} + + /// 添加素材到服务器 + /// - Parameters: + /// - fileId: 文件ID + /// - previewFileId: 预览文件ID + /// - completion: 完成回调,返回结果ID数组或错误 + func addMaterial( + fileId: String, + previewFileId: String, + completion: @escaping (Result<[String]?, Error>) -> Void + ) { + // 创建请求数据 + let materials: [[String: String]] = [[ + "file_id": fileId, + "preview_file_id": previewFileId + ]] + + // 调试信息:检查参数是否为有效的JSON对象 + print("🔍 准备发送的参数: \(materials)") + + + // 使用NetworkService发送请求 + NetworkService.shared.post( + path: "/material", + parameters: materials, + completion: { (result: Result) in + DispatchQueue.main.async { + switch result { + case .success(let response): + if response.code == 0 { + completion(.success(response.data)) + } else { + completion(.failure(NetworkError.serverError("服务器返回错误码: \(response.code)"))) + } + case .failure(let error): + print("❌ 素材上传失败: \(error.localizedDescription)") + completion(.failure(error)) + } + } + } + ) + } +} diff --git a/wake/View/OnBoarding/SecondBoxUpload.swift b/wake/View/OnBoarding/SecondBoxUpload.swift new file mode 100644 index 0000000..e69de29 diff --git a/wake/View/OnBoarding/UserInfo.swift b/wake/View/OnBoarding/UserInfo.swift index 0e98060..c864d11 100644 --- a/wake/View/OnBoarding/UserInfo.swift +++ b/wake/View/OnBoarding/UserInfo.swift @@ -116,7 +116,7 @@ struct UserInfo: View { // Content VStack VStack(spacing: 20) { // Title - Text(showUsername ? "Add Your Avatar" : "What's Your Name?") + Text(showUsername ? "What's Your Name?" : "Add Your Avatar") .font(Typography.font(for: .body, family: .quicksandBold)) .frame(maxWidth: .infinity, alignment: .center) @@ -181,11 +181,32 @@ struct UserInfo: View { self.userName = userData.username } - // 触发盲盒生成 - - // 导航到首页盲盒等待用户开启第一个盲盒 - Router.shared.navigate(to: .blindBox(mediaType: .image)) - + // 上传头像为素材 + MaterialUpload.shared.addMaterial( + fileId: uploadedFileId ?? "", + previewFileId: uploadedFileId ?? "" + ) { result in + switch result { + case .success(let data): + print("素材添加成功,返回ID: \(data ?? [])") + // 触发盲盒生成 + BlindBoxApi.shared.generateBlindBox( + boxType: "First", + materialIds: data ?? [] + ) { result in + switch result { + case .success(let blindBoxData): + print("✅ 盲盒生成成功: \(blindBoxData?.id ?? "0")") + // 导航到首页盲盒等待用户开启第一个盲盒 + Router.shared.navigate(to: .blindBox(mediaType: .image)) + case .failure(let error): + print("❌ 盲盒生成失败: \(error.localizedDescription)") + } + } + case .failure(let error): + print("素材添加失败: \(error.localizedDescription)") + } + } case .failure(let error): print("❌ 用户信息更新失败: \(error.localizedDescription)") self.errorMessage = "更新失败: \(error.localizedDescription)" @@ -199,7 +220,8 @@ struct UserInfo: View { } } }) { - Text(showUsername ? "Open" : "Continue") +// Text(showUsername ? "Open" : "Continue") + Text("Continue") .font(Typography.font(for: .body)) .fontWeight(.bold) .frame(maxWidth: .infinity) diff --git a/wake/WakeApp.swift b/wake/WakeApp.swift index e66f67b..0db40b1 100644 --- a/wake/WakeApp.swift +++ b/wake/WakeApp.swift @@ -46,10 +46,11 @@ struct WakeApp: App { if authState.isAuthenticated { // 已登录:显示主页面 NavigationStack(path: $router.path) { - BlindBoxView(mediaType: .all) - .navigationDestination(for: AppRoute.self) { route in - route.view - } +// BlindBoxView(mediaType: .all) +// .navigationDestination(for: AppRoute.self) { route in +// route.view +// } + UserInfo() } } else { // 未登录:显示登录界面 @@ -108,4 +109,4 @@ struct WakeApp: App { } } } -} \ No newline at end of file +} -- 2.47.2 From 8f369867b23a936d2a49b12150eb50357d582304 Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sun, 7 Sep 2025 13:55:31 +0800 Subject: [PATCH 09/18] =?UTF-8?q?feat:=20=E7=9B=B2=E7=9B=92=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=B1=BB=E5=9E=8B=E6=89=93=E9=80=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/Models/BlindModels.swift | 87 ++++-- wake/Utils/ApiClient/BlindBoxApi.swift | 98 +++---- wake/Utils/NetworkService.swift | 37 +++ wake/Utils/Router.swift | 6 +- wake/View/Blind/ContentView.swift | 390 +++++++++++++++---------- wake/View/OnBoarding/UserInfo.swift | 2 +- 6 files changed, 385 insertions(+), 235 deletions(-) diff --git a/wake/Models/BlindModels.swift b/wake/Models/BlindModels.swift index 76412f5..c3faf16 100644 --- a/wake/Models/BlindModels.swift +++ b/wake/Models/BlindModels.swift @@ -68,19 +68,37 @@ struct BlindCount: Codable { // MARK: - Blind Box Data struct BlindBoxData: Codable { - let id: Int64 + let id: String let boxCode: String - let userId: Int64 + let userId: String let name: String let boxType: String let features: String? - let url: String? + let resultFile: FileInfo? let status: String let workflowInstanceId: String? - // 视频生成时间 let videoGenerateTime: String? let createTime: String - let description: String? + let coverFile: FileInfo? + let description: String + + // 添加计算属性以获取Int64值 + var idValue: Int64 { Int64(id) ?? 0 } + var userIdValue: Int64 { Int64(userId) ?? 0 } + + struct FileInfo: Codable { + let id: String + let fileName: String? + let url: String? + let metadata: [String: String]? + + enum CodingKeys: String, CodingKey { + case id + case fileName = "file_name" + case url + case metadata + } + } enum CodingKeys: String, CodingKey { case id @@ -89,43 +107,68 @@ struct BlindBoxData: Codable { case name case boxType = "box_type" case features - case url + case resultFile = "result_file" case status case workflowInstanceId = "workflow_instance_id" case videoGenerateTime = "video_generate_time" case createTime = "create_time" + case coverFile = "cover_file" case description } - init(id: Int64, boxCode: String, userId: Int64, name: String, boxType: String, features: String?, url: String?, status: String, workflowInstanceId: String?, videoGenerateTime: String?, createTime: String, description: String?) { + init(id: String, boxCode: String, userId: String, name: String, boxType: String, features: String?, resultFile: FileInfo?, status: String, workflowInstanceId: String?, videoGenerateTime: String?, createTime: String, coverFile: FileInfo?, description: String) { self.id = id self.boxCode = boxCode self.userId = userId self.name = name self.boxType = boxType self.features = features - self.url = url + self.resultFile = resultFile self.status = status self.workflowInstanceId = workflowInstanceId self.videoGenerateTime = videoGenerateTime self.createTime = createTime + self.coverFile = coverFile self.description = description } init(from listItem: BlindList) { - self.init( - id: Int64(listItem.id) ?? 0, - boxCode: listItem.boxCode, - userId: Int64(listItem.userId) ?? 0, - name: listItem.name, - boxType: listItem.boxType, - features: listItem.features, - url: listItem.resultFile?.url, - status: listItem.status, - workflowInstanceId: listItem.workflowInstanceId, - videoGenerateTime: listItem.videoGenerateTime, - createTime: listItem.createTime, - description: listItem.description - ) + self.id = listItem.id + self.boxCode = listItem.boxCode + self.userId = listItem.userId + self.name = listItem.name + self.boxType = listItem.boxType + self.features = listItem.features + + // 转换FileInfo类型 + if let resultFileInfo = listItem.resultFile { + self.resultFile = FileInfo( + id: resultFileInfo.id, + fileName: resultFileInfo.fileName, + url: resultFileInfo.url, + metadata: resultFileInfo.metadata + ) + } else { + self.resultFile = nil + } + + self.status = listItem.status + self.workflowInstanceId = listItem.workflowInstanceId + self.videoGenerateTime = listItem.videoGenerateTime + self.createTime = listItem.createTime + + // 转换coverFile的FileInfo类型 + if let coverFileInfo = listItem.coverFile { + self.coverFile = FileInfo( + id: coverFileInfo.id, + fileName: coverFileInfo.fileName, + url: coverFileInfo.url, + metadata: coverFileInfo.metadata + ) + } else { + self.coverFile = nil + } + + self.description = listItem.description ?? "" } } diff --git a/wake/Utils/ApiClient/BlindBoxApi.swift b/wake/Utils/ApiClient/BlindBoxApi.swift index 2d13cb4..7e9ab8b 100644 --- a/wake/Utils/ApiClient/BlindBoxApi.swift +++ b/wake/Utils/ApiClient/BlindBoxApi.swift @@ -14,57 +14,7 @@ import Foundation // MARK: - Generate Blind Box Response Model struct GenerateBlindBoxResponse: Codable { let code: Int - let data: BlindBoxDataWrapper? - - struct BlindBoxDataWrapper: Codable { - let id: String - let boxCode: String - let userId: String - let name: String - let boxType: String - let features: String? - let resultFile: FileInfo? - let status: String - let workflowInstanceId: String? - let videoGenerateTime: String? - let createTime: String - let coverFile: FileInfo? - let description: String - - // 添加计算属性以获取Int64值 - var idValue: Int64 { Int64(id) ?? 0 } - var userIdValue: Int64 { Int64(userId) ?? 0 } - - struct FileInfo: Codable { - let id: String - let fileName: String? - let url: String? - let metadata: [String: String]? - - enum CodingKeys: String, CodingKey { - case id - case fileName = "file_name" - case url - case metadata - } - } - - enum CodingKeys: String, CodingKey { - case id - case boxCode = "box_code" - case userId = "user_id" - case name - case boxType = "box_type" - case features - case resultFile = "result_file" - case status - case workflowInstanceId = "workflow_instance_id" - case videoGenerateTime = "video_generate_time" - case createTime = "create_time" - case coverFile = "cover_file" - case description - } - } + let data: BlindBoxData? } // MARK: - Blind Box API Client @@ -81,7 +31,7 @@ class BlindBoxApi { func generateBlindBox( boxType: String, materialIds: [String], - completion: @escaping (Result) -> Void + completion: @escaping (Result) -> Void ) { // 将Codable结构体转换为字典 let parameters: [String: Any] = [ @@ -108,4 +58,48 @@ class BlindBoxApi { } ) } + + /// 获取盲盒信息 + /// - Parameters: + /// - boxId: 盲盒ID + /// - completion: 完成回调,返回盲盒数据或错误 + func getBlindBox( + boxId: String, + completion: @escaping (Result) -> Void + ) { + let path = "/blind_box/query/\(boxId)" + + NetworkService.shared.getWithToken( + path: path, + completion: { (result: Result) in + DispatchQueue.main.async { + switch result { + case .success(let response): + if response.code == 0 { + completion(.success(response.data)) + } else { + completion(.failure(NetworkError.serverError("服务器返回错误码: \(response.code)"))) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + ) + } + + /// 使用 async/await 获取盲盒信息 + /// - Parameter boxId: 盲盒ID + /// - Returns: 盲盒数据 + @available(iOS 13.0, *) + func getBlindBox(boxId: String) async throws -> BlindBoxData? { + let path = "/blind_box/query/\(boxId)" + let response: GenerateBlindBoxResponse = try await NetworkService.shared.getWithToken(path: path) + + if response.code == 0 { + return response.data + } else { + throw NetworkError.serverError("服务器返回错误码: \(response.code)") + } + } } \ No newline at end of file diff --git a/wake/Utils/NetworkService.swift b/wake/Utils/NetworkService.swift index d4d8d4a..75eb87c 100644 --- a/wake/Utils/NetworkService.swift +++ b/wake/Utils/NetworkService.swift @@ -108,6 +108,43 @@ extension NetworkService: NetworkServiceProtocol { } } +// MARK: - Async/Await Extensions +extension NetworkService { + /// 使用 async/await 的 GET 请求(带Token) + public func getWithToken( + path: String, + parameters: [String: Any]? = nil + ) async throws -> T { + return try await withCheckedThrowingContinuation { continuation in + getWithToken(path: path, parameters: parameters) { (result: Result) in + switch result { + case .success(let value): + continuation.resume(returning: value) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + /// 使用 async/await 的 POST 请求(带Token) + public func postWithToken( + path: String, + parameters: [String: Any] + ) async throws -> T { + return try await withCheckedThrowingContinuation { continuation in + postWithToken(path: path, parameters: parameters) { (result: Result) in + switch result { + case .success(let value): + continuation.resume(returning: value) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } +} + public enum NetworkError: Error { case invalidURL case noData diff --git a/wake/Utils/Router.swift b/wake/Utils/Router.swift index 609229b..a33b5e6 100644 --- a/wake/Utils/Router.swift +++ b/wake/Utils/Router.swift @@ -7,7 +7,7 @@ enum AppRoute: Hashable { case feedbackView case feedbackDetail(type: FeedbackView.FeedbackType) case mediaUpload - case blindBox(mediaType: BlindBoxMediaType) + case blindBox(mediaType: BlindBoxMediaType, blindBoxId: String? = nil) case blindOutcome(media: MediaType, time: String? = nil, description: String? = nil) case memories case subscribe @@ -31,8 +31,8 @@ enum AppRoute: Hashable { FeedbackDetailView(feedbackType: type) case .mediaUpload: MediaUploadView() - case .blindBox(let mediaType): - BlindBoxView(mediaType: mediaType) + case .blindBox(let mediaType, let blindBoxId): + BlindBoxView(mediaType: mediaType, blindBoxId: blindBoxId) case .blindOutcome(let media, let time, let description): BlindOutcomeView(media: media, time: time, description: description) case .memories: diff --git a/wake/View/Blind/ContentView.swift b/wake/View/Blind/ContentView.swift index 20a9558..724ba8e 100644 --- a/wake/View/Blind/ContentView.swift +++ b/wake/View/Blind/ContentView.swift @@ -1,6 +1,7 @@ import SwiftUI import SwiftData import AVKit +import Foundation // 添加通知名称 extension Notification.Name { @@ -69,6 +70,8 @@ struct AVPlayerController: UIViewControllerRepresentable { struct BlindBoxView: View { let mediaType: BlindBoxMediaType + let currentBoxId: String? + @State private var showModal = false // 控制用户资料弹窗显示 @State private var showSettings = false // 控制设置页面显示 @State private var isMember = false // 是否是会员 @@ -78,7 +81,7 @@ struct BlindBoxView: View { @State private var blindCount: BlindCount? = nil @State private var blindList: [BlindList] = [] // Changed to array // 生成盲盒 - @State private var blindGenerate : BlindBoxData? + @State private var blindGenerate: BlindBoxData? @State private var showLottieAnimation = true // 轮询接口 @State private var isPolling = false @@ -106,8 +109,9 @@ struct BlindBoxView: View { // 查询数据 - 简单查询 @Query private var login: [Login] - init(mediaType: BlindBoxMediaType) { + init(mediaType: BlindBoxMediaType, blindBoxId: String? = nil) { self.mediaType = mediaType + self.currentBoxId = blindBoxId } // 倒计时 @@ -143,88 +147,138 @@ struct BlindBoxView: View { } } - private func loadMedia() { + private func loadBlindBox() async { print("loadMedia called with mediaType: \(mediaType)") - switch mediaType { - case .video: - loadVideo() - currentBoxType = "Video" - startPolling() - case .image: - loadImage() - currentBoxType = "Image" - startPolling() - case .all: - print("Loading all content...") - // 检查盲盒列表,如果不存在First/Second盲盒,则跳转到对应的页面重新触发新手引导 - NetworkService.shared.get( - path: "/blind_boxs/query", - parameters: nil - ) { (result: Result, NetworkError>) in - DispatchQueue.main.async { - switch result { - case .success(let response): - if response.data.count == 0 { - // 跳转到新手引导-First盲盒页面 - print("❌ 没有盲盒,跳转到新手引导-First盲盒页面") - // return - } - if response.data.count == 1 && response.data[0].boxType == "First" { - // 跳转到新手引导-Second盲盒页面 - print("❌ 只有First盲盒,跳转到新手引导-Second盲盒页面") - // return - } + if self.currentBoxId != nil { + print("指定监听某盲盒结果: ", self.currentBoxId! as Any) + // 启动轮询查询盲盒状态 + await pollingToQuerySingleBox() + } + + // switch mediaType { + // case .video: + // loadVideo() + // currentBoxType = "Video" + // startPolling() + // case .image: + // loadImage() + // currentBoxType = "Image" + // startPolling() + // case .all: + // print("Loading all content...") + // // 检查盲盒列表,如果不存在First/Second盲盒,则跳转到对应的页面重新触发新手引导 + // // 注意:这部分代码仍使用传统的闭包方式,因为NetworkService.shared.get不支持async/await + // NetworkService.shared.get( + // path: "/blind_boxs/query", + // parameters: nil + // ) { (result: Result, NetworkError>) in + // DispatchQueue.main.async { + // switch result { + // case .success(let response): + // if response.data.count == 0 { + // // 跳转到新手引导-First盲盒页面 + // print("❌ 没有盲盒,跳转到新手引导-First盲盒页面") + // // return + // } + // if response.data.count == 1 && response.data[0].boxType == "First" { + // // 跳转到新手引导-Second盲盒页面 + // print("❌ 只有First盲盒,跳转到新手引导-Second盲盒页面") + // // return + // } - self.blindList = response.data ?? [] - // 如果列表为空数组 设置盲盒状态为none - if self.blindList.isEmpty { - self.animationPhase = .none - } - print("✅ 成功获取 \(self.blindList.count) 个盲盒") - case .failure(let error): - self.blindList = [] - self.animationPhase = .none - print("❌ 获取盲盒列表失败:", error.localizedDescription) - } - } - } + // self.blindList = response.data ?? [] + // // 如果列表为空数组 设置盲盒状态为none + // if self.blindList.isEmpty { + // self.animationPhase = .none + // } + // print("✅ 成功获取 \(self.blindList.count) 个盲盒") + // case .failure(let error): + // self.blindList = [] + // self.animationPhase = .none + // print("❌ 获取盲盒列表失败:", error.localizedDescription) + // } + // } + // } - // 会员信息 - NetworkService.shared.get( - path: "/membership/personal-center-info", - parameters: nil - ) { (result: Result) in - DispatchQueue.main.async { - switch result { - case .success(let response): - self.memberProfile = response.data - self.isMember = response.data.membershipLevel == "Pioneer" - self.memberDate = response.data.membershipEndAt ?? "" - print("✅ 成功获取会员信息:", response.data) - print("✅ 用户ID:", response.data.userInfo.userId) - case .failure(let error): - print("❌ 获取会员信息失败:", error) - } - } - } - // 盲盒数量 - NetworkService.shared.get( - path: "/blind_box/available/quantity", - parameters: nil - ) { (result: Result, NetworkError>) in - DispatchQueue.main.async { - switch result { - case .success(let response): - self.blindCount = response.data - print("✅ 成功获取盲盒数量:", response.data) - case .failure(let error): - print("❌ 获取数量失败:", error) - } - } - } - } + // // // 会员信息 + // // NetworkService.shared.get( + // // path: "/membership/personal-center-info", + // // parameters: nil + // // ) { (result: Result) in + // // DispatchQueue.main.async { + // // switch result { + // // case .success(let response): + // // self.memberProfile = response.data + // // self.isMember = response.data.membershipLevel == "Pioneer" + // // self.memberDate = response.data.membershipEndAt ?? "" + // // print("✅ 成功获取会员信息:", response.data) + // // print("✅ 用户ID:", response.data.userInfo.userId) + // // case .failure(let error): + // // print("❌ 获取会员信息失败:", error) + // // } + // // } + // // } + // // // 盲盒数量 + // // NetworkService.shared.get( + // // path: "/blind_box/available/quantity", + // // parameters: nil + // // ) { (result: Result, NetworkError>) in + // // DispatchQueue.main.async { + // // switch result { + // // case .success(let response): + // // self.blindCount = response.data + // // print("✅ 成功获取盲盒数量:", response.data) + // // case .failure(let error): + // // print("❌ 获取数量失败:", error) + // // } + // // } + // // } + // } } + + private func pollingToQuerySingleBox() async { + stopPolling() + isPolling = true + + // 轮询查询盲盒状态,直到状态为Unopened + while isPolling { + do { + let blindBoxData = try await BlindBoxApi.shared.getBlindBox(boxId: self.currentBoxId!) + + // 更新UI + if let data = blindBoxData { + self.blindGenerate = data + + // 根据盲盒类型设置媒体URL + if mediaType == .video { + self.videoURL = data.resultFile?.url ?? "" + } else if mediaType == .image { + self.imageURL = data.resultFile?.url ?? "" + } + + print("✅ 成功获取盲盒数据: \(data.name), 状态: \(data.status)") + + // 检查状态是否为Unopened,如果是则停止轮询 + if data.status == "Unopened" { + print("✅ 盲盒已准备就绪,停止轮询") + stopPolling() + break + } + } + + // 等待2秒后继续轮询 + try await Task.sleep(nanoseconds: 2_000_000_000) + } catch { + print("❌ 获取盲盒数据失败: \(error)") + // 处理错误情况 + self.animationPhase = .none + stopPolling() + break + } + } + } + // 轮询接口 private func startPolling() { stopPolling() @@ -244,52 +298,54 @@ struct BlindBoxView: View { return } - NetworkService.shared.postWithToken( - path: "/blind_box/generate/mock", - parameters: ["box_type": currentBoxType] - ) { (result: Result, NetworkError>) in - DispatchQueue.main.async { - switch result { - case .success(let response): - let data = response.data - self.blindGenerate = data - print("当前盲盒状态: \(data.status)") - // 更新显示数据 - if self.mediaType == .all, let firstItem = self.blindList.first { - self.displayData = BlindBoxData(from: firstItem) - } else { - self.displayData = data - } - - // 发送状态变更通知 - NotificationCenter.default.post( - name: .blindBoxStatusChanged, - object: nil, - userInfo: ["status": data.status] - ) - - if data.status != "Preparing" { - self.stopPolling() - print("✅ 盲盒准备就绪,状态: \(data.status)") - if self.mediaType == .video { - self.videoURL = data.url ?? "" - } else if self.mediaType == .image { - self.imageURL = data.url ?? "" - } - } else { - self.pollingTimer = Timer.scheduledTimer( - withTimeInterval: 2.0, - repeats: false - ) { _ in - self.checkBlindBoxStatus() - } - } - case .failure(let error): - print("❌ 获取盲盒状态失败: \(error.localizedDescription)") - self.stopPolling() - } - } - } +// NetworkService.shared.postWithToken( +// path: "/blind_box/generate/mock", +// parameters: ["box_type": currentBoxType] +// ) { (result: Result) in +// DispatchQueue.main.async { +// switch result { +// case .success(let response): +// let data = response.data +// self.blindGenerate = data +// print("当前盲盒状态: \(data?.status ?? "Unknown")") +// // 更新显示数据 +// if self.mediaType == .all, let firstItem = self.blindList.first { +// self.displayData = BlindBoxData(from: firstItem) +// } else { +// self.displayData = data +// } +// +// // 发送状态变更通知 +// if let status = data?.status { +// NotificationCenter.default.post( +// name: .blindBoxStatusChanged, +// object: nil, +// userInfo: ["status": status] +// ) +// } +// +// if data?.status != "Preparing" { +// self.stopPolling() +// print("✅ 盲盒准备就绪,状态: \(data?.status ?? "Unknown")") +// if self.mediaType == .video { +// self.videoURL = data?.resultFile?.url ?? "" +// } else if self.mediaType == .image { +// self.imageURL = data?.resultFile?.url ?? "" +// } +// } else { +// self.pollingTimer = Timer.scheduledTimer( +// withTimeInterval: 2.0, +// repeats: false +// ) { _ in +// self.checkBlindBoxStatus() +// } +// } +// case .failure(let error): +// print("❌ 获取盲盒状态失败: \(error.localizedDescription)") +// self.stopPolling() +// } +// } +// } } private func loadImage() { @@ -409,40 +465,45 @@ struct BlindBoxView: View { .onAppear { print("🎯 BlindBoxView appeared with mediaType: \(mediaType)") print("🎯 Current thread: \(Thread.current)") + + + // 初始化显示数据 - if mediaType == .all, let firstItem = blindList.first { - displayData = BlindBoxData(from: firstItem) - } else { - displayData = blindGenerate - } + // if mediaType == .all, let firstItem = blindList.first { + // displayData = BlindBoxData(from: firstItem) + // } else { + // displayData = blindGenerate + // } // 添加盲盒状态变化监听 - NotificationCenter.default.addObserver( - forName: .blindBoxStatusChanged, - object: nil, - queue: .main - ) { notification in - if let status = notification.userInfo?["status"] as? String { - switch status { - case "Preparing": - withAnimation { - self.animationPhase = .loading - } - case "Unopened": - withAnimation { - self.animationPhase = .ready - } - default: - // 其他状态不处理 - withAnimation { - self.animationPhase = .ready - } - break - } - } - } + // NotificationCenter.default.addObserver( + // forName: .blindBoxStatusChanged, + // object: nil, + // queue: .main + // ) { notification in + // if let status = notification.userInfo?["status"] as? String { + // switch status { + // case "Preparing": + // withAnimation { + // self.animationPhase = .loading + // } + // case "Unopened": + // withAnimation { + // self.animationPhase = .ready + // } + // default: + // // 其他状态不处理 + // withAnimation { + // self.animationPhase = .ready + // } + // break + // } + // } + // } // 调用接口 - loadMedia() + Task { + await loadBlindBox() + } } .onDisappear { stopPolling() @@ -722,10 +783,10 @@ struct BlindBoxView: View { if !showScalingOverlay && !showMedia { VStack(alignment: .leading, spacing: 8) { // 从变量blindGenerate中获取description - Text(blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn") + Text(blindGenerate?.name ?? "Some box") .font(Typography.font(for: .body, family: .quicksandBold)) .foregroundColor(Color.themeTextMessageMain) - Text(blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation") + Text(blindGenerate?.description ?? "") .font(.system(size: 14)) .foregroundColor(Color.themeTextMessageMain) } @@ -876,6 +937,21 @@ struct BlindBoxView: View { } } +// 预览第一个盲盒 +#Preview("First Blind Box") { + BlindBoxView(mediaType: .image, blindBoxId: "7370140297747107840") + .onAppear { + // 仅在Preview中设置模拟令牌(不要在生产代码中使用) + #if DEBUG + if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { + // 设置模拟令牌用于Preview + let previewToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJqdGkiOjczNzAwMTY5NzMzODE1NzA1NjAsImlkZW50aXR5IjoiNzM1MDQzOTY2MzExNjYxOTc3NyIsImV4cCI6MTc1Nzc1Mzc3NH0.tZ8p5sW4KX6HFoJpJN0e4VmJOAGhTrYD2yTwQwilKpufzqOAfXX4vpGYBurgBIcHj2KmXKX2PQMOeeAtvAypDA" + let _ = KeychainHelper.saveAccessToken(previewToken) + print("🔑 Preview token set for testing") + } + #endif + } +} // struct TransparentVideoPlayer: UIViewRepresentable { // func makeUIView(context: Context) -> UIView { // let view = UIView() diff --git a/wake/View/OnBoarding/UserInfo.swift b/wake/View/OnBoarding/UserInfo.swift index c864d11..18d1202 100644 --- a/wake/View/OnBoarding/UserInfo.swift +++ b/wake/View/OnBoarding/UserInfo.swift @@ -198,7 +198,7 @@ struct UserInfo: View { case .success(let blindBoxData): print("✅ 盲盒生成成功: \(blindBoxData?.id ?? "0")") // 导航到首页盲盒等待用户开启第一个盲盒 - Router.shared.navigate(to: .blindBox(mediaType: .image)) + Router.shared.navigate(to: .blindBox(mediaType: .image, blindBoxId: blindBoxData?.id ?? "0")) case .failure(let error): print("❌ 盲盒生成失败: \(error.localizedDescription)") } -- 2.47.2 From 1e3ec86377c93798b604737d7f05a22c56dbba28 Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sun, 7 Sep 2025 14:08:18 +0800 Subject: [PATCH 10/18] =?UTF-8?q?feat:=20=E5=8A=A8=E7=94=BB=E8=BF=87?= =?UTF-8?q?=E6=B8=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/View/Blind/ContentView.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/wake/View/Blind/ContentView.swift b/wake/View/Blind/ContentView.swift index 724ba8e..539374b 100644 --- a/wake/View/Blind/ContentView.swift +++ b/wake/View/Blind/ContentView.swift @@ -262,6 +262,7 @@ struct BlindBoxView: View { // 检查状态是否为Unopened,如果是则停止轮询 if data.status == "Unopened" { print("✅ 盲盒已准备就绪,停止轮询") + self.animationPhase = .ready stopPolling() break } @@ -558,9 +559,9 @@ struct BlindBoxView: View { Button(action: { // 导航到BlindOutcomeView if mediaType == .video, !videoURL.isEmpty, let url = URL(string: videoURL) { - Router.shared.navigate(to: .blindOutcome(media: .video(url, nil), time: blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn", description:blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation")) + Router.shared.navigate(to: .blindOutcome(media: .video(url, nil), time: blindGenerate?.name ?? "Your box", description:blindGenerate?.description ?? "")) } else if mediaType == .image, let image = displayImage { - Router.shared.navigate(to: .blindOutcome(media: .image(image), time: blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn", description:blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation")) + Router.shared.navigate(to: .blindOutcome(media: .image(image), time: blindGenerate?.name ?? "Your box", description:blindGenerate?.description ?? "")) } }) { Image(systemName: "chevron.left") @@ -656,10 +657,11 @@ struct BlindBoxView: View { .padding(.horizontal) .padding(.top, 20) } + // 标题 VStack(alignment: .leading, spacing: 4) { Text("Hi! Click And") - Text("Open Your First Box~") + Text("Open Your Box~") } .font(Typography.font(for: .smallLargeTitle)) .fontWeight(.bold) @@ -804,7 +806,8 @@ struct BlindBoxView: View { .animation(.easeOut(duration: 1.5), value: showScalingOverlay) .offset(y: showScalingOverlay ? -100 : 0) .animation(.easeInOut(duration: 1.5), value: showScalingOverlay) - // 打开 + + // 打开 TODO 引导时,也要有按钮 if mediaType == .all { Button(action: { if animationPhase == .ready { @@ -856,6 +859,7 @@ struct BlindBoxView: View { .animation(.spring(response: 0.6, dampingFraction: 0.8), value: showModal) .edgesIgnoringSafeArea(.all) } + // 用户资料弹窗 SlideInModal( isPresented: $showModal, -- 2.47.2 From 7467789bf72bb3a3bbe2e92b2ea345d7dd5da4a1 Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sun, 7 Sep 2025 16:15:05 +0800 Subject: [PATCH 11/18] fix: router --- .vscode/settings.json | 3 ++- wake/WakeApp.swift | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 17dc782..a23185a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "lldb.library": "/Library/Developer/CommandLineTools/Library/PrivateFrameworks/LLDB.framework/Versions/A/LLDB", - "lldb.launch.expressions": "native" + "lldb.launch.expressions": "native", + "sweetpad.build.xcodeWorkspacePath": "wake.xcodeproj/project.xcworkspace" } \ No newline at end of file diff --git a/wake/WakeApp.swift b/wake/WakeApp.swift index 0db40b1..e9dbbae 100644 --- a/wake/WakeApp.swift +++ b/wake/WakeApp.swift @@ -51,6 +51,9 @@ struct WakeApp: App { // route.view // } UserInfo() + .navigationDestination(for: AppRoute.self) { route in + route.view + } } } else { // 未登录:显示登录界面 -- 2.47.2 From 3dc301d6c77edc1b40e9fd9b4230e6e6ff5dcc92 Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sun, 7 Sep 2025 17:25:56 +0800 Subject: [PATCH 12/18] =?UTF-8?q?feat:=20=E7=AC=AC=E4=BA=8C=E4=B8=AA?= =?UTF-8?q?=E7=9B=B2=E7=9B=92=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/View/OnBoarding/SecondBoxUpload.swift | 0 wake/View/Upload/MediaUploadView.swift | 719 --------------------- 2 files changed, 719 deletions(-) delete mode 100644 wake/View/OnBoarding/SecondBoxUpload.swift delete mode 100644 wake/View/Upload/MediaUploadView.swift diff --git a/wake/View/OnBoarding/SecondBoxUpload.swift b/wake/View/OnBoarding/SecondBoxUpload.swift deleted file mode 100644 index e69de29..0000000 diff --git a/wake/View/Upload/MediaUploadView.swift b/wake/View/Upload/MediaUploadView.swift deleted file mode 100644 index ab0250c..0000000 --- a/wake/View/Upload/MediaUploadView.swift +++ /dev/null @@ -1,719 +0,0 @@ -import SwiftUI -import PhotosUI -import AVKit -import CoreTransferable -import CoreImage.CIFilterBuiltins - -extension Notification.Name { - static let didAddFirstMedia = Notification.Name("didAddFirstMedia") -} -/// 主上传视图 -/// 提供媒体选择、预览和上传功能 -@MainActor -struct MediaUploadView: View { - // MARK: - 属性 - - /// 上传管理器,负责处理上传逻辑 - @StateObject private var uploadManager = MediaUploadManager() - /// 控制媒体选择器的显示/隐藏 - @State private var showMediaPicker = false - /// 当前选中的媒体项 - @State private var selectedMedia: MediaType? = nil - /// 当前选中的媒体索引集合 - @State private var selectedIndices: Set = [] - @State private var mediaPickerSelection: [MediaType] = [] // 添加这个状态变量 - /// 上传完成状态 - @State private var uploadComplete = false - /// 上传完成的文件ID列表 - @State private var uploadedFileIds: [[String: String]] = [] - - // MARK: - 视图主体 - - var body: some View { - VStack(spacing: 0) { - // 顶部导航栏 - topNavigationBar - - // 上传提示信息 - uploadHintView - Spacer() - .frame(height: uploadManager.selectedMedia.isEmpty ? 80 : 40) - // 主上传区域 - MainUploadArea( - uploadManager: uploadManager, - showMediaPicker: $showMediaPicker, - selectedMedia: $selectedMedia - ) - .id("mainUploadArea\(uploadManager.selectedMedia.count)") - - Spacer() - - // // 上传结果展示 - // if uploadComplete && !uploadedFileIds.isEmpty { - // VStack(alignment: .leading) { - // Text("上传完成!") - // .font(.headline) - - // ScrollView { - // ForEach(Array(uploadedFileIds.enumerated()), id: \.offset) { index, fileInfo in - // VStack(alignment: .leading) { - // Text("文件 \(index + 1):") - // .font(.subheadline) - // Text("ID: \(fileInfo["file_id"] ?? "")") - // .font(.caption) - // .foregroundColor(.gray) - // } - // .padding() - // .frame(maxWidth: .infinity, alignment: .leading) - // .background(Color.gray.opacity(0.1)) - // .cornerRadius(8) - // } - // } - // .frame(height: 200) - // } - // .padding() - // } - - // 继续按钮 - continueButton - .padding(.bottom, 24) - } - .background(Color.themeTextWhiteSecondary) - .navigationBarTitleDisplayMode(.inline) - .navigationBarBackButtonHidden(true) - .sheet(isPresented: $showMediaPicker) { - // 媒体选择器 - mediaPickerView - } - .onChange(of: uploadManager.uploadResults) { newResults in - handleUploadCompletion(results: newResults) - } - } - - // MARK: - 子视图 - - /// 顶部导航栏 - private var topNavigationBar: some View { - HStack { - // 返回按钮 - Button(action: { Router.shared.pop() }) { - Image(systemName: "chevron.left") - .font(.system(size: 17, weight: .semibold)) - .foregroundColor(.themeTextMessageMain) - } - .padding(.leading, 16) - - Spacer() - - // 标题 - Text("Complete Your Profile") - .font(Typography.font(for: .title2, family: .quicksandBold)) - .foregroundColor(.themeTextMessageMain) - - Spacer() - - // 右侧占位视图(保持布局平衡) - Color.clear - .frame(width: 24, height: 24) - .padding(.trailing, 16) - } - .background(Color.themeTextWhiteSecondary) - // .padding(.horizontal) - .zIndex(1) // 确保导航栏显示在最上层 - } - - /// 上传提示视图 - private var uploadHintView: some View { - HStack (spacing: 6) { - SVGImage(svgName: "Tips") - .frame(width: 16, height: 16) - .padding(.leading,6) - Text("The upload process will take approximately 2 minutes. Thank you for your patience.") - .font(.caption) - .foregroundColor(.black) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(3) - } - .background( - Color.themeTextWhite - .cornerRadius(6) - ) - .padding(.vertical, 8) - .padding(.horizontal) - } - - /// 继续按钮 - private var continueButton: some View { - Button(action: handleContinue) { - Text("Continue") - .font(.headline) - .foregroundColor(uploadManager.selectedMedia.isEmpty ? Color.themeTextMessage : Color.themeTextMessageMain) - .frame(maxWidth: .infinity) - .frame(height: 56) - .background(uploadManager.selectedMedia.isEmpty ? Color.white : Color.themePrimary) - .cornerRadius(28) - .padding(.horizontal, 24) - } - .buttonStyle(PlainButtonStyle()) - .disabled(uploadManager.selectedMedia.isEmpty) - } - - /// 媒体选择器视图 - private var mediaPickerView: some View { - MediaPicker( - selectedMedia: Binding( - get: { mediaPickerSelection }, - set: { newSelections in - print("🔄 开始处理用户选择的媒体文件") - print("📌 新选择的媒体数量: \(newSelections.count)") - - // 1. 去重处理:过滤掉已经存在的媒体项 - var uniqueNewMedia: [MediaType] = [] - - for newItem in newSelections { - let isDuplicate = uploadManager.selectedMedia.contains { existingItem in - switch (existingItem, newItem) { - case (.image(let existingImage), .image(let newImage)): - return existingImage.pngData() == newImage.pngData() - case (.video(let existingURL, _), .video(let newURL, _)): - return existingURL == newURL - default: - return false - } - } - - if !isDuplicate { - uniqueNewMedia.append(newItem) - } else { - print("⚠️ 检测到重复文件,已跳过: \(newItem)") - } - } - - // 2. 添加新文件 - if !uniqueNewMedia.isEmpty { - print("✅ 添加 \(uniqueNewMedia.count) 个新文件") - uploadManager.addMedia(uniqueNewMedia) - - // 如果没有当前选中的媒体,则选择第一个新添加的 - if selectedMedia == nil, let firstNewItem = uniqueNewMedia.first { - selectedMedia = firstNewItem - } - - // 开始上传 - uploadManager.startUpload() - } else { - print("ℹ️ 没有新文件需要添加,所有选择的文件都已存在") - } - } - ), - imageSelectionLimit: max(0, 20 - uploadManager.selectedMedia.filter { - if case .image = $0 { return true } - return false - }.count), - videoSelectionLimit: max(0, 5 - uploadManager.selectedMedia.filter { - if case .video = $0 { return true } - return false - }.count), - selectionMode: .multiple, - onDismiss: handleMediaPickerDismiss, - onUploadProgress: { index, progress in - print("文件 \(index) 上传进度: \(progress * 100)%") - } - ) - .onAppear { - // 重置选择状态当选择器出现时 - mediaPickerSelection = [] - } - } - - // MARK: - 私有方法 - - /// 处理媒体选择器关闭事件 - private func handleMediaPickerDismiss() { - showMediaPicker = false - print("媒体选择器关闭 - 开始处理") - - // 如果有选中的媒体,开始上传 - if !uploadManager.selectedMedia.isEmpty { - // 不需要在这里开始上传,因为handleMediaChange会处理 - } - } - - /// 处理媒体变化 - /// - Parameters: - /// - newMedia: 新的媒体数组 - /// - oldMedia: 旧的媒体数组 - private func handleMediaChange(_ newMedia: [MediaType], oldMedia: [MediaType]) { - print("开始处理媒体变化,新数量: \(newMedia.count), 原数量: \(oldMedia.count)") - - // 如果没有变化,直接返回 - guard newMedia != oldMedia else { - print("媒体未发生变化,跳过处理") - return - } - - // 在后台线程处理媒体变化 - DispatchQueue.global(qos: .userInitiated).async { [self] in - // 找出新增的媒体(在newMedia中但不在oldMedia中的项) - let newItems = newMedia.filter { newItem in - !oldMedia.contains { $0.id == newItem.id } - } - - print("检测到\(newItems.count)个新增媒体项") - - // 如果有新增媒体 - if !newItems.isEmpty { - print("准备添加\(newItems.count)个新项...") - - // 在主线程更新UI - DispatchQueue.main.async { [self] in - // 创建新的数组,包含原有媒体和新媒体 - var updatedMedia = uploadManager.selectedMedia - updatedMedia.append(contentsOf: newItems) - - // 更新选中的媒体 - uploadManager.clearAllMedia() - uploadManager.addMedia(updatedMedia) - - // 如果当前没有选中的媒体,则选中第一个新增的媒体 - if selectedIndices.isEmpty && !newItems.isEmpty { - selectedIndices = [oldMedia.count] // 选择第一个新增项的索引 - selectedMedia = newItems.first - } - - // 开始上传新添加的媒体 - uploadManager.startUpload() - print("媒体添加完成,总数量: \(uploadManager.selectedMedia.count)") - } - } - } - } - - /// 检查是否有正在上传的文件 - /// - Returns: 是否正在上传 - private func isUploading() -> Bool { - return uploadManager.uploadStatus.values.contains { status in - if case .uploading = status { return true } - return false - } - } - - /// 处理上传完成 - private func handleUploadCompletion(results: [String: MediaUploadManager.UploadResult]) { - // 转换为需要的格式 - let formattedResults = results.map { (_, result) -> [String: String] in - return [ - "file_id": result.fileId, - "preview_file_id": result.thumbnailId ?? result.fileId - ] - } - - uploadedFileIds = formattedResults - uploadComplete = !uploadedFileIds.isEmpty - } - - /// 处理继续按钮点击 - private func handleContinue() { - // 获取所有已上传文件的结果 - let uploadResults = uploadManager.uploadResults - guard !uploadResults.isEmpty else { - print("⚠️ 没有可用的文件ID") - return - } - - // 准备请求参数 - let files = uploadResults.map { (_, result) -> [String: String] in - return [ - "file_id": result.fileId, - "preview_file_id": result.thumbnailId ?? result.fileId - ] - } - - // 发送POST请求到/material接口 - NetworkService.shared.postWithToken( - path: "/material", - parameters: files - ) { (result: Result) in - switch result { - case .success: - print("✅ 素材提交成功") - // 跳转到盲盒页面 - DispatchQueue.main.async { - Router.shared.navigate(to: .blindBox(mediaType: .video)) - } - case .failure(let error): - print("❌ 素材提交失败: \(error.localizedDescription)") - // 这里可以添加错误处理逻辑,比如显示错误提示 - } - } - } -} - -// MARK: - 主上传区域 - -/// 主上传区域视图 -/// 显示上传提示、媒体预览和添加更多按钮 -struct MainUploadArea: View { - // MARK: - 属性 - - /// 上传管理器 - @ObservedObject var uploadManager: MediaUploadManager - /// 控制媒体选择器的显示/隐藏 - @Binding var showMediaPicker: Bool - /// 当前选中的媒体 - @Binding var selectedMedia: MediaType? - - // MARK: - 视图主体 - - var body: some View { - VStack() { - Spacer() - .frame(height: 30) - // 标题 - Text("Click to upload 20 images and 5 videos to generate your next blind box.") - .font(Typography.font(for: .title2, family: .quicksandBold)) - .fontWeight(.bold) - .foregroundColor(.black) - .multilineTextAlignment(.center) - .padding(.horizontal) - Spacer() - .frame(height: 50) - // 主显示区域 - if let mediaToDisplay = selectedMedia ?? uploadManager.selectedMedia.first { - Button(action: { showMediaPicker = true }) { - MediaPreview(media: mediaToDisplay) - .id(mediaToDisplay.id) - .frame(width: 225, height: 225) - .overlay( - RoundedRectangle(cornerRadius: 16) - .stroke(Color.themePrimary, lineWidth: 5) - ) - .cornerRadius(16) - .padding(.horizontal) - .transition(.opacity) - } - } else { - UploadPromptView(showMediaPicker: $showMediaPicker) - } - // 媒体预览区域 - mediaPreviewSection - Spacer() - .frame(height: 10) - } - .onAppear { - print("MainUploadArea appeared") - print("Selected media count: \(uploadManager.selectedMedia.count)") - - if selectedMedia == nil, let firstMedia = uploadManager.selectedMedia.first { - print("Selecting first media: \(firstMedia.id)") - selectedMedia = firstMedia - } - } - .onReceive(NotificationCenter.default.publisher(for: .didAddFirstMedia)) { notification in - if let media = notification.userInfo?["media"] as? MediaType, selectedMedia == nil { - selectedMedia = media - } - } - .background(Color.white) - .cornerRadius(18) - .animation(.default, value: selectedMedia?.id) - } - - // MARK: - 子视图 - - /// 媒体预览区域 - private var mediaPreviewSection: some View { - Group { - if !uploadManager.selectedMedia.isEmpty { - VStack(spacing: 4) { - // 横向滚动的缩略图列表 - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack(spacing: 10) { - ForEach(Array(uploadManager.selectedMedia.enumerated()), id: \.element.id) { index, media in - mediaItemView(for: media, at: index) - } - // 当没有选择媒体时显示添加更多按钮 - if !uploadManager.selectedMedia.isEmpty { - addMoreButton - } - } - .padding(.horizontal) - } - .frame(height: 70) - } - .padding(.top, 10) - } - } - } - - /// 单个媒体项视图 - /// - Parameters: - /// - media: 媒体项 - /// - index: 索引 - /// - Returns: 媒体项视图 - private func mediaItemView(for media: MediaType, at index: Int) -> some View { - ZStack(alignment: .topTrailing) { - // 媒体预览 - 始终使用本地资源 - MediaPreview(media: media) - .frame(width: 58, height: 58) - .cornerRadius(8) - .shadow(radius: 1) - .overlay( - // 左上角序号 - ZStack(alignment: .topLeading) { - Path { path in - let radius: CGFloat = 4 - let width: CGFloat = 14 - let height: CGFloat = 10 - - // 从左上角开始(带圆角) - path.move(to: CGPoint(x: 0, y: radius)) - path.addQuadCurve(to: CGPoint(x: radius, y: 0), - control: CGPoint(x: 0, y: 0)) - - // 上边缘(右上角保持直角) - path.addLine(to: CGPoint(x: width, y: 0)) - - // 右边缘(右下角保持直角) - path.addLine(to: CGPoint(x: width, y: height - radius)) - - // 右下角圆角 - path.addQuadCurve(to: CGPoint(x: width - radius, y: height), - control: CGPoint(x: width, y: height)) - - // 下边缘(左下角保持直角) - path.addLine(to: CGPoint(x: 0, y: height)) - - // 闭合路径 - path.closeSubpath() - } - .fill(Color(hex: "BEBEBE").opacity(0.6)) - .frame(width: 14, height: 10) - .overlay( - Text("\(index + 1)") - .font(.system(size: 8, weight: .bold)) - .foregroundColor(.black) - .frame(width: 14, height: 10) - .offset(y: -1), - alignment: .topLeading - ) - .padding([.top, .leading], 2) - - // 右下角视频时长 - if case .video(let url, _) = media, let videoURL = url as? URL { - VStack { - Spacer() - HStack { - Spacer() - Text(getVideoDuration(url: videoURL)) - .font(.system(size: 8, weight: .bold)) - .foregroundColor(.black) - .padding(.horizontal, 4) - .frame(height: 10) - .background(Color(hex: "BEBEBE").opacity(0.6)) - .cornerRadius(2) - } - .padding([.trailing, .bottom], 0) - } - }else{ - // 占位 - VStack { - Spacer() - HStack { - Spacer() - Text("占位") - .font(.system(size: 8, weight: .bold)) - .foregroundColor(.black) - .padding(.horizontal, 4) - .frame(height: 10) - .background(Color(hex: "BEBEBE").opacity(0.6)) - .cornerRadius(2) - } - .padding([.trailing, .bottom], 0) - } - .opacity(0) - } - }, - alignment: .topLeading - ) - .onTapGesture { - print("点击了媒体项,索引: \(index)") - withAnimation { - selectedMedia = media - } - } - .contentShape(Rectangle()) - - // 右上角关闭按钮 - Button(action: { - uploadManager.removeMedia(id: media.id) - if selectedMedia == media { - selectedMedia = nil - } - }) { - Image(systemName: "xmark") - .font(.system(size: 8, weight: .bold)) - .foregroundColor(.black) - .frame(width: 12, height: 12) - .background( - Circle() - .fill(Color(hex: "BEBEBE").opacity(0.6)) - .frame(width: 12, height: 12) - ) - } - .offset(x: 6, y: -6) - } - .padding(.horizontal, 4) - .contentShape(Rectangle()) - } - - /// 添加更多按钮 - private var addMoreButton: some View { - Button(action: { showMediaPicker = true }) { - Image(systemName: "plus") - .font(.system(size: 8, weight: .bold)) - .foregroundColor(.black) - .frame(width: 58, height: 58) - .background(Color.white) - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .strokeBorder(style: StrokeStyle( - lineWidth: 2, - dash: [4, 4] - )) - .foregroundColor(Color.themePrimary) - ) - } - } -} - -// MARK: - 上传提示视图 - -/// 上传提示视图 -/// 显示上传区域的占位图和提示 -struct UploadPromptView: View { - /// 控制媒体选择器的显示/隐藏 - @Binding var showMediaPicker: Bool - - var body: some View { - Button(action: { showMediaPicker = true }) { - // 上传图标 - SVGImageHtml(svgName: "IP") - .frame(width: 225, height: 225) - .contentShape(Rectangle()) - .overlay( - ZStack { - RoundedRectangle(cornerRadius: 20) - .stroke(style: StrokeStyle( - lineWidth: 5, - lineCap: .round, - dash: [12, 8] - )) - .foregroundColor(Color.themePrimary) - - // Add plus icon in the center - Image(systemName: "plus") - .font(.system(size: 32, weight: .bold)) - .foregroundColor(.black) - } - ) - } - } -} - -// MARK: - 媒体预览视图 - -/// 媒体预览视图 -/// 显示图片或视频的预览图,始终使用本地资源 -struct MediaPreview: View { - // MARK: - 属性 - - /// 媒体类型 - let media: MediaType - - // MARK: - 计算属性 - - /// 获取要显示的图片 - private var displayImage: UIImage? { - switch media { - case .image(let uiImage): - return uiImage - case .video(_, let thumbnail): - return thumbnail - } - } - - // MARK: - 视图主体 - - var body: some View { - ZStack { - // 1. 显示图片或视频缩略图 - if let image = displayImage { - Image(uiImage: image) - .resizable() - .scaledToFill() - .transition(.opacity.animation(.easeInOut(duration: 0.2))) - } else { - // 2. 加载中的占位图 - Color.gray.opacity(0.1) - } - } - .aspectRatio(1, contentMode: .fill) - .clipped() - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.themePrimary.opacity(0.3), lineWidth: 1) - ) - } -} - -private func getVideoDuration(url: URL) -> String { - let asset = AVURLAsset(url: url) - let durationInSeconds = CMTimeGetSeconds(asset.duration) - guard durationInSeconds.isFinite else { return "0:00" } - - let minutes = Int(durationInSeconds) / 60 - let seconds = Int(durationInSeconds) % 60 - return String(format: "%d:%02d", minutes, seconds) -} - -// MARK: - Response Types - -private struct EmptyResponse: Decodable { - // Empty response type for endpoints that don't return data -} - -// MARK: - 扩展 - -/// 扩展 MediaType 以支持 Identifiable 协议 -extension MediaType: Identifiable { - /// 唯一标识符 - public var id: String { - switch self { - case .image(let uiImage): - return "image_\(uiImage.hashValue)" - case .video(let url, _): - return "video_\(url.absoluteString.hashValue)" - } -} -} - -extension TimeInterval { - var formattedDuration: String { - let minutes = Int(self) / 60 - let seconds = Int(self) % 60 - return String(format: "%d:%02d", minutes, seconds) - } -} - -// MARK: - 预览 - -struct MediaUploadView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - MediaUploadView() - } - } -} -- 2.47.2 From 9c9404785f5b74ba5f2bdc5fa0114d8281a50922 Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sun, 7 Sep 2025 17:26:14 +0800 Subject: [PATCH 13/18] =?UTF-8?q?feat:=20=E7=AC=AC=E4=BA=8C=E4=B8=AA?= =?UTF-8?q?=E7=9B=B2=E7=9B=92=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/Utils/ApiClient/BlindBoxApi.swift | 22 + .../{ => ApiClient}/MaterialUpload.swift | 46 ++ wake/Utils/NetworkService.swift | 36 + wake/View/OnBoarding/MediaUploadView.swift | 722 ++++++++++++++++++ 4 files changed, 826 insertions(+) rename wake/Utils/{ => ApiClient}/MaterialUpload.swift (57%) create mode 100644 wake/View/OnBoarding/MediaUploadView.swift diff --git a/wake/Utils/ApiClient/BlindBoxApi.swift b/wake/Utils/ApiClient/BlindBoxApi.swift index 7e9ab8b..3a7d846 100644 --- a/wake/Utils/ApiClient/BlindBoxApi.swift +++ b/wake/Utils/ApiClient/BlindBoxApi.swift @@ -59,6 +59,28 @@ class BlindBoxApi { ) } + /// 使用 async/await 生成盲盒 + /// - Parameters: + /// - boxType: 盲盒类型 (如 "First") + /// - materialIds: 素材ID数组 + /// - Returns: 盲盒数据 + @available(iOS 13.0, *) + func generateBlindBox(boxType: String, materialIds: [String]) async throws -> BlindBoxData? { + let parameters: [String: Any] = [ + "box_type": boxType, + "material_ids": materialIds + ] + let response: GenerateBlindBoxResponse = try await NetworkService.shared.postWithToken( + path: "/blind_box/generate", + parameters: parameters + ) + if response.code == 0 { + return response.data + } else { + throw NetworkError.serverError("服务器返回错误码: \(response.code)") + } + } + /// 获取盲盒信息 /// - Parameters: /// - boxId: 盲盒ID diff --git a/wake/Utils/MaterialUpload.swift b/wake/Utils/ApiClient/MaterialUpload.swift similarity index 57% rename from wake/Utils/MaterialUpload.swift rename to wake/Utils/ApiClient/MaterialUpload.swift index be39171..08ba3bd 100644 --- a/wake/Utils/MaterialUpload.swift +++ b/wake/Utils/ApiClient/MaterialUpload.swift @@ -63,4 +63,50 @@ class MaterialUpload { } ) } + + /// 使用 async/await 方式添加素材到服务器 + /// - Parameters: + /// - fileId: 文件ID + /// - previewFileId: 预览文件ID + /// - Returns: 结果ID数组(可为空) + /// - Throws: NetworkError 或其他错误 + func addMaterial( + fileId: String, + previewFileId: String + ) async throws -> [String]? { + // 创建请求数据(数组结构,与现有接口保持一致) + let materials: [[String: String]] = [[ + "file_id": fileId, + "preview_file_id": previewFileId + ]] + + // 调试信息 + print("🔍(async) 准备发送的参数: \(materials)") + + // 直接使用 async/await 版本的 post + let response: AddMaterialResponse = try await NetworkService.shared.post( + path: "/material", + parameters: materials + ) + + // 按业务约定检查 code + if response.code == 0 { + return response.data + } else { + throw NetworkError.serverError("服务器返回错误码: \(response.code)") + } + } + + func addMaterials(files: [[String: String]]) async throws -> [String]? { + let response: AddMaterialResponse = try await NetworkService.shared.post( + path: "/material", + parameters: files + ) + if response.code == 0 { + return response.data + } else { + throw NetworkError.serverError("服务器返回错误码: \(response.code)") + } + } } + diff --git a/wake/Utils/NetworkService.swift b/wake/Utils/NetworkService.swift index 75eb87c..c3aa9a7 100644 --- a/wake/Utils/NetworkService.swift +++ b/wake/Utils/NetworkService.swift @@ -143,6 +143,42 @@ extension NetworkService { } } } + + /// 使用 async/await 的 POST 请求(支持数组或字典参数) + public func post( + path: String, + parameters: Any? = nil, + headers: [String: String]? = nil + ) async throws -> T { + return try await withCheckedThrowingContinuation { continuation in + post(path: path, parameters: parameters, headers: headers) { (result: Result) in + switch result { + case .success(let value): + continuation.resume(returning: value) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + /// 使用 async/await 的 POST 请求(带Token,支持数组或字典参数) + public func postWithToken( + path: String, + parameters: Any? = nil, + headers: [String: String]? = nil + ) async throws -> T { + return try await withCheckedThrowingContinuation { continuation in + postWithToken(path: path, parameters: parameters, headers: headers) { (result: Result) in + switch result { + case .success(let value): + continuation.resume(returning: value) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } } public enum NetworkError: Error { diff --git a/wake/View/OnBoarding/MediaUploadView.swift b/wake/View/OnBoarding/MediaUploadView.swift new file mode 100644 index 0000000..8beff4b --- /dev/null +++ b/wake/View/OnBoarding/MediaUploadView.swift @@ -0,0 +1,722 @@ +import SwiftUI +import PhotosUI +import AVKit +import CoreTransferable +import CoreImage.CIFilterBuiltins + +extension Notification.Name { + static let didAddFirstMedia = Notification.Name("didAddFirstMedia") +} +/// 主上传视图 +/// 提供媒体选择、预览和上传功能 +@MainActor +struct MediaUploadView: View { + // MARK: - 属性 + + /// 上传管理器,负责处理上传逻辑 + @StateObject private var uploadManager = MediaUploadManager() + /// 控制媒体选择器的显示/隐藏 + @State private var showMediaPicker = false + /// 当前选中的媒体项 + @State private var selectedMedia: MediaType? = nil + /// 当前选中的媒体索引集合 + @State private var selectedIndices: Set = [] + @State private var mediaPickerSelection: [MediaType] = [] // 添加这个状态变量 + /// 上传完成状态 + @State private var uploadComplete = false + /// 上传完成的文件ID列表 + @State private var uploadedFileIds: [[String: String]] = [] + + // MARK: - 视图主体 + + var body: some View { + VStack(spacing: 0) { + // 顶部导航栏 + topNavigationBar + + // 上传提示信息 + uploadHintView + Spacer() + .frame(height: uploadManager.selectedMedia.isEmpty ? 80 : 40) + // 主上传区域 + MainUploadArea( + uploadManager: uploadManager, + showMediaPicker: $showMediaPicker, + selectedMedia: $selectedMedia + ) + .id("mainUploadArea\(uploadManager.selectedMedia.count)") + + Spacer() + + // // 上传结果展示 + // if uploadComplete && !uploadedFileIds.isEmpty { + // VStack(alignment: .leading) { + // Text("上传完成!") + // .font(.headline) + + // ScrollView { + // ForEach(Array(uploadedFileIds.enumerated()), id: \.offset) { index, fileInfo in + // VStack(alignment: .leading) { + // Text("文件 \(index + 1):") + // .font(.subheadline) + // Text("ID: \(fileInfo["file_id"] ?? "")") + // .font(.caption) + // .foregroundColor(.gray) + // } + // .padding() + // .frame(maxWidth: .infinity, alignment: .leading) + // .background(Color.gray.opacity(0.1)) + // .cornerRadius(8) + // } + // } + // .frame(height: 200) + // } + // .padding() + // } + + // 继续按钮 + continueButton + .padding(.bottom, 24) + } + .background(Color.themeTextWhiteSecondary) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .sheet(isPresented: $showMediaPicker) { + // 媒体选择器 + mediaPickerView + } + .onChange(of: uploadManager.uploadResults) { newResults in + handleUploadCompletion(results: newResults) + } + } + + // MARK: - 子视图 + + /// 顶部导航栏 + private var topNavigationBar: some View { + HStack { + // 返回按钮 + Button(action: { Router.shared.pop() }) { + Image(systemName: "chevron.left") + .font(.system(size: 17, weight: .semibold)) + .foregroundColor(.themeTextMessageMain) + } + .padding(.leading, 16) + + Spacer() + + // 标题 + Text("Complete Your Profile") + .font(Typography.font(for: .title2, family: .quicksandBold)) + .foregroundColor(.themeTextMessageMain) + + Spacer() + + // 右侧占位视图(保持布局平衡) + Color.clear + .frame(width: 24, height: 24) + .padding(.trailing, 16) + } + .background(Color.themeTextWhiteSecondary) + // .padding(.horizontal) + .zIndex(1) // 确保导航栏显示在最上层 + } + + /// 上传提示视图 + private var uploadHintView: some View { + HStack (spacing: 6) { + SVGImage(svgName: "Tips") + .frame(width: 16, height: 16) + .padding(.leading,6) + Text("The upload process will take approximately 2 minutes. Thank you for your patience.") + .font(.caption) + .foregroundColor(.black) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(3) + } + .background( + Color.themeTextWhite + .cornerRadius(6) + ) + .padding(.vertical, 8) + .padding(.horizontal) + } + + /// 继续按钮 + private var continueButton: some View { + Button(action: handleContinue) { + Text("Continue") + .font(.headline) + .foregroundColor(uploadManager.selectedMedia.isEmpty ? Color.themeTextMessage : Color.themeTextMessageMain) + .frame(maxWidth: .infinity) + .frame(height: 56) + .background(uploadManager.selectedMedia.isEmpty ? Color.white : Color.themePrimary) + .cornerRadius(28) + .padding(.horizontal, 24) + } + .buttonStyle(PlainButtonStyle()) + .disabled(uploadManager.selectedMedia.isEmpty) + } + + /// 媒体选择器视图 + private var mediaPickerView: some View { + MediaPicker( + selectedMedia: Binding( + get: { mediaPickerSelection }, + set: { newSelections in + print("🔄 开始处理用户选择的媒体文件") + print("📌 新选择的媒体数量: \(newSelections.count)") + + // 1. 去重处理:过滤掉已经存在的媒体项 + var uniqueNewMedia: [MediaType] = [] + + for newItem in newSelections { + let isDuplicate = uploadManager.selectedMedia.contains { existingItem in + switch (existingItem, newItem) { + case (.image(let existingImage), .image(let newImage)): + return existingImage.pngData() == newImage.pngData() + case (.video(let existingURL, _), .video(let newURL, _)): + return existingURL == newURL + default: + return false + } + } + + if !isDuplicate { + uniqueNewMedia.append(newItem) + } else { + print("⚠️ 检测到重复文件,已跳过: \(newItem)") + } + } + + // 2. 添加新文件 + if !uniqueNewMedia.isEmpty { + print("✅ 添加 \(uniqueNewMedia.count) 个新文件") + uploadManager.addMedia(uniqueNewMedia) + + // 如果没有当前选中的媒体,则选择第一个新添加的 + if selectedMedia == nil, let firstNewItem = uniqueNewMedia.first { + selectedMedia = firstNewItem + } + + // 开始上传 + uploadManager.startUpload() + } else { + print("ℹ️ 没有新文件需要添加,所有选择的文件都已存在") + } + } + ), + imageSelectionLimit: max(0, 20 - uploadManager.selectedMedia.filter { + if case .image = $0 { return true } + return false + }.count), + videoSelectionLimit: max(0, 5 - uploadManager.selectedMedia.filter { + if case .video = $0 { return true } + return false + }.count), + selectionMode: .multiple, + onDismiss: handleMediaPickerDismiss, + onUploadProgress: { index, progress in + print("文件 \(index) 上传进度: \(progress * 100)%") + } + ) + .onAppear { + // 重置选择状态当选择器出现时 + mediaPickerSelection = [] + } + } + + // MARK: - 私有方法 + + /// 处理媒体选择器关闭事件 + private func handleMediaPickerDismiss() { + showMediaPicker = false + print("媒体选择器关闭 - 开始处理") + + // 如果有选中的媒体,开始上传 + if !uploadManager.selectedMedia.isEmpty { + // 不需要在这里开始上传,因为handleMediaChange会处理 + } + } + + /// 处理媒体变化 + /// - Parameters: + /// - newMedia: 新的媒体数组 + /// - oldMedia: 旧的媒体数组 + private func handleMediaChange(_ newMedia: [MediaType], oldMedia: [MediaType]) { + print("开始处理媒体变化,新数量: \(newMedia.count), 原数量: \(oldMedia.count)") + + // 如果没有变化,直接返回 + guard newMedia != oldMedia else { + print("媒体未发生变化,跳过处理") + return + } + + // 在后台线程处理媒体变化 + DispatchQueue.global(qos: .userInitiated).async { [self] in + // 找出新增的媒体(在newMedia中但不在oldMedia中的项) + let newItems = newMedia.filter { newItem in + !oldMedia.contains { $0.id == newItem.id } + } + + print("检测到\(newItems.count)个新增媒体项") + + // 如果有新增媒体 + if !newItems.isEmpty { + print("准备添加\(newItems.count)个新项...") + + // 在主线程更新UI + DispatchQueue.main.async { [self] in + // 创建新的数组,包含原有媒体和新媒体 + var updatedMedia = uploadManager.selectedMedia + updatedMedia.append(contentsOf: newItems) + + // 更新选中的媒体 + uploadManager.clearAllMedia() + uploadManager.addMedia(updatedMedia) + + // 如果当前没有选中的媒体,则选中第一个新增的媒体 + if selectedIndices.isEmpty && !newItems.isEmpty { + selectedIndices = [oldMedia.count] // 选择第一个新增项的索引 + selectedMedia = newItems.first + } + + // 开始上传新添加的媒体 + uploadManager.startUpload() + print("媒体添加完成,总数量: \(uploadManager.selectedMedia.count)") + } + } + } + } + + /// 检查是否有正在上传的文件 + /// - Returns: 是否正在上传 + private func isUploading() -> Bool { + return uploadManager.uploadStatus.values.contains { status in + if case .uploading = status { return true } + return false + } + } + + /// 处理上传完成 + private func handleUploadCompletion(results: [String: MediaUploadManager.UploadResult]) { + // 转换为需要的格式 + let formattedResults = results.map { (_, result) -> [String: String] in + return [ + "file_id": result.fileId, + "preview_file_id": result.thumbnailId ?? result.fileId + ] + } + + uploadedFileIds = formattedResults + uploadComplete = !uploadedFileIds.isEmpty + } + + /// 处理继续按钮点击 + private func handleContinue() { + // 获取所有已上传文件的结果 + let uploadResults = uploadManager.uploadResults + guard !uploadResults.isEmpty else { + print("⚠️ 没有可用的文件ID") + return + } + + // 准备请求参数 + let files = uploadResults.map { (_, result) -> [String: String] in + return [ + "file_id": result.fileId, + "preview_file_id": result.thumbnailId ?? result.fileId + ] + } + + // 提交素材,并利用返回的素材id数组,创建第二个盲盒 + Task { + do { + let materialIds = try await MaterialUpload.shared.addMaterials(files: files) + print("🚀 素材ID: \(materialIds ?? [])") + // 创建盲盒 + if let materialIds = materialIds { + let result = try await BlindBoxApi.shared.generateBlindBox(boxType: "Second", materialIds: materialIds) + print("🎉 盲盒结果: \(result ?? nil)") + if let result = result { + let blindBoxId = result.id ?? "" + print("🎉 盲盒ID: \(blindBoxId)") + // 导航到盲盒首页等待盲盒开启 + Router.shared.navigate(to: .blindBox(mediaType: .video, blindBoxId: blindBoxId)) + } + } + } catch { + print("❌ 添加素材失败: \(error)") + } + } + } +} + +// MARK: - 主上传区域 + +/// 主上传区域视图 +/// 显示上传提示、媒体预览和添加更多按钮 +struct MainUploadArea: View { + // MARK: - 属性 + + /// 上传管理器 + @ObservedObject var uploadManager: MediaUploadManager + /// 控制媒体选择器的显示/隐藏 + @Binding var showMediaPicker: Bool + /// 当前选中的媒体 + @Binding var selectedMedia: MediaType? + + // MARK: - 视图主体 + + var body: some View { + VStack() { + Spacer() + .frame(height: 30) + // 标题 + Text("Click to upload 5+ videos to generate your next blind box.") + .font(Typography.font(for: .title2, family: .quicksandBold)) + .fontWeight(.bold) + .foregroundColor(.black) + .multilineTextAlignment(.center) + .padding(.horizontal) + Spacer() + .frame(height: 50) + // 主显示区域 + if let mediaToDisplay = selectedMedia ?? uploadManager.selectedMedia.first { + Button(action: { showMediaPicker = true }) { + MediaPreview(media: mediaToDisplay) + .id(mediaToDisplay.id) + .frame(width: 225, height: 225) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color.themePrimary, lineWidth: 5) + ) + .cornerRadius(16) + .padding(.horizontal) + .transition(.opacity) + } + } else { + UploadPromptView(showMediaPicker: $showMediaPicker) + } + // 媒体预览区域 + mediaPreviewSection + Spacer() + .frame(height: 10) + } + .onAppear { + print("MainUploadArea appeared") + print("Selected media count: \(uploadManager.selectedMedia.count)") + + if selectedMedia == nil, let firstMedia = uploadManager.selectedMedia.first { + print("Selecting first media: \(firstMedia.id)") + selectedMedia = firstMedia + } + } + .onReceive(NotificationCenter.default.publisher(for: .didAddFirstMedia)) { notification in + if let media = notification.userInfo?["media"] as? MediaType, selectedMedia == nil { + selectedMedia = media + } + } + .background(Color.white) + .cornerRadius(18) + .animation(.default, value: selectedMedia?.id) + } + + // MARK: - 子视图 + + /// 媒体预览区域 + private var mediaPreviewSection: some View { + Group { + if !uploadManager.selectedMedia.isEmpty { + VStack(spacing: 4) { + // 横向滚动的缩略图列表 + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 10) { + ForEach(Array(uploadManager.selectedMedia.enumerated()), id: \.element.id) { index, media in + mediaItemView(for: media, at: index) + } + // 当没有选择媒体时显示添加更多按钮 + if !uploadManager.selectedMedia.isEmpty { + addMoreButton + } + } + .padding(.horizontal) + } + .frame(height: 70) + } + .padding(.top, 10) + } + } + } + + /// 单个媒体项视图 + /// - Parameters: + /// - media: 媒体项 + /// - index: 索引 + /// - Returns: 媒体项视图 + private func mediaItemView(for media: MediaType, at index: Int) -> some View { + ZStack(alignment: .topTrailing) { + // 媒体预览 - 始终使用本地资源 + MediaPreview(media: media) + .frame(width: 58, height: 58) + .cornerRadius(8) + .shadow(radius: 1) + .overlay( + // 左上角序号 + ZStack(alignment: .topLeading) { + Path { path in + let radius: CGFloat = 4 + let width: CGFloat = 14 + let height: CGFloat = 10 + + // 从左上角开始(带圆角) + path.move(to: CGPoint(x: 0, y: radius)) + path.addQuadCurve(to: CGPoint(x: radius, y: 0), + control: CGPoint(x: 0, y: 0)) + + // 上边缘(右上角保持直角) + path.addLine(to: CGPoint(x: width, y: 0)) + + // 右边缘(右下角保持直角) + path.addLine(to: CGPoint(x: width, y: height - radius)) + + // 右下角圆角 + path.addQuadCurve(to: CGPoint(x: width - radius, y: height), + control: CGPoint(x: width, y: height)) + + // 下边缘(左下角保持直角) + path.addLine(to: CGPoint(x: 0, y: height)) + + // 闭合路径 + path.closeSubpath() + } + .fill(Color(hex: "BEBEBE").opacity(0.6)) + .frame(width: 14, height: 10) + .overlay( + Text("\(index + 1)") + .font(.system(size: 8, weight: .bold)) + .foregroundColor(.black) + .frame(width: 14, height: 10) + .offset(y: -1), + alignment: .topLeading + ) + .padding([.top, .leading], 2) + + // 右下角视频时长 + if case .video(let url, _) = media, let videoURL = url as? URL { + VStack { + Spacer() + HStack { + Spacer() + Text(getVideoDuration(url: videoURL)) + .font(.system(size: 8, weight: .bold)) + .foregroundColor(.black) + .padding(.horizontal, 4) + .frame(height: 10) + .background(Color(hex: "BEBEBE").opacity(0.6)) + .cornerRadius(2) + } + .padding([.trailing, .bottom], 0) + } + }else{ + // 占位 + VStack { + Spacer() + HStack { + Spacer() + Text("占位") + .font(.system(size: 8, weight: .bold)) + .foregroundColor(.black) + .padding(.horizontal, 4) + .frame(height: 10) + .background(Color(hex: "BEBEBE").opacity(0.6)) + .cornerRadius(2) + } + .padding([.trailing, .bottom], 0) + } + .opacity(0) + } + }, + alignment: .topLeading + ) + .onTapGesture { + print("点击了媒体项,索引: \(index)") + withAnimation { + selectedMedia = media + } + } + .contentShape(Rectangle()) + + // 右上角关闭按钮 + Button(action: { + uploadManager.removeMedia(id: media.id) + if selectedMedia == media { + selectedMedia = nil + } + }) { + Image(systemName: "xmark") + .font(.system(size: 8, weight: .bold)) + .foregroundColor(.black) + .frame(width: 12, height: 12) + .background( + Circle() + .fill(Color(hex: "BEBEBE").opacity(0.6)) + .frame(width: 12, height: 12) + ) + } + .offset(x: 6, y: -6) + } + .padding(.horizontal, 4) + .contentShape(Rectangle()) + } + + /// 添加更多按钮 + private var addMoreButton: some View { + Button(action: { showMediaPicker = true }) { + Image(systemName: "plus") + .font(.system(size: 8, weight: .bold)) + .foregroundColor(.black) + .frame(width: 58, height: 58) + .background(Color.white) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .strokeBorder(style: StrokeStyle( + lineWidth: 2, + dash: [4, 4] + )) + .foregroundColor(Color.themePrimary) + ) + } + } +} + +// MARK: - 上传提示视图 + +/// 上传提示视图 +/// 显示上传区域的占位图和提示 +struct UploadPromptView: View { + /// 控制媒体选择器的显示/隐藏 + @Binding var showMediaPicker: Bool + + var body: some View { + Button(action: { showMediaPicker = true }) { + // 上传图标 + SVGImageHtml(svgName: "IP") + .frame(width: 225, height: 225) + .contentShape(Rectangle()) + .overlay( + ZStack { + RoundedRectangle(cornerRadius: 20) + .stroke(style: StrokeStyle( + lineWidth: 5, + lineCap: .round, + dash: [12, 8] + )) + .foregroundColor(Color.themePrimary) + + // Add plus icon in the center + Image(systemName: "plus") + .font(.system(size: 32, weight: .bold)) + .foregroundColor(.black) + } + ) + } + } +} + +// MARK: - 媒体预览视图 + +/// 媒体预览视图 +/// 显示图片或视频的预览图,始终使用本地资源 +struct MediaPreview: View { + // MARK: - 属性 + + /// 媒体类型 + let media: MediaType + + // MARK: - 计算属性 + + /// 获取要显示的图片 + private var displayImage: UIImage? { + switch media { + case .image(let uiImage): + return uiImage + case .video(_, let thumbnail): + return thumbnail + } + } + + // MARK: - 视图主体 + + var body: some View { + ZStack { + // 1. 显示图片或视频缩略图 + if let image = displayImage { + Image(uiImage: image) + .resizable() + .scaledToFill() + .transition(.opacity.animation(.easeInOut(duration: 0.2))) + } else { + // 2. 加载中的占位图 + Color.gray.opacity(0.1) + } + } + .aspectRatio(1, contentMode: .fill) + .clipped() + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.themePrimary.opacity(0.3), lineWidth: 1) + ) + } +} + +private func getVideoDuration(url: URL) -> String { + let asset = AVURLAsset(url: url) + let durationInSeconds = CMTimeGetSeconds(asset.duration) + guard durationInSeconds.isFinite else { return "0:00" } + + let minutes = Int(durationInSeconds) / 60 + let seconds = Int(durationInSeconds) % 60 + return String(format: "%d:%02d", minutes, seconds) +} + +// MARK: - Response Types + +private struct EmptyResponse: Decodable { + // Empty response type for endpoints that don't return data +} + +// MARK: - 扩展 + +/// 扩展 MediaType 以支持 Identifiable 协议 +extension MediaType: Identifiable { + /// 唯一标识符 + public var id: String { + switch self { + case .image(let uiImage): + return "image_\(uiImage.hashValue)" + case .video(let url, _): + return "video_\(url.absoluteString.hashValue)" + } +} +} + +extension TimeInterval { + var formattedDuration: String { + let minutes = Int(self) / 60 + let seconds = Int(self) % 60 + return String(format: "%d:%02d", minutes, seconds) + } +} + +// MARK: - 预览 + +struct MediaUploadView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + MediaUploadView() + } + } +} -- 2.47.2 From 07e72c3b70972d731af8e7675d49a2b17e8f93ff Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sun, 7 Sep 2025 17:35:47 +0800 Subject: [PATCH 14/18] =?UTF-8?q?chore:=20=E4=B8=BB=E9=A1=B5=E4=BF=A1?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/View/Blind/ContentView.swift | 66 +++++++++++++++---------------- wake/WakeApp.swift | 1 + 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/wake/View/Blind/ContentView.swift b/wake/View/Blind/ContentView.swift index 539374b..05e2024 100644 --- a/wake/View/Blind/ContentView.swift +++ b/wake/View/Blind/ContentView.swift @@ -201,39 +201,39 @@ struct BlindBoxView: View { // } // } - // // // 会员信息 - // // NetworkService.shared.get( - // // path: "/membership/personal-center-info", - // // parameters: nil - // // ) { (result: Result) in - // // DispatchQueue.main.async { - // // switch result { - // // case .success(let response): - // // self.memberProfile = response.data - // // self.isMember = response.data.membershipLevel == "Pioneer" - // // self.memberDate = response.data.membershipEndAt ?? "" - // // print("✅ 成功获取会员信息:", response.data) - // // print("✅ 用户ID:", response.data.userInfo.userId) - // // case .failure(let error): - // // print("❌ 获取会员信息失败:", error) - // // } - // // } - // // } - // // // 盲盒数量 - // // NetworkService.shared.get( - // // path: "/blind_box/available/quantity", - // // parameters: nil - // // ) { (result: Result, NetworkError>) in - // // DispatchQueue.main.async { - // // switch result { - // // case .success(let response): - // // self.blindCount = response.data - // // print("✅ 成功获取盲盒数量:", response.data) - // // case .failure(let error): - // // print("❌ 获取数量失败:", error) - // // } - // // } - // // } + // 会员信息 + NetworkService.shared.get( + path: "/membership/personal-center-info", + parameters: nil + ) { (result: Result) in + DispatchQueue.main.async { + switch result { + case .success(let response): + self.memberProfile = response.data + self.isMember = response.data.membershipLevel == "Pioneer" + self.memberDate = response.data.membershipEndAt ?? "" + print("✅ 成功获取会员信息:", response.data) + print("✅ 用户ID:", response.data.userInfo.userId) + case .failure(let error): + print("❌ 获取会员信息失败:", error) + } + } + } + // 盲盒数量 + NetworkService.shared.get( + path: "/blind_box/available/quantity", + parameters: nil + ) { (result: Result, NetworkError>) in + DispatchQueue.main.async { + switch result { + case .success(let response): + self.blindCount = response.data + print("✅ 成功获取盲盒数量:", response.data) + case .failure(let error): + print("❌ 获取数量失败:", error) + } + } + } // } } diff --git a/wake/WakeApp.swift b/wake/WakeApp.swift index e9dbbae..4c5752f 100644 --- a/wake/WakeApp.swift +++ b/wake/WakeApp.swift @@ -46,6 +46,7 @@ struct WakeApp: App { if authState.isAuthenticated { // 已登录:显示主页面 NavigationStack(path: $router.path) { + // FIXME 调回来 // BlindBoxView(mediaType: .all) // .navigationDestination(for: AppRoute.self) { route in // route.view -- 2.47.2 From 0ac103dc86ec051b37de105ca18958f60568ff7e Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sun, 7 Sep 2025 17:44:44 +0800 Subject: [PATCH 15/18] =?UTF-8?q?feat:=20=E8=BD=AE=E8=AF=A2=E6=99=AE?= =?UTF-8?q?=E9=80=9A=E7=9B=B2=E7=9B=92=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/Utils/ApiClient/BlindBoxApi.swift | 19 +++++++++++++++- wake/View/Blind/ContentView.swift | 31 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/wake/Utils/ApiClient/BlindBoxApi.swift b/wake/Utils/ApiClient/BlindBoxApi.swift index 3a7d846..a6501c3 100644 --- a/wake/Utils/ApiClient/BlindBoxApi.swift +++ b/wake/Utils/ApiClient/BlindBoxApi.swift @@ -17,6 +17,12 @@ struct GenerateBlindBoxResponse: Codable { let data: BlindBoxData? } +// MARK: - Get Blind Box List Response Model +struct BlindBoxListResponse: Codable { + let code: Int + let data: [BlindBoxData] +} + // MARK: - Blind Box API Client class BlindBoxApi { static let shared = BlindBoxApi() @@ -124,4 +130,15 @@ class BlindBoxApi { throw NetworkError.serverError("服务器返回错误码: \(response.code)") } } -} \ No newline at end of file + + /// 获取盲盒列表 + @available(iOS 13.0, *) + func getBlindBoxList() async throws -> [BlindBoxData]? { + let response: BlindBoxListResponse = try await NetworkService.shared.getWithToken(path: "/blind_boxs/query") + if response.code == 0 { + return response.data + } else { + throw NetworkError.serverError("服务器返回错误码: \(response.code)") + } + } +} diff --git a/wake/View/Blind/ContentView.swift b/wake/View/Blind/ContentView.swift index 05e2024..caa499b 100644 --- a/wake/View/Blind/ContentView.swift +++ b/wake/View/Blind/ContentView.swift @@ -154,6 +154,9 @@ struct BlindBoxView: View { print("指定监听某盲盒结果: ", self.currentBoxId! as Any) // 启动轮询查询盲盒状态 await pollingToQuerySingleBox() + } else { + // 启动轮询查询普通盲盒列表 + await pollingToQueryBlindBox() } // switch mediaType { @@ -279,6 +282,34 @@ struct BlindBoxView: View { } } } + + private func pollingToQueryBlindBox() async { + stopPolling() + isPolling = true + + while isPolling { + do { + let blindBoxList = try await BlindBoxApi.shared.getBlindBoxList() + print("✅ 获取盲盒列表: \(blindBoxList?.count ?? 0) 条") + // 检查是否有未打开的盲盒 + if let blindBox = blindBoxList?.first(where: { $0.status == "Unopened" }) { + self.blindGenerate = blindBox + stopPolling() + break + } else { + if self.animationPhase != .none { + self.animationPhase = .none + } + } + // 等待2秒后继续轮询 + try await Task.sleep(nanoseconds: 2_000_000_000) + } catch { + print("❌ 获取盲盒列表失败: \(error)") + stopPolling() + break + } + } + } // 轮询接口 private func startPolling() { -- 2.47.2 From 62c12a3bf8e5c565e0f1cab1e103a603e52066d4 Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sun, 7 Sep 2025 17:48:13 +0800 Subject: [PATCH 16/18] =?UTF-8?q?fix:=20=E6=97=A0=E7=9B=B2=E7=9B=92?= =?UTF-8?q?=E8=B4=AD=E4=B9=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/View/Blind/ContentView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wake/View/Blind/ContentView.swift b/wake/View/Blind/ContentView.swift index caa499b..4ddd81b 100644 --- a/wake/View/Blind/ContentView.swift +++ b/wake/View/Blind/ContentView.swift @@ -804,8 +804,11 @@ struct BlindBoxView: View { .frame(width: 300, height: 300) case .none: - SVGImage(svgName: "BlindNone") + // FIXME: 临时使用 BlindLoading GIF + GIFView(name: "BlindLoading") .frame(width: 300, height: 300) + // SVGImage(svgName: "BlindNone") + // .frame(width: 300, height: 300) } } .offset(y: -50) -- 2.47.2 From 373456dfd8154b40a59d26d35d5379f0c143534c Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sun, 7 Sep 2025 21:42:19 +0800 Subject: [PATCH 17/18] =?UTF-8?q?feat:=20=E5=BC=80=E5=90=AF=E7=9B=B2?= =?UTF-8?q?=E7=9B=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/Utils/ApiClient/BlindBoxApi.swift | 24 ++++++++++++++++++++++++ wake/View/Blind/ContentView.swift | 12 ++++++++++++ 2 files changed, 36 insertions(+) diff --git a/wake/Utils/ApiClient/BlindBoxApi.swift b/wake/Utils/ApiClient/BlindBoxApi.swift index a6501c3..05696a5 100644 --- a/wake/Utils/ApiClient/BlindBoxApi.swift +++ b/wake/Utils/ApiClient/BlindBoxApi.swift @@ -23,6 +23,12 @@ struct BlindBoxListResponse: Codable { let data: [BlindBoxData] } +// MARK: - Open Blind Box Response Model +struct OpenBlindBoxResponse: Codable { + let code: Int + let data: BlindBoxData? +} + // MARK: - Blind Box API Client class BlindBoxApi { static let shared = BlindBoxApi() @@ -141,4 +147,22 @@ class BlindBoxApi { throw NetworkError.serverError("服务器返回错误码: \(response.code)") } } + + + /// 将盲盒标记为开启状态 + /// - Parameter boxId: 盲盒ID + /// - Returns: 开启后的盲盒数据(可能为null) + @available(iOS 13.0, *) + func openBlindBox(boxId: String) async throws { + let response: OpenBlindBoxResponse = try await NetworkService.shared.postWithToken( + path: "/blind_box/open", + parameters: ["box_id": boxId] + ) + if response.code == 0 { + // API返回成功,data可能为null,这是正常的 + print("✅ 盲盒开启成功") + } else { + throw NetworkError.serverError("服务器返回错误码: \(response.code)") + } + } } diff --git a/wake/View/Blind/ContentView.swift b/wake/View/Blind/ContentView.swift index 4ddd81b..e5d7098 100644 --- a/wake/View/Blind/ContentView.swift +++ b/wake/View/Blind/ContentView.swift @@ -753,6 +753,18 @@ struct BlindBoxView: View { .frame(width: 300, height: 300) .onTapGesture { print("点击了盲盒") + + // 标记盲盒开启 + if let boxId = self.currentBoxId { + Task { + do { + try await BlindBoxApi.shared.openBlindBox(boxId: boxId) + print("✅ 盲盒开启成功") + } catch { + print("❌ 开启盲盒失败: \(error)") + } + } + } withAnimation { animationPhase = .opening } -- 2.47.2 From 4d1486560e698a22384ee646e6ca0217b4c1d507 Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sun, 7 Sep 2025 22:22:10 +0800 Subject: [PATCH 18/18] =?UTF-8?q?feat:=20=E9=87=8D=E5=A4=8D=E8=B4=AD?= =?UTF-8?q?=E4=B9=B0=E7=AC=AC=E4=BA=8C=E4=B8=AA=E7=9B=B2=E7=9B=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/Utils/Router.swift | 6 +- wake/View/Blind/BlindOutCome.swift | 6 +- wake/View/Blind/ContentView.swift | 105 +++++++++++++++------ wake/View/OnBoarding/MediaUploadView.swift | 2 +- wake/WakeApp.swift | 16 ++-- 5 files changed, 93 insertions(+), 42 deletions(-) diff --git a/wake/Utils/Router.swift b/wake/Utils/Router.swift index a33b5e6..39ee741 100644 --- a/wake/Utils/Router.swift +++ b/wake/Utils/Router.swift @@ -8,7 +8,7 @@ enum AppRoute: Hashable { case feedbackDetail(type: FeedbackView.FeedbackType) case mediaUpload case blindBox(mediaType: BlindBoxMediaType, blindBoxId: String? = nil) - case blindOutcome(media: MediaType, time: String? = nil, description: String? = nil) + case blindOutcome(media: MediaType, time: String? = nil, description: String? = nil, isMember: Bool) case memories case subscribe case userInfo @@ -33,8 +33,8 @@ enum AppRoute: Hashable { MediaUploadView() case .blindBox(let mediaType, let blindBoxId): BlindBoxView(mediaType: mediaType, blindBoxId: blindBoxId) - case .blindOutcome(let media, let time, let description): - BlindOutcomeView(media: media, time: time, description: description) + case .blindOutcome(let media, let time, let description, let isMember): + BlindOutcomeView(media: media, time: time, description: description, isMember: isMember) case .memories: MemoriesView() case .subscribe: diff --git a/wake/View/Blind/BlindOutCome.swift b/wake/View/Blind/BlindOutCome.swift index cdd4bc9..ed1ee34 100644 --- a/wake/View/Blind/BlindOutCome.swift +++ b/wake/View/Blind/BlindOutCome.swift @@ -6,6 +6,7 @@ struct BlindOutcomeView: View { let media: MediaType let time: String? let description: String? + let isMember: Bool @Environment(\.presentationMode) var presentationMode @State private var isFullscreen = false @State private var isPlaying = false @@ -13,10 +14,11 @@ struct BlindOutcomeView: View { @State private var showIPListModal = false @State private var player: AVPlayer? - init(media: MediaType, time: String? = nil, description: String? = nil) { + init(media: MediaType, time: String? = nil, description: String? = nil, isMember: Bool = false) { self.media = media self.time = time self.description = description + self.isMember = isMember } var body: some View { @@ -351,4 +353,4 @@ class PlayerView: UIView { deinit { cleanup() } -} \ No newline at end of file +} diff --git a/wake/View/Blind/ContentView.swift b/wake/View/Blind/ContentView.swift index e5d7098..916dbdb 100644 --- a/wake/View/Blind/ContentView.swift +++ b/wake/View/Blind/ContentView.swift @@ -223,20 +223,20 @@ struct BlindBoxView: View { } } // 盲盒数量 - NetworkService.shared.get( - path: "/blind_box/available/quantity", - parameters: nil - ) { (result: Result, NetworkError>) in - DispatchQueue.main.async { - switch result { - case .success(let response): - self.blindCount = response.data - print("✅ 成功获取盲盒数量:", response.data) - case .failure(let error): - print("❌ 获取数量失败:", error) - } - } - } + // NetworkService.shared.get( + // path: "/blind_box/available/quantity", + // parameters: nil + // ) { (result: Result, NetworkError>) in + // DispatchQueue.main.async { + // switch result { + // case .success(let response): + // self.blindCount = response.data + // print("✅ 成功获取盲盒数量:", response.data) + // case .failure(let error): + // print("❌ 获取数量失败:", error) + // } + // } + // } // } } @@ -254,11 +254,12 @@ struct BlindBoxView: View { self.blindGenerate = data // 根据盲盒类型设置媒体URL - if mediaType == .video { - self.videoURL = data.resultFile?.url ?? "" - } else if mediaType == .image { + if mediaType == .image { self.imageURL = data.resultFile?.url ?? "" } + else { + self.videoURL = data.resultFile?.url ?? "" + } print("✅ 成功获取盲盒数据: \(data.name), 状态: \(data.status)") @@ -291,9 +292,25 @@ struct BlindBoxView: View { do { let blindBoxList = try await BlindBoxApi.shared.getBlindBoxList() print("✅ 获取盲盒列表: \(blindBoxList?.count ?? 0) 条") - // 检查是否有未打开的盲盒 + + // 统计未开启盲盒数量 + self.blindCount = BlindCount(availableQuantity: blindBoxList?.filter({ $0.status == "Unopened" }).count ?? 0) + + // 设置第一个未开启的盲盒 if let blindBox = blindBoxList?.first(where: { $0.status == "Unopened" }) { self.blindGenerate = blindBox + self.animationPhase = .ready + + // 更新UI + // 根据盲盒类型设置媒体URL + if mediaType == .image { + self.imageURL = blindBox.resultFile?.url ?? "" + } + else { + self.videoURL = blindBox.resultFile?.url ?? "" + } + + print("✅ 成功获取盲盒数据: \(blindBox.name), 状态: \(blindBox.status)") stopPolling() break } else { @@ -561,7 +578,7 @@ struct BlindBoxView: View { .edgesIgnoringSafeArea(.all) Group { - if mediaType == .video, let player = videoPlayer { + if mediaType == .all, let player = videoPlayer { // Video Player AVPlayerController(player: $videoPlayer) .frame(width: scaledWidth, height: scaledHeight) @@ -589,10 +606,10 @@ struct BlindBoxView: View { HStack { Button(action: { // 导航到BlindOutcomeView - if mediaType == .video, !videoURL.isEmpty, let url = URL(string: videoURL) { - Router.shared.navigate(to: .blindOutcome(media: .video(url, nil), time: blindGenerate?.name ?? "Your box", description:blindGenerate?.description ?? "")) + if mediaType == .all, !videoURL.isEmpty, let url = URL(string: videoURL) { + Router.shared.navigate(to: .blindOutcome(media: .video(url, nil), time: blindGenerate?.name ?? "Your box", description:blindGenerate?.description ?? "", isMember: self.isMember)) } else if mediaType == .image, let image = displayImage { - Router.shared.navigate(to: .blindOutcome(media: .image(image), time: blindGenerate?.name ?? "Your box", description:blindGenerate?.description ?? "")) + Router.shared.navigate(to: .blindOutcome(media: .image(image), time: blindGenerate?.name ?? "Your box", description:blindGenerate?.description ?? "", isMember: self.isMember)) } }) { Image(systemName: "chevron.left") @@ -765,6 +782,16 @@ struct BlindBoxView: View { } } } + if let boxId = self.blindGenerate?.id { + Task { + do { + try await BlindBoxApi.shared.openBlindBox(boxId: boxId) + print("✅ 盲盒开启成功") + } catch { + print("❌ 开启盲盒失败: \(error)") + } + } + } withAnimation { animationPhase = .opening } @@ -799,7 +826,7 @@ struct BlindBoxView: View { // 显示媒体内容 self.showScalingOverlay = true - if mediaType == .video { + if mediaType == .all { loadVideo() } else if mediaType == .image { loadImage() @@ -857,11 +884,33 @@ struct BlindBoxView: View { if mediaType == .all { Button(action: { if animationPhase == .ready { - // 处理准备就绪状态的操作 - // 导航到订阅页面 - Router.shared.navigate(to: .subscribe) - } else { - showUserProfile() + // 准备就绪点击,开启盲盒 + // 标记盲盒开启 + if let boxId = self.currentBoxId { + Task { + do { + try await BlindBoxApi.shared.openBlindBox(boxId: boxId) + print("✅ 盲盒开启成功") + } catch { + print("❌ 开启盲盒失败: \(error)") + } + } + } + if let boxId = self.blindGenerate?.id { + Task { + do { + try await BlindBoxApi.shared.openBlindBox(boxId: boxId) + print("✅ 盲盒开启成功") + } catch { + print("❌ 开启盲盒失败: \(error)") + } + } + } + withAnimation { + animationPhase = .opening + } + } else if animationPhase == .none { + Router.shared.navigate(to: .mediaUpload) } }) { if animationPhase == .loading { diff --git a/wake/View/OnBoarding/MediaUploadView.swift b/wake/View/OnBoarding/MediaUploadView.swift index 8beff4b..4f25b55 100644 --- a/wake/View/OnBoarding/MediaUploadView.swift +++ b/wake/View/OnBoarding/MediaUploadView.swift @@ -342,7 +342,7 @@ struct MediaUploadView: View { let blindBoxId = result.id ?? "" print("🎉 盲盒ID: \(blindBoxId)") // 导航到盲盒首页等待盲盒开启 - Router.shared.navigate(to: .blindBox(mediaType: .video, blindBoxId: blindBoxId)) + Router.shared.navigate(to: .blindBox(mediaType: .all, blindBoxId: blindBoxId)) } } } catch { diff --git a/wake/WakeApp.swift b/wake/WakeApp.swift index 4c5752f..5b1f2f1 100644 --- a/wake/WakeApp.swift +++ b/wake/WakeApp.swift @@ -47,14 +47,14 @@ struct WakeApp: App { // 已登录:显示主页面 NavigationStack(path: $router.path) { // FIXME 调回来 -// BlindBoxView(mediaType: .all) -// .navigationDestination(for: AppRoute.self) { route in -// route.view -// } - UserInfo() - .navigationDestination(for: AppRoute.self) { route in - route.view - } + BlindBoxView(mediaType: .all) + .navigationDestination(for: AppRoute.self) { route in + route.view + } + // UserInfo() + // .navigationDestination(for: AppRoute.self) { route in + // route.view + // } } } else { // 未登录:显示登录界面 -- 2.47.2