Compare commits
4 Commits
e9cdb82b70
...
3dc301d6c7
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dc301d6c7 | |||
| 7467789bf7 | |||
| 1e3ec86377 | |||
| 8f369867b2 |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"lldb.library": "/Library/Developer/CommandLineTools/Library/PrivateFrameworks/LLDB.framework/Versions/A/LLDB",
|
"lldb.library": "/Library/Developer/CommandLineTools/Library/PrivateFrameworks/LLDB.framework/Versions/A/LLDB",
|
||||||
"lldb.launch.expressions": "native"
|
"lldb.launch.expressions": "native",
|
||||||
|
"sweetpad.build.xcodeWorkspacePath": "wake.xcodeproj/project.xcworkspace"
|
||||||
}
|
}
|
||||||
@ -68,19 +68,37 @@ struct BlindCount: Codable {
|
|||||||
|
|
||||||
// MARK: - Blind Box Data
|
// MARK: - Blind Box Data
|
||||||
struct BlindBoxData: Codable {
|
struct BlindBoxData: Codable {
|
||||||
let id: Int64
|
let id: String
|
||||||
let boxCode: String
|
let boxCode: String
|
||||||
let userId: Int64
|
let userId: String
|
||||||
let name: String
|
let name: String
|
||||||
let boxType: String
|
let boxType: String
|
||||||
let features: String?
|
let features: String?
|
||||||
let url: String?
|
let resultFile: FileInfo?
|
||||||
let status: String
|
let status: String
|
||||||
let workflowInstanceId: String?
|
let workflowInstanceId: String?
|
||||||
// 视频生成时间
|
|
||||||
let videoGenerateTime: String?
|
let videoGenerateTime: String?
|
||||||
let createTime: String
|
let createTime: String
|
||||||
let description: String?
|
let coverFile: FileInfo?
|
||||||
|
let description: String
|
||||||
|
|
||||||
|
// 添加计算属性以获取Int64值
|
||||||
|
var idValue: Int64 { Int64(id) ?? 0 }
|
||||||
|
var userIdValue: Int64 { Int64(userId) ?? 0 }
|
||||||
|
|
||||||
|
struct FileInfo: Codable {
|
||||||
|
let id: String
|
||||||
|
let fileName: String?
|
||||||
|
let url: String?
|
||||||
|
let metadata: [String: String]?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case fileName = "file_name"
|
||||||
|
case url
|
||||||
|
case metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case id
|
case id
|
||||||
@ -89,43 +107,68 @@ struct BlindBoxData: Codable {
|
|||||||
case name
|
case name
|
||||||
case boxType = "box_type"
|
case boxType = "box_type"
|
||||||
case features
|
case features
|
||||||
case url
|
case resultFile = "result_file"
|
||||||
case status
|
case status
|
||||||
case workflowInstanceId = "workflow_instance_id"
|
case workflowInstanceId = "workflow_instance_id"
|
||||||
case videoGenerateTime = "video_generate_time"
|
case videoGenerateTime = "video_generate_time"
|
||||||
case createTime = "create_time"
|
case createTime = "create_time"
|
||||||
|
case coverFile = "cover_file"
|
||||||
case description
|
case description
|
||||||
}
|
}
|
||||||
|
|
||||||
init(id: Int64, boxCode: String, userId: Int64, name: String, boxType: String, features: String?, url: String?, status: String, workflowInstanceId: String?, videoGenerateTime: String?, createTime: String, description: String?) {
|
init(id: String, boxCode: String, userId: String, name: String, boxType: String, features: String?, resultFile: FileInfo?, status: String, workflowInstanceId: String?, videoGenerateTime: String?, createTime: String, coverFile: FileInfo?, description: String) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.boxCode = boxCode
|
self.boxCode = boxCode
|
||||||
self.userId = userId
|
self.userId = userId
|
||||||
self.name = name
|
self.name = name
|
||||||
self.boxType = boxType
|
self.boxType = boxType
|
||||||
self.features = features
|
self.features = features
|
||||||
self.url = url
|
self.resultFile = resultFile
|
||||||
self.status = status
|
self.status = status
|
||||||
self.workflowInstanceId = workflowInstanceId
|
self.workflowInstanceId = workflowInstanceId
|
||||||
self.videoGenerateTime = videoGenerateTime
|
self.videoGenerateTime = videoGenerateTime
|
||||||
self.createTime = createTime
|
self.createTime = createTime
|
||||||
|
self.coverFile = coverFile
|
||||||
self.description = description
|
self.description = description
|
||||||
}
|
}
|
||||||
|
|
||||||
init(from listItem: BlindList) {
|
init(from listItem: BlindList) {
|
||||||
self.init(
|
self.id = listItem.id
|
||||||
id: Int64(listItem.id) ?? 0,
|
self.boxCode = listItem.boxCode
|
||||||
boxCode: listItem.boxCode,
|
self.userId = listItem.userId
|
||||||
userId: Int64(listItem.userId) ?? 0,
|
self.name = listItem.name
|
||||||
name: listItem.name,
|
self.boxType = listItem.boxType
|
||||||
boxType: listItem.boxType,
|
self.features = listItem.features
|
||||||
features: listItem.features,
|
|
||||||
url: listItem.resultFile?.url,
|
// 转换FileInfo类型
|
||||||
status: listItem.status,
|
if let resultFileInfo = listItem.resultFile {
|
||||||
workflowInstanceId: listItem.workflowInstanceId,
|
self.resultFile = FileInfo(
|
||||||
videoGenerateTime: listItem.videoGenerateTime,
|
id: resultFileInfo.id,
|
||||||
createTime: listItem.createTime,
|
fileName: resultFileInfo.fileName,
|
||||||
description: listItem.description
|
url: resultFileInfo.url,
|
||||||
)
|
metadata: resultFileInfo.metadata
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self.resultFile = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.status = listItem.status
|
||||||
|
self.workflowInstanceId = listItem.workflowInstanceId
|
||||||
|
self.videoGenerateTime = listItem.videoGenerateTime
|
||||||
|
self.createTime = listItem.createTime
|
||||||
|
|
||||||
|
// 转换coverFile的FileInfo类型
|
||||||
|
if let coverFileInfo = listItem.coverFile {
|
||||||
|
self.coverFile = FileInfo(
|
||||||
|
id: coverFileInfo.id,
|
||||||
|
fileName: coverFileInfo.fileName,
|
||||||
|
url: coverFileInfo.url,
|
||||||
|
metadata: coverFileInfo.metadata
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self.coverFile = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.description = listItem.description ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,57 +14,7 @@ import Foundation
|
|||||||
// MARK: - Generate Blind Box Response Model
|
// MARK: - Generate Blind Box Response Model
|
||||||
struct GenerateBlindBoxResponse: Codable {
|
struct GenerateBlindBoxResponse: Codable {
|
||||||
let code: Int
|
let code: Int
|
||||||
let data: BlindBoxDataWrapper?
|
let data: BlindBoxData?
|
||||||
|
|
||||||
struct BlindBoxDataWrapper: Codable {
|
|
||||||
let id: String
|
|
||||||
let boxCode: String
|
|
||||||
let userId: String
|
|
||||||
let name: String
|
|
||||||
let boxType: String
|
|
||||||
let features: String?
|
|
||||||
let resultFile: FileInfo?
|
|
||||||
let status: String
|
|
||||||
let workflowInstanceId: String?
|
|
||||||
let videoGenerateTime: String?
|
|
||||||
let createTime: String
|
|
||||||
let coverFile: FileInfo?
|
|
||||||
let description: String
|
|
||||||
|
|
||||||
// 添加计算属性以获取Int64值
|
|
||||||
var idValue: Int64 { Int64(id) ?? 0 }
|
|
||||||
var userIdValue: Int64 { Int64(userId) ?? 0 }
|
|
||||||
|
|
||||||
struct FileInfo: Codable {
|
|
||||||
let id: String
|
|
||||||
let fileName: String?
|
|
||||||
let url: String?
|
|
||||||
let metadata: [String: String]?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case id
|
|
||||||
case fileName = "file_name"
|
|
||||||
case url
|
|
||||||
case metadata
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case id
|
|
||||||
case boxCode = "box_code"
|
|
||||||
case userId = "user_id"
|
|
||||||
case name
|
|
||||||
case boxType = "box_type"
|
|
||||||
case features
|
|
||||||
case resultFile = "result_file"
|
|
||||||
case status
|
|
||||||
case workflowInstanceId = "workflow_instance_id"
|
|
||||||
case videoGenerateTime = "video_generate_time"
|
|
||||||
case createTime = "create_time"
|
|
||||||
case coverFile = "cover_file"
|
|
||||||
case description
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Blind Box API Client
|
// MARK: - Blind Box API Client
|
||||||
@ -81,7 +31,7 @@ class BlindBoxApi {
|
|||||||
func generateBlindBox(
|
func generateBlindBox(
|
||||||
boxType: String,
|
boxType: String,
|
||||||
materialIds: [String],
|
materialIds: [String],
|
||||||
completion: @escaping (Result<GenerateBlindBoxResponse.BlindBoxDataWrapper?, Error>) -> Void
|
completion: @escaping (Result<BlindBoxData?, Error>) -> Void
|
||||||
) {
|
) {
|
||||||
// 将Codable结构体转换为字典
|
// 将Codable结构体转换为字典
|
||||||
let parameters: [String: Any] = [
|
let parameters: [String: Any] = [
|
||||||
@ -108,4 +58,48 @@ class BlindBoxApi {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取盲盒信息
|
||||||
|
/// - Parameters:
|
||||||
|
/// - boxId: 盲盒ID
|
||||||
|
/// - completion: 完成回调,返回盲盒数据或错误
|
||||||
|
func getBlindBox(
|
||||||
|
boxId: String,
|
||||||
|
completion: @escaping (Result<BlindBoxData?, Error>) -> Void
|
||||||
|
) {
|
||||||
|
let path = "/blind_box/query/\(boxId)"
|
||||||
|
|
||||||
|
NetworkService.shared.getWithToken(
|
||||||
|
path: path,
|
||||||
|
completion: { (result: Result<GenerateBlindBoxResponse, NetworkError>) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
switch result {
|
||||||
|
case .success(let response):
|
||||||
|
if response.code == 0 {
|
||||||
|
completion(.success(response.data))
|
||||||
|
} else {
|
||||||
|
completion(.failure(NetworkError.serverError("服务器返回错误码: \(response.code)")))
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用 async/await 获取盲盒信息
|
||||||
|
/// - Parameter boxId: 盲盒ID
|
||||||
|
/// - Returns: 盲盒数据
|
||||||
|
@available(iOS 13.0, *)
|
||||||
|
func getBlindBox(boxId: String) async throws -> BlindBoxData? {
|
||||||
|
let path = "/blind_box/query/\(boxId)"
|
||||||
|
let response: GenerateBlindBoxResponse = try await NetworkService.shared.getWithToken(path: path)
|
||||||
|
|
||||||
|
if response.code == 0 {
|
||||||
|
return response.data
|
||||||
|
} else {
|
||||||
|
throw NetworkError.serverError("服务器返回错误码: \(response.code)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -108,6 +108,43 @@ extension NetworkService: NetworkServiceProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Async/Await Extensions
|
||||||
|
extension NetworkService {
|
||||||
|
/// 使用 async/await 的 GET 请求(带Token)
|
||||||
|
public func getWithToken<T: Decodable>(
|
||||||
|
path: String,
|
||||||
|
parameters: [String: Any]? = nil
|
||||||
|
) async throws -> T {
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
getWithToken(path: path, parameters: parameters) { (result: Result<T, NetworkError>) in
|
||||||
|
switch result {
|
||||||
|
case .success(let value):
|
||||||
|
continuation.resume(returning: value)
|
||||||
|
case .failure(let error):
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用 async/await 的 POST 请求(带Token)
|
||||||
|
public func postWithToken<T: Decodable>(
|
||||||
|
path: String,
|
||||||
|
parameters: [String: Any]
|
||||||
|
) async throws -> T {
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
postWithToken(path: path, parameters: parameters) { (result: Result<T, NetworkError>) in
|
||||||
|
switch result {
|
||||||
|
case .success(let value):
|
||||||
|
continuation.resume(returning: value)
|
||||||
|
case .failure(let error):
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum NetworkError: Error {
|
public enum NetworkError: Error {
|
||||||
case invalidURL
|
case invalidURL
|
||||||
case noData
|
case noData
|
||||||
|
|||||||
@ -7,7 +7,7 @@ enum AppRoute: Hashable {
|
|||||||
case feedbackView
|
case feedbackView
|
||||||
case feedbackDetail(type: FeedbackView.FeedbackType)
|
case feedbackDetail(type: FeedbackView.FeedbackType)
|
||||||
case mediaUpload
|
case mediaUpload
|
||||||
case blindBox(mediaType: BlindBoxMediaType)
|
case blindBox(mediaType: BlindBoxMediaType, blindBoxId: String? = nil)
|
||||||
case blindOutcome(media: MediaType, time: String? = nil, description: String? = nil)
|
case blindOutcome(media: MediaType, time: String? = nil, description: String? = nil)
|
||||||
case memories
|
case memories
|
||||||
case subscribe
|
case subscribe
|
||||||
@ -31,8 +31,8 @@ enum AppRoute: Hashable {
|
|||||||
FeedbackDetailView(feedbackType: type)
|
FeedbackDetailView(feedbackType: type)
|
||||||
case .mediaUpload:
|
case .mediaUpload:
|
||||||
MediaUploadView()
|
MediaUploadView()
|
||||||
case .blindBox(let mediaType):
|
case .blindBox(let mediaType, let blindBoxId):
|
||||||
BlindBoxView(mediaType: mediaType)
|
BlindBoxView(mediaType: mediaType, blindBoxId: blindBoxId)
|
||||||
case .blindOutcome(let media, let time, let description):
|
case .blindOutcome(let media, let time, let description):
|
||||||
BlindOutcomeView(media: media, time: time, description: description)
|
BlindOutcomeView(media: media, time: time, description: description)
|
||||||
case .memories:
|
case .memories:
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftData
|
import SwiftData
|
||||||
import AVKit
|
import AVKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
// 添加通知名称
|
// 添加通知名称
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
@ -69,6 +70,8 @@ struct AVPlayerController: UIViewControllerRepresentable {
|
|||||||
|
|
||||||
struct BlindBoxView: View {
|
struct BlindBoxView: View {
|
||||||
let mediaType: BlindBoxMediaType
|
let mediaType: BlindBoxMediaType
|
||||||
|
let currentBoxId: String?
|
||||||
|
|
||||||
@State private var showModal = false // 控制用户资料弹窗显示
|
@State private var showModal = false // 控制用户资料弹窗显示
|
||||||
@State private var showSettings = false // 控制设置页面显示
|
@State private var showSettings = false // 控制设置页面显示
|
||||||
@State private var isMember = false // 是否是会员
|
@State private var isMember = false // 是否是会员
|
||||||
@ -78,7 +81,7 @@ struct BlindBoxView: View {
|
|||||||
@State private var blindCount: BlindCount? = nil
|
@State private var blindCount: BlindCount? = nil
|
||||||
@State private var blindList: [BlindList] = [] // Changed to array
|
@State private var blindList: [BlindList] = [] // Changed to array
|
||||||
// 生成盲盒
|
// 生成盲盒
|
||||||
@State private var blindGenerate : BlindBoxData?
|
@State private var blindGenerate: BlindBoxData?
|
||||||
@State private var showLottieAnimation = true
|
@State private var showLottieAnimation = true
|
||||||
// 轮询接口
|
// 轮询接口
|
||||||
@State private var isPolling = false
|
@State private var isPolling = false
|
||||||
@ -106,8 +109,9 @@ struct BlindBoxView: View {
|
|||||||
// 查询数据 - 简单查询
|
// 查询数据 - 简单查询
|
||||||
@Query private var login: [Login]
|
@Query private var login: [Login]
|
||||||
|
|
||||||
init(mediaType: BlindBoxMediaType) {
|
init(mediaType: BlindBoxMediaType, blindBoxId: String? = nil) {
|
||||||
self.mediaType = mediaType
|
self.mediaType = mediaType
|
||||||
|
self.currentBoxId = blindBoxId
|
||||||
}
|
}
|
||||||
|
|
||||||
// 倒计时
|
// 倒计时
|
||||||
@ -143,88 +147,139 @@ struct BlindBoxView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadMedia() {
|
private func loadBlindBox() async {
|
||||||
print("loadMedia called with mediaType: \(mediaType)")
|
print("loadMedia called with mediaType: \(mediaType)")
|
||||||
|
|
||||||
switch mediaType {
|
if self.currentBoxId != nil {
|
||||||
case .video:
|
print("指定监听某盲盒结果: ", self.currentBoxId! as Any)
|
||||||
loadVideo()
|
// 启动轮询查询盲盒状态
|
||||||
currentBoxType = "Video"
|
await pollingToQuerySingleBox()
|
||||||
startPolling()
|
}
|
||||||
case .image:
|
|
||||||
loadImage()
|
|
||||||
currentBoxType = "Image"
|
|
||||||
startPolling()
|
|
||||||
case .all:
|
|
||||||
print("Loading all content...")
|
|
||||||
// 检查盲盒列表,如果不存在First/Second盲盒,则跳转到对应的页面重新触发新手引导
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
self.blindList = response.data ?? []
|
// switch mediaType {
|
||||||
// 如果列表为空数组 设置盲盒状态为none
|
// case .video:
|
||||||
if self.blindList.isEmpty {
|
// loadVideo()
|
||||||
self.animationPhase = .none
|
// currentBoxType = "Video"
|
||||||
}
|
// startPolling()
|
||||||
print("✅ 成功获取 \(self.blindList.count) 个盲盒")
|
// case .image:
|
||||||
case .failure(let error):
|
// loadImage()
|
||||||
self.blindList = []
|
// currentBoxType = "Image"
|
||||||
self.animationPhase = .none
|
// startPolling()
|
||||||
print("❌ 获取盲盒列表失败:", error.localizedDescription)
|
// 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
|
||||||
|
// }
|
||||||
|
|
||||||
// 会员信息
|
// self.blindList = response.data ?? []
|
||||||
NetworkService.shared.get(
|
// // 如果列表为空数组 设置盲盒状态为none
|
||||||
path: "/membership/personal-center-info",
|
// if self.blindList.isEmpty {
|
||||||
parameters: nil
|
// self.animationPhase = .none
|
||||||
) { (result: Result<MemberProfileResponse, NetworkError>) in
|
// }
|
||||||
DispatchQueue.main.async {
|
// print("✅ 成功获取 \(self.blindList.count) 个盲盒")
|
||||||
switch result {
|
// case .failure(let error):
|
||||||
case .success(let response):
|
// self.blindList = []
|
||||||
self.memberProfile = response.data
|
// self.animationPhase = .none
|
||||||
self.isMember = response.data.membershipLevel == "Pioneer"
|
// print("❌ 获取盲盒列表失败:", error.localizedDescription)
|
||||||
self.memberDate = response.data.membershipEndAt ?? ""
|
// }
|
||||||
print("✅ 成功获取会员信息:", response.data)
|
// }
|
||||||
print("✅ 用户ID:", response.data.userInfo.userId)
|
// }
|
||||||
case .failure(let error):
|
|
||||||
print("❌ 获取会员信息失败:", error)
|
// // // 会员信息
|
||||||
}
|
// // NetworkService.shared.get(
|
||||||
}
|
// // path: "/membership/personal-center-info",
|
||||||
}
|
// // parameters: nil
|
||||||
// 盲盒数量
|
// // ) { (result: Result<MemberProfileResponse, NetworkError>) in
|
||||||
NetworkService.shared.get(
|
// // DispatchQueue.main.async {
|
||||||
path: "/blind_box/available/quantity",
|
// // switch result {
|
||||||
parameters: nil
|
// // case .success(let response):
|
||||||
) { (result: Result<APIResponse<BlindCount>, NetworkError>) in
|
// // self.memberProfile = response.data
|
||||||
DispatchQueue.main.async {
|
// // self.isMember = response.data.membershipLevel == "Pioneer"
|
||||||
switch result {
|
// // self.memberDate = response.data.membershipEndAt ?? ""
|
||||||
case .success(let response):
|
// // print("✅ 成功获取会员信息:", response.data)
|
||||||
self.blindCount = response.data
|
// // print("✅ 用户ID:", response.data.userInfo.userId)
|
||||||
print("✅ 成功获取盲盒数量:", response.data)
|
// // case .failure(let error):
|
||||||
case .failure(let error):
|
// // print("❌ 获取会员信息失败:", 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 {
|
||||||
|
stopPolling()
|
||||||
|
isPolling = true
|
||||||
|
|
||||||
|
// 轮询查询盲盒状态,直到状态为Unopened
|
||||||
|
while isPolling {
|
||||||
|
do {
|
||||||
|
let blindBoxData = try await BlindBoxApi.shared.getBlindBox(boxId: self.currentBoxId!)
|
||||||
|
|
||||||
|
// 更新UI
|
||||||
|
if let data = blindBoxData {
|
||||||
|
self.blindGenerate = data
|
||||||
|
|
||||||
|
// 根据盲盒类型设置媒体URL
|
||||||
|
if mediaType == .video {
|
||||||
|
self.videoURL = data.resultFile?.url ?? ""
|
||||||
|
} else if mediaType == .image {
|
||||||
|
self.imageURL = data.resultFile?.url ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
print("✅ 成功获取盲盒数据: \(data.name), 状态: \(data.status)")
|
||||||
|
|
||||||
|
// 检查状态是否为Unopened,如果是则停止轮询
|
||||||
|
if data.status == "Unopened" {
|
||||||
|
print("✅ 盲盒已准备就绪,停止轮询")
|
||||||
|
self.animationPhase = .ready
|
||||||
|
stopPolling()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待2秒后继续轮询
|
||||||
|
try await Task.sleep(nanoseconds: 2_000_000_000)
|
||||||
|
} catch {
|
||||||
|
print("❌ 获取盲盒数据失败: \(error)")
|
||||||
|
// 处理错误情况
|
||||||
|
self.animationPhase = .none
|
||||||
|
stopPolling()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 轮询接口
|
// 轮询接口
|
||||||
private func startPolling() {
|
private func startPolling() {
|
||||||
stopPolling()
|
stopPolling()
|
||||||
@ -244,52 +299,54 @@ struct BlindBoxView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkService.shared.postWithToken(
|
// NetworkService.shared.postWithToken(
|
||||||
path: "/blind_box/generate/mock",
|
// path: "/blind_box/generate/mock",
|
||||||
parameters: ["box_type": currentBoxType]
|
// parameters: ["box_type": currentBoxType]
|
||||||
) { (result: Result<APIResponse<BlindBoxData>, NetworkError>) in
|
// ) { (result: Result<GenerateBlindBoxResponse, NetworkError>) in
|
||||||
DispatchQueue.main.async {
|
// DispatchQueue.main.async {
|
||||||
switch result {
|
// switch result {
|
||||||
case .success(let response):
|
// case .success(let response):
|
||||||
let data = response.data
|
// let data = response.data
|
||||||
self.blindGenerate = data
|
// self.blindGenerate = data
|
||||||
print("当前盲盒状态: \(data.status)")
|
// print("当前盲盒状态: \(data?.status ?? "Unknown")")
|
||||||
// 更新显示数据
|
// // 更新显示数据
|
||||||
if self.mediaType == .all, let firstItem = self.blindList.first {
|
// if self.mediaType == .all, let firstItem = self.blindList.first {
|
||||||
self.displayData = BlindBoxData(from: firstItem)
|
// self.displayData = BlindBoxData(from: firstItem)
|
||||||
} else {
|
// } else {
|
||||||
self.displayData = data
|
// self.displayData = data
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 发送状态变更通知
|
// // 发送状态变更通知
|
||||||
NotificationCenter.default.post(
|
// if let status = data?.status {
|
||||||
name: .blindBoxStatusChanged,
|
// NotificationCenter.default.post(
|
||||||
object: nil,
|
// name: .blindBoxStatusChanged,
|
||||||
userInfo: ["status": data.status]
|
// object: nil,
|
||||||
)
|
// userInfo: ["status": status]
|
||||||
|
// )
|
||||||
if data.status != "Preparing" {
|
// }
|
||||||
self.stopPolling()
|
//
|
||||||
print("✅ 盲盒准备就绪,状态: \(data.status)")
|
// if data?.status != "Preparing" {
|
||||||
if self.mediaType == .video {
|
// self.stopPolling()
|
||||||
self.videoURL = data.url ?? ""
|
// print("✅ 盲盒准备就绪,状态: \(data?.status ?? "Unknown")")
|
||||||
} else if self.mediaType == .image {
|
// if self.mediaType == .video {
|
||||||
self.imageURL = data.url ?? ""
|
// self.videoURL = data?.resultFile?.url ?? ""
|
||||||
}
|
// } else if self.mediaType == .image {
|
||||||
} else {
|
// self.imageURL = data?.resultFile?.url ?? ""
|
||||||
self.pollingTimer = Timer.scheduledTimer(
|
// }
|
||||||
withTimeInterval: 2.0,
|
// } else {
|
||||||
repeats: false
|
// self.pollingTimer = Timer.scheduledTimer(
|
||||||
) { _ in
|
// withTimeInterval: 2.0,
|
||||||
self.checkBlindBoxStatus()
|
// repeats: false
|
||||||
}
|
// ) { _ in
|
||||||
}
|
// self.checkBlindBoxStatus()
|
||||||
case .failure(let error):
|
// }
|
||||||
print("❌ 获取盲盒状态失败: \(error.localizedDescription)")
|
// }
|
||||||
self.stopPolling()
|
// case .failure(let error):
|
||||||
}
|
// print("❌ 获取盲盒状态失败: \(error.localizedDescription)")
|
||||||
}
|
// self.stopPolling()
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadImage() {
|
private func loadImage() {
|
||||||
@ -409,40 +466,45 @@ struct BlindBoxView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
print("🎯 BlindBoxView appeared with mediaType: \(mediaType)")
|
print("🎯 BlindBoxView appeared with mediaType: \(mediaType)")
|
||||||
print("🎯 Current thread: \(Thread.current)")
|
print("🎯 Current thread: \(Thread.current)")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 初始化显示数据
|
// 初始化显示数据
|
||||||
if mediaType == .all, let firstItem = blindList.first {
|
// if mediaType == .all, let firstItem = blindList.first {
|
||||||
displayData = BlindBoxData(from: firstItem)
|
// displayData = BlindBoxData(from: firstItem)
|
||||||
} else {
|
// } else {
|
||||||
displayData = blindGenerate
|
// displayData = blindGenerate
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 添加盲盒状态变化监听
|
// 添加盲盒状态变化监听
|
||||||
NotificationCenter.default.addObserver(
|
// NotificationCenter.default.addObserver(
|
||||||
forName: .blindBoxStatusChanged,
|
// forName: .blindBoxStatusChanged,
|
||||||
object: nil,
|
// object: nil,
|
||||||
queue: .main
|
// queue: .main
|
||||||
) { notification in
|
// ) { notification in
|
||||||
if let status = notification.userInfo?["status"] as? String {
|
// if let status = notification.userInfo?["status"] as? String {
|
||||||
switch status {
|
// switch status {
|
||||||
case "Preparing":
|
// case "Preparing":
|
||||||
withAnimation {
|
// withAnimation {
|
||||||
self.animationPhase = .loading
|
// self.animationPhase = .loading
|
||||||
}
|
// }
|
||||||
case "Unopened":
|
// case "Unopened":
|
||||||
withAnimation {
|
// withAnimation {
|
||||||
self.animationPhase = .ready
|
// self.animationPhase = .ready
|
||||||
}
|
// }
|
||||||
default:
|
// default:
|
||||||
// 其他状态不处理
|
// // 其他状态不处理
|
||||||
withAnimation {
|
// withAnimation {
|
||||||
self.animationPhase = .ready
|
// self.animationPhase = .ready
|
||||||
}
|
// }
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
// 调用接口
|
// 调用接口
|
||||||
loadMedia()
|
Task {
|
||||||
|
await loadBlindBox()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
stopPolling()
|
stopPolling()
|
||||||
@ -497,9 +559,9 @@ struct BlindBoxView: View {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
// 导航到BlindOutcomeView
|
// 导航到BlindOutcomeView
|
||||||
if mediaType == .video, !videoURL.isEmpty, let url = URL(string: videoURL) {
|
if mediaType == .video, !videoURL.isEmpty, let url = URL(string: videoURL) {
|
||||||
Router.shared.navigate(to: .blindOutcome(media: .video(url, nil), time: blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn", description:blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation"))
|
Router.shared.navigate(to: .blindOutcome(media: .video(url, nil), time: blindGenerate?.name ?? "Your box", description:blindGenerate?.description ?? ""))
|
||||||
} else if mediaType == .image, let image = displayImage {
|
} else if mediaType == .image, let image = displayImage {
|
||||||
Router.shared.navigate(to: .blindOutcome(media: .image(image), time: blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn", description:blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation"))
|
Router.shared.navigate(to: .blindOutcome(media: .image(image), time: blindGenerate?.name ?? "Your box", description:blindGenerate?.description ?? ""))
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Image(systemName: "chevron.left")
|
Image(systemName: "chevron.left")
|
||||||
@ -595,10 +657,11 @@ struct BlindBoxView: View {
|
|||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.padding(.top, 20)
|
.padding(.top, 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标题
|
// 标题
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Hi! Click And")
|
Text("Hi! Click And")
|
||||||
Text("Open Your First Box~")
|
Text("Open Your Box~")
|
||||||
}
|
}
|
||||||
.font(Typography.font(for: .smallLargeTitle))
|
.font(Typography.font(for: .smallLargeTitle))
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
@ -722,10 +785,10 @@ struct BlindBoxView: View {
|
|||||||
if !showScalingOverlay && !showMedia {
|
if !showScalingOverlay && !showMedia {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
// 从变量blindGenerate中获取description
|
// 从变量blindGenerate中获取description
|
||||||
Text(blindGenerate?.videoGenerateTime ?? "hhsdshjsjdhn")
|
Text(blindGenerate?.name ?? "Some box")
|
||||||
.font(Typography.font(for: .body, family: .quicksandBold))
|
.font(Typography.font(for: .body, family: .quicksandBold))
|
||||||
.foregroundColor(Color.themeTextMessageMain)
|
.foregroundColor(Color.themeTextMessageMain)
|
||||||
Text(blindGenerate?.description ?? "informationinformationinformationinformationinformationinformation")
|
Text(blindGenerate?.description ?? "")
|
||||||
.font(.system(size: 14))
|
.font(.system(size: 14))
|
||||||
.foregroundColor(Color.themeTextMessageMain)
|
.foregroundColor(Color.themeTextMessageMain)
|
||||||
}
|
}
|
||||||
@ -743,7 +806,8 @@ struct BlindBoxView: View {
|
|||||||
.animation(.easeOut(duration: 1.5), value: showScalingOverlay)
|
.animation(.easeOut(duration: 1.5), value: showScalingOverlay)
|
||||||
.offset(y: showScalingOverlay ? -100 : 0)
|
.offset(y: showScalingOverlay ? -100 : 0)
|
||||||
.animation(.easeInOut(duration: 1.5), value: showScalingOverlay)
|
.animation(.easeInOut(duration: 1.5), value: showScalingOverlay)
|
||||||
// 打开
|
|
||||||
|
// 打开 TODO 引导时,也要有按钮
|
||||||
if mediaType == .all {
|
if mediaType == .all {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
if animationPhase == .ready {
|
if animationPhase == .ready {
|
||||||
@ -795,6 +859,7 @@ struct BlindBoxView: View {
|
|||||||
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showModal)
|
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showModal)
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户资料弹窗
|
// 用户资料弹窗
|
||||||
SlideInModal(
|
SlideInModal(
|
||||||
isPresented: $showModal,
|
isPresented: $showModal,
|
||||||
@ -876,6 +941,21 @@ struct BlindBoxView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预览第一个盲盒
|
||||||
|
#Preview("First Blind Box") {
|
||||||
|
BlindBoxView(mediaType: .image, blindBoxId: "7370140297747107840")
|
||||||
|
.onAppear {
|
||||||
|
// 仅在Preview中设置模拟令牌(不要在生产代码中使用)
|
||||||
|
#if DEBUG
|
||||||
|
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
|
||||||
|
// 设置模拟令牌用于Preview
|
||||||
|
let previewToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJqdGkiOjczNzAwMTY5NzMzODE1NzA1NjAsImlkZW50aXR5IjoiNzM1MDQzOTY2MzExNjYxOTc3NyIsImV4cCI6MTc1Nzc1Mzc3NH0.tZ8p5sW4KX6HFoJpJN0e4VmJOAGhTrYD2yTwQwilKpufzqOAfXX4vpGYBurgBIcHj2KmXKX2PQMOeeAtvAypDA"
|
||||||
|
let _ = KeychainHelper.saveAccessToken(previewToken)
|
||||||
|
print("🔑 Preview token set for testing")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
// struct TransparentVideoPlayer: UIViewRepresentable {
|
// struct TransparentVideoPlayer: UIViewRepresentable {
|
||||||
// func makeUIView(context: Context) -> UIView {
|
// func makeUIView(context: Context) -> UIView {
|
||||||
// let view = UIView()
|
// let view = UIView()
|
||||||
|
|||||||
@ -198,7 +198,7 @@ struct UserInfo: View {
|
|||||||
case .success(let blindBoxData):
|
case .success(let blindBoxData):
|
||||||
print("✅ 盲盒生成成功: \(blindBoxData?.id ?? "0")")
|
print("✅ 盲盒生成成功: \(blindBoxData?.id ?? "0")")
|
||||||
// 导航到首页盲盒等待用户开启第一个盲盒
|
// 导航到首页盲盒等待用户开启第一个盲盒
|
||||||
Router.shared.navigate(to: .blindBox(mediaType: .image))
|
Router.shared.navigate(to: .blindBox(mediaType: .image, blindBoxId: blindBoxData?.id ?? "0"))
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
print("❌ 盲盒生成失败: \(error.localizedDescription)")
|
print("❌ 盲盒生成失败: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,719 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import PhotosUI
|
|
||||||
import AVKit
|
|
||||||
import CoreTransferable
|
|
||||||
import CoreImage.CIFilterBuiltins
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static let didAddFirstMedia = Notification.Name("didAddFirstMedia")
|
|
||||||
}
|
|
||||||
/// 主上传视图
|
|
||||||
/// 提供媒体选择、预览和上传功能
|
|
||||||
@MainActor
|
|
||||||
struct MediaUploadView: View {
|
|
||||||
// MARK: - 属性
|
|
||||||
|
|
||||||
/// 上传管理器,负责处理上传逻辑
|
|
||||||
@StateObject private var uploadManager = MediaUploadManager()
|
|
||||||
/// 控制媒体选择器的显示/隐藏
|
|
||||||
@State private var showMediaPicker = false
|
|
||||||
/// 当前选中的媒体项
|
|
||||||
@State private var selectedMedia: MediaType? = nil
|
|
||||||
/// 当前选中的媒体索引集合
|
|
||||||
@State private var selectedIndices: Set<Int> = []
|
|
||||||
@State private var mediaPickerSelection: [MediaType] = [] // 添加这个状态变量
|
|
||||||
/// 上传完成状态
|
|
||||||
@State private var uploadComplete = false
|
|
||||||
/// 上传完成的文件ID列表
|
|
||||||
@State private var uploadedFileIds: [[String: String]] = []
|
|
||||||
|
|
||||||
// MARK: - 视图主体
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
// 顶部导航栏
|
|
||||||
topNavigationBar
|
|
||||||
|
|
||||||
// 上传提示信息
|
|
||||||
uploadHintView
|
|
||||||
Spacer()
|
|
||||||
.frame(height: uploadManager.selectedMedia.isEmpty ? 80 : 40)
|
|
||||||
// 主上传区域
|
|
||||||
MainUploadArea(
|
|
||||||
uploadManager: uploadManager,
|
|
||||||
showMediaPicker: $showMediaPicker,
|
|
||||||
selectedMedia: $selectedMedia
|
|
||||||
)
|
|
||||||
.id("mainUploadArea\(uploadManager.selectedMedia.count)")
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// // 上传结果展示
|
|
||||||
// if uploadComplete && !uploadedFileIds.isEmpty {
|
|
||||||
// VStack(alignment: .leading) {
|
|
||||||
// Text("上传完成!")
|
|
||||||
// .font(.headline)
|
|
||||||
|
|
||||||
// ScrollView {
|
|
||||||
// ForEach(Array(uploadedFileIds.enumerated()), id: \.offset) { index, fileInfo in
|
|
||||||
// VStack(alignment: .leading) {
|
|
||||||
// Text("文件 \(index + 1):")
|
|
||||||
// .font(.subheadline)
|
|
||||||
// Text("ID: \(fileInfo["file_id"] ?? "")")
|
|
||||||
// .font(.caption)
|
|
||||||
// .foregroundColor(.gray)
|
|
||||||
// }
|
|
||||||
// .padding()
|
|
||||||
// .frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
// .background(Color.gray.opacity(0.1))
|
|
||||||
// .cornerRadius(8)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .frame(height: 200)
|
|
||||||
// }
|
|
||||||
// .padding()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 继续按钮
|
|
||||||
continueButton
|
|
||||||
.padding(.bottom, 24)
|
|
||||||
}
|
|
||||||
.background(Color.themeTextWhiteSecondary)
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.navigationBarBackButtonHidden(true)
|
|
||||||
.sheet(isPresented: $showMediaPicker) {
|
|
||||||
// 媒体选择器
|
|
||||||
mediaPickerView
|
|
||||||
}
|
|
||||||
.onChange(of: uploadManager.uploadResults) { newResults in
|
|
||||||
handleUploadCompletion(results: newResults)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 子视图
|
|
||||||
|
|
||||||
/// 顶部导航栏
|
|
||||||
private var topNavigationBar: some View {
|
|
||||||
HStack {
|
|
||||||
// 返回按钮
|
|
||||||
Button(action: { Router.shared.pop() }) {
|
|
||||||
Image(systemName: "chevron.left")
|
|
||||||
.font(.system(size: 17, weight: .semibold))
|
|
||||||
.foregroundColor(.themeTextMessageMain)
|
|
||||||
}
|
|
||||||
.padding(.leading, 16)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 标题
|
|
||||||
Text("Complete Your Profile")
|
|
||||||
.font(Typography.font(for: .title2, family: .quicksandBold))
|
|
||||||
.foregroundColor(.themeTextMessageMain)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// 右侧占位视图(保持布局平衡)
|
|
||||||
Color.clear
|
|
||||||
.frame(width: 24, height: 24)
|
|
||||||
.padding(.trailing, 16)
|
|
||||||
}
|
|
||||||
.background(Color.themeTextWhiteSecondary)
|
|
||||||
// .padding(.horizontal)
|
|
||||||
.zIndex(1) // 确保导航栏显示在最上层
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 上传提示视图
|
|
||||||
private var uploadHintView: some View {
|
|
||||||
HStack (spacing: 6) {
|
|
||||||
SVGImage(svgName: "Tips")
|
|
||||||
.frame(width: 16, height: 16)
|
|
||||||
.padding(.leading,6)
|
|
||||||
Text("The upload process will take approximately 2 minutes. Thank you for your patience.")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.black)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(3)
|
|
||||||
}
|
|
||||||
.background(
|
|
||||||
Color.themeTextWhite
|
|
||||||
.cornerRadius(6)
|
|
||||||
)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 继续按钮
|
|
||||||
private var continueButton: some View {
|
|
||||||
Button(action: handleContinue) {
|
|
||||||
Text("Continue")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(uploadManager.selectedMedia.isEmpty ? Color.themeTextMessage : Color.themeTextMessageMain)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.frame(height: 56)
|
|
||||||
.background(uploadManager.selectedMedia.isEmpty ? Color.white : Color.themePrimary)
|
|
||||||
.cornerRadius(28)
|
|
||||||
.padding(.horizontal, 24)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
.disabled(uploadManager.selectedMedia.isEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 媒体选择器视图
|
|
||||||
private var mediaPickerView: some View {
|
|
||||||
MediaPicker(
|
|
||||||
selectedMedia: Binding(
|
|
||||||
get: { mediaPickerSelection },
|
|
||||||
set: { newSelections in
|
|
||||||
print("🔄 开始处理用户选择的媒体文件")
|
|
||||||
print("📌 新选择的媒体数量: \(newSelections.count)")
|
|
||||||
|
|
||||||
// 1. 去重处理:过滤掉已经存在的媒体项
|
|
||||||
var uniqueNewMedia: [MediaType] = []
|
|
||||||
|
|
||||||
for newItem in newSelections {
|
|
||||||
let isDuplicate = uploadManager.selectedMedia.contains { existingItem in
|
|
||||||
switch (existingItem, newItem) {
|
|
||||||
case (.image(let existingImage), .image(let newImage)):
|
|
||||||
return existingImage.pngData() == newImage.pngData()
|
|
||||||
case (.video(let existingURL, _), .video(let newURL, _)):
|
|
||||||
return existingURL == newURL
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isDuplicate {
|
|
||||||
uniqueNewMedia.append(newItem)
|
|
||||||
} else {
|
|
||||||
print("⚠️ 检测到重复文件,已跳过: \(newItem)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 添加新文件
|
|
||||||
if !uniqueNewMedia.isEmpty {
|
|
||||||
print("✅ 添加 \(uniqueNewMedia.count) 个新文件")
|
|
||||||
uploadManager.addMedia(uniqueNewMedia)
|
|
||||||
|
|
||||||
// 如果没有当前选中的媒体,则选择第一个新添加的
|
|
||||||
if selectedMedia == nil, let firstNewItem = uniqueNewMedia.first {
|
|
||||||
selectedMedia = firstNewItem
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始上传
|
|
||||||
uploadManager.startUpload()
|
|
||||||
} else {
|
|
||||||
print("ℹ️ 没有新文件需要添加,所有选择的文件都已存在")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
imageSelectionLimit: max(0, 20 - uploadManager.selectedMedia.filter {
|
|
||||||
if case .image = $0 { return true }
|
|
||||||
return false
|
|
||||||
}.count),
|
|
||||||
videoSelectionLimit: max(0, 5 - uploadManager.selectedMedia.filter {
|
|
||||||
if case .video = $0 { return true }
|
|
||||||
return false
|
|
||||||
}.count),
|
|
||||||
selectionMode: .multiple,
|
|
||||||
onDismiss: handleMediaPickerDismiss,
|
|
||||||
onUploadProgress: { index, progress in
|
|
||||||
print("文件 \(index) 上传进度: \(progress * 100)%")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.onAppear {
|
|
||||||
// 重置选择状态当选择器出现时
|
|
||||||
mediaPickerSelection = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 私有方法
|
|
||||||
|
|
||||||
/// 处理媒体选择器关闭事件
|
|
||||||
private func handleMediaPickerDismiss() {
|
|
||||||
showMediaPicker = false
|
|
||||||
print("媒体选择器关闭 - 开始处理")
|
|
||||||
|
|
||||||
// 如果有选中的媒体,开始上传
|
|
||||||
if !uploadManager.selectedMedia.isEmpty {
|
|
||||||
// 不需要在这里开始上传,因为handleMediaChange会处理
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理媒体变化
|
|
||||||
/// - Parameters:
|
|
||||||
/// - newMedia: 新的媒体数组
|
|
||||||
/// - oldMedia: 旧的媒体数组
|
|
||||||
private func handleMediaChange(_ newMedia: [MediaType], oldMedia: [MediaType]) {
|
|
||||||
print("开始处理媒体变化,新数量: \(newMedia.count), 原数量: \(oldMedia.count)")
|
|
||||||
|
|
||||||
// 如果没有变化,直接返回
|
|
||||||
guard newMedia != oldMedia else {
|
|
||||||
print("媒体未发生变化,跳过处理")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在后台线程处理媒体变化
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
|
||||||
// 找出新增的媒体(在newMedia中但不在oldMedia中的项)
|
|
||||||
let newItems = newMedia.filter { newItem in
|
|
||||||
!oldMedia.contains { $0.id == newItem.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
print("检测到\(newItems.count)个新增媒体项")
|
|
||||||
|
|
||||||
// 如果有新增媒体
|
|
||||||
if !newItems.isEmpty {
|
|
||||||
print("准备添加\(newItems.count)个新项...")
|
|
||||||
|
|
||||||
// 在主线程更新UI
|
|
||||||
DispatchQueue.main.async { [self] in
|
|
||||||
// 创建新的数组,包含原有媒体和新媒体
|
|
||||||
var updatedMedia = uploadManager.selectedMedia
|
|
||||||
updatedMedia.append(contentsOf: newItems)
|
|
||||||
|
|
||||||
// 更新选中的媒体
|
|
||||||
uploadManager.clearAllMedia()
|
|
||||||
uploadManager.addMedia(updatedMedia)
|
|
||||||
|
|
||||||
// 如果当前没有选中的媒体,则选中第一个新增的媒体
|
|
||||||
if selectedIndices.isEmpty && !newItems.isEmpty {
|
|
||||||
selectedIndices = [oldMedia.count] // 选择第一个新增项的索引
|
|
||||||
selectedMedia = newItems.first
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始上传新添加的媒体
|
|
||||||
uploadManager.startUpload()
|
|
||||||
print("媒体添加完成,总数量: \(uploadManager.selectedMedia.count)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 检查是否有正在上传的文件
|
|
||||||
/// - Returns: 是否正在上传
|
|
||||||
private func isUploading() -> Bool {
|
|
||||||
return uploadManager.uploadStatus.values.contains { status in
|
|
||||||
if case .uploading = status { return true }
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理上传完成
|
|
||||||
private func handleUploadCompletion(results: [String: MediaUploadManager.UploadResult]) {
|
|
||||||
// 转换为需要的格式
|
|
||||||
let formattedResults = results.map { (_, result) -> [String: String] in
|
|
||||||
return [
|
|
||||||
"file_id": result.fileId,
|
|
||||||
"preview_file_id": result.thumbnailId ?? result.fileId
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadedFileIds = formattedResults
|
|
||||||
uploadComplete = !uploadedFileIds.isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理继续按钮点击
|
|
||||||
private func handleContinue() {
|
|
||||||
// 获取所有已上传文件的结果
|
|
||||||
let uploadResults = uploadManager.uploadResults
|
|
||||||
guard !uploadResults.isEmpty else {
|
|
||||||
print("⚠️ 没有可用的文件ID")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 准备请求参数
|
|
||||||
let files = uploadResults.map { (_, result) -> [String: String] in
|
|
||||||
return [
|
|
||||||
"file_id": result.fileId,
|
|
||||||
"preview_file_id": result.thumbnailId ?? result.fileId
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送POST请求到/material接口
|
|
||||||
NetworkService.shared.postWithToken(
|
|
||||||
path: "/material",
|
|
||||||
parameters: files
|
|
||||||
) { (result: Result<EmptyResponse, NetworkError>) in
|
|
||||||
switch result {
|
|
||||||
case .success:
|
|
||||||
print("✅ 素材提交成功")
|
|
||||||
// 跳转到盲盒页面
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
Router.shared.navigate(to: .blindBox(mediaType: .video))
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
print("❌ 素材提交失败: \(error.localizedDescription)")
|
|
||||||
// 这里可以添加错误处理逻辑,比如显示错误提示
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主上传区域
|
|
||||||
|
|
||||||
/// 主上传区域视图
|
|
||||||
/// 显示上传提示、媒体预览和添加更多按钮
|
|
||||||
struct MainUploadArea: View {
|
|
||||||
// MARK: - 属性
|
|
||||||
|
|
||||||
/// 上传管理器
|
|
||||||
@ObservedObject var uploadManager: MediaUploadManager
|
|
||||||
/// 控制媒体选择器的显示/隐藏
|
|
||||||
@Binding var showMediaPicker: Bool
|
|
||||||
/// 当前选中的媒体
|
|
||||||
@Binding var selectedMedia: MediaType?
|
|
||||||
|
|
||||||
// MARK: - 视图主体
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack() {
|
|
||||||
Spacer()
|
|
||||||
.frame(height: 30)
|
|
||||||
// 标题
|
|
||||||
Text("Click to upload 20 images and 5 videos to generate your next blind box.")
|
|
||||||
.font(Typography.font(for: .title2, family: .quicksandBold))
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(.black)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.padding(.horizontal)
|
|
||||||
Spacer()
|
|
||||||
.frame(height: 50)
|
|
||||||
// 主显示区域
|
|
||||||
if let mediaToDisplay = selectedMedia ?? uploadManager.selectedMedia.first {
|
|
||||||
Button(action: { showMediaPicker = true }) {
|
|
||||||
MediaPreview(media: mediaToDisplay)
|
|
||||||
.id(mediaToDisplay.id)
|
|
||||||
.frame(width: 225, height: 225)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 16)
|
|
||||||
.stroke(Color.themePrimary, lineWidth: 5)
|
|
||||||
)
|
|
||||||
.cornerRadius(16)
|
|
||||||
.padding(.horizontal)
|
|
||||||
.transition(.opacity)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
UploadPromptView(showMediaPicker: $showMediaPicker)
|
|
||||||
}
|
|
||||||
// 媒体预览区域
|
|
||||||
mediaPreviewSection
|
|
||||||
Spacer()
|
|
||||||
.frame(height: 10)
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
print("MainUploadArea appeared")
|
|
||||||
print("Selected media count: \(uploadManager.selectedMedia.count)")
|
|
||||||
|
|
||||||
if selectedMedia == nil, let firstMedia = uploadManager.selectedMedia.first {
|
|
||||||
print("Selecting first media: \(firstMedia.id)")
|
|
||||||
selectedMedia = firstMedia
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .didAddFirstMedia)) { notification in
|
|
||||||
if let media = notification.userInfo?["media"] as? MediaType, selectedMedia == nil {
|
|
||||||
selectedMedia = media
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(Color.white)
|
|
||||||
.cornerRadius(18)
|
|
||||||
.animation(.default, value: selectedMedia?.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 子视图
|
|
||||||
|
|
||||||
/// 媒体预览区域
|
|
||||||
private var mediaPreviewSection: some View {
|
|
||||||
Group {
|
|
||||||
if !uploadManager.selectedMedia.isEmpty {
|
|
||||||
VStack(spacing: 4) {
|
|
||||||
// 横向滚动的缩略图列表
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
LazyHStack(spacing: 10) {
|
|
||||||
ForEach(Array(uploadManager.selectedMedia.enumerated()), id: \.element.id) { index, media in
|
|
||||||
mediaItemView(for: media, at: index)
|
|
||||||
}
|
|
||||||
// 当没有选择媒体时显示添加更多按钮
|
|
||||||
if !uploadManager.selectedMedia.isEmpty {
|
|
||||||
addMoreButton
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
.frame(height: 70)
|
|
||||||
}
|
|
||||||
.padding(.top, 10)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 单个媒体项视图
|
|
||||||
/// - Parameters:
|
|
||||||
/// - media: 媒体项
|
|
||||||
/// - index: 索引
|
|
||||||
/// - Returns: 媒体项视图
|
|
||||||
private func mediaItemView(for media: MediaType, at index: Int) -> some View {
|
|
||||||
ZStack(alignment: .topTrailing) {
|
|
||||||
// 媒体预览 - 始终使用本地资源
|
|
||||||
MediaPreview(media: media)
|
|
||||||
.frame(width: 58, height: 58)
|
|
||||||
.cornerRadius(8)
|
|
||||||
.shadow(radius: 1)
|
|
||||||
.overlay(
|
|
||||||
// 左上角序号
|
|
||||||
ZStack(alignment: .topLeading) {
|
|
||||||
Path { path in
|
|
||||||
let radius: CGFloat = 4
|
|
||||||
let width: CGFloat = 14
|
|
||||||
let height: CGFloat = 10
|
|
||||||
|
|
||||||
// 从左上角开始(带圆角)
|
|
||||||
path.move(to: CGPoint(x: 0, y: radius))
|
|
||||||
path.addQuadCurve(to: CGPoint(x: radius, y: 0),
|
|
||||||
control: CGPoint(x: 0, y: 0))
|
|
||||||
|
|
||||||
// 上边缘(右上角保持直角)
|
|
||||||
path.addLine(to: CGPoint(x: width, y: 0))
|
|
||||||
|
|
||||||
// 右边缘(右下角保持直角)
|
|
||||||
path.addLine(to: CGPoint(x: width, y: height - radius))
|
|
||||||
|
|
||||||
// 右下角圆角
|
|
||||||
path.addQuadCurve(to: CGPoint(x: width - radius, y: height),
|
|
||||||
control: CGPoint(x: width, y: height))
|
|
||||||
|
|
||||||
// 下边缘(左下角保持直角)
|
|
||||||
path.addLine(to: CGPoint(x: 0, y: height))
|
|
||||||
|
|
||||||
// 闭合路径
|
|
||||||
path.closeSubpath()
|
|
||||||
}
|
|
||||||
.fill(Color(hex: "BEBEBE").opacity(0.6))
|
|
||||||
.frame(width: 14, height: 10)
|
|
||||||
.overlay(
|
|
||||||
Text("\(index + 1)")
|
|
||||||
.font(.system(size: 8, weight: .bold))
|
|
||||||
.foregroundColor(.black)
|
|
||||||
.frame(width: 14, height: 10)
|
|
||||||
.offset(y: -1),
|
|
||||||
alignment: .topLeading
|
|
||||||
)
|
|
||||||
.padding([.top, .leading], 2)
|
|
||||||
|
|
||||||
// 右下角视频时长
|
|
||||||
if case .video(let url, _) = media, let videoURL = url as? URL {
|
|
||||||
VStack {
|
|
||||||
Spacer()
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(getVideoDuration(url: videoURL))
|
|
||||||
.font(.system(size: 8, weight: .bold))
|
|
||||||
.foregroundColor(.black)
|
|
||||||
.padding(.horizontal, 4)
|
|
||||||
.frame(height: 10)
|
|
||||||
.background(Color(hex: "BEBEBE").opacity(0.6))
|
|
||||||
.cornerRadius(2)
|
|
||||||
}
|
|
||||||
.padding([.trailing, .bottom], 0)
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
// 占位
|
|
||||||
VStack {
|
|
||||||
Spacer()
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text("占位")
|
|
||||||
.font(.system(size: 8, weight: .bold))
|
|
||||||
.foregroundColor(.black)
|
|
||||||
.padding(.horizontal, 4)
|
|
||||||
.frame(height: 10)
|
|
||||||
.background(Color(hex: "BEBEBE").opacity(0.6))
|
|
||||||
.cornerRadius(2)
|
|
||||||
}
|
|
||||||
.padding([.trailing, .bottom], 0)
|
|
||||||
}
|
|
||||||
.opacity(0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
alignment: .topLeading
|
|
||||||
)
|
|
||||||
.onTapGesture {
|
|
||||||
print("点击了媒体项,索引: \(index)")
|
|
||||||
withAnimation {
|
|
||||||
selectedMedia = media
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
|
|
||||||
// 右上角关闭按钮
|
|
||||||
Button(action: {
|
|
||||||
uploadManager.removeMedia(id: media.id)
|
|
||||||
if selectedMedia == media {
|
|
||||||
selectedMedia = nil
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Image(systemName: "xmark")
|
|
||||||
.font(.system(size: 8, weight: .bold))
|
|
||||||
.foregroundColor(.black)
|
|
||||||
.frame(width: 12, height: 12)
|
|
||||||
.background(
|
|
||||||
Circle()
|
|
||||||
.fill(Color(hex: "BEBEBE").opacity(0.6))
|
|
||||||
.frame(width: 12, height: 12)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.offset(x: 6, y: -6)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 4)
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 添加更多按钮
|
|
||||||
private var addMoreButton: some View {
|
|
||||||
Button(action: { showMediaPicker = true }) {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
.font(.system(size: 8, weight: .bold))
|
|
||||||
.foregroundColor(.black)
|
|
||||||
.frame(width: 58, height: 58)
|
|
||||||
.background(Color.white)
|
|
||||||
.cornerRadius(8)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.strokeBorder(style: StrokeStyle(
|
|
||||||
lineWidth: 2,
|
|
||||||
dash: [4, 4]
|
|
||||||
))
|
|
||||||
.foregroundColor(Color.themePrimary)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 上传提示视图
|
|
||||||
|
|
||||||
/// 上传提示视图
|
|
||||||
/// 显示上传区域的占位图和提示
|
|
||||||
struct UploadPromptView: View {
|
|
||||||
/// 控制媒体选择器的显示/隐藏
|
|
||||||
@Binding var showMediaPicker: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: { showMediaPicker = true }) {
|
|
||||||
// 上传图标
|
|
||||||
SVGImageHtml(svgName: "IP")
|
|
||||||
.frame(width: 225, height: 225)
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
.overlay(
|
|
||||||
ZStack {
|
|
||||||
RoundedRectangle(cornerRadius: 20)
|
|
||||||
.stroke(style: StrokeStyle(
|
|
||||||
lineWidth: 5,
|
|
||||||
lineCap: .round,
|
|
||||||
dash: [12, 8]
|
|
||||||
))
|
|
||||||
.foregroundColor(Color.themePrimary)
|
|
||||||
|
|
||||||
// Add plus icon in the center
|
|
||||||
Image(systemName: "plus")
|
|
||||||
.font(.system(size: 32, weight: .bold))
|
|
||||||
.foregroundColor(.black)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 媒体预览视图
|
|
||||||
|
|
||||||
/// 媒体预览视图
|
|
||||||
/// 显示图片或视频的预览图,始终使用本地资源
|
|
||||||
struct MediaPreview: View {
|
|
||||||
// MARK: - 属性
|
|
||||||
|
|
||||||
/// 媒体类型
|
|
||||||
let media: MediaType
|
|
||||||
|
|
||||||
// MARK: - 计算属性
|
|
||||||
|
|
||||||
/// 获取要显示的图片
|
|
||||||
private var displayImage: UIImage? {
|
|
||||||
switch media {
|
|
||||||
case .image(let uiImage):
|
|
||||||
return uiImage
|
|
||||||
case .video(_, let thumbnail):
|
|
||||||
return thumbnail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 视图主体
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
// 1. 显示图片或视频缩略图
|
|
||||||
if let image = displayImage {
|
|
||||||
Image(uiImage: image)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFill()
|
|
||||||
.transition(.opacity.animation(.easeInOut(duration: 0.2)))
|
|
||||||
} else {
|
|
||||||
// 2. 加载中的占位图
|
|
||||||
Color.gray.opacity(0.1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.aspectRatio(1, contentMode: .fill)
|
|
||||||
.clipped()
|
|
||||||
.cornerRadius(8)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.stroke(Color.themePrimary.opacity(0.3), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getVideoDuration(url: URL) -> String {
|
|
||||||
let asset = AVURLAsset(url: url)
|
|
||||||
let durationInSeconds = CMTimeGetSeconds(asset.duration)
|
|
||||||
guard durationInSeconds.isFinite else { return "0:00" }
|
|
||||||
|
|
||||||
let minutes = Int(durationInSeconds) / 60
|
|
||||||
let seconds = Int(durationInSeconds) % 60
|
|
||||||
return String(format: "%d:%02d", minutes, seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Response Types
|
|
||||||
|
|
||||||
private struct EmptyResponse: Decodable {
|
|
||||||
// Empty response type for endpoints that don't return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 扩展
|
|
||||||
|
|
||||||
/// 扩展 MediaType 以支持 Identifiable 协议
|
|
||||||
extension MediaType: Identifiable {
|
|
||||||
/// 唯一标识符
|
|
||||||
public var id: String {
|
|
||||||
switch self {
|
|
||||||
case .image(let uiImage):
|
|
||||||
return "image_\(uiImage.hashValue)"
|
|
||||||
case .video(let url, _):
|
|
||||||
return "video_\(url.absoluteString.hashValue)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TimeInterval {
|
|
||||||
var formattedDuration: String {
|
|
||||||
let minutes = Int(self) / 60
|
|
||||||
let seconds = Int(self) % 60
|
|
||||||
return String(format: "%d:%02d", minutes, seconds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 预览
|
|
||||||
|
|
||||||
struct MediaUploadView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
NavigationView {
|
|
||||||
MediaUploadView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -51,6 +51,9 @@ struct WakeApp: App {
|
|||||||
// route.view
|
// route.view
|
||||||
// }
|
// }
|
||||||
UserInfo()
|
UserInfo()
|
||||||
|
.navigationDestination(for: AppRoute.self) { route in
|
||||||
|
route.view
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 未登录:显示登录界面
|
// 未登录:显示登录界面
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user