
- 在PublishFeedRequest中新增resList属性,支持上传图片资源信息。 - 在EditFeedFeature中实现图片上传逻辑,处理图片选择与上传进度。 - 更新EditFeedView以显示图片上传进度,提升用户体验。 - 在COSManager中新增UIImage上传方法,优化图片上传流程。 - 在FeedListView中添加通知以刷新动态列表,确保数据同步。
300 lines
12 KiB
Swift
300 lines
12 KiB
Swift
import SwiftUI
|
||
import ComposableArchitecture
|
||
import PhotosUI
|
||
//import ImagePreviewPager
|
||
|
||
struct EditFeedView: View {
|
||
let onDismiss: () -> Void
|
||
let store: StoreOf<EditFeedFeature>
|
||
@State private var isKeyboardVisible = false
|
||
private let maxCount = 500
|
||
|
||
private func hideKeyboard() {
|
||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||
}
|
||
|
||
var body: some View {
|
||
WithPerceptionTracking {
|
||
GeometryReader { geometry in
|
||
WithViewStore(store, observe: { $0 }) { viewStore in
|
||
WithPerceptionTracking {
|
||
ZStack {
|
||
backgroundView
|
||
mainContent(geometry: geometry, viewStore: viewStore)
|
||
if viewStore.isUploadingImages {
|
||
uploadingImagesOverlay(progress: viewStore.imageUploadProgress)
|
||
} else if viewStore.isLoading {
|
||
loadingOverlay
|
||
}
|
||
}
|
||
.contentShape(Rectangle())
|
||
.onTapGesture {
|
||
if isKeyboardVisible {
|
||
hideKeyboard()
|
||
}
|
||
}
|
||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
|
||
withAnimation(.easeInOut(duration: 0.3)) {
|
||
isKeyboardVisible = true
|
||
}
|
||
}
|
||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
|
||
withAnimation(.easeInOut(duration: 0.3)) {
|
||
isKeyboardVisible = false
|
||
}
|
||
}
|
||
.onChange(of: viewStore.errorMessage) { error in
|
||
if error != nil {
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||
viewStore.send(.clearError)
|
||
}
|
||
}
|
||
}
|
||
.onChange(of: viewStore.shouldDismiss) { shouldDismiss in
|
||
if shouldDismiss {
|
||
onDismiss()
|
||
NotificationCenter.default.post(name: Notification.Name("reloadFeedList"), object: nil)
|
||
viewStore.send(.clearDismissFlag)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private var backgroundView: some View {
|
||
Color(hexString: "0C0527")
|
||
.ignoresSafeArea()
|
||
}
|
||
|
||
private func mainContent(geometry: GeometryProxy, viewStore: ViewStoreOf<EditFeedFeature>) -> some View {
|
||
VStack(spacing: 0) {
|
||
headerView(geometry: geometry, viewStore: viewStore)
|
||
textInputArea(viewStore: viewStore)
|
||
// 新增:图片输入区域
|
||
ModernImageSelectionGrid(
|
||
images: viewStore.processedImages,
|
||
selectedItems: viewStore.selectedImages,
|
||
canAddMore: viewStore.canAddMoreImages,
|
||
onItemsChanged: { items in
|
||
viewStore.send(.photosPickerItemsChanged(items))
|
||
},
|
||
onRemoveImage: { index in
|
||
viewStore.send(.removeImage(index))
|
||
}
|
||
)
|
||
.padding(.horizontal, 24)
|
||
.padding(.bottom, 32)
|
||
Spacer()
|
||
if !isKeyboardVisible {
|
||
publishButtonBottom(viewStore: viewStore, geometry: geometry)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func headerView(geometry: GeometryProxy, viewStore: ViewStoreOf<EditFeedFeature>) -> some View {
|
||
HStack {
|
||
Text(NSLocalizedString("editFeed.title", comment: "Image & Text Edit"))
|
||
.font(.system(size: 20, weight: .semibold))
|
||
.foregroundColor(.white)
|
||
Spacer()
|
||
if isKeyboardVisible {
|
||
WithPerceptionTracking {
|
||
Button(action: {
|
||
hideKeyboard()
|
||
viewStore.send(.publishButtonTapped)
|
||
}) {
|
||
Text(NSLocalizedString("editFeed.publish", comment: "Publish"))
|
||
.font(.system(size: 16, weight: .semibold))
|
||
.foregroundColor(.white)
|
||
.padding(.horizontal, 16)
|
||
.padding(.vertical, 8)
|
||
.background(
|
||
LinearGradient(
|
||
colors: [
|
||
Color(hexString: "A14AC6"),
|
||
Color(hexString: "3B1EEB")
|
||
],
|
||
startPoint: .leading,
|
||
endPoint: .trailing
|
||
)
|
||
.cornerRadius(16)
|
||
)
|
||
}
|
||
.disabled(!viewStore.canPublish)
|
||
}
|
||
}
|
||
}
|
||
.padding(.horizontal, 24)
|
||
.padding(.top, geometry.safeAreaInsets.top + 16)
|
||
.padding(.bottom, 24)
|
||
}
|
||
|
||
private func textInputArea(viewStore: ViewStoreOf<EditFeedFeature>) -> some View {
|
||
ZStack(alignment: .topLeading) {
|
||
RoundedRectangle(cornerRadius: 20)
|
||
.fill(Color(hexString: "1C143A"))
|
||
TextEditor(text: Binding(
|
||
get: { viewStore.content },
|
||
set: { viewStore.send(.contentChanged($0)) }
|
||
))
|
||
.scrollContentBackground(.hidden)
|
||
.padding(16)
|
||
.frame(height: 160)
|
||
.foregroundColor(.white)
|
||
.background(.clear)
|
||
.cornerRadius(20)
|
||
.font(.system(size: 16))
|
||
if viewStore.content.isEmpty {
|
||
Text(NSLocalizedString("editFeed.enterContent", comment: "Enter Content"))
|
||
.foregroundColor(Color.white.opacity(0.4))
|
||
.padding(20)
|
||
.font(.system(size: 16))
|
||
}
|
||
WithPerceptionTracking {
|
||
VStack {
|
||
Spacer()
|
||
HStack {
|
||
Spacer()
|
||
Text("\(viewStore.content.count)/\(maxCount)")
|
||
.foregroundColor(Color.white.opacity(0.4))
|
||
.font(.system(size: 14))
|
||
.padding(.trailing, 16)
|
||
.padding(.bottom, 10)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.frame(height: 160)
|
||
.padding(.horizontal, 24)
|
||
.padding(.bottom, 32)
|
||
}
|
||
|
||
private func publishButtonBottom(viewStore: ViewStoreOf<EditFeedFeature>, geometry: GeometryProxy) -> some View {
|
||
VStack {
|
||
Spacer()
|
||
Button(action: {
|
||
hideKeyboard()
|
||
viewStore.send(.publishButtonTapped)
|
||
}) {
|
||
Text(NSLocalizedString("editFeed.publish", comment: "Publish"))
|
||
.font(.system(size: 18, weight: .semibold))
|
||
.foregroundColor(.white)
|
||
.frame(maxWidth: .infinity)
|
||
.frame(height: 56)
|
||
.background(
|
||
LinearGradient(
|
||
colors: [Color(hexString: "A14AC6"), Color(hexString: "3B1EEB")],
|
||
startPoint: .leading,
|
||
endPoint: .trailing
|
||
)
|
||
.cornerRadius(28)
|
||
)
|
||
}
|
||
.padding(.horizontal, 24)
|
||
.padding(.bottom, geometry.safeAreaInsets.bottom + 24)
|
||
.disabled(!viewStore.canPublish || viewStore.isUploadingImages || viewStore.isLoading)
|
||
.opacity(viewStore.canPublish ? 1.0 : 0.5)
|
||
}
|
||
}
|
||
|
||
private var loadingOverlay: some View {
|
||
Group {
|
||
Color.black.opacity(0.3)
|
||
.ignoresSafeArea()
|
||
ProgressView()
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||
.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 {
|
||
// EditFeedView()
|
||
//}
|
||
|
||
// MARK: - 九宫格图片选择组件
|
||
struct ModernImageSelectionGrid: View {
|
||
let images: [UIImage]
|
||
let selectedItems: [PhotosPickerItem]
|
||
let canAddMore: Bool
|
||
let onItemsChanged: ([PhotosPickerItem]) -> Void
|
||
let onRemoveImage: (Int) -> Void
|
||
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
|
||
@State private var showPreview = false
|
||
@State private var previewIndex = 0
|
||
var body: some View {
|
||
let totalSpacing: CGFloat = 8 * 2
|
||
let totalWidth = UIScreen.main.bounds.width - 24 * 2 - totalSpacing
|
||
let gridItemSize: CGFloat = totalWidth / 3
|
||
WithPerceptionTracking {
|
||
LazyVGrid(columns: columns, spacing: 8) {
|
||
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
|
||
ZStack(alignment: .topTrailing) {
|
||
Image(uiImage: image)
|
||
.resizable()
|
||
.aspectRatio(contentMode: .fill) // aspectFill
|
||
.frame(width: gridItemSize, height: gridItemSize)
|
||
.clipped()
|
||
.cornerRadius(12)
|
||
.onTapGesture {
|
||
previewIndex = index
|
||
showPreview = true
|
||
}
|
||
Button(action: {
|
||
onRemoveImage(index)
|
||
}) {
|
||
Image(systemName: "xmark.circle.fill")
|
||
.font(.system(size: 20))
|
||
.foregroundColor(.white)
|
||
.background(Color.black.opacity(0.6))
|
||
.clipShape(Circle())
|
||
}
|
||
.padding(4)
|
||
}
|
||
}
|
||
if canAddMore {
|
||
PhotosPicker(
|
||
selection: .init(
|
||
get: { selectedItems },
|
||
set: { items in DispatchQueue.main.async { onItemsChanged(items) } }
|
||
),
|
||
maxSelectionCount: 9 - images.count,
|
||
matching: .images
|
||
) {
|
||
RoundedRectangle(cornerRadius: 12)
|
||
.fill(Color(hexString: "1C143A"))
|
||
.frame(width: gridItemSize, height: gridItemSize)
|
||
.overlay(
|
||
Image("add photo")
|
||
.resizable()
|
||
.frame(width: 40, height: 40)
|
||
.opacity(0.6)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
.fullScreenCover(isPresented: $showPreview) {
|
||
ImagePreviewPager(images: images, currentIndex: $previewIndex, onClose: { showPreview = false })
|
||
}
|
||
}
|
||
}
|
||
}
|