// // ProfileViewModel.swift // suixinkan // // Created by Codex on 2026/6/20. // import Foundation import Combine @MainActor /// 个人信息页 ViewModel,负责资料加载、编辑状态和提交更新。 final class ProfileViewModel: ObservableObject { @Published var userInfo: UserInfoResponse? @Published var realNameInfo: RealNameInfo? private var isLoading = false @Published var isSaving = false @Published var isEditingProfile = false @Published var editingNickname = "" @Published private(set) var pendingAvatarData: Data? @Published private(set) var pendingAvatarFileName: String? @Published private(set) var avatarUploadProgress: Int? /// 接口返回前使用的本地账号资料缓存,来自 AccountContext / AccountSnapshot。 private var cachedProfile: AccountProfile? var displayNickname: String { nonEmpty(userInfo?.nickname) ?? nonEmpty(cachedProfile?.displayName) ?? "未设置昵称" } var displayRealName: String { nonEmpty(userInfo?.realName) ?? "--" } var displayPhone: String { nonEmpty(userInfo?.phone) ?? nonEmpty(cachedProfile?.phone) ?? "--" } var displayAvatarURL: String { nonEmpty(userInfo?.avatar) ?? nonEmpty(cachedProfile?.avatarURL) ?? "" } var accountStatusText: String { nonEmpty(userInfo?.statusName) ?? "--" } var realNameStatusText: String { guard let realNameInfo else { return "点击去实名认证" } switch realNameInfo.auditStatus { case 2: return "已实名认证" case 3: return "审核不通过" default: return "审核中" } } /// 用本地缓存的账号资料预填充展示字段,避免冷启动或重新进入页面时先显示占位文案。 func applyCachedProfile(_ profile: AccountProfile?) { guard userInfo == nil else { return } cachedProfile = profile } /// 重新加载用户资料和实名认证状态。 func reload(api: ProfileAPI) async throws { guard !isLoading else { return } isLoading = true defer { isLoading = false } async let user = api.userInfo() async let realName = api.realNameInfo() let result = try await (user, realName) userInfo = result.0 realNameInfo = result.1.realNameInfo } /// 进入昵称编辑状态,并把当前昵称填入编辑框。 func beginEditing() { editingNickname = displayNickname == "未设置昵称" ? "" : displayNickname isEditingProfile = true } /// 退出昵称编辑状态并清空编辑输入。 func cancelEditing() { editingNickname = "" isEditingProfile = false clearPendingAvatar() } /// 准备待上传头像,并进入资料编辑状态。 func prepareAvatarImage(data: Data, timestamp: TimeInterval = Date().timeIntervalSince1970) throws { let processed = try AvatarImageProcessor.process(data: data, timestamp: timestamp) pendingAvatarData = processed.data pendingAvatarFileName = processed.fileName if !isEditingProfile { beginEditing() } } /// 保存昵称和头像修改,并同步更新本地 userInfo。 func saveProfile(api: ProfileAPI, uploader: any OSSUploadServing, scenicId: Int) async throws { let nextNickname = editingNickname.trimmingCharacters(in: .whitespacesAndNewlines) guard !nextNickname.isEmpty else { throw ProfileValidationError.emptyNickname } let nicknameChanged = nextNickname != displayNickname let avatarChanged = pendingAvatarData != nil guard nicknameChanged || avatarChanged else { cancelEditing() return } guard !isSaving else { return } isSaving = true avatarUploadProgress = avatarChanged ? 1 : nil defer { isSaving = false avatarUploadProgress = nil } let uploadedAvatarURL = try await uploadPendingAvatarIfNeeded(uploader: uploader, scenicId: scenicId) if let uploadedAvatarURL { try await api.updateUserAvatarURL(uploadedAvatarURL) } if nicknameChanged { try await api.updateUserInfo(nickname: nextNickname) } var nextUser = userInfo ?? UserInfoResponse() nextUser = UserInfoResponse( avatar: uploadedAvatarURL ?? nextUser.avatar, realName: nextUser.realName, phone: nextUser.phone, nickname: nextNickname, roleName: nextUser.roleName, status: nextUser.status, statusName: nextUser.statusName ) userInfo = nextUser clearPendingAvatar() cancelEditing() } /// 修改用户密码,并校验最小长度。 func updatePassword(_ password: String, api: ProfileAPI) async throws { let nextPassword = password.trimmingCharacters(in: .whitespacesAndNewlines) guard nextPassword.count >= 6 else { throw ProfileValidationError.shortPassword } guard !isSaving else { return } isSaving = true defer { isSaving = false } try await api.updateUserInfo(password: nextPassword) } /// 将最新 userInfo 合成为全局 AccountProfile,保留已有 userId 等兜底字段。 func accountProfileFallback(_ fallback: AccountProfile?) -> AccountProfile? { guard let userInfo else { return fallback } return AccountProfile( userId: fallback?.userId ?? "", displayName: nonEmpty(userInfo.nickname) ?? nonEmpty(userInfo.realName) ?? fallback?.displayName ?? "未设置昵称", phone: nonEmpty(userInfo.phone) ?? fallback?.phone, avatarURL: nonEmpty(userInfo.avatar) ?? fallback?.avatarURL ) } /// 去除空白字符后返回非空字符串。 private func nonEmpty(_ value: String?) -> String? { let text = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return text.isEmpty ? nil : text } /// 上传待保存头像;没有待上传图片时返回 nil。 private func uploadPendingAvatarIfNeeded(uploader: any OSSUploadServing, scenicId: Int) async throws -> String? { guard let pendingAvatarData else { return nil } let fileName = pendingAvatarFileName ?? "avatar_\(Int(Date().timeIntervalSince1970)).jpg" return try await uploader.uploadUserAvatar(data: pendingAvatarData, fileName: fileName, scenicId: scenicId) { [weak self] progress in Task { @MainActor in self?.avatarUploadProgress = progress } } } /// 清空本地待上传头像数据。 private func clearPendingAvatar() { pendingAvatarData = nil pendingAvatarFileName = nil avatarUploadProgress = nil } } /// 个人信息页校验错误实体,表示昵称和密码输入不符合要求。 enum ProfileValidationError: LocalizedError { case emptyNickname case shortPassword var errorDescription: String? { switch self { case .emptyNickname: "请输入昵称" case .shortPassword: "密码至少 6 位" } } }