feat: 新增用户信息更新功能
- 在APIEndpoints中新增用户信息更新端点。 - 实现UpdateUserRequest和UpdateUserResponse结构体,支持用户信息更新请求和响应。 - 在APIService中添加updateUser方法,处理用户信息更新请求。 - 更新AppSettingFeature以支持头像和昵称的修改,整合用户信息更新逻辑。 - 在AppSettingView中实现头像选择和昵称编辑功能,提升用户体验。
This commit is contained in:
@@ -1,31 +1,51 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
import PhotosUI
|
||||
|
||||
struct AppSettingView: View {
|
||||
let store: StoreOf<AppSettingFeature>
|
||||
@State private var showPhotoPicker = false
|
||||
@State private var selectedPhotoItem: PhotosPickerItem? = nil
|
||||
@State private var showNicknameAlert = false
|
||||
@State private var nicknameInput: String = ""
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
ZStack {
|
||||
Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea()
|
||||
// VStack(spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
// 主要内容
|
||||
VStack(spacing: 0) {
|
||||
// 头像区域
|
||||
avatarSection(viewStore: viewStore)
|
||||
|
||||
// 昵称设置项
|
||||
nicknameSection(viewStore: viewStore)
|
||||
|
||||
// 其他设置项
|
||||
settingsSection(viewStore: viewStore)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 底部大按钮
|
||||
logoutButton(viewStore: viewStore)
|
||||
}
|
||||
// }
|
||||
}
|
||||
// Loading & 错误提示
|
||||
if viewStore.isUploadingAvatar || viewStore.isUpdatingUser {
|
||||
Color.black.opacity(0.3).ignoresSafeArea()
|
||||
ProgressView("正在上传/更新...")
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.foregroundColor(.white)
|
||||
.scaleEffect(1.2)
|
||||
}
|
||||
if let error = viewStore.avatarUploadError ?? viewStore.updateUserError {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(error)
|
||||
.foregroundColor(.red)
|
||||
.padding()
|
||||
.background(Color.white.opacity(0.9))
|
||||
.cornerRadius(10)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("appSetting.title", comment: "Edit"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -51,6 +71,35 @@ struct AppSettingView: View {
|
||||
isPresented: privacyPolicyBinding(viewStore: viewStore),
|
||||
url: APIConfiguration.webURL(for: .privacyPolicy)
|
||||
)
|
||||
// 头像选择器
|
||||
.photosPicker(isPresented: $showPhotoPicker, selection: $selectedPhotoItem, matching: .images, photoLibrary: .shared())
|
||||
.onChange(of: selectedPhotoItem) { newItem in
|
||||
if let item = newItem {
|
||||
loadAndProcessImage(item: item) { data in
|
||||
if let data = data {
|
||||
viewStore.send(.avatarSelected(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 昵称修改Alert
|
||||
.alert("修改昵称", isPresented: $showNicknameAlert, actions: {
|
||||
TextField("请输入昵称", text: $nicknameInput)
|
||||
.onChange(of: nicknameInput) { newValue in
|
||||
if newValue.count > 15 {
|
||||
nicknameInput = String(newValue.prefix(15))
|
||||
}
|
||||
}
|
||||
Button("确定") {
|
||||
let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty && trimmed != viewStore.nickname {
|
||||
viewStore.send(.nicknameEditConfirmed(trimmed))
|
||||
}
|
||||
}
|
||||
Button("取消", role: .cancel) {}
|
||||
}, message: {
|
||||
Text("昵称最长15个字符")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +109,13 @@ struct AppSettingView: View {
|
||||
private func avatarSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
avatarImageView(viewStore: viewStore)
|
||||
.onTapGesture {
|
||||
showPhotoPicker = true
|
||||
}
|
||||
cameraButton
|
||||
.onTapGesture {
|
||||
showPhotoPicker = true
|
||||
}
|
||||
}
|
||||
.padding(.top, 24)
|
||||
}
|
||||
@@ -141,7 +196,8 @@ struct AppSettingView: View {
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.vertical, 18)
|
||||
.onTapGesture {
|
||||
viewStore.send(.editNicknameTapped)
|
||||
nicknameInput = viewStore.nickname
|
||||
showNicknameAlert = true
|
||||
}
|
||||
|
||||
Divider().background(Color.gray.opacity(0.3))
|
||||
@@ -255,4 +311,34 @@ struct AppSettingView: View {
|
||||
send: AppSettingFeature.Action.privacyPolicyDismissed
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 图片处理
|
||||
private func loadAndProcessImage(item: PhotosPickerItem, completion: @escaping (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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user