// // MessageCenterViewModel.swift // suixinkan // // Created by Codex on 2026/6/25. // import Foundation @MainActor /// 消息中心 ViewModel,负责消息分页、筛选、已读和删除。 final class MessageCenterViewModel { var onChange: (() -> Void)? var messages: [MessageItem] = [] { didSet { onChange?() } } var selectedFilter: MessageFilter = .all { didSet { onChange?() } } var isLoading = false { didSet { onChange?() } } var isLoadingMore = false { didSet { onChange?() } } var loadFailed = false { didSet { onChange?() } } var loadFailureReason: String? { didSet { onChange?() } } var message: String? { didSet { onChange?() } } private(set) var hasMoreMessages = false { didSet { onChange?() } } private(set) var lastId = 0 { didSet { onChange?() } } private let pageSize = 20 /// 未读消息数量。 var unreadCount: Int { messages.filter { !$0.isRead }.count } /// 当前筛选说明。 var filterDescription: String { selectedFilter == .all ? "显示全部消息" : "仅显示未读消息" } /// 切换筛选并重新加载首屏。 func selectFilter(_ filter: MessageFilter, api: any MessageCenterServing) async { guard selectedFilter != filter else { return } selectedFilter = filter await reloadFirstPage(api: api) } /// 重新加载首屏消息,失败时清空旧数据。 func reloadFirstPage(api: any MessageCenterServing) async { isLoading = true loadFailed = false loadFailureReason = nil message = nil defer { isLoading = false } do { let response = try await api.messageList(lastId: 0, limit: pageSize, unread: selectedFilter.rawValue) messages = mapMessages(response.items) hasMoreMessages = response.hasMore lastId = response.lastId } catch { clearMessages() loadFailed = true loadFailureReason = error.localizedDescription message = error.localizedDescription } } /// 加载更多消息,失败时保留当前数据和分页状态。 func loadMore(api: any MessageCenterServing) async { guard hasMoreMessages, !isLoadingMore, !isLoading else { return } isLoadingMore = true message = nil defer { isLoadingMore = false } do { let response = try await api.messageList(lastId: lastId, limit: pageSize, unread: selectedFilter.rawValue) messages.append(contentsOf: mapMessages(response.items)) messages = deduplicatedAndSorted(messages) hasMoreMessages = response.hasMore lastId = response.lastId } catch { message = error.localizedDescription } } /// 标记单条消息为已读,接口成功后再更新本地状态。 func markAsRead(api: any MessageCenterServing, item: MessageItem) async throws { guard !item.isRead, let msgId = item.msgId else { return } try await api.messageRead(id: msgId) guard let index = messages.firstIndex(where: { $0.id == item.id }) else { return } messages[index].isRead = true } /// 将当前列表中的未读消息逐条标记为已读。 func markAllAsRead(api: any MessageCenterServing) async throws { let original = messages do { for item in messages where !item.isRead { if let msgId = item.msgId { try await api.messageRead(id: msgId) } } messages = messages.map { item in var next = item next.isRead = true return next } } catch { messages = original throw error } } /// 删除消息,接口成功后再移除本地列表。 func deleteMessage(api: any MessageCenterServing, item: MessageItem) async throws { if let msgId = item.msgId { try await api.messageDelete(id: msgId) } messages.removeAll { $0.id == item.id } } /// 将后端实体转换为页面展示实体。 func mapMessages(_ source: [MessageEntity]) -> [MessageItem] { source.map { item in MessageItem( id: "msg_\(item.id)", msgId: item.id, title: item.title.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? item.typeName.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "系统通知", detail: item.content, time: item.pushAt.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? item.createdAt.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "0000-00-00 00:00", isRead: item.isRead, type: Self.messageType(from: item.type) ) } } /// 将后端类型映射为页面消息类型。 static func messageType(from value: Int) -> MessageType { switch value { case 1: return .order case 2: return .writeOff default: return .system } } private func deduplicatedAndSorted(_ source: [MessageItem]) -> [MessageItem] { var unique: [String: MessageItem] = [:] for item in source { unique[item.id] = item } return unique.values.sorted { $0.time > $1.time } } private func clearMessages() { messages = [] hasMoreMessages = false lastId = 0 isLoadingMore = false } } private extension String { var nonEmpty: String? { isEmpty ? nil : self } }