refactor: BlindBoxViewModel驱动状态更新
This commit is contained in:
parent
794742b6fd
commit
5017594762
@ -12,7 +12,7 @@ enum BlindBoxPolling {
|
||||
do {
|
||||
let result = try await BlindBoxApi.shared.getBlindBox(boxId: boxId)
|
||||
if let data = result {
|
||||
if data.status == "Unopened" {
|
||||
if data.status.lowercased() == "unopened" {
|
||||
continuation.yield(data)
|
||||
continuation.finish()
|
||||
break
|
||||
@ -42,7 +42,7 @@ enum BlindBoxPolling {
|
||||
while !Task.isCancelled {
|
||||
do {
|
||||
let list = try await BlindBoxApi.shared.getBlindBoxList()
|
||||
if let item = list?.first(where: { $0.status == "Unopened" }) {
|
||||
if let item = list?.first(where: { $0.status.lowercased() == "unopened" }) {
|
||||
continuation.yield(item)
|
||||
continuation.finish()
|
||||
break
|
||||
|
||||
174
wake/View/Blind/BlindBoxViewModel.swift
Normal file
174
wake/View/Blind/BlindBoxViewModel.swift
Normal file
@ -0,0 +1,174 @@
|
||||
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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -71,33 +71,16 @@ struct AVPlayerController: UIViewControllerRepresentable {
|
||||
struct BlindBoxView: View {
|
||||
let mediaType: BlindBoxMediaType
|
||||
let currentBoxId: String?
|
||||
|
||||
@StateObject private var viewModel: BlindBoxViewModel
|
||||
@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 pollingTask: Task<Void, Never>? = nil
|
||||
@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, 0)
|
||||
@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 animationPhase: BlindBoxAnimationPhase = .none
|
||||
@State private var scale: CGFloat = 0.1
|
||||
@State private var videoPlayer: AVPlayer?
|
||||
@State private var showControls = false
|
||||
@ -113,6 +96,7 @@ struct BlindBoxView: View {
|
||||
init(mediaType: BlindBoxMediaType, blindBoxId: String? = nil) {
|
||||
self.mediaType = mediaType
|
||||
self.currentBoxId = blindBoxId
|
||||
_viewModel = StateObject(wrappedValue: BlindBoxViewModel(mediaType: mediaType, currentBoxId: blindBoxId))
|
||||
}
|
||||
|
||||
// 倒计时
|
||||
@ -142,222 +126,16 @@ struct BlindBoxView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func loadBlindBox() async {
|
||||
print("loadMedia called with mediaType: \(mediaType)")
|
||||
// 已由 ViewModel 承担加载与轮询逻辑
|
||||
|
||||
// 启动轮询任务(可取消)
|
||||
stopPolling()
|
||||
isPolling = true
|
||||
if self.currentBoxId != nil {
|
||||
print("指定监听某盲盒结果: ", self.currentBoxId! as Any)
|
||||
pollingTask = Task { @MainActor in
|
||||
await pollingToQuerySingleBox()
|
||||
}
|
||||
} else {
|
||||
pollingTask = Task { @MainActor in
|
||||
await pollingToQueryBlindBox()
|
||||
}
|
||||
}
|
||||
// 已迁移至 ViewModel
|
||||
|
||||
// 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<APIResponse<[BlindList]>, 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
|
||||
// }
|
||||
// 已迁移至 ViewModel
|
||||
|
||||
// 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<MemberProfileResponse, NetworkError>) 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<APIResponse<BlindCount>, 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 {
|
||||
guard let boxId = self.currentBoxId else { return }
|
||||
do {
|
||||
for try await data in BlindBoxPolling.singleBox(boxId: boxId, intervalSeconds: 2.0) {
|
||||
self.blindGenerate = data
|
||||
if mediaType == .image {
|
||||
self.imageURL = data.resultFile?.url ?? ""
|
||||
} else {
|
||||
self.videoURL = data.resultFile?.url ?? ""
|
||||
}
|
||||
withAnimation { self.animationPhase = .ready }
|
||||
stopPolling()
|
||||
break
|
||||
}
|
||||
} catch is CancellationError {
|
||||
// 任务被取消
|
||||
} catch {
|
||||
print("❌ 获取盲盒数据失败: \(error)")
|
||||
self.animationPhase = .none
|
||||
stopPolling()
|
||||
}
|
||||
}
|
||||
|
||||
private func pollingToQueryBlindBox() async {
|
||||
do {
|
||||
for try await blindBox in BlindBoxPolling.firstUnopened(intervalSeconds: 2.0) {
|
||||
self.blindGenerate = blindBox
|
||||
if mediaType == .image {
|
||||
self.imageURL = blindBox.resultFile?.url ?? ""
|
||||
} else {
|
||||
self.videoURL = blindBox.resultFile?.url ?? ""
|
||||
}
|
||||
withAnimation { self.animationPhase = .ready }
|
||||
print("✅ 成功获取盲盒数据: \(blindBox.name), 状态: \(blindBox.status)")
|
||||
stopPolling()
|
||||
break
|
||||
}
|
||||
} catch is CancellationError {
|
||||
// 任务被取消
|
||||
} catch {
|
||||
print("❌ 获取盲盒列表失败: \(error)")
|
||||
stopPolling()
|
||||
}
|
||||
}
|
||||
|
||||
// 轮询接口
|
||||
private func startPolling() {
|
||||
stopPolling()
|
||||
isPolling = true
|
||||
checkBlindBoxStatus()
|
||||
}
|
||||
|
||||
private func stopPolling() {
|
||||
pollingTimer?.invalidate()
|
||||
pollingTimer = nil
|
||||
isPolling = false
|
||||
pollingTask?.cancel()
|
||||
pollingTask = nil
|
||||
}
|
||||
|
||||
private func checkBlindBoxStatus() {
|
||||
guard !currentBoxType.isEmpty else {
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
|
||||
// NetworkService.shared.postWithToken(
|
||||
// path: "/blind_box/generate/mock",
|
||||
// parameters: ["box_type": currentBoxType]
|
||||
// ) { (result: Result<GenerateBlindBoxResponse, NetworkError>) 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()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
// 已迁移至 ViewModel
|
||||
|
||||
private func loadImage() {
|
||||
guard !imageURL.isEmpty, let url = URL(string: imageURL) else {
|
||||
guard !viewModel.imageURL.isEmpty, let url = URL(string: viewModel.imageURL) else {
|
||||
print("⚠️ 图片URL无效或为空")
|
||||
return
|
||||
}
|
||||
@ -375,7 +153,7 @@ struct BlindBoxView: View {
|
||||
}
|
||||
|
||||
private func loadVideo() {
|
||||
guard !videoURL.isEmpty, let url = URL(string: videoURL) else {
|
||||
guard !viewModel.videoURL.isEmpty, let url = URL(string: viewModel.videoURL) else {
|
||||
print("⚠️ 视频URL无效或为空")
|
||||
return
|
||||
}
|
||||
@ -401,7 +179,7 @@ struct BlindBoxView: View {
|
||||
}
|
||||
|
||||
private func prepareVideo() {
|
||||
guard !videoURL.isEmpty, let url = URL(string: videoURL) else {
|
||||
guard !viewModel.videoURL.isEmpty, let url = URL(string: viewModel.videoURL) else {
|
||||
print("⚠️ 视频URL无效或为空")
|
||||
return
|
||||
}
|
||||
@ -425,7 +203,7 @@ struct BlindBoxView: View {
|
||||
}
|
||||
|
||||
private func prepareImage() {
|
||||
guard !imageURL.isEmpty, let url = URL(string: imageURL) else {
|
||||
guard !viewModel.imageURL.isEmpty, let url = URL(string: viewModel.imageURL) else {
|
||||
print("⚠️ 图片URL无效或为空")
|
||||
return
|
||||
}
|
||||
@ -510,11 +288,11 @@ struct BlindBoxView: View {
|
||||
// }
|
||||
// 调用接口
|
||||
Task {
|
||||
await loadBlindBox()
|
||||
await viewModel.load()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
stopPolling()
|
||||
viewModel.stopPolling()
|
||||
countdownTimer?.invalidate()
|
||||
countdownTimer = nil
|
||||
|
||||
@ -529,6 +307,46 @@ struct BlindBoxView: View {
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
.onChange(of: viewModel.blindGenerate?.status) { status in
|
||||
guard let status = status?.lowercased() else { return }
|
||||
if status == "unopened" {
|
||||
withAnimation { self.animationPhase = .ready }
|
||||
} else if status == "preparing" {
|
||||
withAnimation { self.animationPhase = .loading }
|
||||
}
|
||||
}
|
||||
.onChange(of: animationPhase) { phase in
|
||||
if phase != .loading {
|
||||
countdownTimer?.invalidate()
|
||||
countdownTimer = nil
|
||||
}
|
||||
}
|
||||
.onChange(of: viewModel.videoURL) { url in
|
||||
if !url.isEmpty {
|
||||
withAnimation { self.animationPhase = .ready }
|
||||
countdownTimer?.invalidate()
|
||||
countdownTimer = nil
|
||||
}
|
||||
}
|
||||
.onChange(of: viewModel.imageURL) { url in
|
||||
if !url.isEmpty {
|
||||
withAnimation { self.animationPhase = .ready }
|
||||
countdownTimer?.invalidate()
|
||||
countdownTimer = nil
|
||||
}
|
||||
}
|
||||
.onChange(of: viewModel.didBootstrap) { done in
|
||||
guard done else { return }
|
||||
// 根据首帧状态决定初始动画态,避免先显示 loading 再跳到 ready 的割裂感
|
||||
if viewModel.blindGenerate?.status.lowercased() == "unopened" {
|
||||
withAnimation { self.animationPhase = .ready }
|
||||
} else if viewModel.blindGenerate?.status.lowercased() == "preparing" {
|
||||
withAnimation { self.animationPhase = .loading }
|
||||
} else {
|
||||
// 若未知状态,保持 none;后续 onChange 会驱动到正确态
|
||||
self.animationPhase = .none
|
||||
}
|
||||
}
|
||||
|
||||
if showScalingOverlay {
|
||||
ZStack {
|
||||
@ -565,10 +383,10 @@ struct BlindBoxView: View {
|
||||
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))
|
||||
if mediaType == .all, !viewModel.videoURL.isEmpty, let url = URL(string: viewModel.videoURL) {
|
||||
Router.shared.navigate(to: .blindOutcome(media: .video(url, nil), time: viewModel.blindGenerate?.name ?? "Your box", description:viewModel.blindGenerate?.description ?? "", isMember: viewModel.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))
|
||||
Router.shared.navigate(to: .blindOutcome(media: .image(image), time: viewModel.blindGenerate?.name ?? "Your box", description:viewModel.blindGenerate?.description ?? "", isMember: viewModel.isMember))
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
@ -647,7 +465,7 @@ struct BlindBoxView: View {
|
||||
// LoginView()
|
||||
// }
|
||||
NavigationLink(destination: SubscribeView()) {
|
||||
Text("\(memberProfile?.remainPoints ?? 0)")
|
||||
Text("\(viewModel.memberProfile?.remainPoints ?? 0)")
|
||||
.font(Typography.font(for: .subtitle))
|
||||
.fontWeight(.bold)
|
||||
.padding(.horizontal, 12)
|
||||
@ -694,7 +512,7 @@ struct BlindBoxView: View {
|
||||
SVGImage(svgName: "BlindCount")
|
||||
.frame(width: 100, height: 60)
|
||||
|
||||
Text("\(blindCount?.availableQuantity ?? 0) Boxes")
|
||||
Text("\(viewModel.blindCount?.availableQuantity ?? 0) Boxes")
|
||||
.font(Typography.font(for: .body, family: .quicksandBold))
|
||||
.foregroundColor(.white)
|
||||
.offset(x: 6, y: -18)
|
||||
@ -741,7 +559,7 @@ struct BlindBoxView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
if let boxId = self.blindGenerate?.id {
|
||||
if let boxId = self.viewModel.blindGenerate?.id {
|
||||
Task {
|
||||
do {
|
||||
try await BlindBoxApi.shared.openBlindBox(boxId: boxId)
|
||||
@ -802,8 +620,8 @@ struct BlindBoxView: View {
|
||||
.frame(width: 300, height: 300)
|
||||
|
||||
case .none:
|
||||
// FIXME: 临时使用 BlindLoading GIF
|
||||
GIFView(name: "BlindLoading")
|
||||
// 首帧占位,避免加载时闪烁
|
||||
Color.clear
|
||||
.frame(width: 300, height: 300)
|
||||
// SVGImage(svgName: "BlindNone")
|
||||
// .frame(width: 300, height: 300)
|
||||
@ -817,10 +635,10 @@ struct BlindBoxView: View {
|
||||
if !showScalingOverlay && !showMedia {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// 从变量blindGenerate中获取description
|
||||
Text(blindGenerate?.name ?? "Some box")
|
||||
Text(viewModel.blindGenerate?.name ?? "Some box")
|
||||
.font(Typography.font(for: .body, family: .quicksandBold))
|
||||
.foregroundColor(Color.themeTextMessageMain)
|
||||
Text(blindGenerate?.description ?? "")
|
||||
Text(viewModel.blindGenerate?.description ?? "")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(Color.themeTextMessageMain)
|
||||
}
|
||||
@ -840,7 +658,7 @@ struct BlindBoxView: View {
|
||||
.animation(.easeInOut(duration: 1.5), value: showScalingOverlay)
|
||||
|
||||
// 打开 TODO 引导时,也要有按钮
|
||||
if mediaType == .all {
|
||||
if mediaType == .all, viewModel.didBootstrap {
|
||||
Button(action: {
|
||||
if animationPhase == .ready {
|
||||
// 准备就绪点击,开启盲盒
|
||||
@ -855,7 +673,7 @@ struct BlindBoxView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
if let boxId = self.blindGenerate?.id {
|
||||
if let boxId = self.viewModel.blindGenerate?.id {
|
||||
Task {
|
||||
do {
|
||||
try await BlindBoxApi.shared.openBlindBox(boxId: boxId)
|
||||
@ -922,8 +740,8 @@ struct BlindBoxView: View {
|
||||
UserProfileModal(
|
||||
showModal: $showModal,
|
||||
showSettings: $showSettings,
|
||||
isMember: $isMember,
|
||||
memberDate: $memberDate
|
||||
isMember: .init(get: { viewModel.isMember }, set: { viewModel.isMember = $0 }),
|
||||
memberDate: .init(get: { viewModel.memberDate }, set: { viewModel.memberDate = $0 })
|
||||
)
|
||||
}
|
||||
.offset(x: showSettings ? UIScreen.main.bounds.width : 0)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user