// // ProfileViewModel.swift // suixinkan // import Foundation /// 个人信息页 ViewModel,负责资料加载、编辑状态和提交更新。 final class ProfileViewModel { private(set) var userInfo: UserInfoResponse? private(set) var realNameInfo: RealNameInfo? private(set) var bankCardInfo: BankCardInfo? private(set) var isLoading = false private(set) var isSaving = false private(set) var isEditingProfile = false private(set) var editingNickname = "" private(set) var pendingAvatarData: Data? private(set) var pendingAvatarFileName: String? private(set) var avatarUploadProgress: Int? var onStateChange: (() -> Void)? var displayNickname: String { nonEmpty(userInfo?.nickname) ?? "未设置昵称" } var displayRealName: String { nonEmpty(userInfo?.realName) ?? "--" } var displayPhone: String { nonEmpty(userInfo?.phone) ?? AppStore.shared.phone.nonEmpty ?? "--" } var displayAvatarURL: String { nonEmpty(userInfo?.avatar) ?? AppStore.shared.avatar.nonEmpty ?? "" } var displayUID: String { let uid = AppStore.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines) return uid.isEmpty ? "--" : uid } var accountDisplayName: String { let name = AppStore.shared.accountDisplayName.trimmingCharacters(in: .whitespacesAndNewlines) return name.isEmpty ? "--" : name } var currentScenicName: String { let name = AppStore.shared.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines) return name.isEmpty ? "--" : name } var accountStatusText: String { nonEmpty(userInfo?.statusName) ?? "--" } var showPhotographerFields: Bool { if AppStore.shared.isPhotographerRole { return true } if let roleName = nonEmpty(userInfo?.roleName) { return AppRoleCode.fromRoleName(roleName)?.isPhotographer == true || roleName == "摄影师" } return false } var realNameStatusText: String { guard let realNameInfo else { return "点击去实名认证" } switch realNameInfo.auditStatus { case 2: return "已实名认证" case 3: return "审核不通过" default: return "审核中" } } var bankCardDisplayText: String { guard let bankCardInfo else { return "点击绑定银行卡" } if bankCardInfo.auditStatus == 2 { let masked = Self.maskCardNumber(bankCardInfo.cardNumber) return "\(bankCardInfo.bankName) \(masked)" } let label = bankCardInfo.auditStatusLabel.trimmingCharacters(in: .whitespacesAndNewlines) return label.isEmpty ? "审核中" : label } func updateEditingNickname(_ value: String) { editingNickname = value notifyStateChange() } /// 重新加载用户资料,摄影师额外拉取实名与银行卡信息。 func reload(api: ProfileAPI) async throws { guard !isLoading else { return } isLoading = true notifyStateChange() defer { isLoading = false notifyStateChange() } let info = try await api.userInfo() userInfo = info AppStore.shared.applyUserInfo(info) if !info.roleName.isEmpty { AppStore.shared.roleName = info.roleName } if showPhotographerFields { async let realName = api.realNameInfo() async let bankCard = api.bankCardInfo() let extra = try await (realName, bankCard) realNameInfo = extra.0.realNameInfo bankCardInfo = extra.1.bankCard } else { realNameInfo = nil bankCardInfo = nil } } func beginEditing() { editingNickname = displayNickname == "未设置昵称" ? "" : displayNickname isEditingProfile = true notifyStateChange() } func cancelEditing() { editingNickname = "" isEditingProfile = false clearPendingAvatar() notifyStateChange() } 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() } else { notifyStateChange() } } 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 notifyStateChange() defer { isSaving = false avatarUploadProgress = nil notifyStateChange() } 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) } let previous = userInfo ?? UserInfoResponse() userInfo = UserInfoResponse( avatar: uploadedAvatarURL ?? previous.avatar, realName: previous.realName, phone: previous.phone, nickname: nextNickname, roleName: previous.roleName, status: previous.status, statusName: previous.statusName ) AppStore.shared.userName = nextNickname if let uploadedAvatarURL { AppStore.shared.avatar = uploadedAvatarURL } 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 notifyStateChange() defer { isSaving = false notifyStateChange() } try await api.updateUserInfo(password: nextPassword) } static func maskCardNumber(_ cardNumber: String) -> String { let digits = cardNumber.filter(\.isNumber) guard digits.count >= 4 else { return cardNumber } return "**** **** **** \(digits.suffix(4))" } 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 self?.avatarUploadProgress = progress self?.notifyStateChange() } } private func clearPendingAvatar() { pendingAvatarData = nil pendingAvatarFileName = nil avatarUploadProgress = nil } private func nonEmpty(_ value: String?) -> String? { let text = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return text.isEmpty ? nil : text } private func notifyStateChange() { onStateChange?() } } private extension String { var nonEmpty: String? { let text = trimmingCharacters(in: .whitespacesAndNewlines) return text.isEmpty ? nil : text } }