import SwiftUI import PhotosUI @MainActor final class CreateFeedViewModel: ObservableObject { @Published var content: String = "" @Published var selectedImages: [UIImage] = [] @Published var isPublishing: Bool = false @Published var errorMessage: String? = nil // 仅当有文本时才允许发布 var canPublish: Bool { !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } } struct CreateFeedPage: View { @Environment(\.dismiss) private var dismiss @StateObject private var viewModel = CreateFeedViewModel() let onDismiss: () -> Void // MARK: - UI State @FocusState private var isTextEditorFocused: Bool @State private var isShowingPreview: Bool = false @State private var previewIndex: Int = 0 private let maxCharacters: Int = 500 private let gridSpacing: CGFloat = 8 private let gridCornerRadius: CGFloat = 16 var body: some View { GeometryReader { geometry in ZStack { Color(hex: 0x0C0527) .ignoresSafeArea() .onTapGesture { // 点击背景收起键盘 isTextEditorFocused = false } VStack(spacing: 16) { HStack { Button(action: { onDismiss() dismiss() }) { Image(systemName: "xmark") .foregroundColor(.white) .font(.system(size: 18, weight: .medium)) .frame(width: 44, height: 44, alignment: .center) .contentShape(Rectangle()) } Spacer() Text(LocalizedString ("createFeed.title", comment: "Image & Text Publish")) .foregroundColor(.white) .font(.system(size: 18, weight: .medium)) Spacer() Button(action: publish) { if viewModel.isPublishing { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) } else { Text(LocalizedString("createFeed.publish", comment: "Publish")) .foregroundColor(.white) .font(.system(size: 14, weight: .medium)) } } .disabled(!viewModel.canPublish || viewModel.isPublishing) .opacity((!viewModel.canPublish || viewModel.isPublishing) ? 0.6 : 1) } .padding(.horizontal, 16) .padding(.top, 12) .contentShape(Rectangle()) .zIndex(10) ZStack(alignment: .topLeading) { RoundedRectangle(cornerRadius: 8).fill(Color(hex: 0x1C143A)) if viewModel.content.isEmpty { Text(LocalizedString("createFeed.enterContent", comment: "Enter Content")) .foregroundColor(.white.opacity(0.5)) .padding(.horizontal, 16) .padding(.vertical, 12) } TextEditor(text: $viewModel.content) .foregroundColor(.white) .padding(.horizontal, 12) .padding(.vertical, 8) .scrollContentBackground(.hidden) .focused($isTextEditorFocused) .frame(height: 200) .zIndex(1) // 确保编辑器不会遮挡顶部栏的点击 // 字数统计(右下角) VStack { Spacer() } .overlay(alignment: .bottomTrailing) { Text("\(viewModel.content.count)/\(maxCharacters)") .foregroundColor(.white.opacity(0.6)) .font(.system(size: 14)) .padding(.trailing, 8) .padding(.bottom, 8) } } .frame(height: 200) .padding(.horizontal, 20) .onChange(of: viewModel.content) { _, newValue in // 限制最大字数 if newValue.count > maxCharacters { viewModel.content = String(newValue.prefix(maxCharacters)) } } NineGridImagePicker( images: $viewModel.selectedImages, maxCount: 9, cornerRadius: gridCornerRadius, spacing: gridSpacing, horizontalPadding: 20, onTapImage: { index in previewIndex = index isShowingPreview = true } ) if let error = viewModel.errorMessage { Text(error) .foregroundColor(.red) .font(.system(size: 14)) } Spacer() } } } .navigationBarBackButtonHidden(true) .fullScreenCover(isPresented: $isShowingPreview) { ZStack { Color.black.ignoresSafeArea() VStack(spacing: 0) { HStack { Spacer() Button { isShowingPreview = false } label: { Image(systemName: "xmark") .foregroundColor(.white) .font(.system(size: 18, weight: .medium)) .padding(12) } } .padding(.top, 8) TabView(selection: $previewIndex) { ForEach(viewModel.selectedImages.indices, id: \.self) { idx in ZStack { Color.black Image(uiImage: viewModel.selectedImages[idx]) .resizable() .scaledToFit() .frame(maxWidth: .infinity, maxHeight: .infinity) } .tag(idx) } } .tabViewStyle(.page(indexDisplayMode: .automatic)) } } } } private func publish() { viewModel.isPublishing = true viewModel.errorMessage = nil Task { @MainActor in let apiService: any APIServiceProtocol & Sendable = LiveAPIService() do { // 1) 上传图片(如有) var resList: [ResListItem] = [] if !viewModel.selectedImages.isEmpty { for image in viewModel.selectedImages { if let url = await COSManager.shared.uploadUIImage(image, apiService: apiService) { if let cg = image.cgImage { let item = ResListItem(resUrl: url, width: cg.width, height: cg.height, format: "jpeg") resList.append(item) } else { // 无法获取尺寸也允许发布,尺寸置为 0 let item = ResListItem(resUrl: url, width: 0, height: 0, format: "jpeg") resList.append(item) } } else { viewModel.isPublishing = false viewModel.errorMessage = "图片上传失败" return } } } // 2) 组装并发送发布请求 let trimmed = viewModel.content.trimmingCharacters(in: .whitespacesAndNewlines) let userId = await UserInfoManager.getCurrentUserId() ?? "" let type = resList.isEmpty ? "0" : "2" // 0: 纯文字, 2: 图片/图文 let request = await PublishFeedRequest.make( content: trimmed, uid: userId, type: type, resList: resList.isEmpty ? nil : resList ) let response = try await apiService.request(request) // 3) 结果处理 if response.code == 200 { viewModel.isPublishing = false NotificationCenter.default.post(name: .init("CreateFeedPublished"), object: nil) onDismiss() dismiss() } else { viewModel.isPublishing = false viewModel.errorMessage = response.message.isEmpty ? "发布失败" : response.message } } catch { viewModel.isPublishing = false viewModel.errorMessage = error.localizedDescription } } } private func removeImage(at index: Int) { guard viewModel.selectedImages.indices.contains(index) else { return } viewModel.selectedImages.remove(at: index) if isShowingPreview { if previewIndex >= viewModel.selectedImages.count { previewIndex = max(0, viewModel.selectedImages.count - 1) } if viewModel.selectedImages.isEmpty { isShowingPreview = false } } } }