feat: 新增设置页面及相关功能实现

- 创建SettingPage视图,包含用户信息管理、头像设置、昵称编辑等功能。
- 实现SettingViewModel,处理设置页面的业务逻辑,包括头像上传、昵称更新等。
- 添加相机和相册选择功能,支持头像更换。
- 更新MainPage和MainViewModel,添加导航逻辑以支持设置页面的访问。
- 完善本地化支持,确保多语言兼容性。
- 新增相关测试建议,确保功能完整性和用户体验。
This commit is contained in:
edwinQQQ
2025-08-06 18:51:37 +08:00
parent 428aa95c5e
commit de4428e8a1
19 changed files with 1746 additions and 264 deletions

View File

@@ -0,0 +1,358 @@
import SwiftUI
import PhotosUI
import UIKit
// MARK: - Setting Page
struct SettingPage: View {
@StateObject private var viewModel = SettingViewModel()
let onBack: () -> Void
let onLogout: () -> Void
var body: some View {
GeometryReader { geometry in
ZStack {
//
Color(hex: 0x0C0527)
.ignoresSafeArea(.all)
VStack(spacing: 0) {
//
HStack {
Button(action: {
viewModel.onBackTapped()
}) {
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)
.onAppear {
viewModel.onBack = onBack
viewModel.onLogout = onLogout
viewModel.onAppear()
}
// ActionSheet
.confirmationDialog(
"请选择图片来源",
isPresented: $viewModel.showImageSourceActionSheet,
titleVisibility: .visible
) {
Button(LocalizedString("app_settings.take_photo", comment: "拍照")) {
viewModel.selectImageSource(.camera)
}
Button(LocalizedString("app_settings.select_from_album", comment: "从相册选择")) {
viewModel.selectImageSource(.photoLibrary)
}
Button(LocalizedString("common.cancel", comment: "取消"), role: .cancel) { }
}
//
.sheet(isPresented: $viewModel.showCamera) {
CameraPicker { image in
guard let image = image else {
return
}
viewModel.onCameraImagePicked(image)
}
}
//
.photosPicker(
isPresented: $viewModel.showPhotoPicker,
selection: $viewModel.selectedPhotoItems,
maxSelectionCount: 1,
matching: .images
)
//
.alert(LocalizedString("appSetting.nickname", comment: "编辑昵称"), isPresented: $viewModel.isEditingNickname) {
TextField(LocalizedString("appSetting.nickname", comment: "请输入昵称"), text: $viewModel.nicknameInput)
.onChange(of: viewModel.nicknameInput) { _, newValue in
viewModel.onNicknameInputChanged(newValue)
}
Button(LocalizedString("common.cancel", comment: "取消")) {
viewModel.isEditingNickname = false
}
Button(LocalizedString("common.confirm", comment: "确认")) {
viewModel.onNicknameEditConfirmed()
}
} message: {
Text(LocalizedString("appSetting.nickname", comment: "请输入新的昵称"))
}
//
.alert(LocalizedString("appSetting.logoutConfirmation.title", comment: "确认退出"), isPresented: $viewModel.showLogoutConfirmation) {
Button(LocalizedString("common.cancel", comment: "取消"), role: .cancel) {
viewModel.showLogoutConfirmation = false
}
Button(LocalizedString("appSetting.logoutConfirmation.confirm", comment: "确认退出"), role: .destructive) {
viewModel.onLogoutConfirmed()
viewModel.showLogoutConfirmation = false
}
} message: {
Text(LocalizedString("appSetting.logoutConfirmation.message", comment: "确定要退出当前账户吗?"))
}
//
.alert(LocalizedString("appSetting.aboutUs.title", comment: "关于我们"), isPresented: $viewModel.showAboutUs) {
Button(LocalizedString("common.ok", comment: "确定")) {
viewModel.showAboutUs = false
}
} message: {
VStack(alignment: .leading, spacing: 8) {
Text(LocalizedString("feedList.title", comment: "享受您的生活时光"))
.font(.headline)
Text(LocalizedString("feedList.slogan", comment: "疾病如同残酷的统治者,\n而时间是我们最宝贵的财富。\n我们活着的每一刻,都是对不可避免命运的胜利。"))
.font(.body)
}
}
// WebView
.webView(
isPresented: $viewModel.showPrivacyPolicy,
url: APIConfiguration.webURL(for: .privacyPolicy)
)
.onChange(of: viewModel.showPrivacyPolicy) { _, isPresented in
if !isPresented {
viewModel.onPrivacyPolicyDismissed()
}
}
.webView(
isPresented: $viewModel.showUserAgreement,
url: APIConfiguration.webURL(for: .userAgreement)
)
.onChange(of: viewModel.showUserAgreement) { _, isPresented in
if !isPresented {
viewModel.onUserAgreementDismissed()
}
}
.webView(
isPresented: $viewModel.showDeactivateAccount,
url: APIConfiguration.webURL(for: .deactivateAccount)
)
.onChange(of: viewModel.showDeactivateAccount) { _, isPresented in
if !isPresented {
viewModel.onDeactivateAccountDismissed()
}
}
}
// MARK: -
@ViewBuilder
private func avatarSection() -> some View {
VStack(spacing: 16) {
//
Button(action: {
viewModel.onAvatarTapped()
}) {
ZStack {
AsyncImage(url: URL(string: viewModel.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)
}
}
.disabled(viewModel.isUploadingAvatar || viewModel.isUpdatingUser)
//
if viewModel.isUploadingAvatar {
Text("正在上传头像...")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
if let error = viewModel.avatarUploadError {
Text(error)
.font(.system(size: 14))
.foregroundColor(.red)
}
}
}
// MARK: -
@ViewBuilder
private func personalInfoSection() -> some View {
VStack(spacing: 0) {
//
SettingRow(
title: LocalizedString("appSetting.nickname", comment: "昵称"),
subtitle: viewModel.userInfo?.nick ?? LocalizedString("app_settings.not_set", comment: "未设置"),
action: {
viewModel.onNicknameTapped()
}
)
.disabled(viewModel.isUpdatingUser)
//
if viewModel.isUpdatingUser {
HStack {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
Text("正在更新...")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
.padding(.top, 8)
}
if let error = viewModel.updateUserError {
Text(error)
.font(.system(size: 14))
.foregroundColor(.red)
.padding(.top, 8)
}
}
}
// MARK: -
@ViewBuilder
private func otherSettingsSection() -> some View {
VStack(spacing: 0) {
SettingRow(
title: LocalizedString("appSetting.personalInfoPermissions", comment: "个人信息与权限"),
subtitle: "",
action: { viewModel.onPersonalInfoPermissionsTapped() }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 16)
SettingRow(
title: LocalizedString("appSetting.help", comment: "帮助"),
subtitle: "",
action: { viewModel.onHelpTapped() }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 16)
SettingRow(
title: LocalizedString("appSetting.clearCache", comment: "清除缓存"),
subtitle: "",
action: { viewModel.onClearCacheTapped() }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 16)
SettingRow(
title: LocalizedString("appSetting.checkUpdates", comment: "检查更新"),
subtitle: "",
action: { viewModel.onCheckUpdatesTapped() }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 16)
SettingRow(
title: LocalizedString("appSetting.deactivateAccount", comment: "注销账号"),
subtitle: "",
action: { viewModel.onDeactivateAccountTapped() }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 16)
SettingRow(
title: LocalizedString("appSetting.aboutUs", comment: "关于我们"),
subtitle: "",
action: { viewModel.onAboutUsTapped() }
)
}
}
// MARK: - 退
@ViewBuilder
private func logoutSection() -> some View {
VStack(spacing: 12) {
// 退
Button(action: {
viewModel.onLogoutTapped()
}) {
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)
}
}
}
}
//#Preview {
// SettingPage(
// onBack: {},
// onLogout: {}
// )
//}