Files
e-party-iOS/yana/MVVM/CreateFeedPage.swift
edwinQQQ 6b960f53b4 feat: 更新Splash视图及登录模型逻辑
- 将SplashV2替换为SplashPage,优化应用启动流程。
- 在LoginModels中将用户ID参数更改为加密后的ID,增强安全性。
- 更新AppConfig中的API基础URL,确保与生产环境一致。
- 在CommonComponents中添加底部Tab栏图标映射逻辑,提升用户体验。
- 新增NineGridImagePicker组件,支持多图选择功能,优化CreateFeedPage的图片选择逻辑。
- 移除冗余的BottomTabView组件,简化视图结构,提升代码整洁性。
- 在MainPage中整合创建动态页面的逻辑,优化导航体验。
- 新增MePage视图,提供用户信息管理功能,增强应用的可用性。
2025-09-26 10:53:00 +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 }
}
}
}