diff --git a/yana/APIs/APIEndpoints.swift b/yana/APIs/APIEndpoints.swift index ee911c1..1622a87 100644 --- a/yana/APIs/APIEndpoints.swift +++ b/yana/APIs/APIEndpoints.swift @@ -24,6 +24,7 @@ enum APIEndpoint: String, CaseIterable { case publishFeed = "/dynamic/square/publish" // 发布动态 case getUserInfo = "/user/get" // 新增:获取用户信息端点 case getMyDynamic = "/dynamic/getMyDynamic" + case updateUser = "/user/v2/update" // 新增:用户信息更新端点 // Web 页面路径 case userAgreement = "/modules/rule/protocol.html" diff --git a/yana/APIs/APIModels.swift b/yana/APIs/APIModels.swift index 71524bb..e4ddb2f 100644 --- a/yana/APIs/APIModels.swift +++ b/yana/APIs/APIModels.swift @@ -787,7 +787,7 @@ extension UserInfoManager { debugInfoSync("🔄 开始刷新当前用户信息") debugInfoSync(" 当前UID: \(currentUid)") - if let userInfo = await fetchUserInfoFromServer(uid: currentUid, apiService: apiService) { + if let _ = await fetchUserInfoFromServer(uid: currentUid, apiService: apiService) { debugInfoSync("✅ 用户信息刷新成功") return true } else { @@ -832,7 +832,7 @@ extension UserInfoManager { } // 检查是否已有用户信息缓存 - if let cachedUserInfo = await getUserInfo() { + if let _ = await getUserInfo() { debugInfoSync("📱 APP启动:使用现有用户信息缓存") 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? +} + diff --git a/yana/APIs/APIService.swift b/yana/APIs/APIService.swift index 11e0bd2..983c10c 100644 --- a/yana/APIs/APIService.swift +++ b/yana/APIs/APIService.swift @@ -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 /// 构建完整的请求 URL diff --git a/yana/APIs/LoginModels.swift b/yana/APIs/LoginModels.swift index 38421cf..2f03f0f 100644 --- a/yana/APIs/LoginModels.swift +++ b/yana/APIs/LoginModels.swift @@ -581,7 +581,7 @@ extension LoginHelper { let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26" guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else { - await debugErrorSync("❌ 邮箱DES加密失败") + debugErrorSync("❌ 邮箱DES加密失败") return nil } diff --git a/yana/Features/AppSettingFeature.swift b/yana/Features/AppSettingFeature.swift index 46199b8..a1347ef 100644 --- a/yana/Features/AppSettingFeature.swift +++ b/yana/Features/AppSettingFeature.swift @@ -14,6 +14,14 @@ struct AppSettingFeature { // WebView 导航状态 var showUserAgreement: 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 { @@ -35,89 +43,177 @@ struct AppSettingFeature { // WebView 关闭 case userAgreementDismissed case privacyPolicyDismissed + + // 头像/昵称修改 + case avatarTapped + case avatarSelected(Data) + case avatarUploadResult(Result) + case nicknameEditConfirmed(String) + case updateUser(Result) + case nicknameInputChanged(String) + case nicknameEditAlert(Bool) } @Dependency(\.apiService) var apiService - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .onAppear: - return .send(.loadUserInfo) + func reduce(into state: inout State, action: Action) -> Effect { + switch action { + case .onAppear: + return .send(.loadUserInfo) + + case .editNicknameTapped: + // 预留编辑昵称逻辑 + return .none + + case .logoutTapped: + // 清理所有认证信息,并向上层发送登出事件 + return .run { send in + await UserInfoManager.clearAllAuthenticationData() + // 向上层Feature传递登出事件(需在MainFeature处理) + // 这里直接返回.none,由MainFeature监听AppSettingFeature.Action.logoutTapped后处理 + } - case .editNicknameTapped: - // 预留编辑昵称逻辑 - return .none - - case .logoutTapped: - // 清理所有认证信息,并向上层发送登出事件 - return .run { send in - await UserInfoManager.clearAllAuthenticationData() - // 向上层Feature传递登出事件(需在MainFeature处理) - // 这里直接返回.none,由MainFeature监听AppSettingFeature.Action.logoutTapped后处理 + case .loadUserInfo: + state.isLoadingUserInfo = true + state.userInfoError = nil + return .run { send in + do { + if let userInfo = await UserInfoManager.getUserInfo() { + await send(.userInfoResponse(.success(userInfo))) + } else { + await send(.userInfoResponse(.failure(APIError.custom("用户信息不存在")))) + } + } catch { + let apiError: APIError + if let error = error as? APIError { + apiError = error + } else { + apiError = APIError.custom(error.localizedDescription) + } + await send(.userInfoResponse(.failure(apiError))) } - - case .loadUserInfo: - state.isLoadingUserInfo = true - state.userInfoError = nil + } + + case let .userInfoResponse(.success(userInfo)): + state.userInfo = userInfo + state.nickname = userInfo.nick ?? "" + state.avatarURL = userInfo.avatar + state.isLoadingUserInfo = false + return .none + + case let .userInfoResponse(.failure(error)): + state.userInfoError = error.localizedDescription + state.isLoadingUserInfo = false + return .none + + case .personalInfoPermissionsTapped: + state.showPrivacyPolicy = true + return .none + + case .helpTapped: + state.showUserAgreement = true + return .none + + case .clearCacheTapped: + // 预留清除缓存逻辑 + return .none + + case .checkUpdatesTapped: + // 预留检查更新逻辑 + return .none + + case .aboutUsTapped: + // 预留关于我们逻辑 + return .none + + case .userAgreementDismissed: + state.showUserAgreement = false + return .none + + case .privacyPolicyDismissed: + state.showPrivacyPolicy = false + 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 - do { - if let userInfo = await UserInfoManager.getUserInfo() { - await send(.userInfoResponse(.success(userInfo))) - } else { - await send(.userInfoResponse(.failure(APIError.custom("用户信息不存在")))) - } - } catch { - let apiError: APIError - if let error = error as? APIError { - apiError = error - } else { - apiError = APIError.custom(error.localizedDescription) - } - await send(.userInfoResponse(.failure(apiError))) + 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("获取最新用户信息失败")))) } } - - case let .userInfoResponse(.success(userInfo)): - state.userInfo = userInfo - state.nickname = userInfo.nick ?? "" - state.avatarURL = userInfo.avatar - state.isLoadingUserInfo = false - return .none - - case let .userInfoResponse(.failure(error)): - state.userInfoError = error.localizedDescription - state.isLoadingUserInfo = false - return .none - - case .personalInfoPermissionsTapped: - state.showPrivacyPolicy = true - return .none - - case .helpTapped: - state.showUserAgreement = true - return .none - - case .clearCacheTapped: - // 预留清除缓存逻辑 - return .none - - case .checkUpdatesTapped: - // 预留检查更新逻辑 - return .none - - case .aboutUsTapped: - // 预留关于我们逻辑 - return .none - - case .userAgreementDismissed: - state.showUserAgreement = false - return .none - - case .privacyPolicyDismissed: - state.showPrivacyPolicy = false - return .none } + state.isEditingNickname = false + return .none + case let .updateUser(.failure(error)): + state.isUpdatingUser = false + state.updateUserError = error.localizedDescription + return .none } } -} +} diff --git a/yana/Features/MainFeature.swift b/yana/Features/MainFeature.swift index 54ae296..af5c99c 100644 --- a/yana/Features/MainFeature.swift +++ b/yana/Features/MainFeature.swift @@ -2,11 +2,13 @@ import Foundation import ComposableArchitecture import CasePaths -struct MainFeature: Reducer { +@Reducer +struct MainFeature { enum Tab: Int, Equatable, CaseIterable { case feed, other } + @ObservableState struct State: Equatable { var selectedTab: Tab = .feed var feedList: FeedListFeature.State = .init() @@ -42,10 +44,10 @@ struct MainFeature: Reducer { } var body: some ReducerOf { - Scope(state: \.feedList, action: \.feedList) { + Scope(state: \ .feedList, action: \ .feedList) { FeedListFeature() } - Scope(state: \.me, action: \.me) { + Scope(state: \ .me, action: \ .me) { MeFeature() } Reduce { state, action in @@ -104,7 +106,7 @@ struct MainFeature: Reducer { } } // 设置页作用域 - .ifLet(\ .appSettingState, action: \.appSettingAction) { + .ifLet(\ .appSettingState, action: \ .appSettingAction) { AppSettingFeature() } } diff --git a/yana/Views/AppSettingView.swift b/yana/Views/AppSettingView.swift index 830fd29..7de2980 100644 --- a/yana/Views/AppSettingView.swift +++ b/yana/Views/AppSettingView.swift @@ -1,31 +1,51 @@ import SwiftUI import ComposableArchitecture +import PhotosUI struct AppSettingView: View { let store: StoreOf + @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) -> 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)) + } }