
- 将SplashV2替换为SplashPage,优化应用启动流程。 - 在LoginModels中将用户ID参数更改为加密后的ID,增强安全性。 - 更新AppConfig中的API基础URL,确保与生产环境一致。 - 在CommonComponents中添加底部Tab栏图标映射逻辑,提升用户体验。 - 新增NineGridImagePicker组件,支持多图选择功能,优化CreateFeedPage的图片选择逻辑。 - 移除冗余的BottomTabView组件,简化视图结构,提升代码整洁性。 - 在MainPage中整合创建动态页面的逻辑,优化导航体验。 - 新增MePage视图,提供用户信息管理功能,增强应用的可用性。
230 lines
10 KiB
Swift
230 lines
10 KiB
Swift
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 }
|
||
}
|
||
}
|
||
} |