Files
suixinkan_ios_uikit/suixinkan_ios/Features/Profile/ViewModels/ProfileViewModel.swift
2026-06-26 14:33:31 +08:00

201 lines
6.8 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
//
// Created by Codex on 2026/6/20.
//
import Foundation
@MainActor
/// ViewModel
final class ProfileViewModel {
var onChange: (() -> Void)?
var userInfo: UserInfoResponse? { didSet { onChange?() } }
var realNameInfo: RealNameInfo? { didSet { onChange?() } }
var isLoading = false { didSet { onChange?() } }
var isSaving = false { didSet { onChange?() } }
var isEditingProfile = false { didSet { onChange?() } }
var editingNickname = "" { didSet { onChange?() } }
private(set) var pendingAvatarData: Data? { didSet { onChange?() } }
private(set) var pendingAvatarFileName: String? { didSet { onChange?() } }
private(set) var avatarUploadProgress: Int? { didSet { onChange?() } }
var displayNickname: String {
nonEmpty(userInfo?.nickname) ?? "未设置昵称"
}
var displayRealName: String {
nonEmpty(userInfo?.realName) ?? "--"
}
var displayPhone: String {
nonEmpty(userInfo?.phone) ?? "--"
}
var displayAvatarURL: String {
nonEmpty(userInfo?.avatar) ?? ""
}
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 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 位"
}
}
}