将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
103 lines
3.5 KiB
Swift
103 lines
3.5 KiB
Swift
//
|
||
// ProfileEditViewModel.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// 资料编辑页 ViewModel,负责头像与昵称的本地编辑与提交保存。
|
||
final class ProfileEditViewModel {
|
||
|
||
private let initialNickname: String
|
||
private let initialAvatarURL: String
|
||
|
||
private(set) var editingNickname = ""
|
||
private(set) var pendingAvatarData: Data?
|
||
private(set) var pendingAvatarFileName: String?
|
||
private(set) var isSaving = false
|
||
private(set) var avatarUploadProgress: Int?
|
||
|
||
var onStateChange: (() -> Void)?
|
||
|
||
/// 当前展示用的远程头像 URL(未选择新图时使用初始值)。
|
||
var displayAvatarURL: String {
|
||
initialAvatarURL
|
||
}
|
||
|
||
/// 使用「我的」页当前展示的快照初始化编辑态。
|
||
init(nickname: String, avatarURL: String) {
|
||
initialNickname = nickname
|
||
initialAvatarURL = avatarURL
|
||
editingNickname = nickname == "未设置昵称" ? "" : nickname
|
||
}
|
||
|
||
func updateEditingNickname(_ value: String) {
|
||
editingNickname = value
|
||
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
|
||
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 != initialNickname
|
||
let avatarChanged = pendingAvatarData != nil
|
||
guard nicknameChanged || avatarChanged else { 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)
|
||
}
|
||
|
||
AppStore.shared.session.userName = nextNickname
|
||
if let uploadedAvatarURL {
|
||
AppStore.shared.session.avatar = uploadedAvatarURL
|
||
}
|
||
|
||
clearPendingAvatar()
|
||
NotificationCenter.default.post(name: NotificationName.userProfileDidUpdate, object: 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
|
||
self?.avatarUploadProgress = progress
|
||
self?.notifyStateChange()
|
||
}
|
||
}
|
||
|
||
private func clearPendingAvatar() {
|
||
pendingAvatarData = nil
|
||
pendingAvatarFileName = nil
|
||
avatarUploadProgress = nil
|
||
}
|
||
|
||
private func notifyStateChange() {
|
||
onStateChange?()
|
||
}
|
||
}
|