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/Models/BlindModels.swift b/wake/Models/BlindModels.swift new file mode 100644 index 0000000..c3faf16 --- /dev/null +++ b/wake/Models/BlindModels.swift @@ -0,0 +1,174 @@ +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: 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 + } + + 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.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.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/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 diff --git a/wake/Utils/ApiClient/BlindBoxApi.swift b/wake/Utils/ApiClient/BlindBoxApi.swift new file mode 100644 index 0000000..05696a5 --- /dev/null +++ b/wake/Utils/ApiClient/BlindBoxApi.swift @@ -0,0 +1,168 @@ +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: BlindBoxData? +} + +// MARK: - Get Blind Box List Response Model +struct BlindBoxListResponse: Codable { + let code: Int + 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() + + 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)) + } + } + } + ) + } + + /// 使用 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 + /// - 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)") + } + } + + /// 获取盲盒列表 + @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)") + } + } + + + /// 将盲盒标记为开启状态 + /// - 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/Utils/ApiClient/MaterialUpload.swift b/wake/Utils/ApiClient/MaterialUpload.swift new file mode 100644 index 0000000..08ba3bd --- /dev/null +++ b/wake/Utils/ApiClient/MaterialUpload.swift @@ -0,0 +1,112 @@ +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)) + } + } + } + ) + } + + /// 使用 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 d4d8d4a..c3aa9a7 100644 --- a/wake/Utils/NetworkService.swift +++ b/wake/Utils/NetworkService.swift @@ -108,6 +108,79 @@ 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) + } + } + } + } + + /// 使用 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 { case invalidURL case noData diff --git a/wake/Utils/Router.swift b/wake/Utils/Router.swift index 6337967..39ee741 100644 --- a/wake/Utils/Router.swift +++ b/wake/Utils/Router.swift @@ -7,8 +7,8 @@ enum AppRoute: Hashable { case feedbackView case feedbackDetail(type: FeedbackView.FeedbackType) case mediaUpload - case blindBox(mediaType: BlindBoxView.BlindBoxMediaType) - case blindOutcome(media: MediaType, time: String? = nil, description: String? = nil) + case blindBox(mediaType: BlindBoxMediaType, blindBoxId: String? = nil) + case blindOutcome(media: MediaType, time: String? = nil, description: String? = nil, isMember: Bool) case memories case subscribe case userInfo @@ -23,17 +23,18 @@ 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): FeedbackDetailView(feedbackType: type) case .mediaUpload: MediaUploadView() - case .blindBox(let mediaType): - BlindBoxView(mediaType: mediaType) - case .blindOutcome(let media, let time, let description): - BlindOutcomeView(media: media, time: time, description: description) + case .blindBox(let mediaType, let blindBoxId): + BlindBoxView(mediaType: mediaType, blindBoxId: blindBoxId) + 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/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/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/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/ContentView.swift b/wake/View/Blind/ContentView.swift similarity index 66% rename from wake/ContentView.swift rename to wake/View/Blind/ContentView.swift index b6f24cb..916dbdb 100644 --- a/wake/ContentView.swift +++ b/wake/View/Blind/ContentView.swift @@ -1,6 +1,7 @@ import SwiftUI import SwiftData import AVKit +import Foundation // 添加通知名称 extension Notification.Name { @@ -67,119 +68,10 @@ struct AVPlayerController: UIViewControllerRepresentable { } } -struct BlindBoxView: View { - enum BlindBoxMediaType { - case video - case image - case all - } - // 盲盒列表 - struct BlindList: Codable, Identifiable { - let id: Int64 - let boxCode: String - let userId: Int64 - let name: String - let boxType: String - let features: String? - let resultFileId: Int64? - let status: String - let workflowInstanceId: String? - let videoGenerateTime: String? - let createTime: String - let coverFileId: Int64? - 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 resultFileId = "result_file_id" - case status - case workflowInstanceId = "workflow_instance_id" - case videoGenerateTime = "video_generate_time" - case createTime = "create_time" - case coverFileId = "cover_file_id" - 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: listItem.id, - boxCode: listItem.boxCode, - userId: listItem.userId, - name: listItem.name, - boxType: listItem.boxType, - features: listItem.features, - url: nil, - status: listItem.status, - workflowInstanceId: listItem.workflowInstanceId, - videoGenerateTime: listItem.videoGenerateTime, - createTime: listItem.createTime, - description: listItem.description - ) - } - } - +struct BlindBoxView: View { let mediaType: BlindBoxMediaType + let currentBoxId: String? + @State private var showModal = false // 控制用户资料弹窗显示 @State private var showSettings = false // 控制设置页面显示 @State private var isMember = false // 是否是会员 @@ -189,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 @@ -217,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 } // 倒计时 @@ -254,20 +147,63 @@ 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...") + if self.currentBoxId != nil { + print("指定监听某盲盒结果: ", self.currentBoxId! as Any) + // 启动轮询查询盲盒状态 + await pollingToQuerySingleBox() + } else { + // 启动轮询查询普通盲盒列表 + await pollingToQueryBlindBox() + } + + // 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) + // } + // } + // } + // 会员信息 NetworkService.shared.get( path: "/membership/personal-center-info", @@ -287,43 +223,111 @@ 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_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) - } - } - } - } + // 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 == .image { + self.imageURL = data.resultFile?.url ?? "" + } + else { + self.videoURL = data.resultFile?.url ?? "" + } + + print("✅ 成功获取盲盒数据: \(data.name), 状态: \(data.status)") + + // 检查状态是否为Unopened,如果是则停止轮询 + if data.status == "Unopened" { + print("✅ 盲盒已准备就绪,停止轮询") + self.animationPhase = .ready + stopPolling() + break + } + } + + // 等待2秒后继续轮询 + try await Task.sleep(nanoseconds: 2_000_000_000) + } catch { + print("❌ 获取盲盒数据失败: \(error)") + // 处理错误情况 + self.animationPhase = .none + stopPolling() + break + } + } + } + + private func pollingToQueryBlindBox() async { + stopPolling() + isPolling = true + + while isPolling { + 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 { + 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() { stopPolling() @@ -343,52 +347,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() { @@ -508,40 +514,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() @@ -567,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) @@ -595,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?.videoGenerateTime ?? "hhsdshjsjdhn", description:blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation")) + 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?.videoGenerateTime ?? "hhsdshjsjdhn", description:blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation")) + Router.shared.navigate(to: .blindOutcome(media: .image(image), time: blindGenerate?.name ?? "Your box", description:blindGenerate?.description ?? "", isMember: self.isMember)) } }) { Image(systemName: "chevron.left") @@ -694,10 +705,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) @@ -758,6 +770,28 @@ 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)") + } + } + } + if let boxId = self.blindGenerate?.id { + Task { + do { + try await BlindBoxApi.shared.openBlindBox(boxId: boxId) + print("✅ 盲盒开启成功") + } catch { + print("❌ 开启盲盒失败: \(error)") + } + } + } withAnimation { animationPhase = .opening } @@ -792,7 +826,7 @@ struct BlindBoxView: View { // 显示媒体内容 self.showScalingOverlay = true - if mediaType == .video { + if mediaType == .all { loadVideo() } else if mediaType == .image { loadImage() @@ -809,8 +843,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) @@ -821,10 +858,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) } @@ -842,15 +879,38 @@ 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 { - // 处理准备就绪状态的操作 - // 导航到订阅页面 - 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 { @@ -894,6 +954,7 @@ struct BlindBoxView: View { .animation(.spring(response: 0.6, dampingFraction: 0.8), value: showModal) .edgesIgnoringSafeArea(.all) } + // 用户资料弹窗 SlideInModal( isPresented: $showModal, @@ -961,16 +1022,42 @@ struct BlindBoxView: View { // MARK: - 预览 #Preview { - BlindBoxView(mediaType: .video) + BlindBoxView(mediaType: .all) + .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() - view.backgroundColor = .clear - view.isOpaque = false - return view - } - - func updateUIView(_ uiView: UIView, context: Context) {} +// 预览第一个盲盒 +#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() +// view.backgroundColor = .clear +// view.isOpaque = false +// return view +// } + +// func updateUIView(_ uiView: UIView, context: Context) {} +// } diff --git a/wake/View/Upload/MediaUploadView.swift b/wake/View/OnBoarding/MediaUploadView.swift similarity index 96% rename from wake/View/Upload/MediaUploadView.swift rename to wake/View/OnBoarding/MediaUploadView.swift index ab0250c..4f25b55 100644 --- a/wake/View/Upload/MediaUploadView.swift +++ b/wake/View/OnBoarding/MediaUploadView.swift @@ -329,21 +329,24 @@ struct MediaUploadView: View { ] } - // 发送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)) + // 提交素材,并利用返回的素材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: .all, blindBoxId: blindBoxId)) + } } - case .failure(let error): - print("❌ 素材提交失败: \(error.localizedDescription)") - // 这里可以添加错误处理逻辑,比如显示错误提示 + } catch { + print("❌ 添加素材失败: \(error)") } } } @@ -370,7 +373,7 @@ struct MainUploadArea: View { Spacer() .frame(height: 30) // 标题 - Text("Click to upload 20 images and 5 videos to generate your next blind box.") + Text("Click to upload 5+ videos to generate your next blind box.") .font(Typography.font(for: .title2, family: .quicksandBold)) .fontWeight(.bold) .foregroundColor(.black) diff --git a/wake/View/Owner/UserInfo/UserInfo.swift b/wake/View/OnBoarding/UserInfo.swift similarity index 84% rename from wake/View/Owner/UserInfo/UserInfo.swift rename to wake/View/OnBoarding/UserInfo.swift index b2b8ab4..18d1202 100644 --- a/wake/View/Owner/UserInfo/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) @@ -180,8 +180,33 @@ struct UserInfo: View { if let userData = response.data { 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, blindBoxId: blindBoxData?.id ?? "0")) + 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)" @@ -195,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) @@ -302,4 +328,4 @@ struct UserInfo_Previews: PreviewProvider { static var previews: some View { UserInfo() } -} \ No newline at end of file +} 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 diff --git a/wake/WakeApp.swift b/wake/WakeApp.swift index e66f67b..5b1f2f1 100644 --- a/wake/WakeApp.swift +++ b/wake/WakeApp.swift @@ -46,10 +46,15 @@ struct WakeApp: App { if authState.isAuthenticated { // 已登录:显示主页面 NavigationStack(path: $router.path) { - BlindBoxView(mediaType: .all) - .navigationDestination(for: AppRoute.self) { route in - route.view - } + // FIXME 调回来 + BlindBoxView(mediaType: .all) + .navigationDestination(for: AppRoute.self) { route in + route.view + } + // UserInfo() + // .navigationDestination(for: AppRoute.self) { route in + // route.view + // } } } else { // 未登录:显示登录界面 @@ -108,4 +113,4 @@ struct WakeApp: App { } } } -} \ No newline at end of file +}