Add submit-task flow aligned with Android and extract profile edit page.

Implement task creation with cloud/local media, order linking, and OSS upload; wire home entry and move profile nickname/avatar editing to ProfileEditViewController.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 10:12:20 +08:00
co-authored by Cursor
parent 715393e78c
commit 3c4ca7eefb
23 changed files with 3186 additions and 170 deletions
@@ -0,0 +1,102 @@
//
// 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.userName = nextNickname
if let uploadedAvatarURL {
AppStore.shared.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?()
}
}
@@ -5,7 +5,7 @@
import Foundation
/// 个人信息页 ViewModel,负责资料加载、编辑状态和提交更新。
/// 个人信息页 ViewModel,负责资料加载与账号相关操作。
final class ProfileViewModel {
private(set) var userInfo: UserInfoResponse?
@@ -13,11 +13,6 @@ final class ProfileViewModel {
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)?
@@ -85,12 +80,6 @@ final class ProfileViewModel {
return label.isEmpty ? "审核中" : label
}
func updateEditingNickname(_ value: String) {
editingNickname = value
notifyStateChange()
}
/// 重新加载用户资料,摄影师额外拉取实名与银行卡信息。
func reload(api: ProfileAPI) async throws {
guard !isLoading else { return }
isLoading = true
@@ -119,77 +108,21 @@ final class ProfileViewModel {
}
}
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)
}
/// 从 `AppStore` 同步头像与昵称,用于资料保存后的本地即时刷新。
func applyLocalProfileUpdate(from store: AppStore = .shared) {
let previous = userInfo ?? UserInfoResponse()
let nextNickname = store.userName.trimmingCharacters(in: .whitespacesAndNewlines)
let nextAvatar = store.avatar.trimmingCharacters(in: .whitespacesAndNewlines)
userInfo = UserInfoResponse(
avatar: uploadedAvatarURL ?? previous.avatar,
avatar: nextAvatar.isEmpty ? previous.avatar : nextAvatar,
realName: previous.realName,
phone: previous.phone,
nickname: nextNickname,
nickname: nextNickname.isEmpty ? previous.nickname : nextNickname,
roleName: previous.roleName,
status: previous.status,
statusName: previous.statusName
)
AppStore.shared.userName = nextNickname
if let uploadedAvatarURL {
AppStore.shared.avatar = uploadedAvatarURL
}
clearPendingAvatar()
cancelEditing()
notifyStateChange()
}
func updatePassword(_ password: String, api: ProfileAPI) async throws {
@@ -215,21 +148,6 @@ final class ProfileViewModel {
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