import SwiftUI import SwiftData import AVKit // MARK: - Notification Names extension Notification.Name { static let blindBoxStatusChanged = Notification.Name("blindBoxStatusChanged") } struct BlindBoxView: View { let mediaType: BlindBoxMediaType @State private var showModal = false // 控制用户资料弹窗显示 @State private var showSettings = false // 控制设置页面显示 @State private var isMember = false // 是否是会员 @State private var memberDate = "" // 会员到期时间 @State private var showLogin = false @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 @State private var videoPlayer: AVPlayer? @State private var showControls = false @State private var isAnimating = true @State private var aspectRatio: CGFloat = 1.0 @State private var isPortrait: Bool = false @State private var displayImage: UIImage? @State private var showMedia = false // 查询数据 - 简单查询 @Query private var login: [Login] init(mediaType: BlindBoxMediaType) { 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() 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) 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: "/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) } } } } } // 轮询接口 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 } // 发送状态变更通知 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 loadImage() { 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 { self.displayImage = image self.aspectRatio = image.size.width / image.size.height self.isPortrait = image.size.height > image.size.width self.showScalingOverlay = true // 确保显示媒体内容 } } }.resume() } private func loadVideo() { 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) let videoTracks = asset.tracks(withMediaType: .video) if let videoTrack = videoTracks.first { let size = videoTrack.naturalSize.applying(videoTrack.preferredTransform) let width = abs(size.width) let height = abs(size.height) aspectRatio = width / height isPortrait = height > width } // 更新视频播放器 videoPlayer = player videoPlayer?.play() showScalingOverlay = true // 确保显示媒体内容 } private func prepareVideo() { 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) let videoTracks = asset.tracks(withMediaType: .video) if let videoTrack = videoTracks.first { let size = videoTrack.naturalSize.applying(videoTrack.preferredTransform) let width = abs(size.width) let height = abs(size.height) aspectRatio = width / height isPortrait = height > width } // 更新视频播放器 videoPlayer = player } private func prepareImage() { 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 { self.displayImage = image self.aspectRatio = image.size.width / image.size.height self.isPortrait = image.size.height > image.size.width } } }.resume() } private func startScalingAnimation() { self.scale = 0.1 self.showScalingOverlay = true withAnimation(.spring(response: 2.0, dampingFraction: 0.5, blendDuration: 0.8)) { self.scale = 1.0 } } // MARK: - Computed Properties private var scaledWidth: CGFloat { if isPortrait { return UIScreen.main.bounds.height * scale * 1/aspectRatio } else { return UIScreen.main.bounds.width * scale } } private var scaledHeight: CGFloat { if isPortrait { return UIScreen.main.bounds.height * scale } else { return UIScreen.main.bounds.width * scale * 1/aspectRatio } } var body: some View { ZStack { Color.themeTextWhiteSecondary.ignoresSafeArea() .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 // Clean up video player videoPlayer?.pause() videoPlayer?.replaceCurrentItem(with: nil) videoPlayer = nil NotificationCenter.default.removeObserver( self, name: .blindBoxStatusChanged, object: nil ) } if showScalingOverlay { MediaOverlayView( videoPlayer: $videoPlayer, showControls: $showControls, mediaType: mediaType, displayImage: displayImage, scaledWidth: scaledWidth, scaledHeight: scaledHeight, scale: scale, videoURL: videoURL, imageURL: imageURL, blindGenerate: blindGenerate, onBackTap: { // 导航到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")) } else if mediaType == .image, let image = displayImage { Router.shared.navigate(to: .blindOutcome(media: .image(image), time: blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn", description:blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation")) } } ) .frame(maxWidth: .infinity, maxHeight: .infinity) .animation(.easeInOut(duration: 1.0), value: scale) .ignoresSafeArea() .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { withAnimation(.spring(response: 2.5, dampingFraction: 0.6, blendDuration: 1.0)) { self.scale = 1.0 } } } } else { // Original content VStack { VStack(spacing: 20) { if mediaType == .all { BlindBoxNavigationBar( memberProfile: memberProfile, showLogin: showLogin, showUserProfile: showUserProfile ) } // 标题 VStack(alignment: .leading, spacing: 4) { Text("Hi! Click And") Text("Open Your First Box~") } .font(Typography.font(for: .smallLargeTitle)) .fontWeight(.bold) .foregroundColor(Color.themeTextMessageMain) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal) .opacity(showScalingOverlay ? 0 : 1) .offset(y: showScalingOverlay ? -UIScreen.main.bounds.height * 0.2 : 0) .animation(.easeInOut(duration: 0.5), value: showScalingOverlay) // 盲盒动画 BlindBoxAnimationView( mediaType: mediaType, animationPhase: animationPhase, blindCount: blindCount, blindGenerate: blindGenerate, scale: scale, showScalingOverlay: showScalingOverlay, showMedia: showMedia, onBoxTap: { print("点击了盲盒") withAnimation { animationPhase = .opening } } ) // 控制按钮 BlindBoxControls( mediaType: mediaType, animationPhase: animationPhase, countdown: countdown, onButtonTap: { if animationPhase == .ready { // 处理准备就绪状态的操作 // 导航到订阅页面 Router.shared.navigate(to: .subscribe) } else { showUserProfile() } }, onCountdownStart: startCountdown ) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.themeTextWhiteSecondary) .offset(x: showModal ? UIScreen.main.bounds.width * 0.8 : 0) .animation(.spring(response: 0.6, dampingFraction: 0.8), value: showModal) .edgesIgnoringSafeArea(.all) } // 用户资料弹窗 SlideInModal( isPresented: $showModal, onDismiss: hideUserProfile ) { UserProfileModal( showModal: $showModal, showSettings: $showSettings, isMember: $isMember, memberDate: $memberDate ) } .offset(x: showSettings ? UIScreen.main.bounds.width : 0) .animation(.spring(response: 0.6, dampingFraction: 0.8), value: showSettings) // 设置页面遮罩层 ZStack { if showSettings { Color.black.opacity(0.3) .edgesIgnoringSafeArea(.all) .onTapGesture(perform: hideSettings) .transition(.opacity) } if showSettings { SettingsView(isPresented: $showSettings) .transition(.move(edge: .leading)) .zIndex(1) } } .animation(.spring(response: 0.6, dampingFraction: 0.8), value: showSettings) } } .navigationBarBackButtonHidden(true) } /// 显示用户资料弹窗 private func showUserProfile() { withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { // print("登录记录数量: \(login.count)") // for (index, item) in login.enumerated() { // print("记录 \(index + 1): 邮箱=\(item.email), 姓名=\(item.name)") // } print("当前登录记录:") for (index, item) in login.enumerated() { print("记录 \(index + 1): 邮箱=\(item.email), 姓名=\(item.name)") } showModal.toggle() } } /// 隐藏用户资料弹窗 private func hideUserProfile() { withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { showModal = false } } /// 隐藏设置页面 private func hideSettings() { withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { showSettings = false } } } // MARK: - 预览 #Preview { BlindBoxView(mediaType: .video) }