202 lines
6.6 KiB
Swift
202 lines
6.6 KiB
Swift
//
|
||
// ProfileViewModel.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/20.
|
||
//
|
||
|
||
import Foundation
|
||
import Observation
|
||
|
||
@MainActor
|
||
@Observable
|
||
/// 个人信息页 ViewModel,负责资料加载、编辑状态和提交更新。
|
||
final class ProfileViewModel {
|
||
var userInfo: UserInfoResponse?
|
||
var realNameInfo: RealNameInfo?
|
||
var isLoading = false
|
||
var isSaving = false
|
||
var isEditingProfile = false
|
||
var editingNickname = ""
|
||
private(set) var pendingAvatarData: Data?
|
||
private(set) var pendingAvatarFileName: String?
|
||
private(set) var avatarUploadProgress: Int?
|
||
|
||
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 位"
|
||
}
|
||
}
|
||
}
|