feat: 视频缩略图

This commit is contained in:
jinyaqiu 2025-08-29 20:44:01 +08:00
parent 008778d9a6
commit 96e58806d4
5 changed files with 258 additions and 201 deletions

View File

@ -199,32 +199,48 @@ public class ImageUploadService {
self.uploader.uploadImage( self.uploader.uploadImage(
compressedThumbnail, compressedThumbnail,
progress: { _ in }, progress: { uploadProgress in
// 10%
let progressInfo = UploadProgress(
current: 90 + Int(uploadProgress * 10),
total: 100,
progress: 0.9 + (uploadProgress * 0.1),
isOriginal: false
)
progress(progressInfo)
},
completion: { thumbnailResult in completion: { thumbnailResult in
switch thumbnailResult { switch thumbnailResult {
case .success(let thumbnailUploadResult): case .success(let thumbnailUploadResult):
print("✅ 视频缩略图上传完成, fileId: \(thumbnailUploadResult.fileId)") print("✅ 视频缩略图上传完成, fileId: \(thumbnailUploadResult.fileId)")
let result = MediaUploadResult.video(
video: videoResult, // preview_file_id ID
thumbnail: thumbnailUploadResult let finalVideoResult = ImageUploaderGetID.UploadResult(
fileUrl: videoResult.fileUrl,
fileName: videoResult.fileName,
fileSize: videoResult.fileSize,
fileId: videoResult.fileId,
previewFileId: thumbnailUploadResult.fileId // 使IDpreview_file_id
) )
completion(.success(result))
completion(.success(.video(video: finalVideoResult, thumbnail: thumbnailUploadResult)))
case .failure(let error): case .failure(let error):
print("❌ 视频缩略图上传失败: \(error.localizedDescription)") print("❌ 视频缩略图上传失败: \(error.localizedDescription)")
completion(.failure(error)) // 使
completion(.success(.video(video: videoResult, thumbnail: nil)))
} }
} }
) )
} else { } else {
let error = NSError(domain: "ImageUploadService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to compress thumbnail"]) //
print("❌ 视频缩略图压缩失败") completion(.success(.video(video: videoResult, thumbnail: nil)))
completion(.failure(error))
} }
case .failure(let error): case .failure(let error):
print("❌ 视频缩略图提取失败: \(error.localizedDescription)") print("❌ 视频缩略图提取失败: \(error.localizedDescription)")
completion(.failure(error)) //
completion(.success(.video(video: videoResult, thumbnail: nil)))
} }
} }
@ -352,7 +368,7 @@ public class ImageUploadService {
/// ///
public enum MediaUploadResult { public enum MediaUploadResult {
case file(ImageUploaderGetID.UploadResult) case file(ImageUploaderGetID.UploadResult)
case video(video: ImageUploaderGetID.UploadResult, thumbnail: ImageUploaderGetID.UploadResult) case video(video: ImageUploaderGetID.UploadResult, thumbnail: ImageUploaderGetID.UploadResult?)
/// IDID /// IDID
public var fileId: String { public var fileId: String {
@ -363,6 +379,36 @@ public class ImageUploadService {
return videoResult.fileId return videoResult.fileId
} }
} }
/// IDID
public var previewFileId: String? {
switch self {
case .file:
return nil
case .video(_, let thumbnailResult):
return thumbnailResult?.fileId
}
}
/// URLURL
public var fileUrl: String {
switch self {
case .file(let result):
return result.fileUrl
case .video(let videoResult, _):
return videoResult.fileUrl
}
}
/// URL
public var thumbnailUrl: String? {
switch self {
case .file:
return nil
case .video(_, let thumbnailResult):
return thumbnailResult?.fileUrl
}
}
} }
/// ///

View File

@ -12,12 +12,14 @@ public class ImageUploaderGetID: ObservableObject {
public let fileName: String public let fileName: String
public let fileSize: Int public let fileSize: Int
public let fileId: String public let fileId: String
public let previewFileId: String?
public init(fileUrl: String, fileName: String, fileSize: Int, fileId: String) { public init(fileUrl: String, fileName: String, fileSize: Int, fileId: String, previewFileId: String? = nil) {
self.fileUrl = fileUrl self.fileUrl = fileUrl
self.fileName = fileName self.fileName = fileName
self.fileSize = fileSize self.fileSize = fileSize
self.fileId = fileId self.fileId = fileId
self.previewFileId = previewFileId
} }
} }
@ -90,7 +92,11 @@ public class ImageUploaderGetID: ObservableObject {
} }
// 2. URL // 2. URL
getUploadURL(for: imageData) { [weak self] result in getUploadURL(
for: imageData,
mimeType: "image/jpeg",
originalFilename: "image_\(UUID().uuidString).jpg"
) { [weak self] result in
switch result { switch result {
case .success((let fileId, let uploadURL)): case .success((let fileId, let uploadURL)):
print("📤 获取到上传URL开始上传文件...") print("📤 获取到上传URL开始上传文件...")
@ -110,7 +116,7 @@ public class ImageUploaderGetID: ObservableObject {
// 4. // 4.
self?.confirmUpload( self?.confirmUpload(
fileId: fileId, fileId: fileId,
fileName: "avatar_\(UUID().uuidString).jpg", fileName: "image_\(UUID().uuidString).jpg",
fileSize: imageData.count, fileSize: imageData.count,
completion: completion completion: completion
) )
@ -206,76 +212,6 @@ public class ImageUploaderGetID: ObservableObject {
// MARK: - // MARK: -
/// URL
private func getUploadURL(
for imageData: Data,
completion: @escaping (Result<(fileId: String, uploadURL: URL), Error>) -> Void
) {
let fileName = "avatar_\(UUID().uuidString).jpg"
let parameters: [String: Any] = [
"filename": fileName,
"content_type": "image/jpeg",
"file_size": imageData.count
]
let urlString = "\(apiConfig.baseURL)/file/generate-upload-url"
guard let url = URL(string: urlString) else {
completion(.failure(UploadError.invalidURL))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = apiConfig.authHeaders
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
print("📤 准备上传请求,文件名: \(fileName), 大小: \(Double(imageData.count) / 1024.0) KB")
} catch {
print("❌ 序列化请求参数失败: \(error.localizedDescription)")
completion(.failure(error))
return
}
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(UploadError.uploadFailed(error)))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
completion(.failure(UploadError.invalidResponse))
return
}
guard let data = data else {
completion(.failure(UploadError.invalidResponse))
return
}
//
if let responseString = String(data: data, encoding: .utf8) {
print("📥 获取上传URL响应: \(responseString)")
}
do {
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let dataDict = json["data"] as? [String: Any],
let fileId = dataDict["file_id"] as? String,
let uploadURLString = dataDict["upload_url"] as? String,
let uploadURL = URL(string: uploadURLString) else {
throw UploadError.invalidResponse
}
completion(.success((fileId: fileId, uploadURL: uploadURL)))
} catch {
completion(.failure(UploadError.invalidResponse))
}
}
task.resume()
}
/// URL /// URL
private func getUploadURL( private func getUploadURL(
for fileData: Data, for fileData: Data,
@ -291,7 +227,14 @@ public class ImageUploaderGetID: ObservableObject {
] ]
let urlString = "\(apiConfig.baseURL)/file/generate-upload-url" let urlString = "\(apiConfig.baseURL)/file/generate-upload-url"
print("🌐 准备请求上传URL...")
print(" - 目标URL: \(urlString)")
print(" - 文件名: \(fileName)")
print(" - 文件大小: \(Double(fileData.count) / 1024.0) KB")
print(" - MIME类型: \(mimeType)")
guard let url = URL(string: urlString) else { guard let url = URL(string: urlString) else {
print("❌ 错误: 无效的URL: \(urlString)")
completion(.failure(UploadError.invalidURL)) completion(.failure(UploadError.invalidURL))
return return
} }
@ -302,7 +245,9 @@ public class ImageUploaderGetID: ObservableObject {
do { do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters) request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
print("📤 准备上传请求,文件名: \(fileName), 大小: \(Double(fileData.count) / 1024.0) KB") if let bodyString = String(data: request.httpBody ?? Data(), encoding: .utf8) {
print("📤 请求体: \(bodyString)")
}
} catch { } catch {
print("❌ 序列化请求参数失败: \(error.localizedDescription)") print("❌ 序列化请求参数失败: \(error.localizedDescription)")
completion(.failure(error)) completion(.failure(error))
@ -311,37 +256,61 @@ public class ImageUploaderGetID: ObservableObject {
let task = session.dataTask(with: request) { data, response, error in let task = session.dataTask(with: request) { data, response, error in
if let error = error { if let error = error {
print("❌ 获取上传URL请求失败: \(error.localizedDescription)")
completion(.failure(UploadError.uploadFailed(error))) completion(.failure(UploadError.uploadFailed(error)))
return return
} }
guard let httpResponse = response as? HTTPURLResponse else { guard let httpResponse = response as? HTTPURLResponse else {
print("❌ 无效的服务器响应")
completion(.failure(UploadError.invalidResponse)) completion(.failure(UploadError.invalidResponse))
return return
} }
print("📥 收到上传URL响应")
print(" - 状态码: \(httpResponse.statusCode)")
guard let data = data else { guard let data = data else {
print("❌ 响应数据为空")
completion(.failure(UploadError.invalidResponse)) completion(.failure(UploadError.invalidResponse))
return return
} }
// //
print(" - 响应头: \(httpResponse.allHeaderFields)")
//
if let responseString = String(data: data, encoding: .utf8) { if let responseString = String(data: data, encoding: .utf8) {
print("📥 上传URL响应: \(responseString)") print(" - 响应体: \(responseString)")
} }
do { do {
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
guard let code = json?["code"] as? Int, code == 0, print(" - 解析的JSON: \(String(describing: json))")
let dataDict = json?["data"] as? [String: Any],
guard let code = json?["code"] as? Int, code == 0 else {
let errorMessage = json?["message"] as? String ?? "未知错误"
print("❌ 服务器返回错误: \(errorMessage)")
completion(.failure(UploadError.serverError(errorMessage)))
return
}
guard let dataDict = json?["data"] as? [String: Any],
let fileId = dataDict["file_id"] as? String, let fileId = dataDict["file_id"] as? String,
let uploadURLString = dataDict["upload_url"] as? String, let uploadURLString = dataDict["upload_url"] as? String,
let uploadURL = URL(string: uploadURLString) else { let uploadURL = URL(string: uploadURLString) else {
throw UploadError.invalidResponse print("❌ 响应数据格式错误")
completion(.failure(UploadError.invalidResponse))
return
} }
print("✅ 成功获取上传URL")
print(" - 文件ID: \(fileId)")
print(" - 上传URL: \(uploadURLString)")
completion(.success((fileId: fileId, uploadURL: uploadURL))) completion(.success((fileId: fileId, uploadURL: uploadURL)))
} catch { } catch {
print("❌ 解析响应数据失败: \(error.localizedDescription)")
completion(.failure(UploadError.invalidResponse)) completion(.failure(UploadError.invalidResponse))
} }
} }
@ -421,75 +390,61 @@ public class ImageUploaderGetID: ObservableObject {
public func uploadFile( public func uploadFile(
fileData: Data, fileData: Data,
to uploadURL: URL, to uploadURL: URL,
mimeType: String = "application/octet-stream", mimeType: String,
onProgress: @escaping (Double) -> Void, onProgress: @escaping (Double) -> Void,
completion: @escaping (Result<Void, Error>) -> Void completion: @escaping (Result<Void, Error>) -> Void
) -> URLSessionUploadTask { ) -> URLSessionUploadTask {
print("📤 开始上传文件...")
var request = URLRequest(url: uploadURL) var request = URLRequest(url: uploadURL)
request.httpMethod = "PUT" request.httpMethod = "PUT"
request.setValue(mimeType, forHTTPHeaderField: "Content-Type") request.setValue(mimeType, forHTTPHeaderField: "Content-Type")
let task = session.uploadTask(with: request, from: fileData) { _, response, error in let task = session.uploadTask(
with: request,
from: fileData
) { data, response, error in
if let error = error { if let error = error {
completion(.failure(error)) print("❌ 文件上传失败: \(error.localizedDescription)")
completion(.failure(UploadError.uploadFailed(error)))
return return
} }
guard let httpResponse = response as? HTTPURLResponse else { guard let httpResponse = response as? HTTPURLResponse else {
print("❌ 无效的响应")
completion(.failure(UploadError.invalidResponse)) completion(.failure(UploadError.invalidResponse))
return return
} }
guard (200...299).contains(httpResponse.statusCode) else { guard (200...299).contains(httpResponse.statusCode) else {
let statusCode = httpResponse.statusCode let statusCode = httpResponse.statusCode
completion(.failure(UploadError.serverError("上传失败,状态码: \(statusCode)"))) print("❌ 服务器返回错误状态码: \(statusCode)")
completion(.failure(UploadError.serverError("HTTP \(statusCode)")))
return return
} }
print("✅ 文件上传成功")
completion(.success(())) completion(.success(()))
} }
// //
if #available(iOS 11.0, *) { let progressObserver = task.progress.observe(\.fractionCompleted) { progress, _ in
let progressObserver = task.progress.observe(\.fractionCompleted) { progressValue, _ in let percentComplete = progress.fractionCompleted
DispatchQueue.main.async { print("📊 文件上传进度: \(Int(percentComplete * 100))%")
onProgress(progressValue.fractionCompleted) onProgress(percentComplete)
}
}
task.addCompletionHandler { [weak task] in
progressObserver.invalidate()
task?.progress.cancel()
}
} else {
var lastProgress: Double = 0
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
let bytesSent = task.countOfBytesSent
let totalBytes = task.countOfBytesExpectedToSend
let currentProgress = totalBytes > 0 ? Double(bytesSent) / Double(totalBytes) : 0
// UI
if abs(currentProgress - lastProgress) > 0.01 || currentProgress >= 1.0 {
lastProgress = currentProgress
DispatchQueue.main.async {
onProgress(min(currentProgress, 1.0))
}
}
if currentProgress >= 1.0 {
timer.invalidate()
}
}
task.addCompletionHandler {
timer.invalidate()
}
} }
//
objc_setAssociatedObject(task, &AssociatedKeys.progressObserver, progressObserver, .OBJC_ASSOCIATION_RETAIN)
task.resume() task.resume()
return task return task
} }
private struct AssociatedKeys {
static var progressObserver = "progressObserver"
}
// MARK: - // MARK: -
/// ///

View File

@ -60,7 +60,7 @@ public class MediaUploadManager: ObservableObject {
/// ///
@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 @Published public private(set) var uploadResults: [String: UploadResult] = [:]
private let uploader = ImageUploadService() // Use 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")
@ -125,14 +125,8 @@ public class MediaUploadManager: ObservableObject {
} }
/// ///
public func getUploadResults() -> [String: String] { public func getUploadResults() -> [String: UploadResult] {
var results: [String: String] = [:] return uploadResults
for (id, status) in uploadStatus {
if case .completed(let fileId) = status {
results[id] = fileId
}
}
return results
} }
/// ///
@ -146,6 +140,21 @@ public class MediaUploadManager: ObservableObject {
// MARK: - Private Methods // MARK: - Private Methods
///
public struct UploadResult: Codable, Equatable {
public let fileId: String
public let thumbnailId: String?
public init(fileId: String, thumbnailId: String? = nil) {
self.fileId = fileId
self.thumbnailId = thumbnailId
}
public static func == (lhs: UploadResult, rhs: UploadResult) -> Bool {
return lhs.fileId == rhs.fileId && lhs.thumbnailId == rhs.thumbnailId
}
}
private func uploadMedia(_ media: MediaType) { private func uploadMedia(_ media: MediaType) {
logger.info("🔄 开始处理媒体: \(media.id)") logger.info("🔄 开始处理媒体: \(media.id)")
@ -175,9 +184,23 @@ public class MediaUploadManager: ObservableObject {
Task { @MainActor in Task { @MainActor in
switch result { switch result {
case .success(let uploadResult): case .success(let uploadResult):
let fileId = uploadResult.fileId //
self.logger.info("✅ 上传成功 (\(media.id)): \(fileId)") let fileId: String
self.uploadResults[media.id] = fileId let thumbnailId: String?
switch uploadResult {
case .file(let result):
fileId = result.fileId
thumbnailId = nil
case .video(let video, let thumbnail):
fileId = video.fileId
thumbnailId = thumbnail?.fileId
}
//
let result = UploadResult(fileId: fileId, thumbnailId: thumbnailId)
self.uploadResults[media.id] = result
self.logger.info("✅ 上传成功 (\(media.id)): \(fileId), 缩略图ID: \(thumbnailId ?? "")")
self.updateStatus(for: media.id, status: .completed(fileId: fileId)) self.updateStatus(for: media.id, status: .completed(fileId: fileId))
// //
@ -205,8 +228,8 @@ public class MediaUploadManager: ObservableObject {
let results = self.selectedMedia.compactMap { media -> [String: String]? in let results = self.selectedMedia.compactMap { media -> [String: String]? in
guard let result = self.uploadResults[media.id] else { return nil } guard let result = self.uploadResults[media.id] else { return nil }
return [ return [
"file_id": result, "file_id": result.fileId,
"preview_file_id": result "preview_file_id": result.thumbnailId ?? result.fileId
] ]
} }

View File

@ -8,23 +8,38 @@ struct MaterialResponse: Decodable {
struct MaterialData: Decodable { struct MaterialData: Decodable {
let items: [MemoryItem] let items: [MemoryItem]
let hasMore: Bool
enum CodingKeys: String, CodingKey {
case items
case hasMore = "has_more"
}
} }
} }
struct MemoryItem: Identifiable, Decodable { struct MemoryItem: Identifiable, Decodable {
let id: String let id: String
let name: String let name: String?
let description: String let description: String?
let fileInfo: FileInfo let fileInfo: FileInfo
let previewFileInfo: FileInfo
var title: String { name } var title: String { name ?? "Untitled" }
var subtitle: String { description } var subtitle: String { description ?? "" }
var mediaType: MemoryMediaType { .image(fileInfo.url) } var mediaType: MemoryMediaType {
var aspectRatio: CGFloat { 1.0 } // Default to square, adjust based on actual image dimensions if needed let url = fileInfo.url.lowercased()
if url.hasSuffix(".mp4") || url.hasSuffix(".mov") {
return .video(url: fileInfo.url, previewUrl: previewFileInfo.url)
} else {
return .image(fileInfo.url)
}
}
var aspectRatio: CGFloat { 1.0 }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case id, name, description case id, name, description
case fileInfo = "file_info" case fileInfo = "file_info"
case previewFileInfo = "preview_file_info"
} }
} }
@ -42,7 +57,7 @@ struct FileInfo: Decodable {
enum MemoryMediaType: Equatable { enum MemoryMediaType: Equatable {
case image(String) case image(String)
case video(String) case video(url: String, previewUrl: String)
} }
struct MemoriesView: View { struct MemoriesView: View {
@ -57,28 +72,38 @@ struct MemoriesView: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
Group { ZStack {
if isLoading { Color.themeTextWhiteSecondary.ignoresSafeArea()
ProgressView()
.scaleEffect(1.5) Group {
} else if let error = errorMessage { if isLoading {
Text("Error: \(error)") ProgressView()
.foregroundColor(.red) .scaleEffect(1.5)
} else { } else if let error = errorMessage {
ScrollView { Text("Error: \(error)")
LazyVGrid(columns: columns, spacing: 4) { .foregroundColor(.red)
ForEach(memories) { memory in } else {
MemoryCard(memory: memory) ScrollView {
.padding(.horizontal, 2) LazyVGrid(columns: columns, spacing: 4) {
ForEach(memories) { memory in
MemoryCard(memory: memory)
.padding(.horizontal, 2)
}
} }
.padding(.top, 4)
.padding(.horizontal, 4)
} }
.padding(.top, 4)
.padding(.horizontal, 4)
} }
} }
} }
.navigationTitle("My Memories") .navigationTitle("My Memories")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
EmptyView()
}
}
.onAppear { .onAppear {
fetchMemories() fetchMemories()
} }
@ -97,9 +122,9 @@ struct MemoriesView: View {
switch result { switch result {
case .success(let response): case .success(let response):
print("✅ Successfully fetched \(response.data.items.count) memory items") print("✅ Successfully fetched \(response.data.items) memory items")
response.data.items.forEach { item in response.data.items.forEach { item in
print("📝 Item ID: \(item.id), Title: \(item.name), URL: \(item.fileInfo.url)") print("📝 Item ID: \(item.id), Title: \(item.name ?? "Untitled"), URL: \(item)")
} }
self.memories = response.data.items self.memories = response.data.items
case .failure(let error): case .failure(let error):
@ -135,32 +160,37 @@ struct MemoryCard: View {
} }
} }
case .video(let urlString): case .video(let url, let previewUrl):
if let url = URL(string: urlString) { // Use preview image for video
VideoPlayer(player: AVPlayer(url: url)) if let previewUrl = URL(string: previewUrl) {
.aspectRatio(memory.aspectRatio, contentMode: .fill) AsyncImage(url: previewUrl) { phase in
.onAppear { if let image = phase.image {
// The video will be shown with a play button overlay image
// and will only play when tapped .resizable()
.aspectRatio(contentMode: .fill)
} else if phase.error != nil {
Color.gray.opacity(0.3)
} else {
ProgressView()
} }
}
} else { } else {
Color.gray.opacity(0.3) Color.gray.opacity(0.3)
.aspectRatio(memory.aspectRatio, contentMode: .fill)
} }
} }
} }
.frame(width: (UIScreen.main.bounds.width / 2) - 24, height: (UIScreen.main.bounds.width / 2 - 24) * (1/memory.aspectRatio)) .frame(width: (UIScreen.main.bounds.width / 2) - 24,
height: (UIScreen.main.bounds.width / 2 - 24) * (1/memory.aspectRatio))
.clipped() .clipped()
.cornerRadius(12) .cornerRadius(12)
.overlay(
Group { // Show play button for videos
if case .video = memory.mediaType { if case .video = memory.mediaType {
Image(systemName: "play.circle.fill") Image(systemName: "play.circle.fill")
.font(.system(size: 40)) .font(.system(size: 40))
.foregroundColor(.white.opacity(0.9)) .foregroundColor(.white.opacity(0.9))
} .shadow(radius: 3)
} }
)
} }
// Title and Subtitle // Title and Subtitle

View File

@ -1,5 +1,8 @@
import SwiftUI import SwiftUI
import AVFoundation import PhotosUI
import AVKit
import CoreTransferable
import CoreImage.CIFilterBuiltins
extension Notification.Name { extension Notification.Name {
static let didAddFirstMedia = Notification.Name("didAddFirstMedia") static let didAddFirstMedia = Notification.Name("didAddFirstMedia")
@ -296,33 +299,33 @@ struct MediaUploadView: View {
} }
/// ///
private func handleUploadCompletion(results: [String: String]) { private func handleUploadCompletion(results: [String: MediaUploadManager.UploadResult]) {
uploadedFileIds = results.map { ["file_id": $0.value, "preview_file_id": $0.value] } //
uploadComplete = !uploadedFileIds.isEmpty let formattedResults = results.map { (_, result) -> [String: String] in
return [
// "file_id": result.fileId,
if let jsonData = try? JSONSerialization.data(withJSONObject: uploadedFileIds, options: .prettyPrinted), "preview_file_id": result.thumbnailId ?? result.fileId
let jsonString = String(data: jsonData, encoding: .utf8) { ]
print("📦 上传完成文件ID列表:")
print(jsonString)
} }
uploadedFileIds = formattedResults
uploadComplete = !uploadedFileIds.isEmpty
} }
/// ///
private func handleContinue() { private func handleContinue() {
// ID //
let fileIds = uploadManager.uploadResults.map { $0.value } let uploadResults = uploadManager.uploadResults
guard !uploadResults.isEmpty else {
guard !fileIds.isEmpty else {
print("⚠️ 没有可用的文件ID") print("⚠️ 没有可用的文件ID")
return return
} }
// //
let files = fileIds.map { fileId -> [String: String] in let files = uploadResults.map { (_, result) -> [String: String] in
return [ return [
"file_id": fileId, "file_id": result.fileId,
"preview_file_id": fileId "preview_file_id": result.thumbnailId ?? result.fileId
] ]
} }