import Foundation import Combine @MainActor final class BlindBoxViewModel: ObservableObject { // Inputs let mediaType: BlindBoxMediaType let currentBoxId: String? // Published state @Published var isMember: Bool = false @Published var memberDate: String = "" @Published var memberProfile: MemberProfile? = nil @Published var blindCount: BlindCount? = nil @Published var blindGenerate: BlindBoxData? = nil @Published var videoURL: String = "" @Published var imageURL: String = "" @Published var didBootstrap: Bool = false @Published var countdownText: String = "" // Tasks private var pollingTask: Task? = nil private var countdownTask: Task? = nil private var remainingSeconds: Int = 0 init(mediaType: BlindBoxMediaType, currentBoxId: String?) { self.mediaType = mediaType self.currentBoxId = currentBoxId } func load() async { await bootstrapInitialState() await startPolling() loadMemberProfile() await loadBlindCount() } func startPolling() async { // 如果已经是 Unopened,无需继续轮询 if blindGenerate?.status == "Unopened" { return } stopPolling() if let boxId = currentBoxId { // Poll a single box until unopened pollingTask = Task { @MainActor [weak self] in guard let self else { return } do { for try await data in BlindBoxPolling.singleBox(boxId: boxId, intervalSeconds: 2.0) { print("[VM] SingleBox polled status: \(data.status)") self.blindGenerate = data if self.mediaType == .image { self.imageURL = data.resultFile?.url ?? "" } else { self.videoURL = data.resultFile?.url ?? "" } self.applyStatusSideEffects() break } } catch is CancellationError { // cancelled } catch { print("❌ BlindBoxViewModel polling error (single): \(error)") } } } else { // Poll list and yield first unopened pollingTask = Task { @MainActor [weak self] in guard let self else { return } do { for try await item in BlindBoxPolling.firstUnopened(intervalSeconds: 2.0) { print("[VM] List polled first unopened: id=\(item.id ?? "nil"), status=\(item.status)") self.blindGenerate = item if self.mediaType == .image { self.imageURL = item.resultFile?.url ?? "" } else { self.videoURL = item.resultFile?.url ?? "" } self.applyStatusSideEffects() break } } catch is CancellationError { // cancelled } catch { print("❌ BlindBoxViewModel polling error (list): \(error)") } } } } private func bootstrapInitialState() async { if let boxId = currentBoxId { do { let data = try await BlindBoxApi.shared.getBlindBox(boxId: boxId) if let data = data { self.blindGenerate = data if mediaType == .image { self.imageURL = data.resultFile?.url ?? "" } else { self.videoURL = data.resultFile?.url ?? "" } self.applyStatusSideEffects() } } catch { print("❌ bootstrapInitialState (single) failed: \(error)") } } else { do { let list = try await BlindBoxApi.shared.getBlindBoxList() // 更新未开启数量 let count = (list ?? []).filter { $0.status == "Unopened" }.count self.blindCount = BlindCount(availableQuantity: count) if let item = list?.first(where: { $0.status == "Unopened" }) { self.blindGenerate = item if mediaType == .image { self.imageURL = item.resultFile?.url ?? "" } else { self.videoURL = item.resultFile?.url ?? "" } self.applyStatusSideEffects() } else if let first = list?.first { // 没有 Unopened,选取第一个用于展示状态(通常是 Preparing) self.blindGenerate = first self.applyStatusSideEffects() } } catch { print("❌ bootstrapInitialState (list) failed: \(error)") } } // 标记首帧状态已准备,供视图决定是否显示 loading/ready self.didBootstrap = true } func stopPolling() { pollingTask?.cancel() pollingTask = nil } func openBlindBox(for id: String) async throws { try await BlindBoxApi.shared.openBlindBox(boxId: id) } private func loadMemberProfile() { NetworkService.shared.get( path: "/membership/personal-center-info", parameters: nil ) { [weak self] (result: Result) in Task { @MainActor in guard let self else { return } 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) } } } } private func loadBlindCount() async { do { let list = try await BlindBoxApi.shared.getBlindBoxList() let count = (list ?? []).filter { $0.status == "Unopened" }.count self.blindCount = BlindCount(availableQuantity: count) } catch { print("❌ 获取盲盒列表失败: \(error)") } } }