fix: 盲盒展示
This commit is contained in:
parent
0ab33cab47
commit
411bf440d4
@ -0,0 +1,9 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// 盲盒动画阶段
|
||||||
|
enum BlindBoxAnimationPhase {
|
||||||
|
case loading
|
||||||
|
case ready
|
||||||
|
case opening
|
||||||
|
case none
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Lottie
|
||||||
|
|
||||||
|
/// 统一管理盲盒开启动画 4 状态的组件:loading / ready / opening / none
|
||||||
|
struct BlindBoxAnimationView: View {
|
||||||
|
@Binding var phase: BlindBoxAnimationPhase
|
||||||
|
let onTapReady: () -> Void
|
||||||
|
let onOpeningCompleted: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
switch phase {
|
||||||
|
case .loading:
|
||||||
|
LottieView(name: "loading", isPlaying: true)
|
||||||
|
case .ready:
|
||||||
|
ZStack {
|
||||||
|
LottieView(name: "ready", isPlaying: true)
|
||||||
|
Color.clear
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
onTapReady()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .opening:
|
||||||
|
BlindBoxLottieOnceView(name: "opening") {
|
||||||
|
onOpeningCompleted()
|
||||||
|
}
|
||||||
|
case .none:
|
||||||
|
Image("Empty")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 300, height: 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Lottie
|
||||||
|
|
||||||
|
/// 仅播放一次并在完成时回调的 Lottie 视图
|
||||||
|
struct BlindBoxLottieOnceView: UIViewRepresentable {
|
||||||
|
let name: String
|
||||||
|
var animationSpeed: CGFloat = 1.0
|
||||||
|
let onCompleted: () -> Void
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> LottieAnimationView {
|
||||||
|
let animationView = LottieAnimationView()
|
||||||
|
if let animation = LottieAnimation.named(name) {
|
||||||
|
animationView.animation = animation
|
||||||
|
} else if let path = Bundle.main.path(forResource: name, ofType: "json") {
|
||||||
|
let animation = LottieAnimation.filepath(path)
|
||||||
|
animationView.animation = animation
|
||||||
|
}
|
||||||
|
animationView.loopMode = .playOnce
|
||||||
|
animationView.animationSpeed = animationSpeed
|
||||||
|
animationView.contentMode = .scaleAspectFit
|
||||||
|
animationView.backgroundBehavior = .pauseAndRestore
|
||||||
|
animationView.play { _ in
|
||||||
|
onCompleted()
|
||||||
|
}
|
||||||
|
return animationView
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: LottieAnimationView, context: Context) {
|
||||||
|
// 单次播放,不需要在更新时重复触发
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,13 +8,6 @@ extension Notification.Name {
|
|||||||
static let blindBoxStatusChanged = Notification.Name("blindBoxStatusChanged")
|
static let blindBoxStatusChanged = Notification.Name("blindBoxStatusChanged")
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum BlindBoxAnimationPhase {
|
|
||||||
case loading
|
|
||||||
case ready
|
|
||||||
case opening
|
|
||||||
case none
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let navigateToMediaViewer = Notification.Name("navigateToMediaViewer")
|
static let navigateToMediaViewer = Notification.Name("navigateToMediaViewer")
|
||||||
}
|
}
|
||||||
@ -28,33 +21,18 @@ struct BlindBoxView: View {
|
|||||||
@State private var showSettings = false // 控制设置页面显示
|
@State private var showSettings = false // 控制设置页面显示
|
||||||
@State private var showLogin = false
|
@State private var showLogin = false
|
||||||
// 倒计时由 ViewModel 管理(countdownText)
|
// 倒计时由 ViewModel 管理(countdownText)
|
||||||
// 盲盒数据
|
|
||||||
@State private var showScalingOverlay = false
|
|
||||||
@State private var animationPhase: BlindBoxAnimationPhase = .none
|
@State private var animationPhase: BlindBoxAnimationPhase = .none
|
||||||
@State private var scale: CGFloat = 0.1
|
|
||||||
// showControls 状态已迁移至 BlindBoxMediaOverlay 组件内
|
|
||||||
@State private var isAnimating = true
|
|
||||||
@State private var showMedia = false
|
|
||||||
|
|
||||||
// 查询数据 - 简单查询
|
// 查询数据 - 简单查询
|
||||||
@Query private var login: [Login]
|
@Query private var login: [Login]
|
||||||
|
|
||||||
init(mediaType: BlindBoxMediaType, blindBoxId: String? = nil) {
|
init(mediaType: BlindBoxMediaType, blindBoxId: String? = nil) {
|
||||||
self.mediaType = mediaType
|
self.mediaType = mediaType
|
||||||
self.currentBoxId = blindBoxId
|
self.currentBoxId = blindBoxId
|
||||||
_viewModel = StateObject(wrappedValue: BlindBoxViewModel(mediaType: mediaType, currentBoxId: blindBoxId))
|
_viewModel = StateObject(wrappedValue: BlindBoxViewModel(mediaType: mediaType, currentBoxId: blindBoxId))
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算尺寸逻辑已迁移至 BlindBoxMediaOverlay 组件
|
// 计算尺寸逻辑已迁移至 BlindBoxMediaOverlay 组件(已不再使用)
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@ -63,7 +41,7 @@ struct BlindBoxView: View {
|
|||||||
Perf.event("BlindBox_Appear")
|
Perf.event("BlindBox_Appear")
|
||||||
print("🎯 BlindBoxView appeared with mediaType: \(mediaType)")
|
print("🎯 BlindBoxView appeared with mediaType: \(mediaType)")
|
||||||
print("🎯 Current thread: \(Thread.current)")
|
print("🎯 Current thread: \(Thread.current)")
|
||||||
|
|
||||||
// 调用接口
|
// 调用接口
|
||||||
Task {
|
Task {
|
||||||
await viewModel.load()
|
await viewModel.load()
|
||||||
@ -100,12 +78,12 @@ struct BlindBoxView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: viewModel.videoURL) { _, url in
|
.onChange(of: viewModel.videoURL) { _, url in
|
||||||
if !url.isEmpty {
|
if !url.isEmpty && self.animationPhase != .opening {
|
||||||
withAnimation { self.animationPhase = .ready }
|
withAnimation { self.animationPhase = .ready }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: viewModel.imageURL) { _, url in
|
.onChange(of: viewModel.imageURL) { _, url in
|
||||||
if !url.isEmpty {
|
if !url.isEmpty && self.animationPhase != .opening {
|
||||||
withAnimation { self.animationPhase = .ready }
|
withAnimation { self.animationPhase = .ready }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,243 +101,144 @@ struct BlindBoxView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if showScalingOverlay {
|
// 原 overlay 分支已移除,直接展示内容
|
||||||
BlindBoxMediaOverlay(
|
// Original content
|
||||||
mediaType: mediaType,
|
VStack {
|
||||||
player: .init(get: { viewModel.player }, set: { viewModel.player = $0 }),
|
VStack(spacing: 20) {
|
||||||
displayImage: viewModel.displayImage,
|
if mediaType == .all {
|
||||||
isPortrait: viewModel.isPortrait,
|
BlindBoxHeaderBar(
|
||||||
aspectRatio: viewModel.aspectRatio,
|
onMenuTap: showUserProfile,
|
||||||
scale: $scale,
|
remainPoints: viewModel.memberProfile?.remainPoints ?? 0,
|
||||||
onBack: {
|
showLogin: $showLogin
|
||||||
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 = viewModel.displayImage {
|
|
||||||
Router.shared.navigate(to: .blindOutcome(media: .image(image), time: viewModel.blindGenerate?.name ?? "Your box", description: viewModel.blindGenerate?.description ?? "", isMember: viewModel.isMember))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Original content
|
|
||||||
VStack {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
if mediaType == .all {
|
|
||||||
BlindBoxHeaderBar(
|
|
||||||
onMenuTap: showUserProfile,
|
|
||||||
remainPoints: viewModel.memberProfile?.remainPoints ?? 0,
|
|
||||||
showLogin: $showLogin
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标题
|
|
||||||
BlindBoxTitleView()
|
|
||||||
.opacity(showScalingOverlay ? 0 : 1)
|
|
||||||
.offset(y: showScalingOverlay ? -UIScreen.main.bounds.height * 0.2 : 0)
|
|
||||||
.animation(.easeInOut(duration: 0.5), value: showScalingOverlay)
|
|
||||||
|
|
||||||
// 盲盒
|
|
||||||
ZStack {
|
|
||||||
// 1. 背景Card
|
|
||||||
if !showScalingOverlay {
|
|
||||||
CardBlindBackground()
|
|
||||||
}
|
|
||||||
if mediaType == .all && !showScalingOverlay {
|
|
||||||
BlindCountBadge(text: "\(viewModel.blindCount?.availableQuantity ?? 0) Boxes")
|
|
||||||
.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:
|
|
||||||
LottieView(name: "ready", isPlaying: animationPhase == .loading && !showScalingOverlay)
|
|
||||||
.frame(width: 300, height: 300)
|
|
||||||
// .onAppear {
|
|
||||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
|
|
||||||
// withAnimation {
|
|
||||||
// animationPhase = .ready
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
case .ready:
|
|
||||||
ZStack {
|
|
||||||
LottieView(name: "ready", isPlaying: animationPhase == .ready && !showScalingOverlay)
|
|
||||||
.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 {
|
|
||||||
Perf.event("BlindBox_Open_Tapped")
|
|
||||||
print("点击了盲盒")
|
|
||||||
|
|
||||||
let boxIdToOpen = self.currentBoxId ?? self.viewModel.blindGenerate?.id
|
|
||||||
if let boxId = boxIdToOpen {
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
try await viewModel.openBlindBox(for: boxId)
|
|
||||||
print("✅ 盲盒开启成功")
|
|
||||||
} catch {
|
|
||||||
print("❌ 开启盲盒失败: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
withAnimation {
|
|
||||||
animationPhase = .opening
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(width: 300, height: 300)
|
|
||||||
|
|
||||||
case .opening:
|
|
||||||
ZStack {
|
|
||||||
if !showMedia {
|
|
||||||
LottieView(name: "ready", loopMode: .playOnce, isPlaying: !showMedia)
|
|
||||||
.frame(width: 300, height: 300)
|
|
||||||
.scaleEffect(scale)
|
|
||||||
}
|
|
||||||
// 当显示媒体时,移除 GIFView 避免后台播放
|
|
||||||
Color.clear
|
|
||||||
.onAppear {
|
|
||||||
Perf.event("BlindBox_Opening_Begin")
|
|
||||||
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
|
|
||||||
|
|
||||||
// 显示媒体内容
|
|
||||||
Perf.event("BlindBox_Opening_ShowMedia")
|
|
||||||
self.showScalingOverlay = true
|
|
||||||
Task { await viewModel.prepareMedia() }
|
|
||||||
|
|
||||||
// 标记显示媒体,隐藏GIF
|
|
||||||
self.showMedia = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(width: 300, height: 300)
|
|
||||||
|
|
||||||
case .none:
|
|
||||||
// 首帧占位,避免加载时闪烁
|
|
||||||
LottieView(name: "ready", loopMode: .loop, isPlaying: true)
|
|
||||||
.frame(width: 300, height: 300)
|
|
||||||
.scaleEffect(scale)
|
|
||||||
// Color.clear
|
|
||||||
// .frame(width: 300, height: 300)
|
|
||||||
// SVGImage(svgName: "BlindNone")
|
|
||||||
// .frame(width: 300, height: 300)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.offset(y: -50)
|
|
||||||
.compositingGroup()
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
// 只在未显示媒体且未播放动画时显示文字
|
|
||||||
if !showScalingOverlay && !showMedia {
|
|
||||||
BlindBoxDescriptionView(
|
|
||||||
name: viewModel.blindGenerate?.name ?? "Some box",
|
|
||||||
description: viewModel.blindGenerate?.description ?? ""
|
|
||||||
)
|
|
||||||
.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)
|
BlindBoxTitleView()
|
||||||
|
.opacity(animationPhase == .opening ? 0 : 1)
|
||||||
// 打开 TODO 引导时,也要有按钮
|
|
||||||
if mediaType == .all, viewModel.didBootstrap {
|
// 盲盒
|
||||||
BlindBoxActionButton(
|
ZStack {
|
||||||
phase: animationPhase,
|
// 1. 背景Card
|
||||||
countdownText: viewModel.countdownText,
|
CardBlindBackground()
|
||||||
onOpen: {
|
if mediaType == .all {
|
||||||
|
BlindCountBadge(text: "\(viewModel.blindCount?.availableQuantity ?? 0) Boxes")
|
||||||
|
.position(x: UIScreen.main.bounds.width * 0.7,
|
||||||
|
y: UIScreen.main.bounds.height * 0.18)
|
||||||
|
}
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
BlindBoxAnimationView(
|
||||||
|
phase: $animationPhase,
|
||||||
|
onTapReady: {
|
||||||
|
Perf.event("BlindBox_Open_Tapped")
|
||||||
|
print("点击了盲盒")
|
||||||
let boxIdToOpen = self.currentBoxId ?? self.viewModel.blindGenerate?.id
|
let boxIdToOpen = self.currentBoxId ?? self.viewModel.blindGenerate?.id
|
||||||
if let boxId = boxIdToOpen {
|
if let boxId = boxIdToOpen {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await viewModel.openBlindBox(for: boxId)
|
try await viewModel.openBlindBox(for: boxId)
|
||||||
print("✅ 盲盒开启成功")
|
print("✅ 盲盒开启成功")
|
||||||
|
await viewModel.startPolling()
|
||||||
|
withAnimation {
|
||||||
|
animationPhase = .opening
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("❌ 开启盲盒失败: \(error)")
|
print("❌ 开启盲盒失败: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
withAnimation {
|
|
||||||
animationPhase = .opening
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onGoToBuy: {
|
onOpeningCompleted: {
|
||||||
Router.shared.navigate(to: .mediaUpload)
|
navigateToOutcome()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.padding(.horizontal)
|
}
|
||||||
|
.offset(y: -50)
|
||||||
|
.compositingGroup()
|
||||||
|
.padding()
|
||||||
|
// 非 opening 阶段显示文字
|
||||||
|
if animationPhase != .opening {
|
||||||
|
BlindBoxDescriptionView(
|
||||||
|
name: viewModel.blindGenerate?.name ?? "Some box",
|
||||||
|
description: viewModel.blindGenerate?.description ?? ""
|
||||||
|
)
|
||||||
|
.offset(x: -10, y: UIScreen.main.bounds.height * 0.2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.padding()
|
||||||
.background(Color.themeTextWhiteSecondary)
|
.frame(
|
||||||
.offset(x: showModal ? UIScreen.main.bounds.width * 0.8 : 0)
|
maxWidth: .infinity,
|
||||||
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showModal)
|
maxHeight: UIScreen.main.bounds.height * 0.65
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户资料弹窗
|
|
||||||
SlideInModal(
|
|
||||||
isPresented: $showModal,
|
|
||||||
onDismiss: hideUserProfile
|
|
||||||
) {
|
|
||||||
UserProfileModal(
|
|
||||||
showModal: $showModal,
|
|
||||||
showSettings: $showSettings,
|
|
||||||
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)
|
|
||||||
.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)
|
// 打开 TODO 引导时,也要有按钮
|
||||||
.transition(.move(edge: .leading))
|
if mediaType == .all, viewModel.didBootstrap {
|
||||||
.zIndex(1)
|
BlindBoxActionButton(
|
||||||
|
phase: animationPhase,
|
||||||
|
countdownText: viewModel.countdownText,
|
||||||
|
onOpen: {
|
||||||
|
let boxIdToOpen = self.currentBoxId ?? self.viewModel.blindGenerate?.id
|
||||||
|
if let boxId = boxIdToOpen {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await viewModel.openBlindBox(for: boxId)
|
||||||
|
print("✅ 盲盒开启成功")
|
||||||
|
await viewModel.startPolling()
|
||||||
|
withAnimation {
|
||||||
|
animationPhase = .opening
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("❌ 开启盲盒失败: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onGoToBuy: {
|
||||||
|
Router.shared.navigate(to: .mediaUpload)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showSettings)
|
.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: .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)
|
||||||
|
.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)
|
.navigationBarBackButtonHidden(true)
|
||||||
}
|
}
|
||||||
@ -374,7 +253,7 @@ struct BlindBoxView: View {
|
|||||||
for (index, item) in login.enumerated() {
|
for (index, item) in login.enumerated() {
|
||||||
print("记录 \(index + 1): 邮箱=\(item.email), 姓名=\(item.name)")
|
print("记录 \(index + 1): 邮箱=\(item.email), 姓名=\(item.name)")
|
||||||
}
|
}
|
||||||
showModal.toggle()
|
showModal.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,13 +263,65 @@ struct BlindBoxView: View {
|
|||||||
showModal = false
|
showModal = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 隐藏设置页面
|
/// 隐藏设置页面
|
||||||
private func hideSettings() {
|
private func hideSettings() {
|
||||||
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
|
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
|
||||||
showSettings = false
|
showSettings = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 开启动画播放完成后,准备媒体并跳转到结果页
|
||||||
|
private func navigateToOutcome() {
|
||||||
|
Perf.event("BlindBox_Opening_Completed")
|
||||||
|
Task { @MainActor in
|
||||||
|
let interval: UInt64 = 300_000_000 // 300ms
|
||||||
|
let timeout: UInt64 = 6_000_000_000 // 6s
|
||||||
|
var waited: UInt64 = 0
|
||||||
|
|
||||||
|
if mediaType == .all {
|
||||||
|
// 等待视频 URL 就绪
|
||||||
|
while viewModel.videoURL.isEmpty && waited < timeout {
|
||||||
|
try? await Task.sleep(nanoseconds: interval)
|
||||||
|
waited += interval
|
||||||
|
}
|
||||||
|
// 拿到 URL 即可跳转;不强依赖 player 准备
|
||||||
|
if !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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if mediaType == .image {
|
||||||
|
// 等到有 imageURL 后再加载 UIImage
|
||||||
|
while viewModel.imageURL.isEmpty && waited < timeout {
|
||||||
|
try? await Task.sleep(nanoseconds: interval)
|
||||||
|
waited += interval
|
||||||
|
}
|
||||||
|
if viewModel.displayImage == nil && !viewModel.imageURL.isEmpty {
|
||||||
|
await viewModel.prepareMedia()
|
||||||
|
}
|
||||||
|
if let image = viewModel.displayImage {
|
||||||
|
Router.shared.navigate(
|
||||||
|
to: .blindOutcome(
|
||||||
|
media: .image(image),
|
||||||
|
time: viewModel.blindGenerate?.name ?? "Your box",
|
||||||
|
description: viewModel.blindGenerate?.description ?? "",
|
||||||
|
isMember: viewModel.isMember
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 若仍未获取到媒体,记录日志以便排查
|
||||||
|
print("⚠️ navigateToOutcome: 媒体尚未准备好,videoURL=\(viewModel.videoURL), image=\(String(describing: viewModel.displayImage))")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 预览
|
// MARK: - 预览
|
||||||
@ -423,4 +354,4 @@ struct BlindBoxView: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user