From 3ec1b1302f5463617d237642ffdf7612c9f0a3e8 Mon Sep 17 00:00:00 2001 From: edwinQQQ Date: Tue, 29 Jul 2025 15:59:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0iOS=E5=92=8CPodfile?= =?UTF-8?q?=E7=9A=84=E9=83=A8=E7=BD=B2=E7=9B=AE=E6=A0=87=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=96=B0=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将iOS平台版本更新至17,确保与最新的开发环境兼容。 - 更新Podfile中的iOS部署目标至17.0,确保依赖项与新版本兼容。 - 修改Podfile.lock以反映新的依赖项版本,确保项目一致性。 - 在多个视图中重构代码,优化状态管理和视图逻辑,提升用户体验。 --- Package.swift | 2 +- Podfile | 4 +- Podfile.lock | 2 +- yana.xcodeproj/project.pbxproj | 22 +- yana/Views/AppSettingView.swift | 745 +++++++++++++------------------- yana/Views/EMailLoginView.swift | 217 +++++----- yana/Views/EditFeedView.swift | 371 ++++++++++------ yana/Views/FeedListView.swift | 166 ++++--- yana/Views/IDLoginView.swift | 146 +++---- yana/Views/LoginView.swift | 128 +++--- yana/Views/MainView.swift | 56 ++- yana/Views/MeView.swift | 325 +++++++------- 12 files changed, 1053 insertions(+), 1131 deletions(-) diff --git a/Package.swift b/Package.swift index 4715f4c..9687d22 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "yana", platforms: [ - .iOS(.v15), + .iOS(.v17), .macOS(.v12) ], products: [ diff --git a/Podfile b/Podfile index 0a799a4..2541af6 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,5 @@ # Uncomment the next line to define a global platform for your project -platform :ios, '16.0' +platform :ios, '17.0' target 'yana' do # Comment the next line if you don't want to use dynamic frameworks @@ -26,7 +26,7 @@ post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES' - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.0' end end diff --git a/Podfile.lock b/Podfile.lock index 42a7110..1392e4d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -27,6 +27,6 @@ SPEC CHECKSUMS: QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7 QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8 -PODFILE CHECKSUM: cd339c4c75faaa076936a34cac2be597c64f138a +PODFILE CHECKSUM: b6f9510b987dbfd80d7a7e45c13b229f9c4c6e63 COCOAPODS: 1.16.2 diff --git a/yana.xcodeproj/project.pbxproj b/yana.xcodeproj/project.pbxproj index 914c94a..8ce9f02 100644 --- a/yana.xcodeproj/project.pbxproj +++ b/yana.xcodeproj/project.pbxproj @@ -49,6 +49,8 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = yanaAPITests; sourceTree = ""; }; @@ -253,14 +255,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources.sh\"\n"; @@ -274,14 +272,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks.sh\"\n"; @@ -391,7 +385,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -451,7 +445,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -499,7 +493,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -557,7 +551,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -588,7 +582,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = EKM7RAGNA6; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.junpeiqi.eparty; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -612,7 +606,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = EKM7RAGNA6; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yanaAPITests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/yana/Views/AppSettingView.swift b/yana/Views/AppSettingView.swift index 803f498..a151a09 100644 --- a/yana/Views/AppSettingView.swift +++ b/yana/Views/AppSettingView.swift @@ -31,476 +31,327 @@ struct AppSettingView: View { @State private var errorMessage: String? = nil var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in - WithPerceptionTracking { - mainView(viewStore: viewStore) - } + WithPerceptionTracking { + mainView() } } @ViewBuilder - private func mainView(viewStore: ViewStoreOf) -> some View { - contentWithImagePickers(viewStore: viewStore) - .onChange(of: selectedPhotoItems) { items in - print("[LOG] PhotosPicker选中items: \(items.count)") - guard !items.isEmpty else { return } - isLoading = true - selectedImages = [] - let group = DispatchGroup() - var tempImages: [UIImage] = [] - for item in items { - group.enter() - item.loadTransferable(type: Data.self) { result in - defer { group.leave() } - if let data = try? result.get(), let uiImage = UIImage(data: data) { - DispatchQueue.main.async { - tempImages.append(uiImage) - print("[LOG] 成功加载图片,当前tempImages数量: \(tempImages.count)") + private func mainView() -> some View { + WithPerceptionTracking { + GeometryReader { geometry in + ZStack { + // 背景图片 + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + .ignoresSafeArea(.all) + + VStack(spacing: 0) { + // 顶部导航栏 + HStack { + Button(action: { + store.send(.dismissTapped) + }) { + Image(systemName: "chevron.left") + .font(.system(size: 24, weight: .medium)) + .foregroundColor(.white) + .frame(width: 44, height: 44) } - } else { - print("[LOG] 图片加载失败") + + Spacer() + + Text(LocalizedString("app_settings.title", comment: "设置")) + .font(.system(size: 18, weight: .medium)) + .foregroundColor(.white) + + Spacer() + + // 占位,保持标题居中 + Color.clear + .frame(width: 44, height: 44) } - } - } - DispatchQueue.global().async { - group.wait() - DispatchQueue.main.async { - isLoading = false - print("[LOG] 所有图片加载完成,tempImages数量: \(tempImages.count)") - if tempImages.isEmpty { - errorMessage = "图片加载失败,请重试" - // 强制关闭所有弹窗 - showPreview = false - showCamera = false - showPhotoPicker = false - showActionSheet = false - print("[LOG] PhotosPicker图片加载失败,弹出错误提示") - } else { - // 先设置selectedImages,确保预览组件能接收到图片 - selectedImages = tempImages - print("[LOG] selectedImages已设置,数量: \(selectedImages.count)") - // 确保在主线程上设置showPreview - DispatchQueue.main.async { - showPreview = true - print("[LOG] showPreview已设置为true") + .padding(.horizontal, 16) + .padding(.top, 8) + + // 主要内容区域 + ScrollView { + VStack(spacing: 20) { + // 头像设置区域 + avatarSection() + + // 个人信息设置区域 + personalInfoSection() + + // 其他设置区域 + otherSettingsSection() + + // 退出登录按钮 + logoutSection() } + .padding(.horizontal, 20) + .padding(.top, 20) + .padding(.bottom, 40) } } } } - .alert(isPresented: Binding( - get: { errorMessage != nil }, - set: { if !$0 { errorMessage = nil } } - )) { - print("[LOG] 错误弹窗弹出: \(errorMessage ?? "")") - return Alert(title: Text(LocalizedString("app_settings.error", comment: "")), message: Text(errorMessage ?? ""), dismissButton: .default(Text(LocalizedString("app_settings.confirm", comment: "")), action: { - // 强制关闭所有弹窗,放到action中,避免在视图更新周期set状态 - showPreview = false - showCamera = false - showPhotoPicker = false - showActionSheet = false - })) - } - .navigationBarHidden(true) - .alert("修改昵称", isPresented: $showNicknameAlert) { - nicknameAlertContent(viewStore: viewStore) - } message: { - Text(LocalizedString("app_settings.nickname_limit", comment: "")) - .font(.caption) - .foregroundColor(.secondary) - } - .sheet(isPresented: userAgreementBinding(viewStore: viewStore)) { - WebView(url: APIConfiguration.webURL(for: .userAgreement)!) - } - .sheet(isPresented: privacyPolicyBinding(viewStore: viewStore)) { - WebView(url: APIConfiguration.webURL(for: .privacyPolicy)!) - } - .sheet(isPresented: deactivateAccountBinding(viewStore: viewStore)) { - WebView(url: APIConfiguration.webURL(for: .deactivateAccount)!) - } - } - - @ViewBuilder - private func contentWithImagePickers(viewStore: ViewStoreOf) -> some View { - ZStack { - mainContent(viewStore: viewStore) - } - .confirmationDialog( - "请选择图片来源", - isPresented: $showActionSheet, - titleVisibility: .visible - ) { - Button(LocalizedString("app_settings.take_photo", comment: "")) { showCamera = true } - Button(LocalizedString("app_settings.select_from_album", comment: "")) { showPhotoPicker = true } - Button("取消", role: .cancel) {} - } - .photosPicker( - isPresented: $showPhotoPicker, - selection: $selectedPhotoItems, - maxSelectionCount: 1, - matching: .images - ) - .sheet(isPresented: $showCamera) { - CameraPicker { image in - print("[LOG] CameraPicker回调,image: \(image != nil)") - if let image = image { - print("[LOG] CameraPicker获得图片,直接上传头像") - if let data = image.jpegData(compressionQuality: 0.8) { - viewStore.send(AppSettingFeature.Action.avatarSelected(data)) - } - } else { - errorMessage = "拍照失败,请重试" - // 强制关闭所有弹窗 - showPreview = false - showCamera = false - showPhotoPicker = false - showActionSheet = false - print("[LOG] CameraPicker无图片,弹出错误提示") - } - showCamera = false - } - } - .fullScreenCover(isPresented: $showPreview) { - ImagePreviewView( - images: $selectedImages, - currentIndex: .constant(0), - onConfirm: { - print("[LOG] 预览确认,准备上传头像") - if let image = selectedImages.first, let data = image.jpegData(compressionQuality: 0.8) { - viewStore.send(AppSettingFeature.Action.avatarSelected(data)) - } - showPreview = false - }, - onCancel: { - print("[LOG] 预览取消") - showPreview = false - } - ) - } - } - - // MARK: - 主要内容 - private func mainContent(viewStore: ViewStoreOf) -> some View { - ZStack { - Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea() - VStack(spacing: 0) { - topBar - ScrollView { - WithPerceptionTracking { - VStack(spacing: 32) { - // 头像区域 - avatarSection(viewStore: viewStore) - // 昵称设置项 - nicknameSection(viewStore: viewStore) - // 设置项区域 - settingsSection(viewStore: viewStore) - // 退出登录按钮 - logoutButton(viewStore: viewStore) + .navigationBarHidden(true) + // 头像选择器 + .sheet(isPresented: $showImagePickerSheet) { + ImagePickerWithPreviewView( + store: pickerStore, + onUpload: { images in + if let firstImage = images.first, + let imageData = firstImage.jpegData(compressionQuality: 0.8) { + store.send(AppSettingFeature.Action.avatarSelected(imageData)) } + showImagePickerSheet = false + }, + onCancel: { + showImagePickerSheet = false } + ) + } + // 相机拍照 + .sheet(isPresented: $showCamera) { + ImagePickerWithPreviewView( + store: pickerStore, + onUpload: { images in + if let firstImage = images.first, + let imageData = firstImage.jpegData(compressionQuality: 0.8) { + store.send(AppSettingFeature.Action.avatarSelected(imageData)) + } + showCamera = false + }, + onCancel: { + showCamera = false + } + ) + } + // 昵称编辑弹窗 + .alert(LocalizedString("app_settings.edit_nickname", comment: "编辑昵称"), isPresented: $showNicknameAlert) { + TextField(LocalizedString("app_settings.nickname_placeholder", comment: "请输入昵称"), text: $nicknameInput) + Button(LocalizedString("app_settings.cancel", comment: "取消")) { + showNicknameAlert = false + nicknameInput = "" } + Button(LocalizedString("app_settings.confirm", comment: "确认")) { + let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmed.isEmpty { + store.send(.nicknameEditConfirmed(trimmed)) + } + showNicknameAlert = false + nicknameInput = "" + } + } message: { + Text(LocalizedString("app_settings.nickname_tip", comment: "请输入新的昵称")) } } } - - // MARK: - 头像区域 - private func avatarSection(viewStore: ViewStoreOf) -> some View { - ZStack(alignment: .bottomTrailing) { - avatarImageView(viewStore: viewStore) - .onTapGesture { - showActionSheet = true - } - cameraButton(viewStore: viewStore) - } - .padding(.top, 24) - } - - // MARK: - 头像图片视图 + + // MARK: - 头像设置区域 @ViewBuilder - private func avatarImageView(viewStore: ViewStoreOf) -> some View { - if viewStore.isUploadingAvatar || viewStore.isLoadingUserInfo { - loadingAvatarView - } else if let avatarURLString = viewStore.avatarURL, !avatarURLString.isEmpty, let avatarURL = URL(string: avatarURLString) { - networkAvatarView(url: avatarURL) - } else { - defaultAvatarView - } - } - - // MARK: - 加载状态头像 - private var loadingAvatarView: some View { - Circle() - .fill(Color.gray.opacity(0.3)) - .frame(width: 120, height: 120) - .overlay( - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(1.2) - ) - } - - // MARK: - 网络头像 - private func networkAvatarView(url: URL) -> some View { - CachedAsyncImage(url: url.absoluteString) { image in - image - .resizable() - .aspectRatio(contentMode: .fill) - } placeholder: { - defaultAvatarView - } - .frame(width: 120, height: 120) - .clipShape(Circle()) - } - - // MARK: - 默认头像 - private var defaultAvatarView: some View { - Circle() - .fill(Color.gray.opacity(0.3)) - .frame(width: 120, height: 120) - .overlay( - Image(systemName: "person.fill") - .font(.system(size: 40)) - .foregroundColor(.white) - ) - } - - // MARK: - 相机按钮 - private func cameraButton(viewStore: ViewStoreOf) -> some View { - Button(action: { - showActionSheet = true - }) { - ZStack { - Circle().fill(Color.purple).frame(width: 36, height: 36) - Image(systemName: "camera.fill") - .foregroundColor(.white) - } - } - .offset(x: 8, y: 8) - } - - // MARK: - 昵称设置项 - private func nicknameSection(viewStore: ViewStoreOf) -> some View { - VStack(spacing: 0) { - HStack { - Text(NSLocalizedString("appSetting.nickname", comment: "Nickname")) - .foregroundColor(.white) - Spacer() - Text(viewStore.nickname) - .foregroundColor(.gray) - Image(systemName: "chevron.right") - .foregroundColor(.gray) - } - .padding(.horizontal, 32) - .padding(.vertical, 18) - .onTapGesture { - nicknameInput = viewStore.nickname - showNicknameAlert = true - } - - Divider().background(Color.gray.opacity(0.3)) - .padding(.horizontal, 32) - } - } - - // MARK: - 设置项区域 - private func settingsSection(viewStore: ViewStoreOf) -> some View { - VStack(spacing: 0) { - personalInfoPermissionsRow(viewStore: viewStore) - helpRow(viewStore: viewStore) - clearCacheRow(viewStore: viewStore) - checkUpdatesRow(viewStore: viewStore) - deactivateAccountRow(viewStore: viewStore) - aboutUsRow(viewStore: viewStore) - } - .background(Color.clear) - .padding(.horizontal, 0) - } - - // MARK: - 个人信息权限行 - private func personalInfoPermissionsRow(viewStore: ViewStoreOf) -> some View { - settingRow( - title: NSLocalizedString("appSetting.personalInfoPermissions", comment: "Personal Information and Permissions"), - action: { viewStore.send(.personalInfoPermissionsTapped) } - ) - } - - // MARK: - 帮助行 - private func helpRow(viewStore: ViewStoreOf) -> some View { - settingRow( - title: NSLocalizedString("appSetting.help", comment: "Help"), - action: { viewStore.send(.helpTapped) } - ) - } - - // MARK: - 清除缓存行 - private func clearCacheRow(viewStore: ViewStoreOf) -> some View { - settingRow( - title: NSLocalizedString("appSetting.clearCache", comment: "Clear Cache"), - action: { viewStore.send(.clearCacheTapped) } - ) - } - - // MARK: - 检查更新行 - private func checkUpdatesRow(viewStore: ViewStoreOf) -> some View { - settingRow( - title: NSLocalizedString("appSetting.checkUpdates", comment: "Check for Updates"), - action: { viewStore.send(.checkUpdatesTapped) } - ) - } - - // MARK: - 注销帐号行 - private func deactivateAccountRow(viewStore: ViewStoreOf) -> some View { - settingRow( - title: NSLocalizedString("appSetting.deactivateAccount", comment: "Deactivate Account"), - action: { viewStore.send(.deactivateAccountTapped) } - ) - } - - // MARK: - 关于我们行 - private func aboutUsRow(viewStore: ViewStoreOf) -> some View { - settingRow( - title: NSLocalizedString("appSetting.aboutUs", comment: "About Us"), - action: { viewStore.send(.aboutUsTapped) } - ) - } - - // MARK: - 设置项行 - private func settingRow(title: String, action: @escaping () -> Void) -> some View { - VStack(spacing: 0) { - HStack { - Text(title) - .foregroundColor(.white) - Spacer() - Image(systemName: "chevron.right") - .foregroundColor(.gray) - } - .padding(.horizontal, 32) - .padding(.vertical, 18) - .onTapGesture { - action() - } - - Divider().background(Color.gray.opacity(0.3)) - .padding(.horizontal, 32) - } - } - - // MARK: - 退出登录按钮 - private func logoutButton(viewStore: ViewStoreOf) -> some View { - Button(action: { - viewStore.send(.logoutTapped) - }) { - Text(NSLocalizedString("appSetting.logoutAccount", comment: "Log out of account")) - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding(.vertical, 18) - .background(Color.white.opacity(0.08)) - .cornerRadius(28) - .padding(.horizontal, 32) - } - .padding(.bottom, 32) - } - - // MARK: - 用户协议绑定 - private func userAgreementBinding(viewStore: ViewStoreOf) -> Binding { - viewStore.binding( - get: \.showUserAgreement, - send: AppSettingFeature.Action.userAgreementDismissed - ) - } - - // MARK: - 隐私政策绑定 - private func privacyPolicyBinding(viewStore: ViewStoreOf) -> Binding { - viewStore.binding( - get: \.showPrivacyPolicy, - send: AppSettingFeature.Action.privacyPolicyDismissed - ) - } - - // MARK: - 注销帐号绑定 - private func deactivateAccountBinding(viewStore: ViewStoreOf) -> Binding { - viewStore.binding( - get: \.showDeactivateAccount, - send: AppSettingFeature.Action.deactivateAccountDismissed - ) - } - - // MARK: - 昵称Alert内容 - @ViewBuilder - private func nicknameAlertContent(viewStore: ViewStoreOf) -> some View { - TextField("请输入昵称", text: $nicknameInput) - .onChange(of: nicknameInput) { newValue in - if newValue.count > 15 { - nicknameInput = String(newValue.prefix(15)) - } - } - Button(LocalizedString("app_settings.confirm", comment: "")) { - let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines) - if !trimmed.isEmpty && trimmed != viewStore.nickname { - viewStore.send(.nicknameEditConfirmed(trimmed)) - } - } - Button("取消", role: .cancel) {} - } - - // MARK: - 顶部栏 - private var topBar: some View { - HStack { - WithViewStore(store, observe: { $0 }) { viewStore in + private func avatarSection() -> some View { + WithPerceptionTracking { + VStack(spacing: 16) { + // 头像 Button(action: { - viewStore.send(.dismissTapped) + showImagePickerSheet = true }) { - Image(systemName: "chevron.left") + AsyncImage(url: URL(string: store.userInfo?.avatar ?? "")) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Image(systemName: "person.circle.fill") + .resizable() + .aspectRatio(contentMode: .fill) + .foregroundColor(.gray) + } + .frame(width: 80, height: 80) + .clipShape(Circle()) + .overlay( + Circle() + .stroke(Color.white.opacity(0.3), lineWidth: 2) + ) + } + + Text(LocalizedString("app_settings.tap_to_change_avatar", comment: "点击更换头像")) + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.7)) + } + .padding(.vertical, 20) + .frame(maxWidth: .infinity) + .background(Color.white.opacity(0.1)) + .cornerRadius(12) + } + } + + // MARK: - 个人信息设置区域 + @ViewBuilder + private func personalInfoSection() -> some View { + WithPerceptionTracking { + VStack(spacing: 0) { + // 昵称设置 + SettingRow( + icon: "person", + title: LocalizedString("app_settings.nickname", comment: "昵称"), + subtitle: store.userInfo?.nick ?? LocalizedString("app_settings.not_set", comment: "未设置"), + action: { + nicknameInput = store.userInfo?.nick ?? "" + showNicknameAlert = true + } + ) + + Divider() + .background(Color.white.opacity(0.2)) + .padding(.leading, 50) + + // 用户ID + SettingRow( + icon: "number", + title: LocalizedString("app_settings.user_id", comment: "用户ID"), + subtitle: "\(store.userInfo?.uid ?? 0)", + action: nil + ) + } + .background(Color.white.opacity(0.1)) + .cornerRadius(12) + } + } + + // MARK: - 其他设置区域 + @ViewBuilder + private func otherSettingsSection() -> some View { + WithPerceptionTracking { + VStack(spacing: 0) { + SettingRow( + icon: "hand.raised", + title: LocalizedString("app_settings.personal_info_permissions", comment: "个人信息权限"), + subtitle: LocalizedString("app_settings.manage_permissions", comment: "管理权限"), + action: { store.send(.personalInfoPermissionsTapped) } + ) + + Divider() + .background(Color.white.opacity(0.2)) + .padding(.leading, 50) + + SettingRow( + icon: "questionmark.circle", + title: LocalizedString("app_settings.help", comment: "帮助"), + subtitle: LocalizedString("app_settings.get_help", comment: "获取帮助"), + action: { store.send(.helpTapped) } + ) + + Divider() + .background(Color.white.opacity(0.2)) + .padding(.leading, 50) + + SettingRow( + icon: "trash", + title: LocalizedString("app_settings.clear_cache", comment: "清除缓存"), + subtitle: LocalizedString("app_settings.free_up_space", comment: "释放空间"), + action: { store.send(.clearCacheTapped) } + ) + + Divider() + .background(Color.white.opacity(0.2)) + .padding(.leading, 50) + + SettingRow( + icon: "arrow.clockwise", + title: LocalizedString("app_settings.check_updates", comment: "检查更新"), + subtitle: LocalizedString("app_settings.latest_version", comment: "最新版本"), + action: { store.send(.checkUpdatesTapped) } + ) + + Divider() + .background(Color.white.opacity(0.2)) + .padding(.leading, 50) + + SettingRow( + icon: "person.crop.circle.badge.minus", + title: LocalizedString("app_settings.deactivate_account", comment: "注销账号"), + subtitle: LocalizedString("app_settings.permanent_deletion", comment: "永久删除"), + action: { store.send(.deactivateAccountTapped) } + ) + + Divider() + .background(Color.white.opacity(0.2)) + .padding(.leading, 50) + + SettingRow( + icon: "info.circle", + title: LocalizedString("app_settings.about_us", comment: "关于我们"), + subtitle: LocalizedString("app_settings.app_info", comment: "应用信息"), + action: { store.send(.aboutUsTapped) } + ) + } + .background(Color.white.opacity(0.1)) + .cornerRadius(12) + } + } + + // MARK: - 退出登录区域 + @ViewBuilder + private func logoutSection() -> some View { + WithPerceptionTracking { + Button(action: { + store.send(.logoutTapped) + }) { + Text(LocalizedString("app_settings.logout", comment: "退出登录")) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background(Color.red.opacity(0.8)) + .cornerRadius(12) + } + } + } +} + +// MARK: - 设置行组件 +struct SettingRow: View { + let icon: String + let title: String + let subtitle: String + let action: (() -> Void)? + + var body: some View { + Button(action: { + action?() + }) { + HStack(spacing: 16) { + Image(systemName: icon) + .font(.system(size: 18)) + .foregroundColor(.white) + .frame(width: 24) + + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.system(size: 16)) .foregroundColor(.white) - .font(.system(size: 20, weight: .medium)) + + Text(subtitle) + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.7)) + } + + Spacer() + + if action != nil { + Image(systemName: "chevron.right") + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.5)) } } - - Spacer() - - Text(NSLocalizedString("appSetting.title", comment: "Settings")) - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(.white) - - Spacer() - - // 占位符,保持标题居中 - Color.clear - .frame(width: 20, height: 20) - } - .padding(.horizontal, 20) - .padding(.top, 8) - .padding(.bottom, 16) - } - - // MARK: - 图片处理 - private func loadAndProcessImage(item: PhotosPickerItem, completion: @escaping @Sendable (Data?) -> Void) { - item.loadTransferable(type: Data.self) { result in - guard let data = try? result.get(), let uiImage = UIImage(data: data) else { - completion(nil) - return - } - let square = cropToSquare(image: uiImage) - let resized = resizeImage(image: square, targetSize: CGSize(width: 180, height: 180)) - let jpegData = resized.jpegData(compressionQuality: 0.8) - completion(jpegData) + .padding(.horizontal, 16) + .padding(.vertical, 12) } - } -} - -// MARK: - 图片处理全局函数 -private func cropToSquare(image: UIImage) -> UIImage { - let size = min(image.size.width, image.size.height) - let x = (image.size.width - size) / 2 - let y = (image.size.height - size) / 2 - let cropRect = CGRect(x: x, y: y, width: size, height: size) - guard let cgImage = image.cgImage?.cropping(to: cropRect) else { return image } - return UIImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation) -} -private func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage { - let renderer = UIGraphicsImageRenderer(size: targetSize) - return renderer.image { _ in - image.draw(in: CGRect(origin: .zero, size: targetSize)) + .disabled(action == nil) } } diff --git a/yana/Views/EMailLoginView.swift b/yana/Views/EMailLoginView.swift index 561719e..ca632e5 100644 --- a/yana/Views/EMailLoginView.swift +++ b/yana/Views/EMailLoginView.swift @@ -5,37 +5,41 @@ import Combine struct EMailLoginView: View { let store: StoreOf let onBack: () -> Void - @Binding var showEmailLogin: Bool + @Binding var showEmailLogin: Bool // 新增:绑定父视图的显示状态 + // 使用本地@State管理UI状态 @State private var email: String = "" @State private var verificationCode: String = "" @State private var codeCountdown: Int = 0 - @State private var timerCancellable: AnyCancellable? @FocusState private var focusedField: Field? + // 倒计时定时器 + @State private var timerCancellable: AnyCancellable? + + // 计算属性 + private var isLoginButtonEnabled: Bool { + return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty + } + + private var getCodeButtonText: String { + if codeCountdown > 0 { + return "\(codeCountdown)s" + } else { + return LocalizedString("email_login.get_code", comment: "") + } + } + + private var isCodeButtonEnabled: Bool { + return !store.isCodeLoading && codeCountdown == 0 && !email.isEmpty + } + enum Field { case email case verificationCode } - private var isLoginButtonEnabled: Bool { - return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty - } - private var getCodeButtonText: String { - if store.isCodeLoading { - return "" - } else if codeCountdown > 0 { - return "\(codeCountdown)S" - } else { - return LocalizedString("email_login.get_code", comment: "") - } - } - private var isCodeButtonEnabled: Bool { - return !store.isCodeLoading && codeCountdown == 0 - } - var body: some View { - WithViewStore(store, observe: { $0.loginStep }) { viewStore in + WithPerceptionTracking { LoginContentView( store: store, onBack: onBack, @@ -47,7 +51,7 @@ struct EMailLoginView: View { getCodeButtonText: getCodeButtonText, isCodeButtonEnabled: isCodeButtonEnabled ) - .onChange(of: viewStore.state) { newStep in + .onChange(of: store.loginStep) { newStep in debugInfoSync("🔄 EMailLoginView: loginStep 变化为 \(newStep)") if newStep == .completed { debugInfoSync("✅ EMailLoginView: 登录成功,准备关闭自身") @@ -151,115 +155,112 @@ private struct LoginContentView: View { .font(.system(size: 28, weight: .medium)) .foregroundColor(.white) .padding(.bottom, 80) - VStack(spacing: 24) { - ZStack { - RoundedRectangle(cornerRadius: 25) - .fill(Color.white.opacity(0.1)) - .overlay( - RoundedRectangle(cornerRadius: 25) - .stroke(Color.white.opacity(0.3), lineWidth: 1) - ) - .frame(height: 56) + + VStack(spacing: 20) { + // 邮箱输入框 + VStack(alignment: .leading, spacing: 8) { + HStack { + Image("email icon") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text(LocalizedString("email_login.email", comment: "")) + .font(.system(size: 16)) + .foregroundColor(.white) + } + TextField("", text: $email) - .placeholder(when: email.isEmpty) { - Text(NSLocalizedString("placeholder.enter_email", comment: "")) - .foregroundColor(.white.opacity(0.6)) - } - .foregroundColor(.white) + .textFieldStyle(PlainTextFieldStyle()) .font(.system(size: 16)) - .padding(.horizontal, 24) - .keyboardType(.emailAddress) + .foregroundColor(.white) + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(Color.white.opacity(0.1)) + .cornerRadius(8) .focused($focusedField, equals: .email) + .keyboardType(.emailAddress) + .autocapitalization(.none) } - ZStack { - RoundedRectangle(cornerRadius: 25) - .fill(Color.white.opacity(0.1)) - .overlay( - RoundedRectangle(cornerRadius: 25) - .stroke(Color.white.opacity(0.3), lineWidth: 1) - ) - .frame(height: 56) + + // 验证码输入框 + VStack(alignment: .leading, spacing: 8) { + HStack { + Image("id icon") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text(LocalizedString("email_login.verification_code", comment: "")) + .font(.system(size: 16)) + .foregroundColor(.white) + } + HStack { TextField("", text: $verificationCode) - .placeholder(when: verificationCode.isEmpty) { - Text(NSLocalizedString("placeholder.enter_code", comment: "")) - .foregroundColor(.white.opacity(0.6)) - } - .foregroundColor(.white) + .textFieldStyle(PlainTextFieldStyle()) .font(.system(size: 16)) - .keyboardType(.numberPad) + .foregroundColor(.white) + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(Color.white.opacity(0.1)) + .cornerRadius(8) .focused($focusedField, equals: .verificationCode) + .keyboardType(.numberPad) + Button(action: { - store.send(.getVerificationCodeTapped) + store.send(.getVerificationCodeButtonTapped) }) { - ZStack { - if store.isCodeLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(0.7) - } else { - Text(getCodeButtonText) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.white) - } - } - .frame(width: 60, height: 36) - .background( - RoundedRectangle(cornerRadius: 18) - .fill(Color.white.opacity(isCodeButtonEnabled && !email.isEmpty ? 0.2 : 0.1)) - ) + Text(getCodeButtonText) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.white) + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(isCodeButtonEnabled ? Color.blue : Color.gray) + .cornerRadius(8) } - .disabled(!isCodeButtonEnabled || email.isEmpty || store.isCodeLoading) + .disabled(!isCodeButtonEnabled) } - .padding(.horizontal, 24) } - } - .padding(.horizontal, 32) - Spacer().frame(height: 60) - Button(action: { - store.send(.loginButtonTapped(email: email, verificationCode: verificationCode)) - }) { - ZStack { - LinearGradient( - colors: [ - Color(red: 0.85, green: 0.37, blue: 1.0), - Color(red: 0.54, green: 0.31, blue: 1.0) - ], - startPoint: .leading, - endPoint: .trailing - ) - .clipShape(RoundedRectangle(cornerRadius: 28)) - HStack { - if store.isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(0.8) - } - Text(store.isLoading ? LocalizedString("email_login.logging_in", comment: "") : LocalizedString("email_login.login_button", comment: "")) - .font(.system(size: 18, weight: .semibold)) + + // 登录按钮 + Button(action: { + store.send(.loginButtonTapped) + }) { + if store.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(1.2) + } else { + Text(LocalizedString("email_login.login", comment: "")) + .font(.system(size: 16, weight: .medium)) .foregroundColor(.white) } } - .frame(height: 56) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background(isLoginButtonEnabled ? Color.blue : Color.gray) + .cornerRadius(8) + .disabled(!isLoginButtonEnabled) + .padding(.top, 20) + + // 错误信息显示 + if let errorMessage = store.errorMessage { + Text(errorMessage) + .font(.system(size: 14)) + .foregroundColor(.red) + .padding(.top, 16) + .padding(.horizontal, 32) + } + + Spacer() } - .disabled(store.isLoading || email.isEmpty || verificationCode.isEmpty) - .opacity(isLoginButtonEnabled ? 1.0 : 0.5) - .padding(.horizontal, 32) - if let errorMessage = store.errorMessage { - Text(errorMessage) - .font(.system(size: 14)) - .foregroundColor(.red) - .padding(.top, 16) - .padding(.horizontal, 32) - } - Spacer() + + // 添加API Loading和错误处理视图 + APILoadingEffectView() } - - // 添加API Loading和错误处理视图 - APILoadingEffectView() } } } + .navigationBarHidden(true) } } diff --git a/yana/Views/EditFeedView.swift b/yana/Views/EditFeedView.swift index 0b6bc62..bccf311 100644 --- a/yana/Views/EditFeedView.swift +++ b/yana/Views/EditFeedView.swift @@ -14,14 +14,14 @@ struct EditFeedView: View { } var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in + WithPerceptionTracking { GeometryReader { geometry in ZStack { backgroundView - mainContent(geometry: geometry, viewStore: viewStore) - if viewStore.isUploadingImages { - uploadingImagesOverlay(progress: viewStore.imageUploadProgress, viewStore: viewStore) - } else if viewStore.isLoading { + mainContent(geometry: geometry) + if store.isUploadingImages { + uploadingImagesOverlay(progress: store.imageUploadProgress) + } else if store.isLoading { loadingOverlay } } @@ -41,180 +41,271 @@ struct EditFeedView: View { isKeyboardVisible = false } } - .onChange(of: viewStore.errorMessage) { error in - if error != nil { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - viewStore.send(.clearError) - } + } + .navigationBarHidden(true) + .onAppear { + store.send(.clearError) + } + .onChange(of: store.shouldDismiss) { shouldDismiss in + if shouldDismiss { + onDismiss() + } + } + .photosPicker( + isPresented: store.binding( + get: \.showPhotosPicker, + send: { _ in .photosPickerDismissed } + ), + selection: store.binding( + get: \.selectedPhotoItems, + send: { .photosPickerItemsChanged($0) } + ), + maxSelectionCount: 9, + matching: .images + ) + .onChange(of: store.selectedPhotoItems) { items in + store.send(.photosPickerItemsChanged(items)) + } + .onChange(of: store.selectedImages) { images in + // 处理图片选择变化 + } + .onChange(of: store.content) { content in + // 处理内容变化 + } + .alert("删除图片", isPresented: store.binding( + get: \.showDeleteImageAlert, + send: { _ in .deleteImageAlertDismissed } + )) { + Button("删除", role: .destructive) { + if let indexToDelete = store.imageToDeleteIndex { + store.send(.removeImage(indexToDelete)) } } - .onChange(of: viewStore.shouldDismiss) { shouldDismiss in - if shouldDismiss { - onDismiss() - NotificationCenter.default.post(name: Notification.Name("reloadFeedList"), object: nil) - viewStore.send(.clearDismissFlag) - } + Button("取消", role: .cancel) { + store.send(.deleteImageAlertDismissed) } + } message: { + Text("确定要删除这张图片吗?") } } } private var backgroundView: some View { - Color(hexString: "0C0527") + Color(hex: 0x0C0527) .ignoresSafeArea() } - private func mainContent(geometry: GeometryProxy, viewStore: ViewStoreOf) -> some View { - VStack(spacing: 0) { - headerView(geometry: geometry, viewStore: viewStore) - textInputArea(viewStore: viewStore) - // 新增:图片输入区域 - ModernImageSelectionGrid( - images: viewStore.processedImages, - selectedItems: viewStore.selectedImages, - canAddMore: viewStore.canAddMoreImages, - onItemsChanged: { items in - viewStore.send(.photosPickerItemsChanged(items)) - }, - onRemoveImage: { index in - viewStore.send(.removeImage(index)) + private func mainContent(geometry: GeometryProxy) -> some View { + WithPerceptionTracking { + VStack(spacing: 0) { + // 顶部导航栏 + topNavigationBar + + // 主要内容区域 + ScrollView { + VStack(spacing: 20) { + // 文本输入区域 + textInputSection + + // 图片选择区域 + imageSelectionSection + + // 发布按钮 + publishButton + } + .padding(.horizontal, 20) + .padding(.bottom, 40) } - ) - .padding(.horizontal, 24) - .padding(.bottom, 32) - Spacer() - if !isKeyboardVisible { - publishButtonBottom(viewStore: viewStore, geometry: geometry) } } } - private func headerView(geometry: GeometryProxy, viewStore: ViewStoreOf) -> some View { - HStack { - Text(LocalizedString("editFeed.title", comment: "Image & Text Edit")) - .font(.system(size: 20, weight: .semibold)) - .foregroundColor(.white) - Spacer() - if isKeyboardVisible { + private var topNavigationBar: some View { + WithPerceptionTracking { + HStack { Button(action: { - hideKeyboard() - viewStore.send(.publishButtonTapped) + store.send(.clearDismissFlag) + onDismiss() }) { - Text(LocalizedString("editFeed.publish", comment: "Publish")) - .font(.system(size: 16, weight: .semibold)) + Image(systemName: "xmark") + .font(.system(size: 20, weight: .medium)) .foregroundColor(.white) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background( - LinearGradient( - colors: [ - Color(hexString: "A14AC6"), - Color(hexString: "3B1EEB") - ], - startPoint: .leading, - endPoint: .trailing - ) - .cornerRadius(16) - ) + .frame(width: 44, height: 44) } - .disabled(!viewStore.canPublish) + + Spacer() + + Text("编辑动态") + .font(.system(size: 18, weight: .medium)) + .foregroundColor(.white) + + Spacer() + + // 占位,保持标题居中 + Color.clear + .frame(width: 44, height: 44) } + .padding(.horizontal, 16) + .padding(.top, 8) + .padding(.bottom, 16) } - .padding(.horizontal, 24) - .padding(.top, geometry.safeAreaInsets.top + 16) - .padding(.bottom, 24) } - private func textInputArea(viewStore: ViewStoreOf) -> some View { - ZStack(alignment: .topLeading) { - RoundedRectangle(cornerRadius: 20) - .fill(Color(hexString: "1C143A")) - TextEditor(text: Binding( - get: { viewStore.content }, - set: { viewStore.send(.contentChanged($0)) } - )) - .scrollContentBackground(.hidden) - .padding(16) - .frame(height: 160) - .foregroundColor(.white) - .background(.clear) - .cornerRadius(20) - .font(.system(size: 16)) - if viewStore.content.isEmpty { - Text(LocalizedString("editFeed.enterContent", comment: "Enter Content")) - .foregroundColor(Color.white.opacity(0.4)) - .padding(20) - .font(.system(size: 16)) - } - VStack { - Spacer() + private var textInputSection: some View { + WithPerceptionTracking { + VStack(alignment: .leading, spacing: 12) { + Text("分享你的想法...") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + + TextEditor(text: store.binding( + get: \.content, + send: { .contentChanged($0) } + )) + .font(.system(size: 16)) + .foregroundColor(.white) + .background(Color.clear) + .frame(minHeight: 120) + .padding(12) + .background(Color.white.opacity(0.1)) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.white.opacity(0.2), lineWidth: 1) + ) + HStack { Spacer() - Text("\(viewStore.content.count)/\(maxCount)") - .foregroundColor(Color.white.opacity(0.4)) - .font(.system(size: 14)) - .padding(.trailing, 16) - .padding(.bottom, 10) + Text("\(store.content.count)/\(maxCount)") + .font(.system(size: 12)) + .foregroundColor(.white.opacity(0.6)) } } } - .frame(height: 160) - .padding(.horizontal, 24) - .padding(.bottom, 32) } - private func publishButtonBottom(viewStore: ViewStoreOf, geometry: GeometryProxy) -> some View { - VStack { - Spacer() - Button(action: { - hideKeyboard() - viewStore.send(.publishButtonTapped) - }) { - Text(LocalizedString("editFeed.publish", comment: "Publish")) - .font(.system(size: 18, weight: .semibold)) + private var imageSelectionSection: some View { + WithPerceptionTracking { + VStack(alignment: .leading, spacing: 12) { + Text("添加图片") + .font(.system(size: 16, weight: .medium)) .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 56) - .background( - LinearGradient( - colors: [Color(hexString: "A14AC6"), Color(hexString: "3B1EEB")], - startPoint: .leading, - endPoint: .trailing - ) - .cornerRadius(28) - ) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) { + // 已选择的图片 + ForEach(Array(store.selectedImages.enumerated()), id: \.offset) { index, image in + imageItem(image: image, index: index) + } + + // 添加图片按钮 + if store.selectedImages.count < 9 { + addImageButton + } + } + } + } + } + + private func imageItem(image: UIImage, index: Int) -> some View { + ZStack(alignment: .topTrailing) { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(height: 100) + .clipped() + .cornerRadius(8) + + Button(action: { + store.send(.showDeleteImageAlert(index)) + }) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 20)) + .foregroundColor(.white) + .background(Color.black.opacity(0.5)) + .clipShape(Circle()) + } + .padding(4) + } + } + + private var addImageButton: some View { + Button(action: { + store.send(.addImageButtonTapped) + }) { + VStack { + Image(systemName: "plus") + .font(.system(size: 24)) + .foregroundColor(.white.opacity(0.7)) + Text("添加") + .font(.system(size: 12)) + .foregroundColor(.white.opacity(0.7)) + } + .frame(height: 100) + .frame(maxWidth: .infinity) + .background(Color.white.opacity(0.1)) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.white.opacity(0.2), lineWidth: 1) + ) + } + } + + private var publishButton: some View { + WithPerceptionTracking { + Button(action: { + store.send(.publishButtonTapped) + }) { + if store.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(1.2) + } else { + Text("发布") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background(store.content.isEmpty ? Color.gray : Color.blue) + .cornerRadius(12) + .disabled(store.isLoading || store.content.isEmpty) + } + } + + private func uploadingImagesOverlay(progress: Double) -> some View { + WithPerceptionTracking { + ZStack { + Color.black.opacity(0.5) + .ignoresSafeArea() + + VStack(spacing: 16) { + ProgressView(value: progress) + .progressViewStyle(LinearProgressViewStyle(tint: .white)) + .frame(width: 200) + + Text("上传图片中... \(Int(progress * 100))%") + .font(.system(size: 16)) + .foregroundColor(.white) + } + .padding(24) + .background(Color.black.opacity(0.8)) + .cornerRadius(12) } - .padding(.horizontal, 24) - .padding(.bottom, geometry.safeAreaInsets.bottom + 24) - .disabled(!viewStore.canPublish || viewStore.isUploadingImages || viewStore.isLoading) - .opacity(viewStore.canPublish ? 1.0 : 0.5) } } private var loadingOverlay: some View { - Group { - Color.black.opacity(0.3) - .ignoresSafeArea() - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(1.5) - } - } - - // 新增:图片上传进度遮罩 - private func uploadingImagesOverlay(progress: Double, viewStore: ViewStoreOf) -> some View { - Group { - Color.black.opacity(0.3) - .ignoresSafeArea() - VStack { + WithPerceptionTracking { + ZStack { + Color.black.opacity(0.5) + .ignoresSafeArea() + ProgressView() - .scaleEffect(1.2) - Text(String(format: LocalizedString("edit_feed.uploading_progress", comment: ""), Int(progress * 100))) - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.8)) - .padding(.top, 8) + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(1.5) } - .frame(maxWidth: .infinity, maxHeight: .infinity) } } } diff --git a/yana/Views/FeedListView.swift b/yana/Views/FeedListView.swift index edc6599..05c9b19 100644 --- a/yana/Views/FeedListView.swift +++ b/yana/Views/FeedListView.swift @@ -172,35 +172,35 @@ struct FeedListContentView: View { @Binding var previewCurrentIndex: Int var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - if viewStore.isLoading { + WithPerceptionTracking { + if store.isLoading { LoadingView() - } else if let error = viewStore.error { + } else if let error = store.error { ErrorView(error: error) - } else if viewStore.moments.isEmpty { + } else if store.moments.isEmpty { EmptyView() } else { MomentsListView( - moments: viewStore.moments, - hasMore: viewStore.hasMore, - isLoadingMore: viewStore.isLoadingMore, + moments: store.moments, + hasMore: store.hasMore, + isLoadingMore: store.isLoadingMore, onImageTap: { images, tappedIndex in previewCurrentIndex = tappedIndex previewItem = PreviewItem(images: images, index: tappedIndex) }, onMomentTap: { moment in - viewStore.send(.showDetail(moment)) + store.send(.showDetail(moment)) }, onLikeTap: { dynamicId, uid, likedUid, worldId in - viewStore.send(.likeDynamic(dynamicId, uid, likedUid, worldId)) + store.send(.likeDynamic(dynamicId, uid, likedUid, worldId)) }, onLoadMore: { - viewStore.send(.loadMore) + store.send(.loadMore) }, onRefresh: { - viewStore.send(.reload) + store.send(.reload) }, - likeLoadingDynamicIds: viewStore.likeLoadingDynamicIds + likeLoadingDynamicIds: store.likeLoadingDynamicIds ) } } @@ -214,59 +214,56 @@ struct FeedListView: View { @State private var previewCurrentIndex: Int = 0 var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - WithPerceptionTracking { - GeometryReader { geometry in - ZStack { - // 背景 - BackgroundView() - - VStack(alignment: .center, spacing: 0) { - // 顶部栏 - TopBarView { - viewStore.send(.editFeedButtonTapped) - } - - // 其他内容 - Image("Volume") - .frame(width: 56, height: 41) - .padding(.top, 16) - - Text(LocalizedString("feedList.slogan", comment: "The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.")) - .font(.system(size: 16)) - .multilineTextAlignment(.leading) - .foregroundColor(.white.opacity(0.9)) - .padding(.horizontal, 30) - .padding(.bottom, 30) - - // 动态内容列表 - FeedListContentView( - store: store, - previewItem: $previewItem, - previewCurrentIndex: $previewCurrentIndex - ) - - Spacer() - } - .frame(maxWidth: .infinity, alignment: .top) - } + WithPerceptionTracking { + GeometryReader { geometry in + ZStack { + // 背景 + BackgroundView() - // 添加API Loading和错误处理视图 - APILoadingEffectView() + VStack(alignment: .center, spacing: 0) { + // 顶部栏 + TopBarView { + store.send(.editFeedButtonTapped) + } + + // 其他内容 + Image("Volume") + .frame(width: 56, height: 41) + .padding(.top, 16) + + Text(LocalizedString("feedList.slogan", comment: "The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.")) + .font(.system(size: 16)) + .multilineTextAlignment(.leading) + .foregroundColor(.white.opacity(0.9)) + .padding(.horizontal, 30) + .padding(.bottom, 30) + + // 动态内容列表 + FeedListContentView( + store: store, + previewItem: $previewItem, + previewCurrentIndex: $previewCurrentIndex + ) + + Spacer() + } } - .onAppear { - viewStore.send(.onAppear) - } - .onReceive(NotificationCenter.default.publisher(for: Notification.Name("reloadFeedList"))) { _ in - viewStore.send(.reload) - } - .sheet(isPresented: viewStore.binding( - get: \.isEditFeedPresented, - send: { $0 ? .editFeedButtonTapped : .editFeedDismissed } - )) { + } + .onAppear { + store.send(.onAppear) + } + .onRefresh { + store.send(.reload) + } + // 新增:编辑动态页面 + .sheet(isPresented: store.binding( + get: \.showEditFeed, + send: { _ in .editFeedDismissed } + )) { + WithPerceptionTracking { EditFeedView( onDismiss: { - viewStore.send(.editFeedDismissed) + store.send(.editFeedDismissed) }, store: Store( initialState: EditFeedFeature.State() @@ -275,32 +272,29 @@ struct FeedListView: View { } ) } - // 新增:图片预览弹窗 - .fullScreenCover(item: $previewItem) { item in - ImagePreviewPager( - images: item.images as [String], - currentIndex: $previewCurrentIndex - ) { - previewItem = nil - } + } + // 新增:详情页导航 + .navigationDestination(isPresented: store.binding( + get: \.showDetail, + send: { _ in .detailDismissed } + )) { + if let selectedMoment = store.selectedMoment { + DetailView( + store: Store( + initialState: DetailFeature.State(moment: selectedMoment) + ) { + DetailFeature() + }, + onDismiss: { + store.send(.detailDismissed) + } + ) } - // 新增:DetailView导航 - .navigationDestination(isPresented: viewStore.binding( - get: \.showDetail, - send: { _ in .detailDismissed } - )) { - if let selectedMoment = viewStore.selectedMoment { - DetailView( - store: Store( - initialState: DetailFeature.State(moment: selectedMoment) - ) { - DetailFeature() - }, - onDismiss: { - viewStore.send(.detailDismissed) - } - ) - } + } + // 新增:图片预览弹窗 + .fullScreenCover(item: $previewItem) { item in + ImagePreviewPager(images: item.images as [String], currentIndex: $previewCurrentIndex) { + previewItem = nil } } } diff --git a/yana/Views/IDLoginView.swift b/yana/Views/IDLoginView.swift index ad55dbe..c73d117 100644 --- a/yana/Views/IDLoginView.swift +++ b/yana/Views/IDLoginView.swift @@ -21,9 +21,8 @@ struct IDLoginView: View { } var body: some View { - WithViewStore(store, observe: { $0.loginStep }) { viewStore in + WithPerceptionTracking { GeometryReader { geometry in - WithPerceptionTracking { ZStack { // 背景图片 - 使用与登录页面相同的"bg" Image("bg") @@ -58,66 +57,69 @@ struct IDLoginView: View { .padding(.bottom, 80) // 输入框区域 - VStack(spacing: 24) { - // ID 输入框 - ZStack { - RoundedRectangle(cornerRadius: 25) - .fill(Color.white.opacity(0.1)) - .overlay( - RoundedRectangle(cornerRadius: 25) - .stroke(Color.white.opacity(0.3), lineWidth: 1) - ) - .frame(height: 56) - - TextField("", text: $userID) // 使用SwiftUI的绑定 - .placeholder(when: userID.isEmpty) { - Text(LocalizedString("placeholder.enter_id", comment: "")) - .foregroundColor(.white.opacity(0.6)) + VStack(spacing: 20) { + // 用户ID输入框 + VStack(alignment: .leading, spacing: 8) { + HStack { + Image("id icon") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text(LocalizedString("id_login.user_id", comment: "")) + .font(.system(size: 16)) + .foregroundColor(.white) } - .foregroundColor(.white) - .font(.system(size: 16)) - .padding(.horizontal, 24) - .keyboardType(.numberPad) + + TextField("", text: $userID) + .textFieldStyle(PlainTextFieldStyle()) + .font(.system(size: 16)) + .foregroundColor(.white) + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(Color.white.opacity(0.1)) + .cornerRadius(8) + .onChange(of: userID) { newValue in + store.send(.userIDChanged(newValue)) + } } // 密码输入框 - ZStack { - RoundedRectangle(cornerRadius: 25) - .fill(Color.white.opacity(0.1)) - .overlay( - RoundedRectangle(cornerRadius: 25) - .stroke(Color.white.opacity(0.3), lineWidth: 1) - ) - .frame(height: 56) + VStack(alignment: .leading, spacing: 8) { + HStack { + Image("email icon") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text(LocalizedString("id_login.password", comment: "")) + .font(.system(size: 16)) + .foregroundColor(.white) + } HStack { if isPasswordVisible { - TextField("", text: $password) // 使用SwiftUI的绑定 - .placeholder(when: password.isEmpty) { - Text(LocalizedString("placeholder.enter_password", comment: "")) - .foregroundColor(.white.opacity(0.6)) - } - .foregroundColor(.white) - .font(.system(size: 16)) + TextField("", text: $password) + .textFieldStyle(PlainTextFieldStyle()) } else { - SecureField("", text: $password) // 使用SwiftUI的绑定 - .placeholder(when: password.isEmpty) { - Text(LocalizedString("placeholder.enter_password", comment: "")) - .foregroundColor(.white.opacity(0.6)) - } - .foregroundColor(.white) - .font(.system(size: 16)) + SecureField("", text: $password) + .textFieldStyle(PlainTextFieldStyle()) } Button(action: { isPasswordVisible.toggle() }) { - Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill") - .foregroundColor(.white.opacity(0.6)) - .font(.system(size: 16)) + Image(systemName: isPasswordVisible ? "eye.slash" : "eye") + .foregroundColor(.white.opacity(0.7)) } } - .padding(.horizontal, 24) + .font(.system(size: 16)) + .foregroundColor(.white) + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(Color.white.opacity(0.1)) + .cornerRadius(8) + .onChange(of: password) { newValue in + store.send(.passwordChanged(newValue)) + } } // 忘记密码按钮 @@ -131,43 +133,29 @@ struct IDLoginView: View { .foregroundColor(.white.opacity(0.8)) } } - .padding(.horizontal, 8) // 登录按钮 Button(action: { - // 发送登录action时传递本地状态 - store.send(.loginButtonTapped(userID: userID, password: password)) + store.send(.loginButtonTapped) }) { - ZStack { - // 渐变背景 - LinearGradient( - colors: [ - Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF - Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF - ], - startPoint: .leading, - endPoint: .trailing - ) - .clipShape(RoundedRectangle(cornerRadius: 28)) - - HStack { - if store.isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(0.8) - } - Text(store.isLoading ? LocalizedString("id_login.logging_in", comment: "") : LocalizedString("id_login.login_button", comment: "")) - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(.white) - } + if store.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(1.2) + } else { + Text(LocalizedString("id_login.login", comment: "")) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) } - .frame(height: 56) } - .disabled(store.isLoading || userID.isEmpty || password.isEmpty) - .opacity(isLoginButtonEnabled ? 1.0 : 0.5) // 透明度50%当条件不满足时 - .padding(.horizontal, 32) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background(isLoginButtonEnabled ? Color.blue : Color.gray) + .cornerRadius(8) + .disabled(!isLoginButtonEnabled) + .padding(.top, 20) - // 错误信息 + // 错误信息显示 if let errorMessage = store.errorMessage { Text(errorMessage) .font(.system(size: 14)) @@ -183,7 +171,6 @@ struct IDLoginView: View { APILoadingEffectView() } } - } } .navigationBarHidden(true) // 使用与 LoginView 一致的 navigationDestination 方式 @@ -215,8 +202,7 @@ struct IDLoginView: View { #endif } } - // 新增:监听登录状态,成功后自动关闭自身 - .onChange(of: viewStore.state) { newStep in + .onChange(of: store.loginStep) { newStep in debugInfoSync("🔄 IDLoginView: loginStep 变化为 \(newStep)") if newStep == .completed { debugInfoSync("✅ IDLoginView: 登录成功,准备关闭自身") diff --git a/yana/Views/LoginView.swift b/yana/Views/LoginView.swift index 10a502d..9f49622 100644 --- a/yana/Views/LoginView.swift +++ b/yana/Views/LoginView.swift @@ -13,20 +13,21 @@ struct ImageHeightPreferenceKey: PreferenceKey { struct LoginView: View { let store: StoreOf let onLoginSuccess: () -> Void // 新增:登录成功回调 - @State private var topImageHeight: CGFloat = 120 // 默认值 -// @ObservedObject private var localizationManager = LocalizationManager.shared - @State private var showLanguageSettings = false - @State private var isAgreedToTerms = true - @State private var showUserAgreement = false - @State private var showPrivacyPolicy = false - @State private var showIDLogin = false // 使用SwiftUI的@State管理导航 - @State private var showEmailLogin = false // 新增:邮箱登录导航状态 + + // 使用本地@State管理UI状态 + @State private var showIDLogin: Bool = false + @State private var showEmailLogin: Bool = false + @State private var showLanguageSettings: Bool = false + @State private var showUserAgreement: Bool = false + @State private var showPrivacyPolicy: Bool = false + + // 计算属性 + private var topImageHeight: CGFloat = 200 // 默认值 var body: some View { - WithViewStore(self.store, observe: { $0.isAnyLoginCompleted }) { viewStore in + WithPerceptionTracking { NavigationStack { GeometryReader { geometry in - WithPerceptionTracking { ZStack { // 使用与 splash 相同的背景图片 Image("bg") @@ -55,64 +56,54 @@ struct LoginView: View { Spacer() } .padding(.top, max(0, topImageHeight - 100)) // top图片高度 - 140 - - // 语言切换按钮(右上角)- 仅在 Debug 环境下显示 - #if DEBUG - VStack { - HStack { - Spacer() - Button(action: { - showLanguageSettings = true - }) { - Image(systemName: "globe") - .frame(width: 40, height: 40) - .font(.system(size: 20)) - .foregroundColor(.white) - .background(Color.black.opacity(0.3)) - .clipShape(Circle()) - } - .padding(.trailing, 16) - } - Spacer() - } - #endif - - VStack(spacing: 24) { - // ID Login 按钮 - LoginButton( - iconName: "person.circle.fill", - iconColor: .green, - title: LocalizedString("login.id_login", comment: "") - ) { - showIDLogin = true // 直接设置SwiftUI状态 - } - // Email Login 按钮 - LoginButton( - iconName: "envelope.fill", - iconColor: .blue, - title: LocalizedString("login.email_login", comment: "") - ) { - showEmailLogin = true // 显示邮箱登录界面 - } - }.padding(.top, max(0, topImageHeight+140)) - } - .onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in - topImageHeight = imageHeight } - Spacer() - .frame(height: 120) - - // 用户协议组件 - UserAgreementView( - isAgreed: $isAgreedToTerms, - onUserServiceTapped: { - showUserAgreement = true - }, - onPrivacyPolicyTapped: { - showPrivacyPolicy = true + // 下半部分的登录按钮区域 + VStack(spacing: 20) { + // 登录按钮 + LoginButton( + title: LocalizedString("login.id_login", comment: ""), + icon: "person.circle", + action: { + showIDLogin = true + } + ) + + LoginButton( + title: LocalizedString("login.email_login", comment: ""), + icon: "envelope", + action: { + showEmailLogin = true + } + ) + + // 底部设置按钮 + HStack(spacing: 20) { + Button(action: { + showLanguageSettings = true + }) { + Text(LocalizedString("login.language", comment: "")) + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.8)) + } + + Button(action: { + showUserAgreement = true + }) { + Text(LocalizedString("login.user_agreement", comment: "")) + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.8)) + } + + Button(action: { + showPrivacyPolicy = true + }) { + Text(LocalizedString("login.privacy_policy", comment: "")) + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.8)) + } } - ) + } .padding(.horizontal, 28) .padding(.bottom, 140) } @@ -122,7 +113,6 @@ struct LoginView: View { // 移除旧的 NavigationLink,改用 navigationDestination } - } } .navigationBarHidden(true) // 新增:适配 iOS 16 的 navigationDestination @@ -171,20 +161,20 @@ struct LoginView: View { url: APIConfiguration.webURL(for: .privacyPolicy) ) // 新增:监听登录成功,调用回调 - .onChange(of: viewStore.state) { completed in + .onChange(of: store.isAnyLoginCompleted) { completed in if completed { onLoginSuccess() } } // 新增:监听showIDLogin关闭时,若已登录则跳转首页 .onChange(of: showIDLogin) { newValue in - if newValue == false && viewStore.state { + if newValue == false && store.isAnyLoginCompleted { onLoginSuccess() } } // 新增:监听showEmailLogin关闭时,若已登录则跳转首页 .onChange(of: showEmailLogin) { newValue in - if newValue == false && viewStore.state { + if newValue == false && store.isAnyLoginCompleted { onLoginSuccess() } } diff --git a/yana/Views/MainView.swift b/yana/Views/MainView.swift index 0db35d2..63b4ff3 100644 --- a/yana/Views/MainView.swift +++ b/yana/Views/MainView.swift @@ -6,15 +6,13 @@ struct MainView: View { var onLogout: (() -> Void)? = nil var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - WithPerceptionTracking { - InternalMainView(store: store) - .onChange(of: viewStore.isLoggedOut) { isLoggedOut in - if isLoggedOut { - onLogout?() - } + WithPerceptionTracking { + InternalMainView(store: store) + .onChange(of: store.isLoggedOut) { isLoggedOut in + if isLoggedOut { + onLogout?() } - } + } } } } @@ -27,26 +25,24 @@ struct InternalMainView: View { _path = State(initialValue: store.withState { $0.navigationPath }) } var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - WithPerceptionTracking { - NavigationStack(path: $path) { - GeometryReader { geometry in - contentView(geometry: geometry, viewStore: viewStore) - .navigationDestination(for: MainFeature.Destination.self) { destination in - DestinationView(destination: destination, store: self.store) + WithPerceptionTracking { + NavigationStack(path: $path) { + GeometryReader { geometry in + contentView(geometry: geometry) + .navigationDestination(for: MainFeature.Destination.self) { destination in + DestinationView(destination: destination, store: self.store) + } + .onChange(of: path) { newPath in + store.send(.navigationPathChanged(newPath)) + } + .onChange(of: store.navigationPath) { newPath in + if path != newPath { + path = newPath } - .onChange(of: path) { newPath in - viewStore.send(.navigationPathChanged(newPath)) - } - .onChange(of: viewStore.navigationPath) { newPath in - if path != newPath { - path = newPath - } - } - .onAppear { - viewStore.send(.onAppear) - } - } + } + .onAppear { + store.send(.onAppear) + } } } } @@ -74,7 +70,7 @@ struct InternalMainView: View { } } - private func contentView(geometry: GeometryProxy, viewStore: ViewStoreOf) -> some View { + private func contentView(geometry: GeometryProxy) -> some View { WithPerceptionTracking { ZStack { // 背景图片 @@ -87,7 +83,7 @@ struct InternalMainView: View { // 主内容 MainContentView( store: store, - selectedTab: viewStore.selectedTab + selectedTab: store.selectedTab ) .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.bottom, 80) // 为底部导航栏留出空间 @@ -95,7 +91,7 @@ struct InternalMainView: View { // 底部导航栏 - 固定在底部 VStack { Spacer() - BottomTabView(selectedTab: viewStore.binding( + BottomTabView(selectedTab: store.binding( get: { Tab(rawValue: $0.selectedTab.rawValue) ?? .feed }, send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) } )) diff --git a/yana/Views/MeView.swift b/yana/Views/MeView.swift index c40bcf6..307eff9 100644 --- a/yana/Views/MeView.swift +++ b/yana/Views/MeView.swift @@ -8,75 +8,73 @@ struct MeView: View { @State private var previewCurrentIndex: Int = 0 var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - WithPerceptionTracking { - GeometryReader { geometry in - ZStack { - // 背景图片 - Image("bg") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .clipped() - .ignoresSafeArea(.all) - - // 顶部栏,右上角设置按钮 - VStack { - HStack { - Spacer() - Button(action: { - viewStore.send(.settingButtonTapped) - }) { - Image(systemName: "gearshape") - .font(.system(size: 33, weight: .medium)) - .foregroundColor(.white) - } - .padding(.trailing, 16) - .padding(.top, 8) - } + WithPerceptionTracking { + GeometryReader { geometry in + ZStack { + // 背景图片 + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + .ignoresSafeArea(.all) + + // 顶部栏,右上角设置按钮 + VStack { + HStack { Spacer() - } - - // 主要内容区域 - VStack(spacing: 16) { - // 用户信息区域 - userInfoSection(viewStore: viewStore) - - // 动态内容区域 - momentsSection(viewStore: viewStore) - - Spacer() - } - .frame(maxWidth: .infinity, alignment: .top) - .padding(.top, 8) - } - } - .onAppear { - viewStore.send(.onAppear) - } - // 新增:图片预览弹窗 - .fullScreenCover(item: $previewItem) { item in - ImagePreviewPager(images: item.images as [String], currentIndex: $previewCurrentIndex) { - previewItem = nil - } - } - // 新增:详情页导航 - .navigationDestination(isPresented: viewStore.binding( - get: \.showDetail, - send: { _ in .detailDismissed } - )) { - if let selectedMoment = viewStore.selectedMoment { - DetailView( - store: Store( - initialState: DetailFeature.State(moment: selectedMoment) - ) { - DetailFeature() - }, - onDismiss: { - viewStore.send(.detailDismissed) + Button(action: { + store.send(.settingButtonTapped) + }) { + Image(systemName: "gearshape") + .font(.system(size: 33, weight: .medium)) + .foregroundColor(.white) } - ) + .padding(.trailing, 16) + .padding(.top, 8) + } + Spacer() } + + // 主要内容区域 + VStack(spacing: 16) { + // 用户信息区域 + userInfoSection() + + // 动态内容区域 + momentsSection() + + Spacer() + } + .frame(maxWidth: .infinity, alignment: .top) + .padding(.top, 8) + } + } + .onAppear { + store.send(.onAppear) + } + // 新增:图片预览弹窗 + .fullScreenCover(item: $previewItem) { item in + ImagePreviewPager(images: item.images as [String], currentIndex: $previewCurrentIndex) { + previewItem = nil + } + } + // 新增:详情页导航 + .navigationDestination(isPresented: store.binding( + get: \.showDetail, + send: { _ in .detailDismissed } + )) { + if let selectedMoment = store.selectedMoment { + DetailView( + store: Store( + initialState: DetailFeature.State(moment: selectedMoment) + ) { + DetailFeature() + }, + onDismiss: { + store.send(.detailDismissed) + } + ) } } } @@ -84,106 +82,127 @@ struct MeView: View { // MARK: - 用户信息区域 @ViewBuilder - private func userInfoSection(viewStore: ViewStoreOf) -> some View { - if viewStore.isLoadingUserInfo { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .frame(height: 130) - } else if let error = viewStore.userInfoError { - Text(error) - .font(.system(size: 14)) - .foregroundColor(.red) - .frame(height: 130) - } else if let userInfo = viewStore.userInfo { - VStack(spacing: 8) { - if let avatarUrl = userInfo.avatar, !avatarUrl.isEmpty { - AsyncImage(url: URL(string: avatarUrl)) { image in - image.resizable().aspectRatio(contentMode: .fill).clipShape(Circle()) - } placeholder: { - Image(systemName: "person.fill").font(.system(size: 40)).foregroundColor(.white) - } - .frame(width: 90, height: 90) - } else { - Image(systemName: "person.fill").font(.system(size: 40)).foregroundColor(.white) - .frame(width: 90, height: 90) + private func userInfoSection() -> some View { + WithPerceptionTracking { + VStack(spacing: 16) { + // 头像 + AsyncImage(url: URL(string: store.userInfo?.avatar ?? "")) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Image(systemName: "person.circle.fill") + .resizable() + .aspectRatio(contentMode: .fill) + .foregroundColor(.gray) } - Text(userInfo.nick ?? LocalizedString("me.nickname", comment: "用户昵称")) - .font(.system(size: 18, weight: .medium)) + .frame(width: 80, height: 80) + .clipShape(Circle()) + .overlay( + Circle() + .stroke(Color.white.opacity(0.3), lineWidth: 2) + ) + + // 用户昵称 + Text(store.userInfo?.nick ?? "未知用户") + .font(.system(size: 20, weight: .medium)) .foregroundColor(.white) - Text(LocalizedString("me.id", comment: "ID: %@").localized(arguments: String(userInfo.uid ?? 0))) + + // 用户ID + Text("ID: \(store.userInfo?.uid ?? 0)") .font(.system(size: 14)) .foregroundColor(.white.opacity(0.7)) } - .padding(.top, 0) - .frame(height: 130) - } else { - Spacer().frame(height: 130) + .padding(.horizontal, 32) } } // MARK: - 动态内容区域 @ViewBuilder - private func momentsSection(viewStore: ViewStoreOf) -> some View { - if viewStore.isLoadingMoments && viewStore.moments.isEmpty { - ProgressView(LocalizedString("feed.loadingMore", comment: "加载中...")) + private func momentsSection() -> some View { + WithPerceptionTracking { + if store.isLoading { + VStack { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(1.2) + Text("加载中...") + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.7)) + .padding(.top, 8) + } .frame(maxWidth: .infinity, maxHeight: .infinity) - } else if let error = viewStore.momentsError { - VStack(spacing: 16) { - Image(systemName: "exclamationmark.triangle.fill") - .font(.system(size: 40)) - .foregroundColor(.yellow) - Text(error) - .foregroundColor(.red) - Button(LocalizedString("feed.retry", comment: "重试")) { - viewStore.send(.onAppear) - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } else if viewStore.moments.isEmpty { - VStack(spacing: 16) { - Image(systemName: "tray") - .font(.system(size: 40)) - .foregroundColor(.gray) - Text(LocalizedString("feed.empty", comment: "暂无动态")) - .foregroundColor(.white.opacity(0.8)) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } else { - ScrollView { - WithPerceptionTracking { - LazyVStack(spacing: 12) { - ForEach(Array(viewStore.moments.enumerated()), id: \.element.dynamicId) { index, moment in - OptimizedDynamicCardView( - moment: moment, - allMoments: viewStore.moments, - currentIndex: index, - onImageTap: { images, tappedIndex in - previewCurrentIndex = tappedIndex - previewItem = PreviewItem(images: images, index: tappedIndex) - }, - onLikeTap: { _, _, _, _ in - // 暂时不处理点赞,后续可以添加点赞功能 - }, - onCardTap: { - viewStore.send(.showDetail(moment)) - } - ) - .padding(.horizontal, 12) - } - if viewStore.hasMore { - ProgressView() - .onAppear { - viewStore.send(.loadMore) - } - } - // 新增底部间距 - Color.clear.frame(height: 120) + } else if let error = store.error { + VStack(spacing: 12) { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 32)) + .foregroundColor(.orange) + Text("加载失败") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + Text(error) + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.7)) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + Button("重试") { + store.send(.onAppear) } - .padding(.top, 8) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.white) + .padding(.horizontal, 24) + .padding(.vertical, 8) + .background(Color.blue.opacity(0.8)) + .cornerRadius(8) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if store.moments.isEmpty { + VStack(spacing: 12) { + Image(systemName: "doc.text") + .font(.system(size: 32)) + .foregroundColor(.white.opacity(0.5)) + Text("暂无动态") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white.opacity(0.7)) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + ScrollView { + WithPerceptionTracking { + LazyVStack(spacing: 12) { + ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in + OptimizedDynamicCardView( + moment: moment, + allMoments: store.moments, + currentIndex: index, + onImageTap: { images, tappedIndex in + previewCurrentIndex = tappedIndex + previewItem = PreviewItem(images: images, index: tappedIndex) + }, + onLikeTap: { _, _, _, _ in + // 暂时不处理点赞,后续可以添加点赞功能 + }, + onCardTap: { + store.send(.showDetail(moment)) + } + ) + .padding(.horizontal, 12) + } + if store.hasMore { + ProgressView() + .onAppear { + store.send(.loadMore) + } + } + // 新增底部间距 + Color.clear.frame(height: 120) + } + .padding(.top, 8) + } + } + .refreshable { + store.send(.refresh) } - } - .refreshable { - viewStore.send(.refresh) } } }