diff --git a/yana/Features/AppSettingFeature.swift b/yana/Features/AppSettingFeature.swift index e1f9267..fc71d65 100644 --- a/yana/Features/AppSettingFeature.swift +++ b/yana/Features/AppSettingFeature.swift @@ -1,6 +1,12 @@ import Foundation import ComposableArchitecture +// 图片源选择枚举 +enum AppImageSource: Equatable { + case camera + case photoLibrary +} + @Reducer struct AppSettingFeature { @ObservableState @@ -37,6 +43,10 @@ struct AppSettingFeature { } // 新增:TCA驱动图片选择弹窗 var showImagePicker: Bool = false + // 新增:图片源选择 ActionSheet + var showImageSourceActionSheet: Bool = false + // 新增:选择的图片源 + var selectedImageSource: AppImageSource? = nil } enum Action: Equatable { @@ -73,6 +83,10 @@ struct AppSettingFeature { case testPushTapped // 新增:TCA驱动图片选择弹窗 case setShowImagePicker(Bool) + // 新增:图片源选择 ActionSheet + case setShowImageSourceActionSheet(Bool) + // 新增:图片源选择 + case selectImageSource(AppImageSource) } @Dependency(\.apiService) var apiService @@ -254,6 +268,15 @@ struct AppSettingFeature { case .setShowImagePicker(let show): state.showImagePicker = show return .none + case .setShowImageSourceActionSheet(let show): + state.showImageSourceActionSheet = show + return .none + case .selectImageSource(let source): + state.showImageSourceActionSheet = false + state.showImagePicker = true + state.selectedImageSource = source + // 这里可以传递选择的源到 ImagePickerWithPreviewView + return .none } } } diff --git a/yana/Views/AppSettingView.swift b/yana/Views/AppSettingView.swift index a151a09..397f9a3 100644 --- a/yana/Views/AppSettingView.swift +++ b/yana/Views/AppSettingView.swift @@ -16,24 +16,14 @@ struct AppSettingView: View { initialState: ImagePickerWithPreviewReducer.State(inner: ImagePickerWithPreviewState(selectionMode: .single)), reducer: { ImagePickerWithPreviewReducer() } ) - @State private var showNicknameAlert = false - @State private var nicknameInput = "" - @State private var showImagePickerSheet = false - @State private var showActionSheet = false - @State private var showPhotoPicker = false - @State private var showCamera = false - @State private var selectedPhotoItems: [PhotosPickerItem] = [] - @State private var selectedImages: [UIImage] = [] - @State private var cameraImage: UIImage? = nil - @State private var previewIndex: Int = 0 - @State private var showPreview = false - @State private var isLoading = false - @State private var errorMessage: String? = nil var body: some View { WithPerceptionTracking { mainView() } + .onAppear { + store.send(.onAppear) + } } @ViewBuilder @@ -63,7 +53,7 @@ struct AppSettingView: View { Spacer() - Text(LocalizedString("app_settings.title", comment: "设置")) + Text(LocalizedString("appSetting.title", comment: "编辑")) .font(.system(size: 18, weight: .medium)) .foregroundColor(.white) @@ -78,76 +68,92 @@ struct AppSettingView: View { // 主要内容区域 ScrollView { - VStack(spacing: 20) { + VStack(spacing: 0) { // 头像设置区域 avatarSection() + .padding(.top, 20) // 个人信息设置区域 personalInfoSection() + .padding(.top, 30) // 其他设置区域 otherSettingsSection() + .padding(.top, 20) + + Spacer(minLength: 40) // 退出登录按钮 logoutSection() + .padding(.bottom, 40) } .padding(.horizontal, 20) - .padding(.top, 20) - .padding(.bottom, 40) } } } } .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 - } - ) + // 图片源选择 ActionSheet + .confirmationDialog( + "请选择图片来源", + isPresented: Binding( + get: { store.showImageSourceActionSheet }, + set: { store.send(.setShowImageSourceActionSheet($0)) } + ), + titleVisibility: .visible + ) { + Button(LocalizedString("app_settings.take_photo", comment: "拍照")) { + store.send(.selectImageSource(AppImageSource.camera)) + // 直接触发相机 + pickerStore.send(.inner(.selectSource(.camera))) + } + Button(LocalizedString("app_settings.select_from_album", comment: "从相册选择")) { + store.send(.selectImageSource(AppImageSource.photoLibrary)) + // 直接触发相册 + pickerStore.send(.inner(.selectSource(.photoLibrary))) + } + Button(LocalizedString("common.cancel", comment: "取消"), role: .cancel) { } } - // 相机拍照 - .sheet(isPresented: $showCamera) { + // 头像选择器 + .sheet(isPresented: Binding( + get: { store.showImagePicker }, + set: { store.send(.setShowImagePicker($0)) } + )) { ImagePickerWithPreviewView( store: pickerStore, onUpload: { images in if let firstImage = images.first, let imageData = firstImage.jpegData(compressionQuality: 0.8) { - store.send(AppSettingFeature.Action.avatarSelected(imageData)) + store.send(.avatarSelected(imageData)) } - showCamera = false + store.send(.setShowImagePicker(false)) }, onCancel: { - showCamera = false + store.send(.setShowImagePicker(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 = "" + .alert(LocalizedString("appSetting.nickname", comment: "编辑昵称"), isPresented: Binding( + get: { store.isEditingNickname }, + set: { store.send(.nicknameEditAlert($0)) } + )) { + TextField(LocalizedString("appSetting.nickname", comment: "请输入昵称"), text: Binding( + get: { store.nicknameInput }, + set: { store.send(.nicknameInputChanged($0)) } + )) + Button(LocalizedString("common.cancel", comment: "取消")) { + store.send(.nicknameEditAlert(false)) } - Button(LocalizedString("app_settings.confirm", comment: "确认")) { - let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines) + Button(LocalizedString("common.confirm", comment: "确认")) { + let trimmed = store.nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines) if !trimmed.isEmpty { store.send(.nicknameEditConfirmed(trimmed)) } - showNicknameAlert = false - nicknameInput = "" + store.send(.nicknameEditAlert(false)) } } message: { - Text(LocalizedString("app_settings.nickname_tip", comment: "请输入新的昵称")) + Text(LocalizedString("appSetting.nickname", comment: "请输入新的昵称")) } } } @@ -159,34 +165,41 @@ struct AppSettingView: View { VStack(spacing: 16) { // 头像 Button(action: { - showImagePickerSheet = true + store.send(.setShowImageSourceActionSheet(true)) }) { - 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) + ZStack { + 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: 100, height: 100) + .clipShape(Circle()) + + // 相机图标覆盖 + VStack { + Spacer() + HStack { + Spacer() + Circle() + .fill(Color.purple) + .frame(width: 32, height: 32) + .overlay( + Image(systemName: "camera") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + ) + } + } + .frame(width: 100, height: 100) } - .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) } } @@ -198,28 +211,13 @@ struct AppSettingView: View { // 昵称设置 SettingRow( icon: "person", - title: LocalizedString("app_settings.nickname", comment: "昵称"), + title: LocalizedString("appSetting.nickname", comment: "昵称"), subtitle: store.userInfo?.nick ?? LocalizedString("app_settings.not_set", comment: "未设置"), action: { - nicknameInput = store.userInfo?.nick ?? "" - showNicknameAlert = true + store.send(.nicknameEditAlert(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) } } @@ -230,8 +228,8 @@ struct AppSettingView: View { VStack(spacing: 0) { SettingRow( icon: "hand.raised", - title: LocalizedString("app_settings.personal_info_permissions", comment: "个人信息权限"), - subtitle: LocalizedString("app_settings.manage_permissions", comment: "管理权限"), + title: LocalizedString("appSetting.personalInfoPermissions", comment: "个人信息与权限"), + subtitle: "", action: { store.send(.personalInfoPermissionsTapped) } ) @@ -241,8 +239,8 @@ struct AppSettingView: View { SettingRow( icon: "questionmark.circle", - title: LocalizedString("app_settings.help", comment: "帮助"), - subtitle: LocalizedString("app_settings.get_help", comment: "获取帮助"), + title: LocalizedString("appSetting.help", comment: "帮助"), + subtitle: "", action: { store.send(.helpTapped) } ) @@ -252,8 +250,8 @@ struct AppSettingView: View { SettingRow( icon: "trash", - title: LocalizedString("app_settings.clear_cache", comment: "清除缓存"), - subtitle: LocalizedString("app_settings.free_up_space", comment: "释放空间"), + title: LocalizedString("appSetting.clearCache", comment: "清除缓存"), + subtitle: "", action: { store.send(.clearCacheTapped) } ) @@ -263,8 +261,8 @@ struct AppSettingView: View { SettingRow( icon: "arrow.clockwise", - title: LocalizedString("app_settings.check_updates", comment: "检查更新"), - subtitle: LocalizedString("app_settings.latest_version", comment: "最新版本"), + title: LocalizedString("appSetting.checkUpdates", comment: "检查更新"), + subtitle: "", action: { store.send(.checkUpdatesTapped) } ) @@ -274,8 +272,8 @@ struct AppSettingView: View { SettingRow( icon: "person.crop.circle.badge.minus", - title: LocalizedString("app_settings.deactivate_account", comment: "注销账号"), - subtitle: LocalizedString("app_settings.permanent_deletion", comment: "永久删除"), + title: LocalizedString("appSetting.deactivateAccount", comment: "注销账号"), + subtitle: "", action: { store.send(.deactivateAccountTapped) } ) @@ -285,13 +283,11 @@ struct AppSettingView: View { SettingRow( icon: "info.circle", - title: LocalizedString("app_settings.about_us", comment: "关于我们"), - subtitle: LocalizedString("app_settings.app_info", comment: "应用信息"), + title: LocalizedString("appSetting.aboutUs", comment: "关于我们"), + subtitle: "", action: { store.send(.aboutUsTapped) } ) } - .background(Color.white.opacity(0.1)) - .cornerRadius(12) } } @@ -299,16 +295,19 @@ struct AppSettingView: View { @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) + VStack(spacing: 12) { + // 退出登录按钮 + Button(action: { + store.send(.logoutTapped) + }) { + Text(LocalizedString("appSetting.logoutAccount", comment: "退出账户")) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background(Color.red.opacity(0.8)) + .cornerRadius(12) + } } } } @@ -336,9 +335,11 @@ struct SettingRow: View { .font(.system(size: 16)) .foregroundColor(.white) - Text(subtitle) - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.7)) + if !subtitle.isEmpty { + Text(subtitle) + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.7)) + } } Spacer() diff --git a/yana/Views/Components/ImagePickerWithPreview/ImagePickerWithPreviewView.swift b/yana/Views/Components/ImagePickerWithPreview/ImagePickerWithPreviewView.swift index 8c10630..3783625 100644 --- a/yana/Views/Components/ImagePickerWithPreview/ImagePickerWithPreviewView.swift +++ b/yana/Views/Components/ImagePickerWithPreview/ImagePickerWithPreviewView.swift @@ -23,10 +23,10 @@ public struct ImagePickerWithPreviewView: View { Color.clear } .background(.clear) - .modifier(ActionSheetModifier(viewStore: viewStore, onCancel: onCancel)) - .modifier(CameraSheetModifier(viewStore: viewStore)) - .modifier(PhotosPickerModifier(viewStore: viewStore, loadedImages: $loadedImages, isLoadingImages: $isLoadingImages, loadingId: $loadingId)) - .modifier(PreviewCoverModifier(viewStore: viewStore, loadedImages: loadedImages, onUpload: onUpload, loadingId: $loadingId)) + .ignoresSafeArea() + .modifier(CameraSheetModifier(viewStore: viewStore, onCancel: onCancel)) + .modifier(PhotosPickerModifier(viewStore: viewStore, loadedImages: $loadedImages, isLoadingImages: $isLoadingImages, loadingId: $loadingId, onCancel: onCancel)) + .modifier(PreviewCoverModifier(viewStore: viewStore, loadedImages: loadedImages, onUpload: onUpload, loadingId: $loadingId, onCancel: onCancel)) .modifier(ErrorToastModifier(viewStore: viewStore)) .onChange(of: viewStore.inner.isLoading) { isLoading in if isLoading && loadingId == nil { @@ -40,34 +40,21 @@ public struct ImagePickerWithPreviewView: View { } } -private struct ActionSheetModifier: ViewModifier { - let viewStore: ViewStoreOf - let onCancel: () -> Void - func body(content: Content) -> some View { - content.confirmationDialog( - "请选择图片来源", - isPresented: .init( - get: { viewStore.inner.showActionSheet }, - set: { viewStore.send(.inner(.showActionSheet($0))) } - ), - titleVisibility: .visible - ) { - Button(LocalizedString("app_settings.take_photo", comment: "")) { viewStore.send(.inner(.selectSource(.camera))) } - Button(LocalizedString("app_settings.select_from_album", comment: "")) { viewStore.send(.inner(.selectSource(.photoLibrary))) } - Button("取消", role: .cancel) { onCancel() } - } - } -} - private struct CameraSheetModifier: ViewModifier { let viewStore: ViewStoreOf + let onCancel: () -> Void func body(content: Content) -> some View { content.sheet(isPresented: .init( get: { viewStore.inner.showCamera }, set: { viewStore.send(.inner(.setShowCamera($0))) } )) { CameraPicker { image in - viewStore.send(.inner(.cameraImagePicked(image))) + if let image = image { + viewStore.send(.inner(.cameraImagePicked(image))) + } else { + // 相机取消,关闭整个视图 + onCancel() + } } } } @@ -78,12 +65,20 @@ private struct PhotosPickerModifier: ViewModifier { @Binding var loadedImages: [UIImage] @Binding var isLoadingImages: Bool @Binding var loadingId: UUID? + let onCancel: () -> Void + func body(content: Content) -> some View { content .photosPicker( isPresented: .init( get: { viewStore.inner.showPhotoPicker }, - set: { viewStore.send(.inner(.setShowPhotoPicker($0))) } + set: { show in + viewStore.send(.inner(.setShowPhotoPicker(show))) + // 如果相册选择器被关闭且没有选择图片,则关闭整个视图 + if !show && viewStore.inner.selectedPhotoItems.isEmpty { + onCancel() + } + } ), selection: .init( get: { viewStore.inner.selectedPhotoItems }, @@ -131,6 +126,7 @@ private struct PreviewCoverModifier: ViewModifier { let loadedImages: [UIImage] let onUpload: ([UIImage]) -> Void @Binding var loadingId: UUID? + let onCancel: () -> Void func body(content: Content) -> some View { content.fullScreenCover(isPresented: .init( get: { viewStore.inner.showPreview }, @@ -148,6 +144,7 @@ private struct PreviewCoverModifier: ViewModifier { }, onCancel: { viewStore.send(.inner(.previewCancel)) + onCancel() } ) }