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" } private enum BlindBoxAnimationPhase { case loading case ready case opening } 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 { 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 MemberProfile: Codable { let materialCounter: MaterialCounter let userInfo: UserInfo let storiesCount: Int let conversationsCount: Int let remainPoints: Int let totalPoints: Int let usedBytes: Int let totalBytes: Int let titleRankings: [String] let medalInfos: [MedalInfo] let membershipLevel: String let membershipEndAt: String enum CodingKeys: String, CodingKey { case materialCounter = "material_counter" case userInfo = "user_info" case storiesCount = "stories_count" case conversationsCount = "conversations_count" case remainPoints = "remain_points" case totalPoints = "total_points" case usedBytes = "used_bytes" case totalBytes = "total_bytes" case titleRankings = "title_rankings" case medalInfos = "medal_infos" case membershipLevel = "membership_level" case membershipEndAt = "membership_end_at" } } // 盲盒数量 struct BlindCount: Codable { let availableQuantity: Int enum CodingKeys: String, CodingKey { case availableQuantity = "available_quantity" } } struct MaterialCounter: Codable { let userId: Int64 let totalCount: MediaCount let categoryCount: [String: MediaCount] enum CodingKeys: String, CodingKey { case userId = "user_id" case totalCount = "total_count" case categoryCount = "category_count" } } struct MediaCount: Codable { let videoCount: Int let photoCount: Int let liveCount: Int let videoLength: Double let coverUrl: String? enum CodingKeys: String, CodingKey { case videoCount = "video_count" case photoCount = "photo_count" case liveCount = "live_count" case videoLength = "video_length" case coverUrl = "cover_url" } } struct UserInfo: Codable { let userId: String let accessToken: String let avatarFileUrl: String? let nickname: String let account: String let email: String let refreshToken: String? enum CodingKeys: String, CodingKey { case userId = "user_id" case accessToken = "access_token" case avatarFileUrl = "avatar_file_url" case nickname case account case email case refreshToken = "refresh_token" } } struct MedalInfo: Codable, Identifiable { let id: Int let url: String } let mediaType: BlindBoxMediaType @State private var showModal = false // 控制用户资料弹窗显示 @State private var showSettings = false // 控制设置页面显示 @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 showLottieAnimation = true @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? // 查询数据 - 简单查询 @Query private var login: [Login] init(mediaType: BlindBoxMediaType) { self.mediaType = mediaType } 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) } } } // 盲盒数量 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 ?? [] print("✅ 成功获取 \(self.blindList.count) 个盲盒") case .failure(let error): self.blindList = [] print("❌ 获取盲盒列表失败:", error.localizedDescription) } } } } } private func loadData() { // 会员信息 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("✅ Successfully fetched user info:", response.data) case .failure(let error): print("❌ Failed to fetch user info:", error) } } } } private func loadImage() { guard let url = URL(string: MediaURLs.imageURL) else { 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 loadVideo() { guard let url = URL(string: MediaURLs.videoURL) else { 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 } player.isMuted = true self.videoPlayer = player } 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)") loadMedia() } if showScalingOverlay { ZStack { VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialLight)) .opacity(0.3) .edgesIgnoringSafeArea(.all) Group { if mediaType == .video, 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 == .video, let videoURL = URL(string: MediaURLs.videoURL) { Router.shared.navigate(to: .blindOutcome(media: .video(videoURL, nil))) } else if mediaType == .image, let image = displayImage { Router.shared.navigate(to: .blindOutcome(media: .image(image))) } }) { Image(systemName: "chevron.left.circle.fill") .font(.system(size: 36)) .foregroundColor(.black) .padding(12) .clipShape(Circle()) } 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) { // 顶部导航栏 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) // 标题 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) // 盲盒 ZStack { // 1. 背景SVG if !showScalingOverlay { SVGImage(svgName: "BlindBg") .frame( width: UIScreen.main.bounds.width * 1.8, height: UIScreen.main.bounds.height * 0.85 ) .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 !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("点击了盲盒") withAnimation { animationPhase = .opening } } } .frame(width: 300, height: 300) case .opening: GIFView(name: "BlindOpen") .frame(width: 300, height: 300) .onAppear { self.loadMedia() // Start animation after media is loaded DispatchQueue.main.asyncAfter(deadline: .now() + 5) { self.startScalingAnimation() } } } } .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") .font(Typography.font(for: .body, family: .quicksandBold)) .foregroundColor(Color.themeTextMessageMain) Text("informationinformationinformationinformationinformationinformation") .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) } } .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) // 打开 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) } .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 ) } .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) } 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) {} }