feat: 素材上传
This commit is contained in:
parent
2c1c16b389
commit
008778d9a6
@ -285,6 +285,23 @@ struct BlindBoxView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 盲盒列表
|
||||||
|
NetworkService.shared.postWithToken(
|
||||||
|
path: "/blind_box/generate/mock",
|
||||||
|
parameters: ["box_type": "Image"]
|
||||||
|
) { (result: Result<APIResponse<[BlindList]>, NetworkError>) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
switch result {
|
||||||
|
case .success(let response):
|
||||||
|
self.blindList = response.data ?? []
|
||||||
|
print("✅✅✅✅✅✅✅✅✅ 成功获取 \(self.blindList.count) 个盲盒")
|
||||||
|
case .failure(let error):
|
||||||
|
self.blindList = []
|
||||||
|
print("❌❌ ❌ ❌ ❌ ❌ ❌ 获取盲盒列表失败:", error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -104,6 +104,7 @@ public enum NetworkError: Error {
|
|||||||
case other(Error)
|
case other(Error)
|
||||||
case networkError(Error)
|
case networkError(Error)
|
||||||
case unknownError(Error)
|
case unknownError(Error)
|
||||||
|
case invalidParameters
|
||||||
|
|
||||||
public var localizedDescription: String {
|
public var localizedDescription: String {
|
||||||
switch self {
|
switch self {
|
||||||
@ -123,6 +124,8 @@ public enum NetworkError: Error {
|
|||||||
return "网络错误: \(error.localizedDescription)"
|
return "网络错误: \(error.localizedDescription)"
|
||||||
case .unknownError(let error):
|
case .unknownError(let error):
|
||||||
return "未知错误: \(error.localizedDescription)"
|
return "未知错误: \(error.localizedDescription)"
|
||||||
|
case .invalidParameters:
|
||||||
|
return "无效的参数"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,7 +148,7 @@ class NetworkService {
|
|||||||
private func request<T: Decodable>(
|
private func request<T: Decodable>(
|
||||||
_ method: String,
|
_ method: String,
|
||||||
path: String,
|
path: String,
|
||||||
parameters: [String: Any]? = nil,
|
parameters: Any? = nil,
|
||||||
headers: [String: String]? = nil,
|
headers: [String: String]? = nil,
|
||||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||||
) {
|
) {
|
||||||
@ -184,7 +187,13 @@ class NetworkService {
|
|||||||
// 设置请求体(如果是POST/PUT请求)
|
// 设置请求体(如果是POST/PUT请求)
|
||||||
if let parameters = parameters, (method == "POST" || method == "PUT") {
|
if let parameters = parameters, (method == "POST" || method == "PUT") {
|
||||||
do {
|
do {
|
||||||
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
|
if JSONSerialization.isValidJSONObject(parameters) {
|
||||||
|
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
|
||||||
|
} else {
|
||||||
|
print("❌ [Network][#\(requestId)][\(method) \(path)] 参数不是有效的JSON对象")
|
||||||
|
completion(.failure(.invalidParameters))
|
||||||
|
return
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("❌ [Network][#\(requestId)][\(method) \(path)] 参数序列化失败: \(error.localizedDescription)")
|
print("❌ [Network][#\(requestId)][\(method) \(path)] 参数序列化失败: \(error.localizedDescription)")
|
||||||
completion(.failure(.other(error)))
|
completion(.failure(.other(error)))
|
||||||
@ -442,17 +451,29 @@ class NetworkService {
|
|||||||
/// POST 请求
|
/// POST 请求
|
||||||
func post<T: Decodable>(
|
func post<T: Decodable>(
|
||||||
path: String,
|
path: String,
|
||||||
parameters: [String: Any]? = nil,
|
parameters: Any? = nil,
|
||||||
headers: [String: String]? = nil,
|
headers: [String: String]? = nil,
|
||||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||||
) {
|
) {
|
||||||
request("POST", path: path, parameters: parameters, headers: headers, completion: completion)
|
var params: Any?
|
||||||
|
if let parameters = parameters {
|
||||||
|
if let dict = parameters as? [String: Any] {
|
||||||
|
params = dict
|
||||||
|
} else if let array = parameters as? [Any] {
|
||||||
|
params = array
|
||||||
|
} else {
|
||||||
|
print("❌ [Network] POST 请求参数类型不支持")
|
||||||
|
completion(.failure(.invalidParameters))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request("POST", path: path, parameters: params, headers: headers, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST 请求(带Token)
|
/// POST 请求(带Token)
|
||||||
func postWithToken<T: Decodable>(
|
func postWithToken<T: Decodable>(
|
||||||
path: String,
|
path: String,
|
||||||
parameters: [String: Any]? = nil,
|
parameters: Any? = nil,
|
||||||
headers: [String: String]? = nil,
|
headers: [String: String]? = nil,
|
||||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
completion: @escaping (Result<T, NetworkError>) -> Void
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -59,8 +59,10 @@ public class MediaUploadManager: ObservableObject {
|
|||||||
@Published public private(set) var selectedMedia: [MediaType] = []
|
@Published public private(set) var selectedMedia: [MediaType] = []
|
||||||
/// 上传状态
|
/// 上传状态
|
||||||
@Published public private(set) var uploadStatus: [String: MediaUploadStatus] = [:]
|
@Published public private(set) var uploadStatus: [String: MediaUploadStatus] = [:]
|
||||||
|
/// 上传结果
|
||||||
|
@Published public private(set) var uploadResults: [String: String] = [:] // Store fileId as String
|
||||||
|
|
||||||
private let uploader = ImageUploadService()
|
private let uploader = ImageUploadService() // Use ImageUploadService
|
||||||
private let logger = Logger(subsystem: "com.yourapp.media", category: "MediaUploadManager")
|
private let logger = Logger(subsystem: "com.yourapp.media", category: "MediaUploadManager")
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
@ -93,6 +95,7 @@ public class MediaUploadManager: ObservableObject {
|
|||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self.selectedMedia.removeAll { $0.id == id }
|
self.selectedMedia.removeAll { $0.id == id }
|
||||||
self.uploadStatus.removeValue(forKey: id)
|
self.uploadStatus.removeValue(forKey: id)
|
||||||
|
self.uploadResults.removeValue(forKey: id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,6 +103,7 @@ public class MediaUploadManager: ObservableObject {
|
|||||||
public func clearAllMedia() {
|
public func clearAllMedia() {
|
||||||
selectedMedia.removeAll()
|
selectedMedia.removeAll()
|
||||||
uploadStatus.removeAll()
|
uploadStatus.removeAll()
|
||||||
|
uploadResults.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 开始上传所有选中的媒体
|
/// 开始上传所有选中的媒体
|
||||||
@ -112,6 +116,9 @@ public class MediaUploadManager: ObservableObject {
|
|||||||
return !status.isCompleted && !status.isUploading
|
return !status.isCompleted && !status.isUploading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清空之前的上传结果
|
||||||
|
self.uploadResults.removeAll()
|
||||||
|
|
||||||
for media in mediaToUpload {
|
for media in mediaToUpload {
|
||||||
self.uploadMedia(media)
|
self.uploadMedia(media)
|
||||||
}
|
}
|
||||||
@ -146,43 +153,73 @@ public class MediaUploadManager: ObservableObject {
|
|||||||
updateStatus(for: media.id, status: .uploading(progress: 0))
|
updateStatus(for: media.id, status: .uploading(progress: 0))
|
||||||
|
|
||||||
// 转换媒体类型
|
// 转换媒体类型
|
||||||
let uploadMediaType: ImageUploadService.MediaType
|
let uploadMedia: ImageUploadService.MediaType
|
||||||
switch media {
|
switch media {
|
||||||
case .image(let image):
|
case .image(let uiImage):
|
||||||
uploadMediaType = .image(image)
|
uploadMedia = .image(uiImage)
|
||||||
case .video(let url, let thumbnail):
|
case .video(let url, let thumbnail):
|
||||||
uploadMediaType = .video(url, thumbnail)
|
uploadMedia = .video(url as URL, thumbnail)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始上传
|
// 上传媒体文件
|
||||||
uploader.uploadMedia(
|
uploader.uploadMedia(uploadMedia,
|
||||||
uploadMediaType,
|
progress: { progress in
|
||||||
progress: { [weak self] progress in
|
// 更新上传进度
|
||||||
guard let self = self else { return }
|
Task { @MainActor in
|
||||||
Task { @MainActor in
|
self.updateStatus(for: media.id, status: .uploading(progress: progress.progress))
|
||||||
self.updateStatus(for: media.id, status: .uploading(progress: progress.progress))
|
}
|
||||||
}
|
},
|
||||||
},
|
completion: { [weak self] result in
|
||||||
completion: { [weak self] result in
|
guard let self = self else { return }
|
||||||
guard let self = self else { return }
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let uploadResult):
|
case .success(let uploadResult):
|
||||||
self.logger.info("✅ 上传成功 (\(media.id)): \(uploadResult.fileId)")
|
let fileId = uploadResult.fileId
|
||||||
self.updateStatus(for: media.id, status: .completed(fileId: uploadResult.fileId))
|
self.logger.info("✅ 上传成功 (\(media.id)): \(fileId)")
|
||||||
case .failure(let error):
|
self.uploadResults[media.id] = fileId
|
||||||
self.logger.error("❌ 上传失败 (\(media.id)): \(error.localizedDescription)")
|
self.updateStatus(for: media.id, status: .completed(fileId: fileId))
|
||||||
self.updateStatus(for: media.id, status: .failed(error))
|
|
||||||
}
|
// 打印上传结果
|
||||||
}
|
if self.isAllUploaded {
|
||||||
}
|
self.printUploadResults()
|
||||||
)
|
}
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
self.logger.error("❌ 上传失败 (\(media.id)): \(error.localizedDescription)")
|
||||||
|
self.updateStatus(for: media.id, status: .failed(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func updateStatus(for mediaId: String, status: MediaUploadStatus) {
|
private func updateStatus(for mediaId: String, status: MediaUploadStatus) {
|
||||||
uploadStatus[mediaId] = status
|
uploadStatus[mediaId] = status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Upload Results
|
||||||
|
|
||||||
|
/// 打印上传结果
|
||||||
|
private func printUploadResults() {
|
||||||
|
let results = self.selectedMedia.compactMap { media -> [String: String]? in
|
||||||
|
guard let result = self.uploadResults[media.id] else { return nil }
|
||||||
|
return [
|
||||||
|
"file_id": result,
|
||||||
|
"preview_file_id": result
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonData = try JSONSerialization.data(withJSONObject: results, options: [.prettyPrinted, .sortedKeys])
|
||||||
|
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
||||||
|
print("📦 上传完成,文件ID列表:")
|
||||||
|
print(jsonString)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("❌ 无法序列化上传结果: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Preview Helper
|
// MARK: - Preview Helper
|
||||||
|
|||||||
@ -288,12 +288,15 @@ struct LoginView: View {
|
|||||||
case .networkError(let error):
|
case .networkError(let error):
|
||||||
print(" → 网络错误: \(error.localizedDescription)")
|
print(" → 网络错误: \(error.localizedDescription)")
|
||||||
errorMessage = "网络连接失败,请检查网络"
|
errorMessage = "网络连接失败,请检查网络"
|
||||||
|
case .other(let error):
|
||||||
|
print(" → 其他错误: \(error.localizedDescription)")
|
||||||
|
errorMessage = "发生未知错误"
|
||||||
case .unknownError(let error):
|
case .unknownError(let error):
|
||||||
print(" → 未知错误: \(error.localizedDescription)")
|
print(" → 未知错误: \(error.localizedDescription)")
|
||||||
errorMessage = "发生未知错误"
|
errorMessage = "发生未知错误"
|
||||||
case .other(let error):
|
case .invalidParameters:
|
||||||
print(" → 其他错误: \(error.localizedDescription)")
|
print(" → 无效的参数")
|
||||||
errorMessage = "发生错误: \(error.localizedDescription)"
|
errorMessage = "请求参数错误,请重试"
|
||||||
}
|
}
|
||||||
|
|
||||||
self.errorMessage = errorMessage
|
self.errorMessage = errorMessage
|
||||||
|
|||||||
@ -19,6 +19,10 @@ struct MediaUploadView: View {
|
|||||||
/// 当前选中的媒体索引集合
|
/// 当前选中的媒体索引集合
|
||||||
@State private var selectedIndices: Set<Int> = []
|
@State private var selectedIndices: Set<Int> = []
|
||||||
@State private var mediaPickerSelection: [MediaType] = [] // 添加这个状态变量
|
@State private var mediaPickerSelection: [MediaType] = [] // 添加这个状态变量
|
||||||
|
/// 上传完成状态
|
||||||
|
@State private var uploadComplete = false
|
||||||
|
/// 上传完成的文件ID列表
|
||||||
|
@State private var uploadedFileIds: [[String: String]] = []
|
||||||
|
|
||||||
// MARK: - 视图主体
|
// MARK: - 视图主体
|
||||||
|
|
||||||
@ -41,6 +45,32 @@ struct MediaUploadView: View {
|
|||||||
|
|
||||||
Spacer()
|
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
|
continueButton
|
||||||
.padding(.bottom, 24)
|
.padding(.bottom, 24)
|
||||||
@ -52,6 +82,9 @@ struct MediaUploadView: View {
|
|||||||
// 媒体选择器
|
// 媒体选择器
|
||||||
mediaPickerView
|
mediaPickerView
|
||||||
}
|
}
|
||||||
|
.onChange(of: uploadManager.uploadResults) { newResults in
|
||||||
|
handleUploadCompletion(results: newResults)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 子视图
|
// MARK: - 子视图
|
||||||
@ -108,10 +141,7 @@ struct MediaUploadView: View {
|
|||||||
|
|
||||||
/// 继续按钮
|
/// 继续按钮
|
||||||
private var continueButton: some View {
|
private var continueButton: some View {
|
||||||
Button(action: {
|
Button(action: handleContinue) {
|
||||||
// 处理继续操作
|
|
||||||
Router.shared.navigate(to: .blindBox(mediaType: .video))
|
|
||||||
}) {
|
|
||||||
Text("Continue")
|
Text("Continue")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(uploadManager.selectedMedia.isEmpty ? Color.themeTextMessage : Color.themeTextMessageMain)
|
.foregroundColor(uploadManager.selectedMedia.isEmpty ? Color.themeTextMessage : Color.themeTextMessageMain)
|
||||||
@ -264,6 +294,56 @@ struct MediaUploadView: View {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 处理上传完成
|
||||||
|
private func handleUploadCompletion(results: [String: String]) {
|
||||||
|
uploadedFileIds = results.map { ["file_id": $0.value, "preview_file_id": $0.value] }
|
||||||
|
uploadComplete = !uploadedFileIds.isEmpty
|
||||||
|
|
||||||
|
// 打印结果到控制台
|
||||||
|
if let jsonData = try? JSONSerialization.data(withJSONObject: uploadedFileIds, options: .prettyPrinted),
|
||||||
|
let jsonString = String(data: jsonData, encoding: .utf8) {
|
||||||
|
print("📦 上传完成,文件ID列表:")
|
||||||
|
print(jsonString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理继续按钮点击
|
||||||
|
private func handleContinue() {
|
||||||
|
// 获取所有已上传文件的ID
|
||||||
|
let fileIds = uploadManager.uploadResults.map { $0.value }
|
||||||
|
|
||||||
|
guard !fileIds.isEmpty else {
|
||||||
|
print("⚠️ 没有可用的文件ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备请求参数
|
||||||
|
let files = fileIds.map { fileId -> [String: String] in
|
||||||
|
return [
|
||||||
|
"file_id": fileId,
|
||||||
|
"preview_file_id": 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: - 主上传区域
|
// MARK: - 主上传区域
|
||||||
@ -595,6 +675,13 @@ private func getVideoDuration(url: URL) -> String {
|
|||||||
let seconds = Int(durationInSeconds) % 60
|
let seconds = Int(durationInSeconds) % 60
|
||||||
return String(format: "%d:%02d", minutes, seconds)
|
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: - 扩展
|
// MARK: - 扩展
|
||||||
|
|
||||||
/// 扩展 MediaType 以支持 Identifiable 协议
|
/// 扩展 MediaType 以支持 Identifiable 协议
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user