feat: 新增图片源选择功能以增强头像设置体验
- 添加AppImageSource枚举以定义图片源类型(相机和相册)。 - 在AppSettingFeature中新增状态和Action以管理图片源选择。 - 更新AppSettingView以支持图片源选择的ActionSheet和头像选择逻辑。 - 优化ImagePickerWithPreviewView以处理相机和相册选择的取消操作。
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -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<ImagePickerWithPreviewReducer>
|
||||
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<ImagePickerWithPreviewReducer>
|
||||
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()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user