175 lines
6.6 KiB
Swift
175 lines
6.6 KiB
Swift
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<Void, Never>? = nil
|
||
private var countdownTask: Task<Void, Never>? = 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<MemberProfileResponse, NetworkError>) 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)")
|
||
}
|
||
}
|
||
}
|