feat: 增强发布动态功能,支持图片上传与进度显示
- 在PublishFeedRequest中新增resList属性,支持上传图片资源信息。 - 在EditFeedFeature中实现图片上传逻辑,处理图片选择与上传进度。 - 更新EditFeedView以显示图片上传进度,提升用户体验。 - 在COSManager中新增UIImage上传方法,优化图片上传流程。 - 在FeedListView中添加通知以刷新动态列表,确保数据同步。
This commit is contained in:
@@ -161,6 +161,14 @@ struct LatestDynamicsRequest: APIRequestProtocol {
|
||||
|
||||
// MARK: - 发布动态 API 请求与响应
|
||||
|
||||
/// 动态图片资源信息
|
||||
struct ResListItem: Codable, Equatable {
|
||||
let resUrl: String
|
||||
let width: Int
|
||||
let height: Int
|
||||
let format: String
|
||||
}
|
||||
|
||||
/// 发布动态请求
|
||||
struct PublishFeedRequest: APIRequestProtocol {
|
||||
typealias Response = PublishFeedResponse
|
||||
@@ -172,22 +180,32 @@ struct PublishFeedRequest: APIRequestProtocol {
|
||||
let uid: String
|
||||
let type: String
|
||||
var pub_sign: String
|
||||
let resList: [ResListItem]?
|
||||
|
||||
var queryParameters: [String: String]? { nil }
|
||||
var bodyParameters: [String: Any]? {
|
||||
[
|
||||
var params: [String: Any] = [
|
||||
"content": content,
|
||||
"uid": uid,
|
||||
"type": type,
|
||||
"pub_sign": pub_sign
|
||||
]
|
||||
if let resList = resList, !resList.isEmpty {
|
||||
params["resList"] = resList.map { [
|
||||
"resUrl": $0.resUrl,
|
||||
"width": $0.width,
|
||||
"height": $0.height,
|
||||
"format": $0.format
|
||||
] }
|
||||
}
|
||||
return params
|
||||
}
|
||||
var includeBaseParameters: Bool { true }
|
||||
var shouldShowLoading: Bool { true }
|
||||
var shouldShowError: Bool { true }
|
||||
|
||||
/// async 工厂方法,主线程生成 pub_sign
|
||||
static func make(content: String, uid: String, type: String = "0") async -> PublishFeedRequest {
|
||||
static func make(content: String, uid: String, type: String = "0", resList: [ResListItem]? = nil) async -> PublishFeedRequest {
|
||||
let base = await MainActor.run { BaseRequest() }
|
||||
var mutableBase = base
|
||||
mutableBase.generateSignature(with: [
|
||||
@@ -199,16 +217,18 @@ struct PublishFeedRequest: APIRequestProtocol {
|
||||
content: content,
|
||||
uid: uid,
|
||||
type: type,
|
||||
pub_sign: mutableBase.pubSign
|
||||
pub_sign: mutableBase.pubSign,
|
||||
resList: resList
|
||||
)
|
||||
}
|
||||
|
||||
/// 禁止外部直接调用
|
||||
private init(content: String, uid: String, type: String, pub_sign: String) {
|
||||
private init(content: String, uid: String, type: String, pub_sign: String, resList: [ResListItem]?) {
|
||||
self.content = content
|
||||
self.uid = uid
|
||||
self.type = type
|
||||
self.pub_sign = pub_sign
|
||||
self.resList = resList
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -19,8 +19,12 @@ struct EditFeedFeature {
|
||||
}
|
||||
|
||||
var canPublish: Bool {
|
||||
!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading
|
||||
(!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || !processedImages.isEmpty) && !isLoading && !isUploadingImages
|
||||
}
|
||||
// 新增:图片上传相关状态
|
||||
var isUploadingImages: Bool = false
|
||||
var imageUploadProgress: Double = 0.0 // 0.0~1.0
|
||||
var uploadedResList: [ResListItem] = []
|
||||
// 手动实现Equatable,selectedImages只比较数量(PhotosPickerItem不支持Equatable)
|
||||
static func == (lhs: State, rhs: State) -> Bool {
|
||||
lhs.content == rhs.content &&
|
||||
@@ -28,7 +32,10 @@ struct EditFeedFeature {
|
||||
lhs.errorMessage == rhs.errorMessage &&
|
||||
lhs.shouldDismiss == rhs.shouldDismiss &&
|
||||
lhs.processedImages == rhs.processedImages &&
|
||||
lhs.selectedImages.count == rhs.selectedImages.count
|
||||
lhs.selectedImages.count == rhs.selectedImages.count &&
|
||||
lhs.isUploadingImages == rhs.isUploadingImages &&
|
||||
lhs.imageUploadProgress == rhs.imageUploadProgress &&
|
||||
lhs.uploadedResList == rhs.uploadedResList
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +51,11 @@ struct EditFeedFeature {
|
||||
case processPhotosPickerItems([PhotosPickerItem])
|
||||
case updateProcessedImages([UIImage])
|
||||
case removeImage(Int)
|
||||
// 新增:图片上传Action
|
||||
case uploadImages
|
||||
case uploadImagesResponse(Result<[ResListItem], Error>)
|
||||
// 新增:图片上传进度
|
||||
case updateImageUploadProgress(Double)
|
||||
}
|
||||
|
||||
@Dependency(\.apiService) var apiService
|
||||
@@ -62,24 +74,85 @@ struct EditFeedFeature {
|
||||
state.errorMessage = "请输入内容"
|
||||
return .none
|
||||
}
|
||||
state.isLoading = true
|
||||
state.errorMessage = nil
|
||||
|
||||
return .run { [content = state.content] send in
|
||||
let userId = await UserInfoManager.getCurrentUserId() ?? ""
|
||||
let request = await PublishFeedRequest.make(
|
||||
content: content.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
uid: userId,
|
||||
type: "0"
|
||||
)
|
||||
do {
|
||||
let response = try await apiService.request(request)
|
||||
await send(.publishResponse(.success(response)))
|
||||
} catch {
|
||||
await send(.publishResponse(.failure(error)))
|
||||
// 有图片时先上传图片
|
||||
if !state.processedImages.isEmpty {
|
||||
state.isUploadingImages = true
|
||||
state.imageUploadProgress = 0.0
|
||||
state.errorMessage = nil
|
||||
return .send(.uploadImages)
|
||||
} else {
|
||||
state.isLoading = true
|
||||
state.errorMessage = nil
|
||||
return .run { [content = state.content] send in
|
||||
let userId = await UserInfoManager.getCurrentUserId() ?? ""
|
||||
let type = content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "2" : "0"
|
||||
let request = await PublishFeedRequest.make(
|
||||
content: content.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
uid: userId,
|
||||
type: type,
|
||||
resList: nil
|
||||
)
|
||||
do {
|
||||
let response = try await apiService.request(request)
|
||||
await send(.publishResponse(.success(response)))
|
||||
} catch {
|
||||
await send(.publishResponse(.failure(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case .uploadImages:
|
||||
let images = state.processedImages
|
||||
return .run { send in
|
||||
var resList: [ResListItem] = []
|
||||
for (idx, image) in images.enumerated() {
|
||||
guard let data = image.jpegData(compressionQuality: 0.9) else { continue }
|
||||
if let url = await COSManager.shared.uploadImage(data, apiService: apiService),
|
||||
let cgImage = image.cgImage {
|
||||
let width = cgImage.width
|
||||
let height = cgImage.height
|
||||
let format = "jpeg"
|
||||
let item = ResListItem(resUrl: url, width: width, height: height, format: format)
|
||||
resList.append(item)
|
||||
}
|
||||
// 可选:进度回调
|
||||
await MainActor.run {
|
||||
send(.updateImageUploadProgress(Double(idx + 1) / Double(images.count)))
|
||||
}
|
||||
}
|
||||
if resList.count == images.count {
|
||||
await send(.uploadImagesResponse(.success(resList)))
|
||||
} else {
|
||||
await send(.uploadImagesResponse(.failure(NSError(domain: "COSUpload", code: -1, userInfo: [NSLocalizedDescriptionKey: "部分图片上传失败"])) ))
|
||||
}
|
||||
}
|
||||
case .uploadImagesResponse(let result):
|
||||
state.isUploadingImages = false
|
||||
state.imageUploadProgress = 1.0
|
||||
switch result {
|
||||
case .success(let resList):
|
||||
state.uploadedResList = resList
|
||||
state.isLoading = true
|
||||
return .run { [content = state.content, resList] send in
|
||||
let userId = await UserInfoManager.getCurrentUserId() ?? ""
|
||||
// type: 2 表示图片/图文
|
||||
let type = resList.isEmpty ? "0" : (content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "2" : "2")
|
||||
let request = await PublishFeedRequest.make(
|
||||
content: content.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
uid: userId,
|
||||
type: type,
|
||||
resList: resList
|
||||
)
|
||||
do {
|
||||
let response = try await apiService.request(request)
|
||||
await send(.publishResponse(.success(response)))
|
||||
} catch {
|
||||
await send(.publishResponse(.failure(error)))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
state.errorMessage = error.localizedDescription
|
||||
return .none
|
||||
}
|
||||
case .publishResponse(.success(let response)):
|
||||
state.isLoading = false
|
||||
if response.code == 200 {
|
||||
@@ -88,16 +161,13 @@ struct EditFeedFeature {
|
||||
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:
|
||||
state.shouldDismiss = true
|
||||
return .none
|
||||
@@ -134,6 +204,10 @@ struct EditFeedFeature {
|
||||
state.selectedImages.remove(at: index)
|
||||
}
|
||||
return .none
|
||||
// 新增:图片上传进度
|
||||
case .updateImageUploadProgress(let progress):
|
||||
state.imageUploadProgress = progress
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -192,7 +192,8 @@ class COSManager: ObservableObject {
|
||||
} else {
|
||||
// 构建云地址
|
||||
let domain = tokenData.customDomain.isEmpty ? "\(tokenData.bucket).cos.\(tokenData.region).myqcloud.com" : tokenData.customDomain
|
||||
let cloudURL = "https://\(domain)/\(key)"
|
||||
let prefix = domain.hasPrefix("http") ? "" : "https://"
|
||||
let cloudURL = "\(prefix)\(domain)/\(key)"
|
||||
debugInfoSync("✅ 图片上传成功: \(cloudURL)")
|
||||
continuation.resume(returning: cloudURL)
|
||||
}
|
||||
@@ -200,6 +201,19 @@ class COSManager: ObservableObject {
|
||||
QCloudCOSTransferMangerService.defaultCOSTransferManager().uploadObject(request)
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传 UIImage 到腾讯云 COS,自动压缩为 JPEG(0.8)
|
||||
/// - Parameters:
|
||||
/// - image: UIImage 实例
|
||||
/// - apiService: API 服务实例
|
||||
/// - Returns: 上传成功的云地址,如果失败返回 nil
|
||||
func uploadUIImage(_ image: UIImage, apiService: any APIServiceProtocol & Sendable) async -> String? {
|
||||
guard let data = image.jpegData(compressionQuality: 0.8) else {
|
||||
debugInfoSync("❌ 图片压缩失败,无法生成 JPEG 数据")
|
||||
return nil
|
||||
}
|
||||
return await uploadImage(data, apiService: apiService)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 调试扩展
|
||||
|
@@ -21,9 +21,11 @@ struct EditFeedView: View {
|
||||
ZStack {
|
||||
backgroundView
|
||||
mainContent(geometry: geometry, viewStore: viewStore)
|
||||
// if viewStore.isLoading {
|
||||
// loadingOverlay
|
||||
// }
|
||||
if viewStore.isUploadingImages {
|
||||
uploadingImagesOverlay(progress: viewStore.imageUploadProgress)
|
||||
} else if viewStore.isLoading {
|
||||
loadingOverlay
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
@@ -51,6 +53,7 @@ struct EditFeedView: View {
|
||||
.onChange(of: viewStore.shouldDismiss) { shouldDismiss in
|
||||
if shouldDismiss {
|
||||
onDismiss()
|
||||
NotificationCenter.default.post(name: Notification.Name("reloadFeedList"), object: nil)
|
||||
viewStore.send(.clearDismissFlag)
|
||||
}
|
||||
}
|
||||
@@ -191,7 +194,8 @@ struct EditFeedView: View {
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom + 24)
|
||||
.disabled(!viewStore.canPublish)
|
||||
.disabled(!viewStore.canPublish || viewStore.isUploadingImages || viewStore.isLoading)
|
||||
.opacity(viewStore.canPublish ? 1.0 : 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +208,22 @@ struct EditFeedView: View {
|
||||
.scaleEffect(1.5)
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:图片上传进度遮罩
|
||||
private func uploadingImagesOverlay(progress: Double) -> some View {
|
||||
Group {
|
||||
Color.black.opacity(0.3)
|
||||
.ignoresSafeArea()
|
||||
VStack(spacing: 16) {
|
||||
ProgressView(value: progress)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .white))
|
||||
.frame(width: 180)
|
||||
Text("正在上传图片...\(Int(progress * 100))%")
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
|
@@ -101,6 +101,9 @@ struct FeedListView: View {
|
||||
.onAppear {
|
||||
viewStore.send(.onAppear)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("reloadFeedList"))) { _ in
|
||||
viewStore.send(.reload)
|
||||
}
|
||||
.sheet(isPresented: viewStore.binding(
|
||||
get: \.isEditFeedPresented,
|
||||
send: { $0 ? .editFeedButtonTapped : .editFeedDismissed }
|
||||
|
Reference in New Issue
Block a user