feat: 更新COSManager和相关视图以增强图片上传功能
- 修改COSManagerAdapter以支持新的TCCos组件,确保与腾讯云COS的兼容性。 - 在CreateFeedFeature中新增图片上传相关状态和Action,优化图片选择与上传逻辑。 - 更新CreateFeedView以整合图片上传功能,提升用户体验。 - 在多个视图中添加键盘状态管理,改善用户交互体验。 - 新增COS相关的测试文件,确保功能的正确性和稳定性。
This commit is contained in:
@@ -20,6 +20,13 @@ struct CreateFeedFeature {
|
||||
}
|
||||
var isLoading: Bool = false
|
||||
|
||||
// 新增:图片上传相关状态
|
||||
var uploadedImageUrls: [String] = []
|
||||
var uploadedImages: [UIImage] = [] // 保存原始图片用于获取尺寸信息
|
||||
var isUploadingImages: Bool = false
|
||||
var uploadProgress: Double = 0.0
|
||||
var uploadStatus: String = ""
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
@@ -28,13 +35,20 @@ struct CreateFeedFeature {
|
||||
enum Action {
|
||||
case contentChanged(String)
|
||||
case publishButtonTapped
|
||||
case publishResponse(Result<PublishDynamicResponse, Error>)
|
||||
case publishResponse(Result<PublishFeedResponse, Error>)
|
||||
case clearError
|
||||
case dismissView
|
||||
case photosPickerItemsChanged([PhotosPickerItem])
|
||||
case processPhotosPickerItems([PhotosPickerItem])
|
||||
case removeImage(Int)
|
||||
case updateProcessedImages([UIImage])
|
||||
|
||||
// 新增:图片上传相关 Action
|
||||
case uploadImagesToCOS
|
||||
case imageUploadProgress(Double, Int, Int) // progress, current, total
|
||||
case imageUploadCompleted([String], [UIImage]) // urls, images
|
||||
case imageUploadFailed(Error)
|
||||
case publishContent
|
||||
}
|
||||
|
||||
@Dependency(\.apiService) var apiService
|
||||
@@ -48,11 +62,13 @@ struct CreateFeedFeature {
|
||||
state.content = newContent
|
||||
state.characterCount = newContent.count
|
||||
return .none
|
||||
|
||||
case .photosPickerItemsChanged(let items):
|
||||
state.selectedImages = items
|
||||
return .run { send in
|
||||
await send(.processPhotosPickerItems(items))
|
||||
}
|
||||
|
||||
case .processPhotosPickerItems(let items):
|
||||
let currentImages = state.processedImages
|
||||
return .run { send in
|
||||
@@ -68,35 +84,135 @@ struct CreateFeedFeature {
|
||||
send(.updateProcessedImages(newImages))
|
||||
}
|
||||
}
|
||||
|
||||
case .updateProcessedImages(let images):
|
||||
state.processedImages = images
|
||||
// 清空之前的上传结果
|
||||
state.uploadedImageUrls = []
|
||||
return .none
|
||||
|
||||
case .removeImage(let index):
|
||||
guard index < state.processedImages.count else { return .none }
|
||||
state.processedImages.remove(at: index)
|
||||
if index < state.selectedImages.count {
|
||||
state.selectedImages.remove(at: index)
|
||||
}
|
||||
// 同时移除对应的上传链接
|
||||
if index < state.uploadedImageUrls.count {
|
||||
state.uploadedImageUrls.remove(at: index)
|
||||
}
|
||||
return .none
|
||||
|
||||
case .publishButtonTapped:
|
||||
guard state.canPublish else {
|
||||
state.errorMessage = "请输入内容"
|
||||
return .none
|
||||
}
|
||||
|
||||
// 如果有图片且还没有上传,先上传图片
|
||||
if !state.processedImages.isEmpty && state.uploadedImageUrls.isEmpty {
|
||||
return .send(.uploadImagesToCOS)
|
||||
}
|
||||
|
||||
// 直接发布(图片已上传或没有图片)
|
||||
return .send(.publishContent)
|
||||
|
||||
case .uploadImagesToCOS:
|
||||
guard !state.processedImages.isEmpty else {
|
||||
return .send(.publishContent)
|
||||
}
|
||||
|
||||
state.isUploadingImages = true
|
||||
state.uploadProgress = 0.0
|
||||
state.uploadStatus = "正在上传图片..."
|
||||
state.errorMessage = nil
|
||||
|
||||
// 提取状态值到局部变量,避免在 @Sendable 闭包中访问 inout 参数
|
||||
let imagesToUpload = state.processedImages
|
||||
|
||||
return .run { send in
|
||||
var uploadedUrls: [String] = []
|
||||
var uploadedImages: [UIImage] = []
|
||||
let totalImages = imagesToUpload.count
|
||||
|
||||
for (index, image) in imagesToUpload.enumerated() {
|
||||
// 更新上传进度
|
||||
await send(.imageUploadProgress(Double(index) / Double(totalImages), index + 1, totalImages))
|
||||
|
||||
// 上传图片到 COS
|
||||
if let imageUrl = await COSManager.shared.uploadUIImage(image, apiService: apiService) {
|
||||
uploadedUrls.append(imageUrl)
|
||||
uploadedImages.append(image) // 保存原始图片
|
||||
} else {
|
||||
// 上传失败
|
||||
await send(.imageUploadFailed(APIError.custom("图片上传失败")))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 所有图片上传完成
|
||||
await send(.imageUploadProgress(1.0, totalImages, totalImages))
|
||||
await send(.imageUploadCompleted(uploadedUrls, uploadedImages))
|
||||
}
|
||||
|
||||
case .imageUploadProgress(let progress, let current, let total):
|
||||
state.uploadProgress = progress
|
||||
state.uploadStatus = "正在上传图片... (\(current)/\(total))"
|
||||
return .none
|
||||
|
||||
case .imageUploadCompleted(let urls, let images):
|
||||
state.isUploadingImages = false
|
||||
state.uploadedImageUrls = urls
|
||||
state.uploadedImages = images
|
||||
state.uploadStatus = "图片上传完成"
|
||||
// 上传完成后自动发布内容
|
||||
return .send(.publishContent)
|
||||
|
||||
case .imageUploadFailed(let error):
|
||||
state.isUploadingImages = false
|
||||
state.errorMessage = "图片上传失败: \(error.localizedDescription)"
|
||||
return .none
|
||||
|
||||
case .publishContent:
|
||||
state.isLoading = true
|
||||
state.errorMessage = nil
|
||||
let request = PublishDynamicRequest(
|
||||
content: state.content.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
images: state.processedImages
|
||||
)
|
||||
|
||||
// 提取状态值到局部变量,避免在 @Sendable 闭包中访问 inout 参数
|
||||
let content = state.content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let imageUrls = state.uploadedImageUrls
|
||||
let images = state.uploadedImages
|
||||
|
||||
return .run { send in
|
||||
do {
|
||||
// 构建 ResListItem 数组
|
||||
var resList: [ResListItem] = []
|
||||
for (index, imageUrl) in imageUrls.enumerated() {
|
||||
if index < images.count, let cgImage = images[index].cgImage {
|
||||
let width = cgImage.width
|
||||
let height = cgImage.height
|
||||
let format = "jpeg"
|
||||
let item = ResListItem(resUrl: imageUrl, width: width, height: height, format: format)
|
||||
resList.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 PublishFeedRequest 而不是 PublishDynamicRequest
|
||||
let userId = await UserInfoManager.getCurrentUserId() ?? ""
|
||||
let type = resList.isEmpty ? "0" : "2" // 0: 纯文字, 2: 图片
|
||||
let request = await PublishFeedRequest.make(
|
||||
content: content,
|
||||
uid: userId,
|
||||
type: type,
|
||||
resList: resList.isEmpty ? nil : resList
|
||||
)
|
||||
|
||||
let response = try await apiService.request(request)
|
||||
await send(.publishResponse(.success(response)))
|
||||
} catch {
|
||||
await send(.publishResponse(.failure(error)))
|
||||
}
|
||||
}
|
||||
|
||||
case .publishResponse(.success(let response)):
|
||||
state.isLoading = false
|
||||
if response.code == 200 {
|
||||
@@ -105,17 +221,18 @@ struct CreateFeedFeature {
|
||||
state.errorMessage = response.message.isEmpty ? "发布失败" : response.message
|
||||
return .none
|
||||
}
|
||||
|
||||
case .publishResponse(.failure(let error)):
|
||||
state.isLoading = false
|
||||
state.errorMessage = error.localizedDescription
|
||||
return .none
|
||||
|
||||
case .clearError:
|
||||
state.errorMessage = nil
|
||||
return .none
|
||||
|
||||
case .dismissView:
|
||||
// 检查是否在presentation context中
|
||||
guard isPresented else {
|
||||
// 如果不在presentation context中,不执行dismiss
|
||||
return .none
|
||||
}
|
||||
return .run { _ in
|
||||
@@ -139,6 +256,16 @@ extension CreateFeedFeature.Action: Equatable {
|
||||
return true
|
||||
case let (.removeImage(a), .removeImage(b)):
|
||||
return a == b
|
||||
case (.uploadImagesToCOS, .uploadImagesToCOS):
|
||||
return true
|
||||
case let (.imageUploadProgress(a, b, c), .imageUploadProgress(d, e, f)):
|
||||
return a == d && b == e && c == f
|
||||
case let (.imageUploadCompleted(a, c), .imageUploadCompleted(b, d)):
|
||||
return a == b && c.count == d.count // 简化比较,只比较URL数组和图片数量
|
||||
case let (.imageUploadFailed(a), .imageUploadFailed(b)):
|
||||
return a.localizedDescription == b.localizedDescription
|
||||
case (.publishContent, .publishContent):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -147,43 +274,5 @@ extension CreateFeedFeature.Action: Equatable {
|
||||
|
||||
// MARK: - 发布动态相关模型
|
||||
|
||||
struct PublishDynamicRequest: APIRequestProtocol {
|
||||
typealias Response = PublishDynamicResponse
|
||||
let endpoint: String = APIEndpoint.publishFeed.path
|
||||
let method: HTTPMethod = .POST
|
||||
let includeBaseParameters: Bool = true
|
||||
let queryParameters: [String: String]? = nil
|
||||
let timeout: TimeInterval = 30.0
|
||||
let content: String
|
||||
let images: [UIImage]
|
||||
let type: Int // 0: 纯文字, 2: 图片
|
||||
init(content: String, images: [UIImage] = []) {
|
||||
self.content = content
|
||||
self.images = images
|
||||
self.type = images.isEmpty ? 0 : 2
|
||||
}
|
||||
var bodyParameters: [String: Any]? {
|
||||
var params: [String: Any] = [
|
||||
"content": content,
|
||||
"type": type
|
||||
]
|
||||
if !images.isEmpty {
|
||||
let imageData = images.compactMap { image in
|
||||
image.jpegData(compressionQuality: 0.8)?.base64EncodedString()
|
||||
}
|
||||
params["images"] = imageData
|
||||
}
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
struct PublishDynamicResponse: Codable {
|
||||
let code: Int
|
||||
let message: String
|
||||
let data: PublishDynamicData?
|
||||
}
|
||||
|
||||
struct PublishDynamicData: Codable {
|
||||
let dynamicId: Int
|
||||
let publishTime: Int
|
||||
}
|
||||
// 注意:现在使用 DynamicsModels.swift 中的 PublishFeedRequest 和 PublishFeedResponse
|
||||
// 不再需要重复定义这些模型
|
||||
|
Reference in New Issue
Block a user