Files
e-party-iOS/yana/MVVM/CreateFeedPage.swift
edwinQQQ 327d4fd218 feat: 实现动态详情页及相关功能
- 在MePage和MomentListHomePage中新增动态点击事件,支持打开动态详情页。
- 创建MomentDetailPage视图,展示动态详细信息,包括用户信息、动态内容和互动按钮。
- 实现MomentDetailViewModel,管理动态详情页的状态和点赞逻辑。
- 更新MomentListItem组件,添加整体点击回调,提升用户交互体验。
- 优化背景视图组件,确保一致的视觉效果。
2025-09-26 16:49:18 +08:00

230 lines
10 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 }
}
}
}