feat: 新增用户信息更新功能
- 在APIEndpoints中新增用户信息更新端点。 - 实现UpdateUserRequest和UpdateUserResponse结构体,支持用户信息更新请求和响应。 - 在APIService中添加updateUser方法,处理用户信息更新请求。 - 更新AppSettingFeature以支持头像和昵称的修改,整合用户信息更新逻辑。 - 在AppSettingView中实现头像选择和昵称编辑功能,提升用户体验。
This commit is contained in:
@@ -24,6 +24,7 @@ enum APIEndpoint: String, CaseIterable {
|
|||||||
case publishFeed = "/dynamic/square/publish" // 发布动态
|
case publishFeed = "/dynamic/square/publish" // 发布动态
|
||||||
case getUserInfo = "/user/get" // 新增:获取用户信息端点
|
case getUserInfo = "/user/get" // 新增:获取用户信息端点
|
||||||
case getMyDynamic = "/dynamic/getMyDynamic"
|
case getMyDynamic = "/dynamic/getMyDynamic"
|
||||||
|
case updateUser = "/user/v2/update" // 新增:用户信息更新端点
|
||||||
|
|
||||||
// Web 页面路径
|
// Web 页面路径
|
||||||
case userAgreement = "/modules/rule/protocol.html"
|
case userAgreement = "/modules/rule/protocol.html"
|
||||||
|
@@ -787,7 +787,7 @@ extension UserInfoManager {
|
|||||||
debugInfoSync("🔄 开始刷新当前用户信息")
|
debugInfoSync("🔄 开始刷新当前用户信息")
|
||||||
debugInfoSync(" 当前UID: \(currentUid)")
|
debugInfoSync(" 当前UID: \(currentUid)")
|
||||||
|
|
||||||
if let userInfo = await fetchUserInfoFromServer(uid: currentUid, apiService: apiService) {
|
if let _ = await fetchUserInfoFromServer(uid: currentUid, apiService: apiService) {
|
||||||
debugInfoSync("✅ 用户信息刷新成功")
|
debugInfoSync("✅ 用户信息刷新成功")
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
@@ -832,7 +832,7 @@ extension UserInfoManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否已有用户信息缓存
|
// 检查是否已有用户信息缓存
|
||||||
if let cachedUserInfo = await getUserInfo() {
|
if let _ = await getUserInfo() {
|
||||||
debugInfoSync("📱 APP启动:使用现有用户信息缓存")
|
debugInfoSync("📱 APP启动:使用现有用户信息缓存")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -843,3 +843,32 @@ extension UserInfoManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - 用户信息更新
|
||||||
|
struct UpdateUserRequest: APIRequestProtocol {
|
||||||
|
typealias Response = UpdateUserResponse
|
||||||
|
let avatar: String?
|
||||||
|
let nick: String?
|
||||||
|
let uid: Int
|
||||||
|
let ticket: String
|
||||||
|
|
||||||
|
var endpoint: String { APIEndpoint.updateUser.path }
|
||||||
|
var method: HTTPMethod { .POST }
|
||||||
|
// 参数全部通过queryParameters传递
|
||||||
|
var queryParameters: [String: String]? {
|
||||||
|
var params: [String: String] = [
|
||||||
|
"uid": String(uid),
|
||||||
|
"ticket": ticket
|
||||||
|
]
|
||||||
|
if let avatar = avatar { params["avatar"] = avatar }
|
||||||
|
if let nick = nick { params["nick"] = nick }
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
var bodyParameters: [String: Any]? { nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UpdateUserResponse: Codable, Equatable {
|
||||||
|
let code: Int
|
||||||
|
let message: String
|
||||||
|
let data: UserInfo?
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -222,6 +222,11 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - 用户信息更新
|
||||||
|
func updateUser(request: UpdateUserRequest) async throws -> UpdateUserResponse {
|
||||||
|
try await self.request(request)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private Helper Methods
|
// MARK: - Private Helper Methods
|
||||||
|
|
||||||
/// 构建完整的请求 URL
|
/// 构建完整的请求 URL
|
||||||
|
@@ -581,7 +581,7 @@ extension LoginHelper {
|
|||||||
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
|
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
|
||||||
|
|
||||||
guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else {
|
guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else {
|
||||||
await debugErrorSync("❌ 邮箱DES加密失败")
|
debugErrorSync("❌ 邮箱DES加密失败")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -14,6 +14,14 @@ struct AppSettingFeature {
|
|||||||
// WebView 导航状态
|
// WebView 导航状态
|
||||||
var showUserAgreement: Bool = false
|
var showUserAgreement: Bool = false
|
||||||
var showPrivacyPolicy: Bool = false
|
var showPrivacyPolicy: Bool = false
|
||||||
|
|
||||||
|
// 头像/昵称修改相关
|
||||||
|
var isUploadingAvatar: Bool = false
|
||||||
|
var avatarUploadError: String? = nil
|
||||||
|
var isEditingNickname: Bool = false
|
||||||
|
var nicknameInput: String = ""
|
||||||
|
var isUpdatingUser: Bool = false
|
||||||
|
var updateUserError: String? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action: Equatable {
|
enum Action: Equatable {
|
||||||
@@ -35,12 +43,20 @@ struct AppSettingFeature {
|
|||||||
// WebView 关闭
|
// WebView 关闭
|
||||||
case userAgreementDismissed
|
case userAgreementDismissed
|
||||||
case privacyPolicyDismissed
|
case privacyPolicyDismissed
|
||||||
|
|
||||||
|
// 头像/昵称修改
|
||||||
|
case avatarTapped
|
||||||
|
case avatarSelected(Data)
|
||||||
|
case avatarUploadResult(Result<String, APIError>)
|
||||||
|
case nicknameEditConfirmed(String)
|
||||||
|
case updateUser(Result<UpdateUserResponse, APIError>)
|
||||||
|
case nicknameInputChanged(String)
|
||||||
|
case nicknameEditAlert(Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dependency(\.apiService) var apiService
|
@Dependency(\.apiService) var apiService
|
||||||
|
|
||||||
var body: some ReducerOf<Self> {
|
func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
||||||
Reduce { state, action in
|
|
||||||
switch action {
|
switch action {
|
||||||
case .onAppear:
|
case .onAppear:
|
||||||
return .send(.loadUserInfo)
|
return .send(.loadUserInfo)
|
||||||
@@ -117,7 +133,87 @@ struct AppSettingFeature {
|
|||||||
case .privacyPolicyDismissed:
|
case .privacyPolicyDismissed:
|
||||||
state.showPrivacyPolicy = false
|
state.showPrivacyPolicy = false
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
|
case .avatarTapped:
|
||||||
|
// 触发头像选择器
|
||||||
|
return .none
|
||||||
|
case let .avatarSelected(imageData):
|
||||||
|
state.isUploadingAvatar = true
|
||||||
|
state.avatarUploadError = nil
|
||||||
|
return .run { [avatarData = imageData] send in
|
||||||
|
guard let uiImage = UIImage(data: avatarData) else {
|
||||||
|
await send(.avatarUploadResult(.failure(APIError.custom("图片格式错误"))))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 上传图片到腾讯云
|
||||||
|
if let url = await COSManager.shared.uploadUIImage(uiImage, apiService: apiService) {
|
||||||
|
await send(.avatarUploadResult(.success(url)))
|
||||||
|
} else {
|
||||||
|
await send(.avatarUploadResult(.failure(APIError.custom("头像上传失败"))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case let .avatarUploadResult(.success(url)):
|
||||||
|
state.isUploadingAvatar = false
|
||||||
|
// 调用 updateUser API,仅传 avatar
|
||||||
|
state.isUpdatingUser = true
|
||||||
|
state.updateUserError = nil
|
||||||
|
guard let userInfo = state.userInfo else { return .none }
|
||||||
|
return .run { send in
|
||||||
|
let ticket = await UserInfoManager.getCurrentUserTicket() ?? ""
|
||||||
|
let req = UpdateUserRequest(avatar: url, nick: nil, uid: userInfo.uid ?? 0, ticket: ticket)
|
||||||
|
do {
|
||||||
|
let resp: UpdateUserResponse = try await apiService.request(req)
|
||||||
|
await send(.updateUser(.success(resp)))
|
||||||
|
} catch {
|
||||||
|
let apiError = error as? APIError ?? APIError.custom(error.localizedDescription)
|
||||||
|
await send(.updateUser(.failure(apiError)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case let .avatarUploadResult(.failure(error)):
|
||||||
|
state.isUploadingAvatar = false
|
||||||
|
state.avatarUploadError = error.localizedDescription
|
||||||
|
return .none
|
||||||
|
case .nicknameEditAlert(let show):
|
||||||
|
state.isEditingNickname = show
|
||||||
|
state.nicknameInput = state.nickname
|
||||||
|
return .none
|
||||||
|
case .nicknameInputChanged(let text):
|
||||||
|
state.nicknameInput = String(text.prefix(15))
|
||||||
|
return .none
|
||||||
|
case .nicknameEditConfirmed(let newNick):
|
||||||
|
guard let userInfo = state.userInfo else { return .none }
|
||||||
|
state.isUpdatingUser = true
|
||||||
|
state.updateUserError = nil
|
||||||
|
return .run { send in
|
||||||
|
let ticket = await UserInfoManager.getCurrentUserTicket() ?? ""
|
||||||
|
let req = UpdateUserRequest(avatar: nil, nick: newNick, uid: userInfo.uid ?? 0, ticket: ticket)
|
||||||
|
do {
|
||||||
|
let resp: UpdateUserResponse = try await apiService.request(req)
|
||||||
|
await send(.updateUser(.success(resp)))
|
||||||
|
} catch {
|
||||||
|
let apiError = error as? APIError ?? APIError.custom(error.localizedDescription)
|
||||||
|
await send(.updateUser(.failure(apiError)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case let .updateUser(.success(resp)):
|
||||||
|
state.isUpdatingUser = false
|
||||||
|
// 不直接用 resp.data,触发拉取完整 userinfo,延迟1秒
|
||||||
|
if let uid = state.userInfo?.uid {
|
||||||
|
return .run { send in
|
||||||
|
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
if let newUser = await UserInfoManager.fetchUserInfoFromServer(uid: String(uid), apiService: apiService) {
|
||||||
|
await send(.userInfoResponse(.success(newUser)))
|
||||||
|
} else {
|
||||||
|
await send(.userInfoResponse(.failure(APIError.custom("获取最新用户信息失败"))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
state.isEditingNickname = false
|
||||||
|
return .none
|
||||||
|
case let .updateUser(.failure(error)):
|
||||||
|
state.isUpdatingUser = false
|
||||||
|
state.updateUserError = error.localizedDescription
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,11 +2,13 @@ import Foundation
|
|||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
import CasePaths
|
import CasePaths
|
||||||
|
|
||||||
struct MainFeature: Reducer {
|
@Reducer
|
||||||
|
struct MainFeature {
|
||||||
enum Tab: Int, Equatable, CaseIterable {
|
enum Tab: Int, Equatable, CaseIterable {
|
||||||
case feed, other
|
case feed, other
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ObservableState
|
||||||
struct State: Equatable {
|
struct State: Equatable {
|
||||||
var selectedTab: Tab = .feed
|
var selectedTab: Tab = .feed
|
||||||
var feedList: FeedListFeature.State = .init()
|
var feedList: FeedListFeature.State = .init()
|
||||||
|
@@ -1,31 +1,51 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
|
import PhotosUI
|
||||||
|
|
||||||
struct AppSettingView: View {
|
struct AppSettingView: View {
|
||||||
let store: StoreOf<AppSettingFeature>
|
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 {
|
var body: some View {
|
||||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||||
ZStack {
|
ZStack {
|
||||||
Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea()
|
Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea()
|
||||||
// VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// 主要内容
|
// 主要内容
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// 头像区域
|
// 头像区域
|
||||||
avatarSection(viewStore: viewStore)
|
avatarSection(viewStore: viewStore)
|
||||||
|
|
||||||
// 昵称设置项
|
// 昵称设置项
|
||||||
nicknameSection(viewStore: viewStore)
|
nicknameSection(viewStore: viewStore)
|
||||||
|
|
||||||
// 其他设置项
|
// 其他设置项
|
||||||
settingsSection(viewStore: viewStore)
|
settingsSection(viewStore: viewStore)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
// 底部大按钮
|
// 底部大按钮
|
||||||
logoutButton(viewStore: viewStore)
|
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"))
|
.navigationTitle(NSLocalizedString("appSetting.title", comment: "Edit"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
@@ -51,6 +71,35 @@ struct AppSettingView: View {
|
|||||||
isPresented: privacyPolicyBinding(viewStore: viewStore),
|
isPresented: privacyPolicyBinding(viewStore: viewStore),
|
||||||
url: APIConfiguration.webURL(for: .privacyPolicy)
|
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 {
|
private func avatarSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||||
ZStack(alignment: .bottomTrailing) {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
avatarImageView(viewStore: viewStore)
|
avatarImageView(viewStore: viewStore)
|
||||||
|
.onTapGesture {
|
||||||
|
showPhotoPicker = true
|
||||||
|
}
|
||||||
cameraButton
|
cameraButton
|
||||||
|
.onTapGesture {
|
||||||
|
showPhotoPicker = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.top, 24)
|
.padding(.top, 24)
|
||||||
}
|
}
|
||||||
@@ -141,7 +196,8 @@ struct AppSettingView: View {
|
|||||||
.padding(.horizontal, 32)
|
.padding(.horizontal, 32)
|
||||||
.padding(.vertical, 18)
|
.padding(.vertical, 18)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
viewStore.send(.editNicknameTapped)
|
nicknameInput = viewStore.nickname
|
||||||
|
showNicknameAlert = true
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider().background(Color.gray.opacity(0.3))
|
Divider().background(Color.gray.opacity(0.3))
|
||||||
@@ -255,4 +311,34 @@ struct AppSettingView: View {
|
|||||||
send: AppSettingFeature.Action.privacyPolicyDismissed
|
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