refactor: optimize polling and timer with cancellable tasks and reduced update frequency
This commit is contained in:
parent
feadfd92a7
commit
794742b6fd
@ -72,11 +72,11 @@
|
|||||||
## 任务清单(同步 todo)
|
## 任务清单(同步 todo)
|
||||||
- [x] nav-1 统一导航(移除子页面 NavigationView,Router 返回)
|
- [x] nav-1 统一导航(移除子页面 NavigationView,Router 返回)
|
||||||
- [ ] mvvm-1 BlindBox 引入 ViewModel,迁移逻辑
|
- [ ] mvvm-1 BlindBox 引入 ViewModel,迁移逻辑
|
||||||
- [ ] timer-1 计时器降频与取消
|
- [x] timer-1 计时器降频与取消
|
||||||
- [ ] polling-1 轮询可取消化
|
- [x] polling-1 轮询可取消化
|
||||||
- [ ] media-1 媒体与动画优化(GIF->Lottie/可见播放)
|
- [ ] media-1 媒体与动画优化(GIF->Lottie/可见播放)
|
||||||
- [ ] concurrency-1 @MainActor 与线程安全
|
- [ ] concurrency-1 @MainActor 与线程安全
|
||||||
- [ ] netlog-1 网络日志开关与限流
|
- [x] netlog-1 网络日志开关与限流
|
||||||
- [ ] structure-1 目录重组
|
- [ ] structure-1 目录重组
|
||||||
- [ ] perf-1 性能埋点与基线
|
- [ ] perf-1 性能埋点与基线
|
||||||
|
|
||||||
@ -87,6 +87,12 @@
|
|||||||
- 移除 `wake/View/Credits/CreditsDetailView.swift`、`wake/View/Welcome/SplashView.swift`、`wake/View/Owner/SettingsView.swift`、`wake/View/Components/Upload/MediaUpload.swift`(示例 `MediaUploadExample`)、`wake/View/Examples/MediaDemo.swift` 中的 `NavigationView`。
|
- 移除 `wake/View/Credits/CreditsDetailView.swift`、`wake/View/Welcome/SplashView.swift`、`wake/View/Owner/SettingsView.swift`、`wake/View/Components/Upload/MediaUpload.swift`(示例 `MediaUploadExample`)、`wake/View/Examples/MediaDemo.swift` 中的 `NavigationView`。
|
||||||
- 将 `wake/View/Feedback.swift` 中 `FeedbackView` 与 `FeedbackDetailView` 的返回行为从 `dismiss()` 统一为 `Router.shared.pop()`。
|
- 将 `wake/View/Feedback.swift` 中 `FeedbackView` 与 `FeedbackDetailView` 的返回行为从 `dismiss()` 统一为 `Router.shared.pop()`。
|
||||||
- 保留预览(Preview)中的 `NavigationView`,运行时代码已全部依赖顶层 `NavigationStack + Router`。
|
- 保留预览(Preview)中的 `NavigationView`,运行时代码已全部依赖顶层 `NavigationStack + Router`。
|
||||||
|
- 2025-09-08 16:10 +08: 完成 timer-1 与 netlog-1:
|
||||||
|
- 倒计时更新频率由 0.1s 改为 1s,移除毫秒级显示;初始值设为 `(36, 50, 0)`,减少 UI 重绘(`wake/View/Blind/ContentView.swift`)。
|
||||||
|
- 网络日志:将详细请求/成功响应日志置于 `#if DEBUG`,错误响应体截断至约 300 字符;401 刷新 Token 相关提示仅在 Debug 下打印(`wake/Utils/NetworkService.swift`)。
|
||||||
|
- 2025-09-08 16:28 +08: 完成 polling-1:
|
||||||
|
- 新增 `wake/View/Blind/BlindBoxPolling.swift`,提供 `AsyncThrowingStream` 序列:`singleBox(boxId:)` 与 `firstUnopened()`,内部可取消(`Task.isCancelled`)并使用统一的间隔控制。
|
||||||
|
- `wake/View/Blind/ContentView.swift` 轮询改为 `for try await ... in` 形式;通过 `pollingTask` 管理任务,在 `stopPolling()` 与 `onDisappear` 中取消,避免视图中出现 `while + Task.sleep`。
|
||||||
|
|
||||||
## 决策记录
|
## 决策记录
|
||||||
- 采用顶层 `NavigationStack + Router`,子页面取消 `NavigationView`。
|
- 采用顶层 `NavigationStack + Router`,子页面取消 `NavigationView`。
|
||||||
|
|||||||
@ -287,13 +287,15 @@ class NetworkService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打印请求信息
|
// 打印请求信息(仅 Debug)
|
||||||
|
#if DEBUG
|
||||||
print("""
|
print("""
|
||||||
🌐 [Network][#\(requestId)][\(method) \(path)] 开始请求
|
🌐 [Network][#\(requestId)][\(method) \(path)] 开始请求
|
||||||
🔗 URL: \(url.absoluteString)
|
🔗 URL: \(url.absoluteString)
|
||||||
📤 Headers: \(request.allHTTPHeaderFields ?? [:])
|
📤 Headers: \(request.allHTTPHeaderFields ?? [:])
|
||||||
📦 Body: \(request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? "")
|
📦 Body: \(request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? "")
|
||||||
""")
|
""")
|
||||||
|
#endif
|
||||||
|
|
||||||
// 创建任务
|
// 创建任务
|
||||||
let startTime = Date()
|
let startTime = Date()
|
||||||
@ -338,10 +340,12 @@ class NetworkService {
|
|||||||
|
|
||||||
// 处理401未授权
|
// 处理401未授权
|
||||||
if statusCode == 401 {
|
if statusCode == 401 {
|
||||||
|
#if DEBUG
|
||||||
print("""
|
print("""
|
||||||
🔑 [Network][#\(requestId)][\(method) \(path)] 检测到未授权,尝试刷新token...
|
🔑 [Network][#\(requestId)][\(method) \(path)] 检测到未授权,尝试刷新token...
|
||||||
⏱️ 耗时: \(duration)
|
⏱️ 耗时: \(duration)
|
||||||
""")
|
""")
|
||||||
|
#endif
|
||||||
|
|
||||||
// 将请求加入重试队列
|
// 将请求加入重试队列
|
||||||
let dataResult = data.flatMap { Result<Data, NetworkError>.success($0) } ?? .failure(.noData)
|
let dataResult = data.flatMap { Result<Data, NetworkError>.success($0) } ?? .failure(.noData)
|
||||||
@ -351,10 +355,12 @@ class NetworkService {
|
|||||||
do {
|
do {
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
let result = try decoder.decode(T.self, from: data)
|
let result = try decoder.decode(T.self, from: data)
|
||||||
|
#if DEBUG
|
||||||
print("""
|
print("""
|
||||||
✅ [Network][#\(requestId)][\(method) \(path)] 重试成功
|
✅ [Network][#\(requestId)][\(method) \(path)] 重试成功
|
||||||
⏱️ 总耗时: \(duration) (包含token刷新时间)
|
⏱️ 总耗时: \(duration) (包含token刷新时间)
|
||||||
""")
|
""")
|
||||||
|
#endif
|
||||||
completion(.success(result))
|
completion(.success(result))
|
||||||
} catch let decodingError as DecodingError {
|
} catch let decodingError as DecodingError {
|
||||||
print("""
|
print("""
|
||||||
@ -389,22 +395,25 @@ class NetworkService {
|
|||||||
// 处理其他错误状态码
|
// 处理其他错误状态码
|
||||||
if !(200...299).contains(statusCode) {
|
if !(200...299).contains(statusCode) {
|
||||||
let errorMessage = data.flatMap { String(data: $0, encoding: .utf8) } ?? ""
|
let errorMessage = data.flatMap { String(data: $0, encoding: .utf8) } ?? ""
|
||||||
|
let truncated = errorMessage.count > 300 ? String(errorMessage.prefix(300)) + "..." : errorMessage
|
||||||
print("""
|
print("""
|
||||||
❌ [Network][#\(requestId)][\(method) \(path)] 请求失败
|
❌ [Network][#\(requestId)][\(method) \(path)] 请求失败
|
||||||
📊 状态码: \(statusCode) (\(statusMessage))
|
📊 状态码: \(statusCode) (\(statusMessage))
|
||||||
⏱️ 耗时: \(duration)
|
⏱️ 耗时: \(duration)
|
||||||
🔍 错误响应: \(errorMessage)
|
🔍 错误响应: \(truncated)
|
||||||
""")
|
""")
|
||||||
completion(.failure(.serverError("状态码: \(statusCode), 响应: \(errorMessage)")))
|
completion(.failure(.serverError("状态码: \(statusCode), 响应: \(truncated)")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 成功响应
|
// 成功响应(仅 Debug)
|
||||||
|
#if DEBUG
|
||||||
print("""
|
print("""
|
||||||
✅ [Network][#\(requestId)][\(method) \(path)] 请求成功
|
✅ [Network][#\(requestId)][\(method) \(path)] 请求成功
|
||||||
📊 状态码: \(statusCode) (\(statusMessage))
|
📊 状态码: \(statusCode) (\(statusMessage))
|
||||||
⏱️ 耗时: \(duration)
|
⏱️ 耗时: \(duration)
|
||||||
""")
|
""")
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理网络错误
|
// 处理网络错误
|
||||||
@ -428,13 +437,15 @@ class NetworkService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打印响应数据(调试用)
|
// 打印响应数据(仅 Debug)
|
||||||
|
#if DEBUG
|
||||||
if let responseString = String(data: data, encoding: .utf8) {
|
if let responseString = String(data: data, encoding: .utf8) {
|
||||||
print("""
|
print("""
|
||||||
📥 [Network][#\(requestId)][\(method) \(path)] 响应数据:
|
📥 [Network][#\(requestId)][\(method) \(path)] 响应数据:
|
||||||
\(responseString.prefix(1000))\(responseString.count > 1000 ? "..." : "")
|
\(responseString.prefix(1000))\(responseString.count > 1000 ? "..." : "")
|
||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// 解析JSON数据
|
// 解析JSON数据
|
||||||
@ -442,11 +453,18 @@ class NetworkService {
|
|||||||
let result = try decoder.decode(T.self, from: data)
|
let result = try decoder.decode(T.self, from: data)
|
||||||
completion(.success(result))
|
completion(.success(result))
|
||||||
} catch let decodingError as DecodingError {
|
} catch let decodingError as DecodingError {
|
||||||
|
#if DEBUG
|
||||||
print("""
|
print("""
|
||||||
❌ [Network][#\(requestId)][\(method) \(path)] JSON解析失败
|
❌ [Network][#\(requestId)][\(method) \(path)] JSON解析失败
|
||||||
🔍 错误: \(decodingError.localizedDescription)
|
🔍 错误: \(decodingError.localizedDescription)
|
||||||
📦 原始数据: \(String(data: data, encoding: .utf8) ?? "")
|
📦 原始数据: \(String(data: data, encoding: .utf8) ?? "")
|
||||||
""")
|
""")
|
||||||
|
#else
|
||||||
|
print("""
|
||||||
|
❌ [Network][#\(requestId)][\(method) \(path)] JSON解析失败
|
||||||
|
🔍 错误: \(decodingError.localizedDescription)
|
||||||
|
""")
|
||||||
|
#endif
|
||||||
completion(.failure(.decodingError(decodingError)))
|
completion(.failure(.decodingError(decodingError)))
|
||||||
} catch {
|
} catch {
|
||||||
print("""
|
print("""
|
||||||
@ -463,7 +481,9 @@ class NetworkService {
|
|||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
let refreshStartTime = Date()
|
let refreshStartTime = Date()
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
print("🔄 [Network] 开始刷新Token...")
|
print("🔄 [Network] 开始刷新Token...")
|
||||||
|
#endif
|
||||||
|
|
||||||
TokenManager.shared.refreshToken { [weak self] success, _ in
|
TokenManager.shared.refreshToken { [weak self] success, _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
@ -471,11 +491,13 @@ class NetworkService {
|
|||||||
let refreshDuration = String(format: "%.3fs", Date().timeIntervalSince(refreshStartTime))
|
let refreshDuration = String(format: "%.3fs", Date().timeIntervalSince(refreshStartTime))
|
||||||
|
|
||||||
if success {
|
if success {
|
||||||
|
#if DEBUG
|
||||||
print("""
|
print("""
|
||||||
✅ [Network] Token刷新成功
|
✅ [Network] Token刷新成功
|
||||||
⏱️ 耗时: \(refreshDuration)
|
⏱️ 耗时: \(refreshDuration)
|
||||||
🔄 准备重试\(self.requestsToRetry.count)个请求...
|
🔄 准备重试\(self.requestsToRetry.count)个请求...
|
||||||
""")
|
""")
|
||||||
|
#endif
|
||||||
|
|
||||||
// 重试所有待处理的请求
|
// 重试所有待处理的请求
|
||||||
let requestsToRetry = self.requestsToRetry
|
let requestsToRetry = self.requestsToRetry
|
||||||
@ -499,11 +521,13 @@ class NetworkService {
|
|||||||
task.resume()
|
task.resume()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
#if DEBUG
|
||||||
print("""
|
print("""
|
||||||
❌ [Network] Token刷新失败
|
❌ [Network] Token刷新失败
|
||||||
⏱️ 耗时: \(refreshDuration)
|
⏱️ 耗时: \(refreshDuration)
|
||||||
🚪 清除登录状态...
|
🚪 清除登录状态...
|
||||||
""")
|
""")
|
||||||
|
#endif
|
||||||
|
|
||||||
// 清除token并通知需要重新登录
|
// 清除token并通知需要重新登录
|
||||||
TokenManager.shared.clearTokens()
|
TokenManager.shared.clearTokens()
|
||||||
|
|||||||
65
wake/View/Blind/BlindBoxPolling.swift
Normal file
65
wake/View/Blind/BlindBoxPolling.swift
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - BlindBox Async Polling Sequences
|
||||||
|
|
||||||
|
enum BlindBoxPolling {
|
||||||
|
/// Poll a single blind box until it becomes "Unopened".
|
||||||
|
/// Yields once when ready, then finishes.
|
||||||
|
static func singleBox(boxId: String, intervalSeconds: Double = 2.0) -> AsyncThrowingStream<BlindBoxData, Error> {
|
||||||
|
AsyncThrowingStream { continuation in
|
||||||
|
let task = Task {
|
||||||
|
while !Task.isCancelled {
|
||||||
|
do {
|
||||||
|
let result = try await BlindBoxApi.shared.getBlindBox(boxId: boxId)
|
||||||
|
if let data = result {
|
||||||
|
if data.status == "Unopened" {
|
||||||
|
continuation.yield(data)
|
||||||
|
continuation.finish()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try await Task.sleep(nanoseconds: UInt64(intervalSeconds * 1_000_000_000))
|
||||||
|
} catch is CancellationError {
|
||||||
|
continuation.finish()
|
||||||
|
break
|
||||||
|
} catch {
|
||||||
|
continuation.finish(throwing: error)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continuation.onTermination = { @Sendable _ in
|
||||||
|
task.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Poll blind box list and yield first unopened box when available.
|
||||||
|
/// Yields once when found, then finishes.
|
||||||
|
static func firstUnopened(intervalSeconds: Double = 2.0) -> AsyncThrowingStream<BlindBoxData, Error> {
|
||||||
|
AsyncThrowingStream { continuation in
|
||||||
|
let task = Task {
|
||||||
|
while !Task.isCancelled {
|
||||||
|
do {
|
||||||
|
let list = try await BlindBoxApi.shared.getBlindBoxList()
|
||||||
|
if let item = list?.first(where: { $0.status == "Unopened" }) {
|
||||||
|
continuation.yield(item)
|
||||||
|
continuation.finish()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
try await Task.sleep(nanoseconds: UInt64(intervalSeconds * 1_000_000_000))
|
||||||
|
} catch is CancellationError {
|
||||||
|
continuation.finish()
|
||||||
|
break
|
||||||
|
} catch {
|
||||||
|
continuation.finish(throwing: error)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continuation.onTermination = { @Sendable _ in
|
||||||
|
task.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -86,12 +86,13 @@ struct BlindBoxView: View {
|
|||||||
// 轮询接口
|
// 轮询接口
|
||||||
@State private var isPolling = false
|
@State private var isPolling = false
|
||||||
@State private var pollingTimer: Timer?
|
@State private var pollingTimer: Timer?
|
||||||
|
@State private var pollingTask: Task<Void, Never>? = nil
|
||||||
@State private var currentBoxType: String = ""
|
@State private var currentBoxType: String = ""
|
||||||
// 盲盒链接
|
// 盲盒链接
|
||||||
@State private var videoURL: String = ""
|
@State private var videoURL: String = ""
|
||||||
@State private var imageURL: String = ""
|
@State private var imageURL: String = ""
|
||||||
// 按钮状态 倒计时
|
// 按钮状态 倒计时
|
||||||
@State private var countdown: (minutes: Int, seconds: Int, milliseconds: Int) = (36, 50, 20)
|
@State private var countdown: (minutes: Int, seconds: Int, milliseconds: Int) = (36, 50, 0)
|
||||||
@State private var countdownTimer: Timer?
|
@State private var countdownTimer: Timer?
|
||||||
// 盲盒数据
|
// 盲盒数据
|
||||||
@State private var displayData: BlindBoxData? = nil
|
@State private var displayData: BlindBoxData? = nil
|
||||||
@ -117,46 +118,45 @@ struct BlindBoxView: View {
|
|||||||
// 倒计时
|
// 倒计时
|
||||||
private func startCountdown() {
|
private func startCountdown() {
|
||||||
// 重置为36:50:20
|
// 重置为36:50:20
|
||||||
countdown = (36, 50, 20)
|
countdown = (36, 50, 0)
|
||||||
|
|
||||||
countdownTimer?.invalidate()
|
countdownTimer?.invalidate()
|
||||||
countdownTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
|
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||||
var (minutes, seconds, milliseconds) = countdown
|
var (minutes, seconds, _) = countdown
|
||||||
|
|
||||||
// 更新毫秒
|
// 每秒更新
|
||||||
milliseconds -= 10
|
seconds -= 1
|
||||||
if milliseconds < 0 {
|
|
||||||
milliseconds = 90
|
|
||||||
seconds -= 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新秒
|
|
||||||
if seconds < 0 {
|
if seconds < 0 {
|
||||||
seconds = 59
|
seconds = 59
|
||||||
minutes -= 1
|
minutes -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果倒计时结束,停止计时器
|
// 如果倒计时结束,停止计时器
|
||||||
if minutes <= 0 && seconds <= 0 && milliseconds <= 0 {
|
if minutes <= 0 && seconds <= 0 {
|
||||||
countdownTimer?.invalidate()
|
countdownTimer?.invalidate()
|
||||||
countdownTimer = nil
|
countdownTimer = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
countdown = (minutes, seconds, milliseconds)
|
countdown = (minutes, seconds, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadBlindBox() async {
|
private func loadBlindBox() async {
|
||||||
print("loadMedia called with mediaType: \(mediaType)")
|
print("loadMedia called with mediaType: \(mediaType)")
|
||||||
|
|
||||||
|
// 启动轮询任务(可取消)
|
||||||
|
stopPolling()
|
||||||
|
isPolling = true
|
||||||
if self.currentBoxId != nil {
|
if self.currentBoxId != nil {
|
||||||
print("指定监听某盲盒结果: ", self.currentBoxId! as Any)
|
print("指定监听某盲盒结果: ", self.currentBoxId! as Any)
|
||||||
// 启动轮询查询盲盒状态
|
pollingTask = Task { @MainActor in
|
||||||
await pollingToQuerySingleBox()
|
await pollingToQuerySingleBox()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 启动轮询查询普通盲盒列表
|
pollingTask = Task { @MainActor in
|
||||||
await pollingToQueryBlindBox()
|
await pollingToQueryBlindBox()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// switch mediaType {
|
// switch mediaType {
|
||||||
@ -241,90 +241,47 @@ struct BlindBoxView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func pollingToQuerySingleBox() async {
|
private func pollingToQuerySingleBox() async {
|
||||||
stopPolling()
|
guard let boxId = self.currentBoxId else { return }
|
||||||
isPolling = true
|
do {
|
||||||
|
for try await data in BlindBoxPolling.singleBox(boxId: boxId, intervalSeconds: 2.0) {
|
||||||
// 轮询查询盲盒状态,直到状态为Unopened
|
self.blindGenerate = data
|
||||||
while isPolling {
|
if mediaType == .image {
|
||||||
do {
|
self.imageURL = data.resultFile?.url ?? ""
|
||||||
let blindBoxData = try await BlindBoxApi.shared.getBlindBox(boxId: self.currentBoxId!)
|
} else {
|
||||||
|
self.videoURL = data.resultFile?.url ?? ""
|
||||||
// 更新UI
|
|
||||||
if let data = blindBoxData {
|
|
||||||
self.blindGenerate = data
|
|
||||||
|
|
||||||
// 根据盲盒类型设置媒体URL
|
|
||||||
if mediaType == .image {
|
|
||||||
self.imageURL = data.resultFile?.url ?? ""
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.videoURL = data.resultFile?.url ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
print("✅ 成功获取盲盒数据: \(data.name), 状态: \(data.status)")
|
|
||||||
|
|
||||||
// 检查状态是否为Unopened,如果是则停止轮询
|
|
||||||
if data.status == "Unopened" {
|
|
||||||
print("✅ 盲盒已准备就绪,停止轮询")
|
|
||||||
self.animationPhase = .ready
|
|
||||||
stopPolling()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
withAnimation { self.animationPhase = .ready }
|
||||||
// 等待2秒后继续轮询
|
|
||||||
try await Task.sleep(nanoseconds: 2_000_000_000)
|
|
||||||
} catch {
|
|
||||||
print("❌ 获取盲盒数据失败: \(error)")
|
|
||||||
// 处理错误情况
|
|
||||||
self.animationPhase = .none
|
|
||||||
stopPolling()
|
stopPolling()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
} catch is CancellationError {
|
||||||
|
// 任务被取消
|
||||||
|
} catch {
|
||||||
|
print("❌ 获取盲盒数据失败: \(error)")
|
||||||
|
self.animationPhase = .none
|
||||||
|
stopPolling()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func pollingToQueryBlindBox() async {
|
private func pollingToQueryBlindBox() async {
|
||||||
stopPolling()
|
do {
|
||||||
isPolling = true
|
for try await blindBox in BlindBoxPolling.firstUnopened(intervalSeconds: 2.0) {
|
||||||
|
self.blindGenerate = blindBox
|
||||||
while isPolling {
|
if mediaType == .image {
|
||||||
do {
|
self.imageURL = blindBox.resultFile?.url ?? ""
|
||||||
let blindBoxList = try await BlindBoxApi.shared.getBlindBoxList()
|
|
||||||
print("✅ 获取盲盒列表: \(blindBoxList?.count ?? 0) 条")
|
|
||||||
|
|
||||||
// 统计未开启盲盒数量
|
|
||||||
self.blindCount = BlindCount(availableQuantity: blindBoxList?.filter({ $0.status == "Unopened" }).count ?? 0)
|
|
||||||
|
|
||||||
// 设置第一个未开启的盲盒
|
|
||||||
if let blindBox = blindBoxList?.first(where: { $0.status == "Unopened" }) {
|
|
||||||
self.blindGenerate = blindBox
|
|
||||||
self.animationPhase = .ready
|
|
||||||
|
|
||||||
// 更新UI
|
|
||||||
// 根据盲盒类型设置媒体URL
|
|
||||||
if mediaType == .image {
|
|
||||||
self.imageURL = blindBox.resultFile?.url ?? ""
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.videoURL = blindBox.resultFile?.url ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
print("✅ 成功获取盲盒数据: \(blindBox.name), 状态: \(blindBox.status)")
|
|
||||||
stopPolling()
|
|
||||||
break
|
|
||||||
} else {
|
} else {
|
||||||
if self.animationPhase != .none {
|
self.videoURL = blindBox.resultFile?.url ?? ""
|
||||||
self.animationPhase = .none
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 等待2秒后继续轮询
|
withAnimation { self.animationPhase = .ready }
|
||||||
try await Task.sleep(nanoseconds: 2_000_000_000)
|
print("✅ 成功获取盲盒数据: \(blindBox.name), 状态: \(blindBox.status)")
|
||||||
} catch {
|
|
||||||
print("❌ 获取盲盒列表失败: \(error)")
|
|
||||||
stopPolling()
|
stopPolling()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
} catch is CancellationError {
|
||||||
|
// 任务被取消
|
||||||
|
} catch {
|
||||||
|
print("❌ 获取盲盒列表失败: \(error)")
|
||||||
|
stopPolling()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,6 +296,8 @@ struct BlindBoxView: View {
|
|||||||
pollingTimer?.invalidate()
|
pollingTimer?.invalidate()
|
||||||
pollingTimer = nil
|
pollingTimer = nil
|
||||||
isPolling = false
|
isPolling = false
|
||||||
|
pollingTask?.cancel()
|
||||||
|
pollingTask = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func checkBlindBoxStatus() {
|
private func checkBlindBoxStatus() {
|
||||||
@ -914,7 +873,7 @@ struct BlindBoxView: View {
|
|||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
if animationPhase == .loading {
|
if animationPhase == .loading {
|
||||||
Text("Next: \(countdown.minutes):\(String(format: "%02d", countdown.seconds)).\(String(format: "%02d", countdown.milliseconds))")
|
Text("Next: \(countdown.minutes):\(String(format: "%02d", countdown.seconds))")
|
||||||
.font(Typography.font(for: .body))
|
.font(Typography.font(for: .body))
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user