feat: 素材上传

This commit is contained in:
jinyaqiu 2025-08-29 19:24:58 +08:00
parent 2c1c16b389
commit 008778d9a6
5 changed files with 205 additions and 40 deletions

View File

@ -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)
}
}
}
}
}

View File

@ -104,6 +104,7 @@ public enum NetworkError: Error {
case other(Error)
case networkError(Error)
case unknownError(Error)
case invalidParameters
public var localizedDescription: String {
switch self {
@ -123,6 +124,8 @@ public enum NetworkError: Error {
return "网络错误: \(error.localizedDescription)"
case .unknownError(let error):
return "未知错误: \(error.localizedDescription)"
case .invalidParameters:
return "无效的参数"
}
}
}
@ -145,7 +148,7 @@ class NetworkService {
private func request<T: Decodable>(
_ method: String,
path: String,
parameters: [String: Any]? = nil,
parameters: Any? = nil,
headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {
@ -184,7 +187,13 @@ class NetworkService {
// POST/PUT
if let parameters = parameters, (method == "POST" || method == "PUT") {
do {
if JSONSerialization.isValidJSONObject(parameters) {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
} else {
print("❌ [Network][#\(requestId)][\(method) \(path)] 参数不是有效的JSON对象")
completion(.failure(.invalidParameters))
return
}
} catch {
print("❌ [Network][#\(requestId)][\(method) \(path)] 参数序列化失败: \(error.localizedDescription)")
completion(.failure(.other(error)))
@ -442,17 +451,29 @@ class NetworkService {
/// POST
func post<T: Decodable>(
path: String,
parameters: [String: Any]? = nil,
parameters: Any? = nil,
headers: [String: String]? = nil,
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
func postWithToken<T: Decodable>(
path: String,
parameters: [String: Any]? = nil,
parameters: Any? = nil,
headers: [String: String]? = nil,
completion: @escaping (Result<T, NetworkError>) -> Void
) {

View File

@ -59,8 +59,10 @@ public class MediaUploadManager: ObservableObject {
@Published public private(set) var selectedMedia: [MediaType] = []
///
@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")
public init() {}
@ -93,6 +95,7 @@ public class MediaUploadManager: ObservableObject {
Task { @MainActor in
self.selectedMedia.removeAll { $0.id == id }
self.uploadStatus.removeValue(forKey: id)
self.uploadResults.removeValue(forKey: id)
}
}
@ -100,6 +103,7 @@ public class MediaUploadManager: ObservableObject {
public func clearAllMedia() {
selectedMedia.removeAll()
uploadStatus.removeAll()
uploadResults.removeAll()
}
///
@ -112,6 +116,9 @@ public class MediaUploadManager: ObservableObject {
return !status.isCompleted && !status.isUploading
}
//
self.uploadResults.removeAll()
for media in mediaToUpload {
self.uploadMedia(media)
}
@ -146,43 +153,73 @@ public class MediaUploadManager: ObservableObject {
updateStatus(for: media.id, status: .uploading(progress: 0))
//
let uploadMediaType: ImageUploadService.MediaType
let uploadMedia: ImageUploadService.MediaType
switch media {
case .image(let image):
uploadMediaType = .image(image)
case .image(let uiImage):
uploadMedia = .image(uiImage)
case .video(let url, let thumbnail):
uploadMediaType = .video(url, thumbnail)
uploadMedia = .video(url as URL, thumbnail)
}
//
uploader.uploadMedia(
uploadMediaType,
progress: { [weak self] progress in
guard let self = self else { return }
//
uploader.uploadMedia(uploadMedia,
progress: { progress in
//
Task { @MainActor in
self.updateStatus(for: media.id, status: .uploading(progress: progress.progress))
}
},
completion: { [weak self] result in
guard let self = self else { return }
Task { @MainActor in
switch result {
case .success(let uploadResult):
self.logger.info("✅ 上传成功 (\(media.id)): \(uploadResult.fileId)")
self.updateStatus(for: media.id, status: .completed(fileId: uploadResult.fileId))
let fileId = uploadResult.fileId
self.logger.info("✅ 上传成功 (\(media.id)): \(fileId)")
self.uploadResults[media.id] = fileId
self.updateStatus(for: media.id, status: .completed(fileId: fileId))
//
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
private func updateStatus(for mediaId: String, status: MediaUploadStatus) {
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

View File

@ -288,12 +288,15 @@ struct LoginView: View {
case .networkError(let error):
print(" → 网络错误: \(error.localizedDescription)")
errorMessage = "网络连接失败,请检查网络"
case .other(let error):
print(" → 其他错误: \(error.localizedDescription)")
errorMessage = "发生未知错误"
case .unknownError(let error):
print(" → 未知错误: \(error.localizedDescription)")
errorMessage = "发生未知错误"
case .other(let error):
print("其他错误: \(error.localizedDescription)")
errorMessage = "发生错误: \(error.localizedDescription)"
case .invalidParameters:
print("无效的参数")
errorMessage = "请求参数错误,请重试"
}
self.errorMessage = errorMessage

View File

@ -19,6 +19,10 @@ struct MediaUploadView: View {
///
@State private var selectedIndices: Set<Int> = []
@State private var mediaPickerSelection: [MediaType] = [] //
///
@State private var uploadComplete = false
/// ID
@State private var uploadedFileIds: [[String: String]] = []
// MARK: -
@ -41,6 +45,32 @@ struct MediaUploadView: View {
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)
@ -52,6 +82,9 @@ struct MediaUploadView: View {
//
mediaPickerView
}
.onChange(of: uploadManager.uploadResults) { newResults in
handleUploadCompletion(results: newResults)
}
}
// MARK: -
@ -108,10 +141,7 @@ struct MediaUploadView: View {
///
private var continueButton: some View {
Button(action: {
//
Router.shared.navigate(to: .blindBox(mediaType: .video))
}) {
Button(action: handleContinue) {
Text("Continue")
.font(.headline)
.foregroundColor(uploadManager.selectedMedia.isEmpty ? Color.themeTextMessage : Color.themeTextMessageMain)
@ -264,6 +294,56 @@ struct MediaUploadView: View {
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: -
@ -595,6 +675,13 @@ private func getVideoDuration(url: URL) -> String {
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