// // MailboxViewController.swift // QuickLocation // import UIKit import RxSwift import MJRefresh final class MailboxViewController: BaseViewController { override var isNavigationBarHidden: Bool { true } private enum LoadKind { case initial case refresh case more case category } private var rootView: MailboxView! private var messages: [MailboxMessage] = [] private var selectedCategory: MailboxCategory = .interaction private var currentPage = 0 private var total = 0 private var reachedEnd = false private var isLoading = false private var pendingCategory: MailboxCategory? override func loadView() { rootView = MailboxView(frame: UIScreen.main.bounds) view = rootView } override func viewDidLoad() { super.viewDidLoad() setupTableView() bindActions() rootView.selectCategory(selectedCategory) updateContentState(showLoading: true) loadPage( 1, kind: .initial, replacing: true, category: selectedCategory, baselineCount: 0 ) } private var visibleMessages: [MailboxMessage] { messages.filter { $0.category == selectedCategory } } private func setupTableView() { rootView.tableView.dataSource = self rootView.tableView.delegate = self let header = MJRefreshNormalHeader { [weak self] in self?.refreshMessages() } header.addFeedbackGenerator() header.stateLabel?.isHidden = true rootView.tableView.mj_header = header let footer = MJRefreshAutoNormalFooter { [weak self] in self?.loadMoreMessages() } footer.stateLabel?.isHidden = true footer.isHidden = true rootView.tableView.mj_footer = footer } private func bindActions() { rootView.backButton.addTarget(self, action: #selector(backAction), for: .touchUpInside) rootView.clearButton.addTarget(self, action: #selector(clearAction), for: .touchUpInside) rootView.onSelectCategory = { [weak self] category in self?.selectCategory(category) } } private func selectCategory(_ category: MailboxCategory) { guard selectedCategory != category else { return } selectedCategory = category rootView.selectCategory(category) rootView.tableView.reloadData() rootView.tableView.setContentOffset( CGPoint(x: 0, y: -rootView.tableView.adjustedContentInset.top), animated: false ) updateContentState(showLoading: isLoading) ensureMessagesLoaded(for: category) } private func refreshMessages() { guard !isLoading else { rootView.tableView.mj_header?.endRefreshing() return } loadPage( 1, kind: .refresh, replacing: true, category: selectedCategory, baselineCount: 0 ) } private func loadMoreMessages() { guard !isLoading, !reachedEnd else { finishRefreshControls() return } let category = selectedCategory loadPage( currentPage + 1, kind: .more, replacing: false, category: category, baselineCount: messages(for: category).count ) } private func ensureMessagesLoaded(for category: MailboxCategory) { guard messages(for: category).isEmpty, !reachedEnd else { updateContentState(showLoading: false) return } guard !isLoading else { pendingCategory = category return } loadPage( currentPage + 1, kind: .category, replacing: false, category: category, baselineCount: 0 ) } private func loadPage( _ page: Int, kind: LoadKind, replacing: Bool, category: MailboxCategory, baselineCount: Int ) { guard !isLoading else { return } isLoading = true pendingCategory = nil updateContentState(showLoading: visibleMessages.isEmpty) UserService.mailboxList(page: page) .subscribe(onNext: { [weak self] response in guard let self else { return } guard let data = response.data else { self.finishLoadingWithError(message: response.message ?? "消息加载失败") return } if replacing { self.messages = data.messages } else { self.mergeMessages(data.messages) } self.currentPage = max(data.page, page) self.total = max(data.total, self.messages.count) self.reachedEnd = self.calculateReachedEnd(data: data) self.rootView.tableView.reloadData() self.updateUnreadCounts() let categoryCount = self.messages(for: category).count if self.selectedCategory == category, categoryCount <= baselineCount, !self.reachedEnd { self.isLoading = false self.loadPage( self.currentPage + 1, kind: kind, replacing: false, category: category, baselineCount: baselineCount ) return } self.isLoading = false self.finishRefreshControls() self.updateContentState(showLoading: false) self.loadPendingCategoryIfNeeded() }, onError: { [weak self] error in self?.finishLoadingWithError(error: error) }) .disposed(by: disposeBag) } private func mergeMessages(_ newMessages: [MailboxMessage]) { var indices = Dictionary(uniqueKeysWithValues: messages.enumerated().map { ($0.element.id, $0.offset) }) for message in newMessages { if let index = indices[message.id] { messages[index] = message } else { indices[message.id] = messages.count messages.append(message) } } } private func calculateReachedEnd(data: MailboxListData) -> Bool { if data.total == 0 || data.messages.isEmpty { return true } if messages.count >= data.total { return true } let pageSize = max(data.size, 1) return max(data.page, currentPage) * pageSize >= data.total } private func loadPendingCategoryIfNeeded() { guard let category = pendingCategory ?? (visibleMessages.isEmpty ? selectedCategory : nil) else { return } pendingCategory = nil ensureMessagesLoaded(for: category) } private func finishLoadingWithError(message: String) { isLoading = false finishRefreshControls() updateContentState(showLoading: false) DLToast.showError(text: message) } private func finishLoadingWithError(error: Error) { isLoading = false finishRefreshControls() updateContentState(showLoading: false) DLToast.showError(error) } private func finishRefreshControls() { rootView.tableView.mj_header?.endRefreshing() if reachedEnd { rootView.tableView.mj_footer?.endRefreshingWithNoMoreData() } else { rootView.tableView.mj_footer?.endRefreshing() } } private func messages(for category: MailboxCategory) -> [MailboxMessage] { messages.filter { $0.category == category } } private func updateUnreadCounts() { var counts: [MailboxCategory: Int] = [:] MailboxCategory.displayOrder.forEach { category in counts[category] = messages(for: category).filter { !$0.isRead }.count } rootView.updateUnreadCounts(counts) } private func updateContentState(showLoading: Bool) { rootView.updateState( hasMessages: !visibleMessages.isEmpty, isLoading: showLoading, reachedEnd: reachedEnd ) } private func openMessage(_ message: MailboxMessage) { DLToast.showLoading() UserService.mailboxDetail(id: message.id) .subscribe(onNext: { [weak self] response in guard let self else { return } DLToast.dismiss() guard let responseMessage = response.data else { DLToast.showError(text: response.message ?? "消息详情加载失败") return } var detail = self.mergeDetail(responseMessage, fallback: message) detail.isRead = true self.updateMessage(detail) let categoryMessages = self.messages(for: detail.category) let detailController = MailboxDetailViewController( messages: categoryMessages, initialMessage: detail ) detailController.onMessageRead = { [weak self] updatedMessage in self?.updateMessage(updatedMessage) } self.present(detailController, animated: true) }, onError: { error in DLToast.dismiss() DLToast.showError(error) }) .disposed(by: disposeBag) } private func updateMessage(_ updatedMessage: MailboxMessage) { guard let index = messages.firstIndex(where: { $0.id == updatedMessage.id }) else { return } messages[index] = updatedMessage rootView.tableView.reloadData() updateUnreadCounts() } private func mergeDetail(_ detail: MailboxMessage, fallback: MailboxMessage) -> MailboxMessage { var merged = detail if merged.id == 0 { merged.id = fallback.id } if merged.title.isEmpty { merged.title = fallback.title } if merged.content.isEmpty { merged.content = fallback.content } if merged.senderId.isEmpty { merged.senderId = fallback.senderId } if merged.senderIcon.isEmpty { merged.senderIcon = fallback.senderIcon } if merged.modifyTime.isEmpty { merged.modifyTime = fallback.modifyTime } if MailboxCategory(rawValue: merged.type) == nil { merged.type = fallback.type } return merged } @objc private func backAction() { navigationController?.popViewController(animated: true) } @objc private func clearAction() { guard !messages.isEmpty else { DLToast.show(text: "暂无消息") return } showConfirmPop( title: "清空全部消息", message: "清空后无法恢复,是否继续?", confirmText: "清空", confirmBlock: { [weak self] in self?.requestClearAll() }, cancelText: "取消" ) } private func requestClearAll() { DLToast.showLoading() UserService.clearMailbox() .subscribe(onNext: { [weak self] response in guard let self else { return } DLToast.dismiss() self.messages = [] self.currentPage = 0 self.total = 0 self.reachedEnd = true self.pendingCategory = nil self.rootView.tableView.reloadData() self.updateUnreadCounts() self.finishRefreshControls() self.updateContentState(showLoading: false) DLToast.showSuccess(text: "已清空") }, onError: { error in DLToast.dismiss() DLToast.showError(error) }) .disposed(by: disposeBag) } } extension MailboxViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { visibleMessages.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell( withIdentifier: MailboxMessageCell.reuseIdentifier, for: indexPath ) as? MailboxMessageCell else { return UITableViewCell() } cell.configure(message: visibleMessages[indexPath.row]) return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { openMessage(visibleMessages[indexPath.row]) } }