Files
suixinkan_uikit/suixinkan/Features/Profile/ViewModels/ProfileViewModel.swift

249 lines
7.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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
}
}