import SwiftUI import SwiftData import AVKit import Foundation // 添加通知名称 extension Notification.Name { static let blindBoxStatusChanged = Notification.Name("blindBoxStatusChanged") } private enum BlindBoxAnimationPhase { case loading case ready case opening case none } extension Notification.Name { static let navigateToMediaViewer = Notification.Name("navigateToMediaViewer") } // MARK: - 主视图 struct VisualEffectView: UIViewRepresentable { var effect: UIVisualEffect? func makeUIView(context: Context) -> UIVisualEffectView { let view = UIVisualEffectView(effect: nil) // Use a simpler approach without animator let blurEffect = UIBlurEffect(style: .systemUltraThinMaterialLight) // Create a custom blur effect with reduced intensity let blurView = UIVisualEffectView(effect: blurEffect) blurView.alpha = 0.3 // Reduce intensity // Add a white background with low opacity for better frosted effect let backgroundView = UIView() backgroundView.backgroundColor = UIColor.white.withAlphaComponent(0.1) backgroundView.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.contentView.addSubview(backgroundView) view.contentView.addSubview(blurView) blurView.frame = view.bounds blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight] return view } func updateUIView(_ uiView: UIVisualEffectView, context: Context) { // No need to update the effect } } struct AVPlayerController: UIViewControllerRepresentable { @Binding var player: AVPlayer? func makeUIViewController(context: Context) -> AVPlayerViewController { let controller = AVPlayerViewController() controller.player = player controller.showsPlaybackControls = false controller.videoGravity = .resizeAspect controller.view.backgroundColor = .clear return controller } func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { uiViewController.player = player } } struct BlindBoxView: View { let mediaType: BlindBoxMediaType let currentBoxId: String? @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, blindBoxId: String? = nil) { self.mediaType = mediaType self.currentBoxId = blindBoxId } // 倒计时 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 loadBlindBox() async { print("loadMedia called with mediaType: \(mediaType)") 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", parameters: nil ) { (result: Result) in DispatchQueue.main.async { switch result { case .success(let response): self.memberProfile = response.data self.isMember = response.data.membershipLevel == "Pioneer" self.memberDate = response.data.membershipEndAt ?? "" print("✅ 成功获取会员信息:", response.data) print("✅ 用户ID:", response.data.userInfo.userId) case .failure(let error): print("❌ 获取会员信息失败:", error) } } } // 盲盒数量 // NetworkService.shared.get( // path: "/blind_box/available/quantity", // parameters: nil // ) { (result: Result, NetworkError>) in // DispatchQueue.main.async { // switch result { // case .success(let response): // self.blindCount = response.data // print("✅ 成功获取盲盒数量:", response.data) // case .failure(let error): // print("❌ 获取数量失败:", error) // } // } // } // } } private func pollingToQuerySingleBox() async { stopPolling() isPolling = true // 轮询查询盲盒状态,直到状态为Unopened while isPolling { do { let blindBoxData = try await BlindBoxApi.shared.getBlindBox(boxId: self.currentBoxId!) // 更新UI if let data = blindBoxData { self.blindGenerate = data // 根据盲盒类型设置媒体URL if mediaType == .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() 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) 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() { 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 // } // } // } // 调用接口 Task { await loadBlindBox() } } .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 { ZStack { VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialLight)) .opacity(0.3) .edgesIgnoringSafeArea(.all) Group { if mediaType == .all, let player = videoPlayer { // Video Player AVPlayerController(player: $videoPlayer) .frame(width: scaledWidth, height: scaledHeight) .opacity(scale == 1 ? 1 : 0.7) .onAppear { player.play() } } else if mediaType == .image, let image = displayImage { // Image View Image(uiImage: image) .resizable() .scaledToFit() .frame(width: scaledWidth, height: scaledHeight) .opacity(scale == 1 ? 1 : 0.7) } } .onTapGesture { withAnimation(.easeInOut(duration: 0.1)) { showControls.toggle() } } // 返回按钮 if showControls { VStack { HStack { Button(action: { // 导航到BlindOutcomeView if mediaType == .all, !videoURL.isEmpty, let url = URL(string: videoURL) { Router.shared.navigate(to: .blindOutcome(media: .video(url, nil), time: blindGenerate?.name ?? "Your box", description:blindGenerate?.description ?? "", isMember: self.isMember)) } else if mediaType == .image, let image = displayImage { Router.shared.navigate(to: .blindOutcome(media: .image(image), time: blindGenerate?.name ?? "Your box", description:blindGenerate?.description ?? "", isMember: self.isMember)) } }) { Image(systemName: "chevron.left") .font(.system(size: 24)) .foregroundColor(.black) } Spacer() } Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.top, 50) .padding(.leading, 20) .zIndex(1000) .transition(.opacity) .onAppear { // 2秒后显示按钮 DispatchQueue.main.asyncAfter(deadline: .now() + 2) { withAnimation(.easeInOut(duration: 0.3)) { showControls = true } } } } } .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 { // 顶部导航栏 HStack { // 设置按钮 Button(action: showUserProfile) { SVGImage(svgName: "User") .frame(width: 24, height: 24) .padding(13) // Increases tap area while keeping visual size .contentShape(Rectangle()) // Makes the padded area tappable } .buttonStyle(PlainButtonStyle()) // Prevents the button from affecting the layout 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) } // 标题 VStack(alignment: .leading, spacing: 4) { Text("Hi! Click And") Text("Open Your 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) // 盲盒 ZStack { // 1. 背景SVG if !showScalingOverlay { SVGImage(svgName: "BlindBg", contentMode: .fit) // .position(x: UIScreen.main.bounds.width / 2, // y: UIScreen.main.bounds.height * 0.325) .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 // } // } // } case .ready: ZStack { GIFView(name: "BlindReady") .frame(width: 300, height: 300) // Add a transparent overlay to capture taps Color.clear .contentShape(Rectangle()) // Make the entire area tappable .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 } } } .frame(width: 300, height: 300) case .opening: ZStack { GIFView(name: "BlindOpen") .frame(width: 300, height: 300) .scaleEffect(scale) .opacity(showMedia ? 0 : 1) // 当显示媒体时隐藏GIF .onAppear { print("开始播放开启动画") // 初始缩放为1(原始大小) self.scale = 1.0 // 1秒后开始全屏动画 DispatchQueue.main.asyncAfter(deadline: .now() + 1) { withAnimation(.spring(response: 1.0, dampingFraction: 0.7)) { // 缩放到全屏 self.scale = max( UIScreen.main.bounds.width / 300, UIScreen.main.bounds.height / 300 ) * 1.2 // 全屏后稍作停留,然后缩小回原始大小 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { withAnimation(.spring(response: 0.8, dampingFraction: 0.7)) { self.scale = 1.0 // 显示媒体内容 self.showScalingOverlay = true if mediaType == .all { loadVideo() } else if mediaType == .image { loadImage() } // 标记显示媒体,隐藏GIF self.showMedia = true } } } } } } .frame(width: 300, height: 300) case .none: // FIXME: 临时使用 BlindLoading GIF GIFView(name: "BlindLoading") .frame(width: 300, height: 300) // SVGImage(svgName: "BlindNone") // .frame(width: 300, height: 300) } } .offset(y: -50) .compositingGroup() .padding() } // 只在未显示媒体且未播放动画时显示文字 if !showScalingOverlay && !showMedia { VStack(alignment: .leading, spacing: 8) { // 从变量blindGenerate中获取description Text(blindGenerate?.name ?? "Some box") .font(Typography.font(for: .body, family: .quicksandBold)) .foregroundColor(Color.themeTextMessageMain) Text(blindGenerate?.description ?? "") .font(.system(size: 14)) .foregroundColor(Color.themeTextMessageMain) } .frame(width: UIScreen.main.bounds.width * 0.70, alignment: .leading) .padding() .offset(x: -10, y: UIScreen.main.bounds.height * 0.2) } } .padding() .frame( maxWidth: .infinity, maxHeight: UIScreen.main.bounds.height * 0.65 ) .opacity(showScalingOverlay ? 0 : 1) .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 { // 准备就绪点击,开启盲盒 // 标记盲盒开启 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 { 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) } } .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: .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 } } // 预览第一个盲盒 #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) {} // }