170 lines
5.7 KiB
Swift
170 lines
5.7 KiB
Swift
//
|
||
// 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
|
||
}
|
||
}
|