// // ProfileSpaceViewModel.swift // suixinkan // // Created by Codex on 2026/6/29. // import Foundation import Combine @MainActor /// 个人空间配置 ViewModel,负责加载资料、维护编辑草稿、校验并提交空间配置。 final class ProfileSpaceViewModel: ObservableObject { @Published private(set) var loading = false @Published private(set) var saving = false @Published var errorMessage: String? @Published var nickname = "" @Published private(set) var realName = "" @Published private(set) var avatarURL = "" @Published private(set) var scenicCertification: [String] = [] @Published var introduction = "" @Published var attrLabels: [String] = [] @Published var shootLabels: [String] = [] @Published var cameraDevices: [String] = [] @Published var businessStartTime: Date? @Published var businessEndTime: Date? @Published var holidayStartTime: Date? @Published var holidayEndTime: Date? @Published var acceptOrderStatus = 0 @Published private(set) var scheduleMap: [String: [ScheduleItem]] = [:] @Published private(set) var pendingAvatarData: Data? @Published private(set) var pendingAvatarFileName: String? @Published private(set) var avatarUploadProgress: Int? private var originalSnapshot: ProfileSpaceEditableSnapshot? /// 当前页面是否存在未保存改动。 var hasChanges: Bool { guard let originalSnapshot else { return pendingAvatarData != nil } return editableSnapshot() != originalSnapshot || pendingAvatarData != nil } /// 使用当前景区加载个人空间配置。 func load(api: any ProfileSpaceServing, scenicId: Int?) async { guard let scenicId else { resetForMissingScenic() return } guard !loading else { return } loading = true errorMessage = nil defer { loading = false } do { let response = try await api.profileSpaceInfo(scenicId: scenicId) apply(response) } catch { errorMessage = error.localizedDescription } } /// 处理用户选择的头像图片,成功后暂存在内存中等待保存。 func prepareAvatarImage(data: Data, timestamp: TimeInterval = Date().timeIntervalSince1970) throws { let processed = try AvatarImageProcessor.process(data: data, timestamp: timestamp) pendingAvatarData = processed.data pendingAvatarFileName = processed.fileName } /// 添加个人标签。 func addAttrLabel(_ label: String) throws { attrLabels = try adding(label, to: attrLabels) } /// 添加拍摄说明标签。 func addShootLabel(_ label: String) throws { shootLabels = try adding(label, to: shootLabels) } /// 删除个人标签。 func removeAttrLabel(_ label: String) { attrLabels.removeAll { $0 == label } } /// 删除拍摄说明标签。 func removeShootLabel(_ label: String) { shootLabels.removeAll { $0 == label } } /// 添加相机设备名称。 func addCameraDevice(_ device: String) { let value = device.trimmingCharacters(in: .whitespacesAndNewlines) guard !value.isEmpty else { return } cameraDevices.append(value) } /// 删除相机设备名称。 func removeCameraDevice(_ device: String) { cameraDevices.removeAll { $0 == device } } /// 保存个人空间配置、昵称和待上传头像。 func save(api: any ProfileSpaceServing, uploader: any OSSUploadServing, scenicId: Int?) async throws { guard let scenicId else { errorMessage = ProfileSpaceValidationError.missingScenic.localizedDescription throw ProfileSpaceValidationError.missingScenic } do { let request = try makeUpdateRequest(scenicId: scenicId) guard !saving else { return } saving = true errorMessage = nil avatarUploadProgress = pendingAvatarData == nil ? nil : 1 defer { saving = false avatarUploadProgress = nil } let uploadedAvatarURL = try await uploadPendingAvatarIfNeeded(uploader: uploader, scenicId: scenicId) if let uploadedAvatarURL { try await api.updateUserAvatarURL(uploadedAvatarURL) } let nextNickname = nickname.trimmingCharacters(in: .whitespacesAndNewlines) if nextNickname != originalSnapshot?.nickname { try await api.updateUserInfo(nickname: nextNickname, password: nil, avatar: nil) } try await api.updateProfileSpaceInfo(request) if let uploadedAvatarURL { avatarURL = uploadedAvatarURL } nickname = nextNickname pendingAvatarData = nil pendingAvatarFileName = nil originalSnapshot = editableSnapshot() } catch { errorMessage = error.localizedDescription throw error } } /// 删除日程并重新加载个人空间配置。 func deleteSchedule( item: ScheduleItem, profileAPI: any ProfileSpaceServing, scheduleAPI: any ScheduleServing, scenicId: Int? ) async { guard let scenicId else { errorMessage = ProfileSpaceValidationError.missingScenic.localizedDescription return } do { try await scheduleAPI.deleteSchedule(id: item.id) await load(api: profileAPI, scenicId: scenicId) } catch { errorMessage = error.localizedDescription } } /// 生成全局账号资料兜底,用于保存昵称和头像后同步 AccountContext。 func accountProfileFallback(_ fallback: AccountProfile?) -> AccountProfile? { AccountProfile( userId: fallback?.userId ?? "", displayName: nickname.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? (fallback?.displayName ?? "") : nickname, phone: fallback?.phone, avatarURL: avatarURL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? fallback?.avatarURL : avatarURL ) } /// 构造空间配置更新请求并执行表单校验。 func makeUpdateRequest(scenicId: Int) throws -> UpdateProfileSpaceRequest { let nextNickname = nickname.trimmingCharacters(in: .whitespacesAndNewlines) guard !nextNickname.isEmpty else { throw ProfileSpaceValidationError.emptyNickname } guard let businessStartTime else { throw ProfileSpaceValidationError.missingWorkdayStart } guard let businessEndTime else { throw ProfileSpaceValidationError.missingWorkdayEnd } guard let holidayStartTime else { throw ProfileSpaceValidationError.missingHolidayStart } guard let holidayEndTime else { throw ProfileSpaceValidationError.missingHolidayEnd } guard seconds(in: businessStartTime) < seconds(in: businessEndTime) else { throw ProfileSpaceValidationError.invalidWorkdayTime } guard seconds(in: holidayStartTime) < seconds(in: holidayEndTime) else { throw ProfileSpaceValidationError.invalidHolidayTime } return UpdateProfileSpaceRequest( scenicId: "\(scenicId)", description: introduction.trimmingCharacters(in: .whitespacesAndNewlines), cameraDevice: normalized(cameraDevices), attrLabel: normalized(attrLabels), shootLabel: normalized(shootLabels), businessStartTime: businessStartTime.profileSpaceTimeText, businessEndTime: businessEndTime.profileSpaceTimeText, holidayStartTime: holidayStartTime.profileSpaceTimeText, holidayEndTime: holidayEndTime.profileSpaceTimeText, businessRange: [], acceptOrderStatus: acceptOrderStatus ) } /// 将后端响应写入页面状态。 private func apply(_ response: ProfileSpaceInfoResponse) { realName = response.realName nickname = response.nickname avatarURL = response.avatar scenicCertification = response.scenicCertification introduction = response.description attrLabels = response.attrLabel shootLabels = response.shootLabel cameraDevices = response.cameraDevice businessStartTime = Date.profileSpaceTime(from: response.businessStartTime) businessEndTime = Date.profileSpaceTime(from: response.businessEndTime) holidayStartTime = Date.profileSpaceTime(from: response.holidayStartTime) holidayEndTime = Date.profileSpaceTime(from: response.holidayEndTime) acceptOrderStatus = response.acceptOrderStatus scheduleMap = response.schedule pendingAvatarData = nil pendingAvatarFileName = nil originalSnapshot = editableSnapshot() } /// 缺少景区时清空页面业务状态。 private func resetForMissingScenic() { loading = false errorMessage = ProfileSpaceValidationError.missingScenic.localizedDescription realName = "" nickname = "" avatarURL = "" scenicCertification = [] introduction = "" attrLabels = [] shootLabels = [] cameraDevices = [] businessStartTime = nil businessEndTime = nil holidayStartTime = nil holidayEndTime = nil acceptOrderStatus = 0 scheduleMap = [:] originalSnapshot = nil } /// 返回当前编辑态快照。 private func editableSnapshot() -> ProfileSpaceEditableSnapshot { ProfileSpaceEditableSnapshot( nickname: nickname.trimmingCharacters(in: .whitespacesAndNewlines), description: introduction.trimmingCharacters(in: .whitespacesAndNewlines), attrLabel: normalized(attrLabels), shootLabel: normalized(shootLabels), cameraDevice: normalized(cameraDevices), businessStartTime: businessStartTime, businessEndTime: businessEndTime, holidayStartTime: holidayStartTime, holidayEndTime: holidayEndTime, acceptOrderStatus: acceptOrderStatus, avatarURL: avatarURL ) } /// 添加标签并执行数量、长度和重复校验。 private func adding(_ label: String, to labels: [String]) throws -> [String] { let value = label.trimmingCharacters(in: .whitespacesAndNewlines) guard !value.isEmpty else { throw ProfileSpaceValidationError.emptyLabel } guard value.count <= 20 else { throw ProfileSpaceValidationError.labelTooLong } guard labels.count < 8 else { throw ProfileSpaceValidationError.labelLimit } guard !labels.contains(value) else { throw ProfileSpaceValidationError.duplicateLabel } return labels + [value] } /// 清洗字符串数组,移除空白项。 private func normalized(_ values: [String]) -> [String] { values .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } } /// 返回日期的当天秒数,用于只比较时间部分。 private func seconds(in date: Date) -> Int { let components = Calendar.current.dateComponents([.hour, .minute, .second], from: date) return (components.hour ?? 0) * 3600 + (components.minute ?? 0) * 60 + (components.second ?? 0) } /// 上传待保存头像;没有待上传图片时返回 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 } } } }