From 6b960f53b4f76b1eed461b00efd7b125f9ac40bb Mon Sep 17 00:00:00 2001 From: edwinQQQ Date: Fri, 26 Sep 2025 10:53:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0Splash=E8=A7=86?= =?UTF-8?q?=E5=9B=BE=E5=8F=8A=E7=99=BB=E5=BD=95=E6=A8=A1=E5=9E=8B=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将SplashV2替换为SplashPage,优化应用启动流程。 - 在LoginModels中将用户ID参数更改为加密后的ID,增强安全性。 - 更新AppConfig中的API基础URL,确保与生产环境一致。 - 在CommonComponents中添加底部Tab栏图标映射逻辑,提升用户体验。 - 新增NineGridImagePicker组件,支持多图选择功能,优化CreateFeedPage的图片选择逻辑。 - 移除冗余的BottomTabView组件,简化视图结构,提升代码整洁性。 - 在MainPage中整合创建动态页面的逻辑,优化导航体验。 - 新增MePage视图,提供用户信息管理功能,增强应用的可用性。 --- yana/APIs/LoginModels.swift | 2 +- yana/Configs/AppConfig.swift | 2 +- yana/MVVM/CommonComponents.swift | 36 ++- yana/MVVM/CreateFeedPage.swift | 212 ++++++++++-------- yana/MVVM/MainPage.swift | 110 ++------- yana/MVVM/MePage.swift | 45 ++++ .../MVVM/{SplashV2.swift => SplashPage.swift} | 2 +- yana/MVVM/View/MomentListHomePage.swift | 202 +++++++++-------- yana/MVVM/View/MomentListItem.swift | 3 +- yana/MVVM/View/NineGridImagePicker.swift | 123 ++++++++++ yana/Views/Components/BottomTabView.swift | 78 ------- yana/yanaApp.swift | 2 +- 12 files changed, 446 insertions(+), 371 deletions(-) create mode 100644 yana/MVVM/MePage.swift rename yana/MVVM/{SplashV2.swift => SplashPage.swift} (98%) create mode 100644 yana/MVVM/View/NineGridImagePicker.swift delete mode 100644 yana/Views/Components/BottomTabView.swift diff --git a/yana/APIs/LoginModels.swift b/yana/APIs/LoginModels.swift index 478762e..b849dcd 100644 --- a/yana/APIs/LoginModels.swift +++ b/yana/APIs/LoginModels.swift @@ -392,7 +392,7 @@ struct LoginHelper { debugInfoSync(" 加密后密码: \(encryptedPassword)") return IDLoginAPIRequest( - phone: userID, + phone: encryptedID, password: encryptedPassword ) } diff --git a/yana/Configs/AppConfig.swift b/yana/Configs/AppConfig.swift index 290dbc2..c6e8470 100644 --- a/yana/Configs/AppConfig.swift +++ b/yana/Configs/AppConfig.swift @@ -15,7 +15,7 @@ struct AppConfig { static var baseURL: String { switch current { case .development: - return "http://beta.api.molistar.xyz";//"http://beta.api.pekolive.com" + return "http://beta.api.pekolive.com" case .production: return "https://api.epartylive.com" } diff --git a/yana/MVVM/CommonComponents.swift b/yana/MVVM/CommonComponents.swift index cc758dd..5f42de5 100644 --- a/yana/MVVM/CommonComponents.swift +++ b/yana/MVVM/CommonComponents.swift @@ -25,29 +25,49 @@ struct BottomTabBar: View { var contentPadding: EdgeInsets = EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 0) var horizontalPadding: CGFloat = 0 + // 使用 BottomTabView.swift 中的图片资源名进行映射 + private func assetIconName(for item: TabBarItem, isSelected: Bool) -> String? { + switch item.id { + case "feed": + return isSelected ? "feed selected" : "feed unselected" + case "me": + return isSelected ? "me selected" : "me unselected" + default: + return nil + } + } + var body: some View { - HStack(spacing: 0) { + HStack(spacing: 8) { ForEach(items) { item in Button(action: { selectedId = item.id onSelect(item.id) }) { - VStack(spacing: 4) { - Image(systemName: item.systemIconName) - .font(.system(size: 24)) - .foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6)) - Text(item.title) - .font(.system(size: 12)) - .foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6)) + Group { + if let name = assetIconName(for: item, isSelected: selectedId == item.id) { + Image(name) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + } else { + Image(systemName: item.systemIconName) + .font(.system(size: 24)) + .foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6)) + } } } .frame(maxWidth: .infinity) .padding(contentPadding) + .contentShape(Rectangle()) } } + .padding(.horizontal, 8) // 按钮与边缘保持 8 间距 .padding(.horizontal, horizontalPadding) .background(LiquidGlassBackground()) .clipShape(Capsule()) + .contentShape(Capsule()) + .onTapGesture { /* 吸收空白区域点击,避免穿透 */ } .overlay( Capsule() .stroke(Color.white.opacity(0.12), lineWidth: 0.5) diff --git a/yana/MVVM/CreateFeedPage.swift b/yana/MVVM/CreateFeedPage.swift index aa5ace7..ea95702 100644 --- a/yana/MVVM/CreateFeedPage.swift +++ b/yana/MVVM/CreateFeedPage.swift @@ -1,4 +1,5 @@ import SwiftUI +import PhotosUI @MainActor final class CreateFeedViewModel: ObservableObject { @@ -11,19 +12,21 @@ final class CreateFeedViewModel: ObservableObject { } 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 isShowingSourceSheet: Bool = false - @State private var isShowingImagePicker: Bool = false - @State private var imagePickerSource: UIImagePickerController.SourceType = .photoLibrary + @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 { _ in + GeometryReader { geometry in ZStack { Color(hex: 0x0C0527) .ignoresSafeArea() @@ -33,13 +36,18 @@ struct CreateFeedPage: View { } VStack(spacing: 16) { HStack { - Button(action: onDismiss) { + 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")) + Text(LocalizedString ("createFeed.title", comment: "Image & Text Publish")) .foregroundColor(.white) .font(.system(size: 18, weight: .medium)) Spacer() @@ -58,6 +66,8 @@ struct CreateFeedPage: View { } .padding(.horizontal, 16) .padding(.top, 12) + .contentShape(Rectangle()) + .zIndex(10) ZStack(alignment: .topLeading) { RoundedRectangle(cornerRadius: 8).fill(Color(hex: 0x1C143A)) @@ -74,6 +84,7 @@ struct CreateFeedPage: View { .scrollContentBackground(.hidden) .focused($isTextEditorFocused) .frame(height: 200) + .zIndex(1) // 确保编辑器不会遮挡顶部栏的点击 // 字数统计(右下角) VStack { Spacer() } @@ -94,58 +105,17 @@ struct CreateFeedPage: View { } } - // 添加图片按钮 - HStack(alignment: .top, spacing: 12) { - Button { - isShowingSourceSheet = true - } label: { - ZStack { - RoundedRectangle(cornerRadius: 16) - .fill(Color(hex: 0x1C143A)) - .frame(width: 180, height: 180) - Image(systemName: "plus") - .foregroundColor(.white.opacity(0.6)) - .font(.system(size: 36, weight: .semibold)) - } + NineGridImagePicker( + images: $viewModel.selectedImages, + maxCount: 9, + cornerRadius: gridCornerRadius, + spacing: gridSpacing, + horizontalPadding: 20, + onTapImage: { index in + previewIndex = index + isShowingPreview = true } - .buttonStyle(.plain) - - // 已选图片预览(可滚动) - if !viewModel.selectedImages.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - ForEach(viewModel.selectedImages.indices, id: \.self) { index in - Image(uiImage: viewModel.selectedImages[index]) - .resizable() - .scaledToFill() - .frame(width: 100, height: 100) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .clipped() - } - } - } - .frame(height: 180) - } - } - .padding(.horizontal, 20) - .confirmationDialog(LocalizedString("createFeed.chooseSource", comment: "Choose Source"), isPresented: $isShowingSourceSheet, titleVisibility: .visible) { - Button(LocalizedString("createFeed.source.album", comment: "Photo Library")) { - imagePickerSource = .photoLibrary - isShowingImagePicker = true - } - Button(LocalizedString("createFeed.source.camera", comment: "Camera")) { - if UIImagePickerController.isSourceTypeAvailable(.camera) { - imagePickerSource = .camera - isShowingImagePicker = true - } - } - Button(LocalizedString("common.cancel", comment: "Cancel"), role: .cancel) {} - } - .sheet(isPresented: $isShowingImagePicker) { - ImagePicker(sourceType: imagePickerSource) { image in - viewModel.selectedImages.append(image) - } - } + ) if let error = viewModel.errorMessage { Text(error) @@ -158,53 +128,103 @@ struct CreateFeedPage: View { } } .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 - try? await Task.sleep(nanoseconds: 500_000_000) - viewModel.isPublishing = false - onDismiss() - } - } -} + 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 + } + } + } -// MARK: - UIKit Image Picker Wrapper -private struct ImagePicker: UIViewControllerRepresentable { - let sourceType: UIImagePickerController.SourceType - let onImagePicked: (UIImage) -> Void - - func makeCoordinator() -> Coordinator { - Coordinator(onImagePicked: onImagePicked) - } - - func makeUIViewController(context: Context) -> UIImagePickerController { - let picker = UIImagePickerController() - picker.sourceType = sourceType - picker.allowsEditing = false - picker.delegate = context.coordinator - return picker - } - - func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} - - final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { - let onImagePicked: (UIImage) -> Void - - init(onImagePicked: @escaping (UIImage) -> Void) { - self.onImagePicked = onImagePicked - } - - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - if let image = (info[.originalImage] as? UIImage) ?? (info[.editedImage] as? UIImage) { - onImagePicked(image) + // 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 } - picker.dismiss(animated: true) } - - func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - picker.dismiss(animated: true) + } + + 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 } } } } \ No newline at end of file diff --git a/yana/MVVM/MainPage.swift b/yana/MVVM/MainPage.swift index dcc51c1..638a285 100644 --- a/yana/MVVM/MainPage.swift +++ b/yana/MVVM/MainPage.swift @@ -5,6 +5,7 @@ import SwiftUI struct MainPage: View { @StateObject private var viewModel = MainViewModel() let onLogout: () -> Void + @State private var isPresentingCreatePage: Bool = false var body: some View { NavigationStack(path: $viewModel.navigationPath) { @@ -12,16 +13,18 @@ struct MainPage: View { ZStack { // 背景图片 LoginBackgroundView() - // 主内容 - mainContentView(geometry: geometry) - .frame(maxWidth: .infinity, maxHeight: .infinity) + + // 主内容:使用 TabView 常驻子树 + TabView(selection: $viewModel.selectedTab) { + MomentListHomePage(onCreateTapped: { isPresentingCreatePage = true }) + .tag(MainViewModel.Tab.feed) + MePage(onLogout: onLogout) + .tag(MainViewModel.Tab.me) + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .frame(maxWidth: .infinity, maxHeight: .infinity) VStack { - HStack { - Spacer() - // 右上角按钮 - topRightButton - } Spacer() // 底部导航栏(组件化) BottomTabBar( @@ -45,29 +48,6 @@ struct MainPage: View { } } } - .navigationDestination(for: String.self) { _ in EmptyView() } - .navigationDestination(for: AppRoute.self) { route in - switch route { - case .setting: - SettingPage( - onBack: { - viewModel.navigationPath.removeLast() - }, - onLogout: { - viewModel.onLogoutTapped() - } - ) - .navigationBarHidden(true) - case .publish: - CreateFeedPage( - onDismiss: { - viewModel.navigationPath.removeLast() - } - ) - default: - EmptyView() - } - } } .onAppear { viewModel.onLogout = onLogout @@ -77,73 +57,15 @@ struct MainPage: View { } viewModel.onAppear() } + .fullScreenCover(isPresented: $isPresentingCreatePage) { + CreateFeedPage { + isPresentingCreatePage = false + } + } .onChange(of: viewModel.isLoggedOut) { _, isLoggedOut in if isLoggedOut { onLogout() } } } - - // MARK: - UI Components - - private func mainContentView(geometry: GeometryProxy) -> some View { - Group { - switch viewModel.selectedTab { - case .feed: - MomentListHomePage() - case .me: - TempMePage() - } - } - } - - // 底部栏已组件化 - - // MARK: - 右上角按钮 - private var topRightButton: some View { - Button(action: { - viewModel.onTopRightButtonTapped() - }) { - Group { - switch viewModel.selectedTab { - case .feed: - Image("add icon") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 40, height: 40) - case .me: - Image(systemName: "gearshape") - .font(.system(size: 24, weight: .medium)) - .foregroundColor(.white) - .frame(width: 40, height: 40) - .background(Color.black.opacity(0.3)) - .clipShape(Circle()) - } - } - } - .padding(.trailing, 16) - .padding(.top, 8) - } } - - - -// MARK: - MeView (简化版本) - -struct TempMePage: View { - var body: some View { - VStack { - Text("Me View") - .font(.title) - .foregroundColor(.white) - - Text("This is a simplified MeView") - .font(.body) - .foregroundColor(.white.opacity(0.8)) - } - } -} - -//#Preview { -// MainPage(onLogout: {}) -//} diff --git a/yana/MVVM/MePage.swift b/yana/MVVM/MePage.swift new file mode 100644 index 0000000..abdddcd --- /dev/null +++ b/yana/MVVM/MePage.swift @@ -0,0 +1,45 @@ +import SwiftUI + +struct MePage: View { + let onLogout: () -> Void + @State private var isShowingSettings: Bool = false + + var body: some View { + ZStack(alignment: .topTrailing) { + VStack { + Text("Me View") + .font(.title) + .foregroundColor(.white) + + Text("This is a simplified MeView") + .font(.body) + .foregroundColor(.white.opacity(0.8)) + } + + Button(action: { + isShowingSettings = true + }) { + Image(systemName: "gearshape") + .font(.system(size: 24, weight: .medium)) + .foregroundColor(.white) + .frame(width: 40, height: 40) + .background(Color.black.opacity(0.3)) + .clipShape(Circle()) + } + .padding(.trailing, 16) + .padding(.top, 8) + } + .sheet(isPresented: $isShowingSettings) { + SettingPage( + onBack: { isShowingSettings = false }, + onLogout: { + isShowingSettings = false + onLogout() + } + ) + .navigationBarHidden(true) + } + } +} + + diff --git a/yana/MVVM/SplashV2.swift b/yana/MVVM/SplashPage.swift similarity index 98% rename from yana/MVVM/SplashV2.swift rename to yana/MVVM/SplashPage.swift index 12fb5ad..9beb0dd 100644 --- a/yana/MVVM/SplashV2.swift +++ b/yana/MVVM/SplashPage.swift @@ -1,6 +1,6 @@ import SwiftUI -struct SplashV2: View { +struct SplashPage: View { @State private var showLogin = false @State private var showMain = false @State private var hasCheckedAuth = false diff --git a/yana/MVVM/View/MomentListHomePage.swift b/yana/MVVM/View/MomentListHomePage.swift index b688c00..bf0c6ca 100644 --- a/yana/MVVM/View/MomentListHomePage.swift +++ b/yana/MVVM/View/MomentListHomePage.swift @@ -8,72 +8,92 @@ struct MomentListBackgroundView: View { .aspectRatio(contentMode: .fill) .frame(maxWidth: .infinity, maxHeight: .infinity) .clipped() - .ignoresSafeArea(.all) + .ignoresSafeArea(.all) } } // MARK: - MomentListHomePage struct MomentListHomePage: View { @StateObject private var viewModel = MomentListHomeViewModel() + let onCreateTapped: () -> Void // MARK: - 图片预览状态 @State private var previewItem: PreviewItem? = nil @State private var previewCurrentIndex: Int = 0 + // MARK: - 创建动态发布页弹窗 + // 迁移到上层(MainPage)统一管理,避免与 TabView 全屏弹窗冲突 + var body: some View { - GeometryReader { geometry in - ZStack { - // 背景 - MomentListBackgroundView() - - VStack(alignment: .center, spacing: 0) { - // 标题 + ZStack { + // 背景 + MomentListBackgroundView() + + VStack(alignment: .center, spacing: 0) { + // 顶部标题居中 + 右上角添加按钮(垂直居中对齐) + ZStack { + // 居中标题 Text(LocalizedString("feedList.title", comment: "Enjoy your Life Time")) .font(.system(size: 22, weight: .semibold)) .foregroundColor(.white) .frame(maxWidth: .infinity, alignment: .center) - .padding(.top, 60) - - // Volume 图标 - Image("Volume") - .frame(width: 56, height: 41) - .padding(.top, 16) - - // 标语 - Text(LocalizedString("feedList.slogan", - comment: "")) - .font(.system(size: 16)) - .multilineTextAlignment(.leading) - .foregroundColor(.white.opacity(0.9)) - .padding(.horizontal, 30) - .padding(.bottom, 30) - - // 动态列表内容 - if !viewModel.moments.isEmpty { - ScrollView { + + // 右上角 “+” 按钮 + HStack { + Spacer() + Button { + debugInfoSync("➕ MomentListHomePage: 点击添加按钮") + onCreateTapped() + } label: { + Image("add icon") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 40, height: 40) + } + .padding(.trailing, 16) + } + } + .frame(height: 56) + + // 动态列表内容(Volume 与标语随列表滚动) + if !viewModel.moments.isEmpty { + ScrollView { + VStack(spacing: 0) { + // Volume 图标 + 标语(随列表滚动) + Image("Volume") + .frame(width: 56, height: 41) + .padding(.top, 16) + Text(LocalizedString("feedList.slogan", + comment: "")) + .font(.system(size: 16)) + .multilineTextAlignment(.leading) + .foregroundColor(.white.opacity(0.9)) + .padding(.horizontal, 30) + .padding(.bottom, 30) + LazyVStack(spacing: 16) { - ForEach(Array(viewModel.moments.enumerated()), id: \.element.dynamicId) { index, moment in - MomentListItem( - moment: moment, - onImageTap: { images, tappedIndex in - // 处理图片点击事件 - previewCurrentIndex = tappedIndex - previewItem = PreviewItem(images: images, index: tappedIndex) - debugInfoSync("📸 MomentListHomePage: 图片被点击") - debugInfoSync(" 动态索引: \(index)") - debugInfoSync(" 图片索引: \(tappedIndex)") - debugInfoSync(" 图片数量: \(images.count)") - } - ) - .padding(.leading, 16) - .padding(.trailing, 32) - .onAppear { - // 当显示倒数第三个项目时,开始加载更多 - if index == viewModel.moments.count - 3 { - viewModel.loadMoreData() - } + ForEach(Array(viewModel.moments.enumerated()), id: \.element.dynamicId) { index, moment in + MomentListItem( + moment: moment, + onImageTap: { images, tappedIndex in + // 处理图片点击事件 + previewCurrentIndex = tappedIndex + previewItem = PreviewItem(images: images, index: tappedIndex) + debugInfoSync("📸 MomentListHomePage: 图片被点击") + debugInfoSync(" 动态索引: \(index)") + debugInfoSync(" 图片索引: \(tappedIndex)") + debugInfoSync(" 图片数量: \(images.count)") + } + ) + .padding(.leading, 16) + .padding(.trailing, 32) + .onAppear { + // 当显示倒数第三个项目时,开始加载更多 + if index == viewModel.moments.count - 3 { + viewModel.loadMoreData() } } + } // 加载更多状态指示器 if viewModel.isLoadingMore { @@ -98,55 +118,58 @@ struct MomentListHomePage: View { } .padding(.bottom, 160) // 为底部导航栏留出空间 } - .refreshable { - // 下拉刷新 - viewModel.refreshData() - } - .onAppear { - // 调试信息 - debugInfoSync("📱 MomentListHomePage: 显示动态列表") - debugInfoSync(" 动态数量: \(viewModel.moments.count)") - debugInfoSync(" 是否有更多: \(viewModel.hasMore)") - debugInfoSync(" 是否正在加载更多: \(viewModel.isLoadingMore)") - } - } else if viewModel.isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .padding(.top, 20) - } else if let error = viewModel.error { - VStack(spacing: 16) { - Text(error) - .font(.system(size: 14)) - .foregroundColor(.red) - .multilineTextAlignment(.center) - .padding(.horizontal, 20) - - // 重试按钮 - Button(action: { - viewModel.refreshData() - }) { - Text("重试") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.white) - .padding(.horizontal, 20) - .padding(.vertical, 8) - .background(Color.white.opacity(0.2)) - .cornerRadius(8) - } - } - .padding(.top, 20) } - - Spacer() + .refreshable { + // 下拉刷新 + viewModel.refreshData() + } + .onAppear { + // 调试信息 + debugInfoSync("📱 MomentListHomePage: 显示动态列表") + debugInfoSync(" 动态数量: \(viewModel.moments.count)") + debugInfoSync(" 是否有更多: \(viewModel.hasMore)") + debugInfoSync(" 是否正在加载更多: \(viewModel.isLoadingMore)") + } + } else if viewModel.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .padding(.top, 20) + } else if let error = viewModel.error { + VStack(spacing: 16) { + Text(error) + .font(.system(size: 14)) + .foregroundColor(.red) + .multilineTextAlignment(.center) + .padding(.horizontal, 20) + + // 重试按钮 + Button(action: { + viewModel.refreshData() + }) { + Text("重试") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.white) + .padding(.horizontal, 20) + .padding(.vertical, 8) + .background(Color.white.opacity(0.2)) + .cornerRadius(8) + } + } + .padding(.top, 20) } + + Spacer() } + .safeAreaPadding(.top, 8) } - .ignoresSafeArea() .onAppear { viewModel.onAppear() } - // MARK: - 图片预览弹窗 - .fullScreenCover(item: $previewItem) { item in + .onReceive(NotificationCenter.default.publisher(for: .init("CreateFeedPublished"))) { _ in + viewModel.refreshData() + } + // MARK: - 图片预览弹窗(使用 sheet 以避免与发布页全屏弹窗冲突) + .sheet(item: $previewItem) { item in ImagePreviewPager( images: item.images as [String], currentIndex: $previewCurrentIndex @@ -155,5 +178,6 @@ struct MomentListHomePage: View { debugInfoSync("📸 MomentListHomePage: 图片预览已关闭") } } + // 发布页由上层统一控制 } } diff --git a/yana/MVVM/View/MomentListItem.swift b/yana/MVVM/View/MomentListItem.swift index a25f1ac..c9e6d05 100644 --- a/yana/MVVM/View/MomentListItem.swift +++ b/yana/MVVM/View/MomentListItem.swift @@ -23,7 +23,6 @@ struct MomentListItem: View { var body: some View { ZStack { - // 背景层 RoundedRectangle(cornerRadius: 12) .fill(Color.clear) .overlay( @@ -31,7 +30,7 @@ struct MomentListItem: View { .stroke(Color.white.opacity(0.1), lineWidth: 1) ) .shadow(color: Color(red: 0.43, green: 0.43, blue: 0.43, opacity: 0.34), radius: 10.7, x: 0, y: 1.9) - + // 内容层 VStack(alignment: .leading, spacing: 10) { // 用户信息 diff --git a/yana/MVVM/View/NineGridImagePicker.swift b/yana/MVVM/View/NineGridImagePicker.swift new file mode 100644 index 0000000..7ee0ba2 --- /dev/null +++ b/yana/MVVM/View/NineGridImagePicker.swift @@ -0,0 +1,123 @@ +import SwiftUI +import PhotosUI + +struct NineGridImagePicker: View { + @Binding var images: [UIImage] + var maxCount: Int = 9 + var cornerRadius: CGFloat = 16 + var spacing: CGFloat = 8 + var horizontalPadding: CGFloat = 20 + var onTapImage: (Int) -> Void = { _ in } + + @State private var pickerItems: [PhotosPickerItem] = [] + + var body: some View { + GeometryReader { geometry in + let columns = Array(repeating: GridItem(.flexible(), spacing: spacing), count: 3) + let columnsCount: CGFloat = 3 + let totalSpacing = spacing * (columnsCount - 1) + let availableWidth = geometry.size.width - horizontalPadding * 2 + let cellSide = (availableWidth - totalSpacing) / columnsCount + + LazyVGrid(columns: columns, spacing: spacing) { + ForEach(0..= images.count && !(index == images.count && images.count < maxCount) { + RoundedRectangle(cornerRadius: cornerRadius) + .fill(Color.white.opacity(0.08)) + } + #endif + + if index < images.count { + // 图片格子 + ZStack(alignment: .topTrailing) { + Image(uiImage: images[index]) + .resizable() + .scaledToFill() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .contentShape(RoundedRectangle(cornerRadius: cornerRadius)) + .onTapGesture { onTapImage(index) } + + Button { + removeImage(at: index) + } label: { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.white) + .background(Circle().fill(Color.black.opacity(0.4))) + .font(.system(size: 16, weight: .bold)) + } + .padding(6) + .buttonStyle(.plain) + } + } else if index == images.count && images.count < maxCount { + // 添加按钮格子 + PhotosPicker( + selection: $pickerItems, + maxSelectionCount: maxCount - images.count, + selectionBehavior: .ordered, + matching: .images + ) { + ZStack { + RoundedRectangle(cornerRadius: cornerRadius) + .fill(Color(hex: 0x1C143A)) + Image(systemName: "plus") + .foregroundColor(.white.opacity(0.6)) + .font(.system(size: 32, weight: .semibold)) + } + } + .onChange(of: pickerItems) { _, newItems in + handlePickerItems(newItems) + } + } + } + .frame(height: cellSide) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .contentShape(RoundedRectangle(cornerRadius: cornerRadius)) + } + } + .padding(.horizontal, horizontalPadding) + } + .frame(height: gridHeight(forCount: max(images.count, 1))) + } + + private func gridHeight(forCount count: Int) -> CGFloat { + // 通过一个近似:用屏幕宽度估算高度以确保父布局正确测量。 + // 每行 3 个,行数 = ceil(count / 3.0)。在 GeometryReader 中真实高度会覆盖此近似。 + let screenWidth = UIScreen.main.bounds.width + let columnsCount: CGFloat = 3 + let totalSpacing = spacing * (columnsCount - 1) + let availableWidth = screenWidth - horizontalPadding * 2 + let side = (availableWidth - totalSpacing) / columnsCount + let rows = ceil(CGFloat(count) / 3.0) + let totalRowSpacing = spacing * max(rows - 1, 0) + return side * rows + totalRowSpacing + } + + private func handlePickerItems(_ items: [PhotosPickerItem]) { + guard !items.isEmpty else { return } + Task { @MainActor in + var appended: [UIImage] = [] + for item in items { + if images.count + appended.count >= maxCount { break } + if let data = try? await item.loadTransferable(type: Data.self), + let image = UIImage(data: data) { + appended.append(image) + } + } + if !appended.isEmpty { + images.append(contentsOf: appended) + } + pickerItems = [] + } + } + + private func removeImage(at index: Int) { + guard images.indices.contains(index) else { return } + images.remove(at: index) + } +} + + + diff --git a/yana/Views/Components/BottomTabView.swift b/yana/Views/Components/BottomTabView.swift deleted file mode 100644 index e487b09..0000000 --- a/yana/Views/Components/BottomTabView.swift +++ /dev/null @@ -1,78 +0,0 @@ -import SwiftUI - -// MARK: - Tab 枚举 -enum Tab: Int, CaseIterable { - case feed = 0 - case me = 1 - - var title: String { - switch self { - case .feed: - return "动态" - case .me: - return "我的" - } - } - - var iconName: String { - switch self { - case .feed: - return "feed unselected" - case .me: - return "me unselected" - } - } - - var selectedIconName: String { - switch self { - case .feed: - return "feed selected" - case .me: - return "me selected" - } - } -} - -// MARK: - BottomTabView 组件 -struct BottomTabView: View { - @Binding var selectedTab: Tab - - var body: some View { - HStack(spacing: 0) { - ForEach(Tab.allCases, id: \.rawValue) { tab in - Button(action: { - selectedTab = tab - }) { - Image(selectedTab == tab ? tab.selectedIconName : tab.iconName) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 30, height: 30) - .frame(maxWidth: .infinity) - } - .buttonStyle(PlainButtonStyle()) - } - } - .frame(height: 60) - .padding(.horizontal, 16) - .background( - RoundedRectangle(cornerRadius: 30) - .fill(.ultraThinMaterial) - .overlay( - RoundedRectangle(cornerRadius: 30) - .stroke(Color.white.opacity(0.1), lineWidth: 0.5) - ) - .shadow( - color: Color.black.opacity(0.34), - radius: 10.7, - x: 0, - y: 1.9 - ) - ) - .padding(.horizontal, 15) - } -} - -#Preview { - BottomTabView(selectedTab: .constant(.feed)) - .background(Color.purple) // 预览时添加背景色以便查看效果 -} diff --git a/yana/yanaApp.swift b/yana/yanaApp.swift index 512c6e6..6a0a6af 100644 --- a/yana/yanaApp.swift +++ b/yana/yanaApp.swift @@ -23,7 +23,7 @@ struct yanaApp: App { var body: some Scene { WindowGroup { - SplashV2() + SplashPage() } } }