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 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
) { ) {

View File

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

View File

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

View File

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