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 getUserInfo = "/user/get" // 新增:获取用户信息端点
|
||||
case getMyDynamic = "/dynamic/getMyDynamic"
|
||||
case updateUser = "/user/v2/update" // 新增:用户信息更新端点
|
||||
|
||||
// Web 页面路径
|
||||
case userAgreement = "/modules/rule/protocol.html"
|
||||
|
@@ -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?
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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<String, APIError>)
|
||||
case nicknameEditConfirmed(String)
|
||||
case updateUser(Result<UpdateUserResponse, APIError>)
|
||||
case nicknameInputChanged(String)
|
||||
case nicknameEditAlert(Bool)
|
||||
}
|
||||
|
||||
@Dependency(\.apiService) var apiService
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .onAppear:
|
||||
return .send(.loadUserInfo)
|
||||
func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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<Self> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@@ -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