
- 创建SettingPage视图,包含用户信息管理、头像设置、昵称编辑等功能。 - 实现SettingViewModel,处理设置页面的业务逻辑,包括头像上传、昵称更新等。 - 添加相机和相册选择功能,支持头像更换。 - 更新MainPage和MainViewModel,添加导航逻辑以支持设置页面的访问。 - 完善本地化支持,确保多语言兼容性。 - 新增相关测试建议,确保功能完整性和用户体验。
415 lines
16 KiB
Swift
415 lines
16 KiB
Swift
//
|
||
// AppSettingView.swift
|
||
// yana
|
||
//
|
||
// Created by Edwin on 2024/11/20.
|
||
//
|
||
|
||
import SwiftUI
|
||
import ComposableArchitecture
|
||
import PhotosUI
|
||
|
||
struct AppSettingView: View {
|
||
let store: StoreOf<AppSettingFeature>
|
||
|
||
var body: some View {
|
||
WithPerceptionTracking {
|
||
mainView()
|
||
}
|
||
.onAppear {
|
||
store.send(.onAppear)
|
||
}
|
||
// 登出确认弹窗
|
||
.alert(LocalizedString("appSetting.logoutConfirmation.title", comment: "确认退出"), isPresented: Binding(
|
||
get: { store.showLogoutConfirmation },
|
||
set: { store.send(.showLogoutConfirmation($0)) }
|
||
)) {
|
||
Button(LocalizedString("common.cancel", comment: "取消"), role: .cancel) {
|
||
store.send(.showLogoutConfirmation(false))
|
||
}
|
||
Button(LocalizedString("appSetting.logoutConfirmation.confirm", comment: "确认退出"), role: .destructive) {
|
||
store.send(.logoutConfirmed)
|
||
store.send(.showLogoutConfirmation(false))
|
||
}
|
||
} message: {
|
||
Text(LocalizedString("appSetting.logoutConfirmation.message", comment: "确定要退出当前账户吗?"))
|
||
}
|
||
// 关于我们弹窗
|
||
.alert(LocalizedString("appSetting.aboutUs.title", comment: "关于我们"), isPresented: Binding(
|
||
get: { store.showAboutUs },
|
||
set: { store.send(.showAboutUs($0)) }
|
||
)) {
|
||
Button(LocalizedString("common.ok", comment: "确定")) {
|
||
store.send(.showAboutUs(false))
|
||
}
|
||
} message: {
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
Text(LocalizedString("feedList.title", comment: "享受您的生活时光"))
|
||
.font(.headline)
|
||
Text(LocalizedString("feedList.slogan", comment: "疾病如同残酷的统治者,\n而时间是我们最宝贵的财富。\n我们活着的每一刻,都是对不可避免命运的胜利。"))
|
||
.font(.body)
|
||
}
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private func mainView() -> some View {
|
||
WithPerceptionTracking {
|
||
let baseView = GeometryReader { geometry in
|
||
ZStack {
|
||
// 背景颜色
|
||
Color(hex: 0x0C0527)
|
||
.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)
|
||
}
|
||
|
||
Spacer()
|
||
|
||
Text(LocalizedString("appSetting.title", comment: "编辑"))
|
||
.font(.system(size: 18, weight: .medium))
|
||
.foregroundColor(.white)
|
||
|
||
Spacer()
|
||
|
||
// 占位,保持标题居中
|
||
Color.clear
|
||
.frame(width: 44, height: 44)
|
||
}
|
||
.padding(.horizontal, 16)
|
||
.padding(.top, 8)
|
||
|
||
// 主要内容区域
|
||
ScrollView {
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.navigationBarHidden(true)
|
||
|
||
let viewWithActionSheet = baseView
|
||
.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))
|
||
}
|
||
Button(LocalizedString("app_settings.select_from_album", comment: "从相册选择")) {
|
||
store.send(.selectImageSource(AppImageSource.photoLibrary))
|
||
}
|
||
Button(LocalizedString("common.cancel", comment: "取消"), role: .cancel) { }
|
||
}
|
||
|
||
let viewWithCamera = viewWithActionSheet
|
||
.sheet(isPresented: Binding(
|
||
get: { store.showCamera },
|
||
set: { store.send(.setShowCamera($0)) }
|
||
)) {
|
||
CameraPicker { image in
|
||
store.send(.cameraImagePicked(image))
|
||
}
|
||
}
|
||
|
||
let viewWithPhotoPicker = viewWithCamera
|
||
.photosPicker(
|
||
isPresented: Binding(
|
||
get: { store.showPhotoPicker },
|
||
set: { store.send(.setShowPhotoPicker($0)) }
|
||
),
|
||
selection: Binding(
|
||
get: { store.selectedPhotoItems },
|
||
set: { store.send(.photoPickerItemsChanged($0)) }
|
||
),
|
||
maxSelectionCount: 1,
|
||
matching: .images
|
||
)
|
||
|
||
let viewWithAlert = viewWithPhotoPicker
|
||
.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("common.confirm", comment: "确认")) {
|
||
let trimmed = store.nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
if !trimmed.isEmpty {
|
||
store.send(.nicknameEditConfirmed(trimmed))
|
||
}
|
||
store.send(.nicknameEditAlert(false))
|
||
}
|
||
} message: {
|
||
Text(LocalizedString("appSetting.nickname", comment: "请输入新的昵称"))
|
||
}
|
||
|
||
let viewWithPrivacyPolicy = viewWithAlert
|
||
.webView(
|
||
isPresented: Binding(
|
||
get: { store.showPrivacyPolicy },
|
||
set: { isPresented in
|
||
if !isPresented {
|
||
store.send(.privacyPolicyDismissed)
|
||
}
|
||
}
|
||
),
|
||
url: APIConfiguration.webURL(for: .privacyPolicy)
|
||
)
|
||
|
||
let viewWithUserAgreement = viewWithPrivacyPolicy
|
||
.webView(
|
||
isPresented: Binding(
|
||
get: { store.showUserAgreement },
|
||
set: { isPresented in
|
||
if !isPresented {
|
||
store.send(.userAgreementDismissed)
|
||
}
|
||
}
|
||
),
|
||
url: APIConfiguration.webURL(for: .userAgreement)
|
||
)
|
||
|
||
let viewWithDeactivateAccount = viewWithUserAgreement
|
||
.webView(
|
||
isPresented: Binding(
|
||
get: { store.showDeactivateAccount },
|
||
set: { isPresented in
|
||
if !isPresented {
|
||
store.send(.deactivateAccountDismissed)
|
||
}
|
||
}
|
||
),
|
||
url: APIConfiguration.webURL(for: .deactivateAccount)
|
||
)
|
||
|
||
viewWithDeactivateAccount
|
||
}
|
||
}
|
||
|
||
// MARK: - 头像设置区域
|
||
@ViewBuilder
|
||
private func avatarSection() -> some View {
|
||
WithPerceptionTracking {
|
||
VStack(spacing: 16) {
|
||
// 头像
|
||
Button(action: {
|
||
store.send(.setShowImageSourceActionSheet(true))
|
||
}) {
|
||
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: 120, height: 120)
|
||
.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: 120, height: 120)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 个人信息设置区域
|
||
@ViewBuilder
|
||
private func personalInfoSection() -> some View {
|
||
WithPerceptionTracking {
|
||
VStack(spacing: 0) {
|
||
// 昵称设置
|
||
SettingRow(
|
||
title: LocalizedString("appSetting.nickname", comment: "昵称"),
|
||
subtitle: store.userInfo?.nick ?? LocalizedString("app_settings.not_set", comment: "未设置"),
|
||
action: {
|
||
store.send(.nicknameEditAlert(true))
|
||
}
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 其他设置区域
|
||
@ViewBuilder
|
||
private func otherSettingsSection() -> some View {
|
||
WithPerceptionTracking {
|
||
VStack(spacing: 0) {
|
||
SettingRow(
|
||
title: LocalizedString("appSetting.personalInfoPermissions", comment: "个人信息与权限"),
|
||
subtitle: "",
|
||
action: { store.send(.personalInfoPermissionsTapped) }
|
||
)
|
||
|
||
Divider()
|
||
.background(Color.white.opacity(0.2))
|
||
.padding(.leading, 16)
|
||
|
||
SettingRow(
|
||
title: LocalizedString("appSetting.help", comment: "帮助"),
|
||
subtitle: "",
|
||
action: { store.send(.helpTapped) }
|
||
)
|
||
|
||
Divider()
|
||
.background(Color.white.opacity(0.2))
|
||
.padding(.leading, 16)
|
||
|
||
SettingRow(
|
||
title: LocalizedString("appSetting.clearCache", comment: "清除缓存"),
|
||
subtitle: "",
|
||
action: { store.send(.clearCacheTapped) }
|
||
)
|
||
|
||
Divider()
|
||
.background(Color.white.opacity(0.2))
|
||
.padding(.leading, 16)
|
||
|
||
SettingRow(
|
||
title: LocalizedString("appSetting.checkUpdates", comment: "检查更新"),
|
||
subtitle: "",
|
||
action: { store.send(.checkUpdatesTapped) }
|
||
)
|
||
|
||
Divider()
|
||
.background(Color.white.opacity(0.2))
|
||
.padding(.leading, 16)
|
||
|
||
SettingRow(
|
||
title: LocalizedString("appSetting.deactivateAccount", comment: "注销账号"),
|
||
subtitle: "",
|
||
action: { store.send(.deactivateAccountTapped) }
|
||
)
|
||
|
||
Divider()
|
||
.background(Color.white.opacity(0.2))
|
||
.padding(.leading, 16)
|
||
|
||
SettingRow(
|
||
title: LocalizedString("appSetting.aboutUs", comment: "关于我们"),
|
||
subtitle: "",
|
||
action: { store.send(.aboutUsTapped) }
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 退出登录区域
|
||
@ViewBuilder
|
||
private func logoutSection() -> some View {
|
||
WithPerceptionTracking {
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 设置行组件
|
||
//struct SettingRow: View {
|
||
// let title: String
|
||
// let subtitle: String
|
||
// let action: (() -> Void)?
|
||
//
|
||
// var body: some View {
|
||
// Button(action: {
|
||
// action?()
|
||
// }) {
|
||
// HStack(spacing: 16) {
|
||
// HStack {
|
||
// Text(title)
|
||
// .font(.system(size: 16))
|
||
// .foregroundColor(.white)
|
||
// .multilineTextAlignment(.leading)
|
||
//
|
||
// Spacer()
|
||
//
|
||
// if !subtitle.isEmpty {
|
||
// 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))
|
||
// }
|
||
// }
|
||
// .padding(.horizontal, 16)
|
||
// .padding(.vertical, 12)
|
||
// }
|
||
// .disabled(action == nil)
|
||
// }
|
||
//}
|