From aa954ddfb912a67eb416d3f8be989ff27ef41c8f Mon Sep 17 00:00:00 2001 From: jinyaqiu Date: Mon, 1 Sep 2025 11:41:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BB=E9=A1=B5=E9=9D=A2=E8=BD=AE?= =?UTF-8?q?=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/ContentView.swift | 584 +++++++++++++++++++++++++++++------------ wake/WakeApp.swift | 10 +- 2 files changed, 427 insertions(+), 167 deletions(-) diff --git a/wake/ContentView.swift b/wake/ContentView.swift index 1a893f3..f6d2216 100644 --- a/wake/ContentView.swift +++ b/wake/ContentView.swift @@ -2,17 +2,16 @@ import SwiftUI import SwiftData import AVKit -// MARK: - Constants -private enum MediaURLs { - static let videoURL = "https://cdn.memorywake.com/users/7363409620351717377/files/7366657553935241216/39C069E1-7C3E-4261-8486-12058F855B38.mov" - static let imageURL = "https://cdn.fairclip.cn/files/7343228671693557760/20250604-164000.jpg" - static let VideoBlindURL = "https://cdn.memorywake.com/users/7363409620351717377/files/7366658779259211776/AD970D28-9D1E-4817-A245-F11967441B8F.mp4" +// 添加通知名称 +extension Notification.Name { + static let blindBoxStatusChanged = Notification.Name("blindBoxStatusChanged") } private enum BlindBoxAnimationPhase { case loading case ready case opening + case none } extension Notification.Name { @@ -198,7 +197,71 @@ struct BlindBoxView: View { let url: String } - + // 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 + ) + } + } + let mediaType: BlindBoxMediaType @State private var showModal = false // 控制用户资料弹窗显示 @State private var showSettings = false // 控制设置页面显示 @@ -206,7 +269,21 @@ struct BlindBoxView: View { @State private var memberProfile: MemberProfile? = nil @State private var blindCount: BlindCount? = nil @State private var blindList: [BlindList] = [] // Changed to array + // 生成盲盒 + @State private var blindGenerate : BlindBoxData? @State private var showLottieAnimation = true + // 轮询接口 + @State private var isPolling = false + @State private var pollingTimer: Timer? + @State private var currentBoxType: String = "" + // 盲盒链接 + @State private var videoURL: String = "" + @State private var imageURL: String = "" + // 按钮状态 倒计时 + @State private var countdown: (minutes: Int, seconds: Int, milliseconds: Int) = (36, 50, 20) + @State private var countdownTimer: Timer? + // 盲盒数据 + @State private var displayData: BlindBoxData? = nil @State private var showScalingOverlay = false @State private var animationPhase: BlindBoxAnimationPhase = .loading @State private var scale: CGFloat = 0.1 @@ -224,87 +301,176 @@ struct BlindBoxView: View { self.mediaType = mediaType } + // 倒计时 + private func startCountdown() { + // 重置为36:50:20 + countdown = (36, 50, 20) + + countdownTimer?.invalidate() + countdownTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in + var (minutes, seconds, milliseconds) = countdown + + // 更新毫秒 + milliseconds -= 10 + if milliseconds < 0 { + milliseconds = 90 + seconds -= 1 + } + + // 更新秒 + if seconds < 0 { + seconds = 59 + minutes -= 1 + } + + // 如果倒计时结束,停止计时器 + if minutes <= 0 && seconds <= 0 && milliseconds <= 0 { + countdownTimer?.invalidate() + countdownTimer = nil + return + } + + countdown = (minutes, seconds, milliseconds) + } + } + private func loadMedia() { print("loadMedia called with mediaType: \(mediaType)") switch mediaType { - case .video: - loadVideo() - print("Loading video...") - case .image: - loadImage() - print("Loading image...") - case .all: - print("Loading all content...") - // 会员信息 - NetworkService.shared.get( - path: "/membership/personal-center-info", - parameters: nil - ) { (result: Result, NetworkError>) in - DispatchQueue.main.async { - switch result { - case .success(let response): - self.memberProfile = response.data - print("✅ 成功获取会员信息:", response.data) - print("✅ 用户ID:", response.data.userInfo.userId) - print("✅ 用户昵称:", response.data.userInfo.nickname) - print("✅ 用户邮箱:", response.data.userInfo.email) - case .failure(let error): - print("❌ 获取会员信息失败:", error) + case .video: + loadVideo() + currentBoxType = "Video" + startPolling() + case .image: + loadImage() + currentBoxType = "Image" + startPolling() + case .all: + print("Loading all content...") + // 会员信息 + NetworkService.shared.get( + path: "/membership/personal-center-info", + parameters: nil + ) { (result: Result, NetworkError>) in + DispatchQueue.main.async { + switch result { + case .success(let response): + self.memberProfile = response.data + print("✅ 成功获取会员信息:", response.data) + print("✅ 用户ID:", response.data.userInfo.userId) + print("✅ 用户昵称:", response.data.userInfo.nickname) + print("✅ 用户邮箱:", response.data.userInfo.email) + 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: "/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 startPolling() { + stopPolling() + isPolling = true + checkBlindBoxStatus() + } + + private func stopPolling() { + pollingTimer?.invalidate() + pollingTimer = nil + isPolling = false + } + + private func checkBlindBoxStatus() { + guard !currentBoxType.isEmpty else { + stopPolling() + 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 } - } - } - // 盲盒列表 - 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 ?? [] - print("✅ 成功获取 \(self.blindList.count) 个盲盒") - case .failure(let error): - self.blindList = [] - print("❌ 获取盲盒列表失败:", error.localizedDescription) - } - } - } - - // 盲盒列表 - NetworkService.shared.postWithToken( - path: "/blind_box/generate/mock", - parameters: ["box_type": "Image"] - ) { (result: Result, NetworkError>) in - DispatchQueue.main.async { - switch result { - case .success(let response): - self.blindList = response.data ?? [] - print("✅✅✅✅✅✅✅✅✅ 成功获取 \(self.blindList.count) 个盲盒") - case .failure(let error): - self.blindList = [] - print("❌❌ ❌ ❌ ❌ ❌ ❌ 获取盲盒列表失败:", error.localizedDescription) + + // 发送状态变更通知 + 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() } } } } - + private func loadData() { // 会员信息 NetworkService.shared.get( @@ -324,7 +490,11 @@ struct BlindBoxView: View { } private func loadImage() { - guard let url = URL(string: MediaURLs.imageURL) else { return } + guard !imageURL.isEmpty, let url = URL(string: imageURL) else { + print("⚠️ 图片URL无效或为空") + return + } + URLSession.shared.dataTask(with: url) { data, _, _ in if let data = data, let image = UIImage(data: data) { DispatchQueue.main.async { @@ -337,7 +507,11 @@ struct BlindBoxView: View { } private func loadVideo() { - guard let url = URL(string: MediaURLs.videoURL) else { return } + guard !videoURL.isEmpty, let url = URL(string: videoURL) else { + print("⚠️ 视频URL无效或为空") + return + } + let asset = AVAsset(url: url) let playerItem = AVPlayerItem(asset: asset) let player = AVPlayer(playerItem: playerItem) @@ -352,8 +526,9 @@ struct BlindBoxView: View { isPortrait = height > width } - player.isMuted = true - self.videoPlayer = player + // Update the video player + videoPlayer = player + videoPlayer?.play() } private func startScalingAnimation() { @@ -388,8 +563,51 @@ 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 + } + + // 添加盲盒状态变化监听 + 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() } + .onDisappear { + stopPolling() + countdownTimer?.invalidate() + countdownTimer = nil + NotificationCenter.default.removeObserver( + self, + name: .blindBoxStatusChanged, + object: nil + ) + } if showScalingOverlay { ZStack { @@ -426,8 +644,8 @@ struct BlindBoxView: View { HStack { Button(action: { // 导航到BlindOutcomeView - if mediaType == .video, let videoURL = URL(string: MediaURLs.videoURL) { - Router.shared.navigate(to: .blindOutcome(media: .video(videoURL, nil))) + if mediaType == .video, !videoURL.isEmpty, let url = URL(string: videoURL) { + Router.shared.navigate(to: .blindOutcome(media: .video(url, nil))) } else if mediaType == .image, let image = displayImage { Router.shared.navigate(to: .blindOutcome(media: .image(image))) } @@ -471,57 +689,59 @@ struct BlindBoxView: View { // Original content VStack { VStack(spacing: 20) { - // 顶部导航栏 - HStack { - // 设置按钮 - Button(action: showUserProfile) { - SVGImage(svgName: "User") - .frame(width: 24, height: 24) - } - - Spacer() - // // 测试质感页面入口 - // NavigationLink(destination: TestView()) { - // Text("TestView") - // .font(.subheadline) - // .padding(.horizontal, 12) - // .padding(.vertical, 6) - // .background(Color.brown) - // .foregroundColor(.white) - // .cornerRadius(8) - // } - - // // 订阅测试按钮 - // NavigationLink(destination: SubscribeView()) { - // Text("Subscribe") - // .font(.subheadline) - // .padding(.horizontal, 12) - // .padding(.vertical, 6) - // .background(Color.orange) - // .foregroundColor(.white) - // .cornerRadius(8) - // } - // .padding(.trailing) - // .fullScreenCover(isPresented: $showLogin) { - // LoginView() - // } - NavigationLink(destination: SubscribeView()) { - Text("\(memberProfile?.remainPoints ?? 0)") - .font(Typography.font(for: .subtitle)) - .fontWeight(.bold) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(Color.black) - .foregroundColor(.white) - .cornerRadius(16) - } - .padding(.trailing) - .fullScreenCover(isPresented: $showLogin) { - LoginView() + if mediaType == .all { + // 顶部导航栏 + HStack { + // 设置按钮 + Button(action: showUserProfile) { + SVGImage(svgName: "User") + .frame(width: 24, height: 24) + } + + Spacer() + // // 测试质感页面入口 + // NavigationLink(destination: TestView()) { + // Text("TestView") + // .font(.subheadline) + // .padding(.horizontal, 12) + // .padding(.vertical, 6) + // .background(Color.brown) + // .foregroundColor(.white) + // .cornerRadius(8) + // } + + // // 订阅测试按钮 + // NavigationLink(destination: SubscribeView()) { + // Text("Subscribe") + // .font(.subheadline) + // .padding(.horizontal, 12) + // .padding(.vertical, 6) + // .background(Color.orange) + // .foregroundColor(.white) + // .cornerRadius(8) + // } + // .padding(.trailing) + // .fullScreenCover(isPresented: $showLogin) { + // LoginView() + // } + NavigationLink(destination: SubscribeView()) { + Text("\(memberProfile?.remainPoints ?? 0)") + .font(Typography.font(for: .subtitle)) + .fontWeight(.bold) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.black) + .foregroundColor(.white) + .cornerRadius(16) + } + .padding(.trailing) + .fullScreenCover(isPresented: $showLogin) { + LoginView() + } } + .padding(.horizontal) + .padding(.top, 20) } - .padding(.horizontal) - .padding(.top, 20) // 标题 VStack(alignment: .leading, spacing: 4) { Text("Hi! Click And") @@ -550,19 +770,34 @@ struct BlindBoxView: View { .opacity(showScalingOverlay ? 0 : 1) .animation(.easeOut(duration: 1.5), value: showScalingOverlay) } + if mediaType == .all && !showScalingOverlay { + ZStack { + SVGImage(svgName: "BlindCount") + .frame(width: 100, height: 60) + + Text("\(blindCount?.availableQuantity ?? 0) Boxes") + .font(Typography.font(for: .body, family: .quicksandBold)) + .foregroundColor(.white) + .offset(x: 6, y: -18) + } + .position(x: UIScreen.main.bounds.width * 0.7, + y: UIScreen.main.bounds.height * 0.18) + .opacity(showScalingOverlay ? 0 : 1) + .animation(.easeOut(duration: 1.5), value: showScalingOverlay) + } if !showScalingOverlay { VStack(spacing: 20) { switch animationPhase { case .loading: GIFView(name: "BlindLoading") .frame(width: 300, height: 300) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 6) { - withAnimation { - animationPhase = .ready - } - } - } + // .onAppear { + // DispatchQueue.main.asyncAfter(deadline: .now() + 6) { + // withAnimation { + // animationPhase = .ready + // } + // } + // } case .ready: ZStack { @@ -592,33 +827,23 @@ struct BlindBoxView: View { self.startScalingAnimation() } } + + case .none: + SVGImage(svgName: "BlindNone") + .frame(width: 300, height: 300) } } .offset(y: -50) .compositingGroup() .padding() } - if !showScalingOverlay { - ZStack { - SVGImage(svgName: "BlindCount") - .frame(width: 100, height: 60) - - Text("\(blindCount?.availableQuantity ?? 0) Boxes") - .font(Typography.font(for: .body, family: .quicksandBold)) - .foregroundColor(.white) - .offset(x: 6, y: -18) - } - .position(x: UIScreen.main.bounds.width * 0.7, - y: UIScreen.main.bounds.height * 0.18) - .opacity(showScalingOverlay ? 0 : 1) - .animation(.easeOut(duration: 1.5), value: showScalingOverlay) - } if !showScalingOverlay { VStack(alignment: .leading, spacing: 8) { - Text("hhsdshjsjdhn") + // 从变量blindGenerate中获取description + Text(blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn") .font(Typography.font(for: .body, family: .quicksandBold)) .foregroundColor(Color.themeTextMessageMain) - Text("informationinformationinformationinformationinformationinformation") + Text(blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation") .font(.system(size: 14)) .foregroundColor(Color.themeTextMessageMain) } @@ -636,17 +861,50 @@ struct BlindBoxView: View { .offset(y: showScalingOverlay ? -100 : 0) .animation(.easeInOut(duration: 1.5), value: showScalingOverlay) // 打开 - Button(action: showUserProfile) { - Text("Go to Buy") - .font(Typography.font(for: .body)) - .fontWeight(.bold) - .frame(maxWidth: .infinity) - .padding() - .background(Color.themePrimary) - .foregroundColor(Color.themeTextMessageMain) - .cornerRadius(32) + if mediaType == .all { + Button(action: { + if animationPhase == .ready { + // 处理准备就绪状态的操作 + // 导航到订阅页面 + Router.shared.navigate(to: .subscribe) + } else { + showUserProfile() + } + }) { + if animationPhase == .loading { + Text("Next: \(countdown.minutes):\(String(format: "%02d", countdown.seconds)).\(String(format: "%02d", countdown.milliseconds))") + .font(Typography.font(for: .body)) + .fontWeight(.bold) + .frame(maxWidth: .infinity) + .padding() + .background(Color.white) + .foregroundColor(.black) + .cornerRadius(32) + .onAppear { + startCountdown() + } + } else if animationPhase == .ready { + Text("Ready") + .font(Typography.font(for: .body)) + .fontWeight(.bold) + .frame(maxWidth: .infinity) + .padding() + .background(Color.themePrimary) + .foregroundColor(Color.themeTextMessageMain) + .cornerRadius(32) + } else { + Text("Go to Buy") + .font(Typography.font(for: .body)) + .fontWeight(.bold) + .frame(maxWidth: .infinity) + .padding() + .background(Color.themePrimary) + .foregroundColor(Color.themeTextMessageMain) + .cornerRadius(32) + } } - .padding(.horizontal) + .padding(.horizontal) + } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.themeTextWhiteSecondary) diff --git a/wake/WakeApp.swift b/wake/WakeApp.swift index 908ee99..5bb8fba 100644 --- a/wake/WakeApp.swift +++ b/wake/WakeApp.swift @@ -46,17 +46,19 @@ struct WakeApp: App { if authState.isAuthenticated { // 已登录:显示主页面 NavigationStack(path: $router.path) { - BlindBoxView(mediaType: .all) + // BlindBoxView(mediaType: .all) + // .navigationDestination(for: AppRoute.self) { route in + // route.view + // } + LoginView() .navigationDestination(for: AppRoute.self) { route in route.view } } } else { // 未登录:显示登录界面 - // LoginView() - // .environmentObject(authState) NavigationStack(path: $router.path) { - BlindBoxView(mediaType: .all) + LoginView() .navigationDestination(for: AppRoute.self) { route in route.view }