// // MailboxView.swift // QuickLocation // import UIKit import MJRefresh extension MailboxCategory { var tintColor: UIColor { switch self { case .interaction: return UIColor(hexStr: "#3ED989") case .notice: return UIColor(hexStr: "#C274F7") case .system: return UIColor(hexStr: "#55B8F7") } } var backgroundColor: UIColor { switch self { case .interaction: return UIColor(hexStr: "#E7FFF2") case .notice: return UIColor(hexStr: "#F9E9FF") case .system: return UIColor(hexStr: "#E8F8FF") } } var iconAssetName: String { switch self { case .interaction: return "Mailbox/interaction" case .notice: return "Mailbox/notice" case .system: return "Mailbox/system" } } var listIconAssetName: String { switch self { case .system: return "Mailbox/system_message" default: return iconAssetName } } } final class MailboxView: UIView { let backButton = UIButton(type: .custom) let clearButton = UIButton(type: .custom) let tableView = UITableView(frame: .zero, style: .plain) private let headerGradientLayer = CAGradientLayer() private let titleLabel = UILabel() private let categoryStackView = UIStackView() private let contentTopSeparator = UIView() private let emptyView = UIView() private let emptyImageView = UIImageView(image: UIImage(named: "empty_data")) private let emptyLabel = UILabel() private let loadingIndicator = UIActivityIndicatorView(style: .medium) private var categoryViews: [MailboxCategory: MailboxCategoryView] = [:] var onSelectCategory: ((MailboxCategory) -> Void)? override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() headerGradientLayer.frame = CGRect(x: 0, y: 0, width: bounds.width, height: kNaviHeight + 132) } func selectCategory(_ category: MailboxCategory) { categoryViews.forEach { itemCategory, categoryView in categoryView.setSelected(itemCategory == category) } } func updateUnreadCounts(_ counts: [MailboxCategory: Int]) { categoryViews.forEach { category, categoryView in categoryView.setUnreadCount(counts[category] ?? 0) } } func updateState(hasMessages: Bool, isLoading: Bool, reachedEnd: Bool) { loadingIndicator.isHidden = !isLoading if isLoading { loadingIndicator.startAnimating() } else { loadingIndicator.stopAnimating() } emptyImageView.isHidden = isLoading || hasMessages || !reachedEnd emptyLabel.isHidden = emptyImageView.isHidden tableView.mj_footer?.isHidden = !hasMessages || reachedEnd if reachedEnd { tableView.mj_footer?.endRefreshingWithNoMoreData() } else { tableView.mj_footer?.resetNoMoreData() tableView.mj_footer?.endRefreshing() } } private func setupUI() { backgroundColor = .white headerGradientLayer.colors = [ UIColor(hexStr: "#DFF9FF").cgColor, UIColor(hexStr: "#F3FFE9").cgColor, UIColor.white.cgColor ] headerGradientLayer.locations = [0, 0.58, 1] headerGradientLayer.startPoint = CGPoint(x: 0, y: 0) headerGradientLayer.endPoint = CGPoint(x: 1, y: 1) // layer.insertSublayer(headerGradientLayer, at: 0) addSubview(navBgView) navBgView.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(160/375) setupNavigationBar() setupCategories() setupTableView() setupEmptyView() } private func setupNavigationBar() { titleLabel.text = "消息" titleLabel.textColor = UIColor(hexStr: "#18304B") titleLabel.font = .systemFont(ofSize: 18, weight: .medium) titleLabel.textAlignment = .center configureNavigationButton(backButton, symbolName: "chevron.left") configureNavigationButton(clearButton, symbolName: "trash.fill") backButton.accessibilityLabel = "返回" clearButton.accessibilityLabel = "清空全部消息" addSubview(titleLabel) addSubview(backButton) addSubview(clearButton) titleLabel.translatesAutoresizingMaskIntoConstraints = false backButton.translatesAutoresizingMaskIntoConstraints = false clearButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ backButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 18), backButton.topAnchor.constraint(equalTo: topAnchor, constant: kStatusBarHeight + 8), backButton.widthAnchor.constraint(equalToConstant: 36), backButton.heightAnchor.constraint(equalToConstant: 36), clearButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -18), clearButton.centerYAnchor.constraint(equalTo: backButton.centerYAnchor), clearButton.widthAnchor.constraint(equalToConstant: 36), clearButton.heightAnchor.constraint(equalToConstant: 36), titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor), titleLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor) ]) } private func configureNavigationButton(_ button: UIButton, symbolName: String) { let configuration = UIImage.SymbolConfiguration(pointSize: 18, weight: .bold) button.setImage(UIImage(systemName: symbolName, withConfiguration: configuration), for: .normal) button.tintColor = UIColor(hexStr: "#213148") button.backgroundColor = UIColor.white.withAlphaComponent(0.88) button.layer.cornerRadius = 10 button.layer.shadowColor = UIColor.black.withAlphaComponent(0.08).cgColor button.layer.shadowOpacity = 1 button.layer.shadowRadius = 8 button.layer.shadowOffset = CGSize(width: 0, height: 3) } private func setupCategories() { categoryStackView.axis = .horizontal categoryStackView.distribution = .fillEqually categoryStackView.alignment = .fill MailboxCategory.displayOrder.forEach { category in let categoryView = MailboxCategoryView(category: category) categoryView.addTarget(self, action: #selector(categoryTapped(_:)), for: .touchUpInside) categoryStackView.addArrangedSubview(categoryView) categoryViews[category] = categoryView } contentTopSeparator.backgroundColor = UIColor(hexStr: "#E8E8E8") addSubview(categoryStackView) addSubview(contentTopSeparator) categoryStackView.translatesAutoresizingMaskIntoConstraints = false contentTopSeparator.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ categoryStackView.topAnchor.constraint(equalTo: topAnchor, constant: kNaviHeight + 14), categoryStackView.leadingAnchor.constraint(equalTo: leadingAnchor), categoryStackView.trailingAnchor.constraint(equalTo: trailingAnchor), categoryStackView.heightAnchor.constraint(equalToConstant: 112), contentTopSeparator.topAnchor.constraint(equalTo: categoryStackView.bottomAnchor), contentTopSeparator.leadingAnchor.constraint(equalTo: leadingAnchor), contentTopSeparator.trailingAnchor.constraint(equalTo: trailingAnchor), contentTopSeparator.heightAnchor.constraint(equalToConstant: 0.5) ]) } private func setupTableView() { tableView.backgroundColor = .white tableView.separatorStyle = .none tableView.rowHeight = 66 tableView.showsVerticalScrollIndicator = false tableView.tableFooterView = UIView() tableView.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: kSafeBottomMargin + 18, right: 0) tableView.register(MailboxMessageCell.self, forCellReuseIdentifier: MailboxMessageCell.reuseIdentifier) addSubview(tableView) tableView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: contentTopSeparator.bottomAnchor), tableView.leadingAnchor.constraint(equalTo: leadingAnchor), tableView.trailingAnchor.constraint(equalTo: trailingAnchor), tableView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) } private func setupEmptyView() { emptyView.isUserInteractionEnabled = false emptyImageView.contentMode = .scaleAspectFit emptyLabel.text = "这里怎么什么都没有~" emptyLabel.textColor = UIColor(hexStr: "#B1B1B1") emptyLabel.font = .systemFont(ofSize: 14) emptyLabel.textAlignment = .center loadingIndicator.color = UIColor(hexStr: "#16B3FF") addSubview(emptyView) emptyView.addSubview(emptyImageView) emptyView.addSubview(emptyLabel) emptyView.addSubview(loadingIndicator) emptyView.translatesAutoresizingMaskIntoConstraints = false emptyImageView.translatesAutoresizingMaskIntoConstraints = false emptyLabel.translatesAutoresizingMaskIntoConstraints = false loadingIndicator.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ emptyView.topAnchor.constraint(equalTo: contentTopSeparator.bottomAnchor), emptyView.leadingAnchor.constraint(equalTo: leadingAnchor), emptyView.trailingAnchor.constraint(equalTo: trailingAnchor), emptyView.bottomAnchor.constraint(equalTo: bottomAnchor), emptyImageView.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor), emptyImageView.centerYAnchor.constraint(equalTo: emptyView.centerYAnchor, constant: -56), emptyImageView.widthAnchor.constraint(equalToConstant: 140), emptyImageView.heightAnchor.constraint(equalToConstant: 120), emptyLabel.topAnchor.constraint(equalTo: emptyImageView.bottomAnchor, constant: 8), emptyLabel.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor), loadingIndicator.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor), loadingIndicator.centerYAnchor.constraint(equalTo: emptyView.centerYAnchor, constant: -40) ]) } lazy var navBgView: UIImageView = { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_1") iv.contentMode = .scaleAspectFill return iv }() @objc private func categoryTapped(_ sender: MailboxCategoryView) { onSelectCategory?(sender.category) } } final class MailboxCategoryView: UIControl { let category: MailboxCategory private let iconBackgroundView = UIView() private let iconImageView = UIImageView() private let titleLabel = UILabel() private let underlineView = UIView() private let badgeLabel = UILabel() init(category: MailboxCategory) { self.category = category super.init(frame: .zero) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setSelected(_ selected: Bool) { titleLabel.textColor = UIColor(hexStr: "#26364A") underlineView.isHidden = !selected } func setUnreadCount(_ count: Int) { badgeLabel.isHidden = count <= 0 badgeLabel.text = count > 99 ? "99+" : "\(count)" } private func setupUI() { accessibilityLabel = category.title iconBackgroundView.isUserInteractionEnabled = false iconBackgroundView.backgroundColor = .clear iconImageView.image = UIImage(named: category.iconAssetName) iconImageView.contentMode = .scaleAspectFit titleLabel.text = category.title titleLabel.textAlignment = .center titleLabel.textColor = UIColor(hexStr: "#26364A") titleLabel.font = .systemFont(ofSize: 15) underlineView.backgroundColor = UIColor(hexStr: "#13B6FB") underlineView.layer.cornerRadius = 1.5 underlineView.isHidden = true badgeLabel.backgroundColor = UIColor(hexStr: "#FF5260") badgeLabel.textColor = .white badgeLabel.font = .systemFont(ofSize: 10, weight: .medium) badgeLabel.textAlignment = .center badgeLabel.layer.cornerRadius = 9 badgeLabel.clipsToBounds = true badgeLabel.isHidden = true addSubview(iconBackgroundView) iconBackgroundView.addSubview(iconImageView) addSubview(titleLabel) addSubview(underlineView) addSubview(badgeLabel) [iconBackgroundView, iconImageView, titleLabel, underlineView, badgeLabel].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ iconBackgroundView.topAnchor.constraint(equalTo: topAnchor, constant: 12), iconBackgroundView.centerXAnchor.constraint(equalTo: centerXAnchor), iconBackgroundView.widthAnchor.constraint(equalToConstant: 50), iconBackgroundView.heightAnchor.constraint(equalToConstant: 50), iconImageView.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor), iconImageView.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor), iconImageView.widthAnchor.constraint(equalToConstant: 50), iconImageView.heightAnchor.constraint(equalToConstant: 50), titleLabel.topAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: 7), titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor), underlineView.bottomAnchor.constraint(equalTo: bottomAnchor), underlineView.centerXAnchor.constraint(equalTo: centerXAnchor), underlineView.widthAnchor.constraint(equalToConstant: 18), underlineView.heightAnchor.constraint(equalToConstant: 3), badgeLabel.centerXAnchor.constraint(equalTo: iconBackgroundView.trailingAnchor, constant: -2), badgeLabel.centerYAnchor.constraint(equalTo: iconBackgroundView.topAnchor, constant: 2), badgeLabel.heightAnchor.constraint(equalToConstant: 18), badgeLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 18) ]) } } final class MailboxMessageCell: UITableViewCell { static let reuseIdentifier = "MailboxMessageCell" private let avatarView = UIImageView() private let defaultIconView = MailboxTypeIconView() private let unreadDotView = UIView() private let titleLabel = UILabel() private let contentLabel = UILabel() private let timeLabel = UILabel() private let separatorView = UIView() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func prepareForReuse() { super.prepareForReuse() avatarView.setHeadPic(nil) } func configure(message: MailboxMessage) { titleLabel.text = message.title contentLabel.text = Self.previewText(for: message) timeLabel.text = Self.timeText(from: message.modifyTime) unreadDotView.isHidden = message.isRead let senderIcon = message.senderIcon.trimmingCharacters(in: .whitespacesAndNewlines) let shouldShowAvatar = message.category != .system && !senderIcon.isEmpty && senderIcon != "0" avatarView.isHidden = !shouldShowAvatar defaultIconView.isHidden = shouldShowAvatar if shouldShowAvatar { avatarView.setHeadPic(senderIcon) } else { defaultIconView.configure(category: message.category) } } private func setupUI() { selectionStyle = .none backgroundColor = .white avatarView.contentMode = .scaleAspectFill avatarView.layer.cornerRadius = 11 avatarView.clipsToBounds = true unreadDotView.backgroundColor = UIColor(hexStr: "#FF5260") unreadDotView.layer.cornerRadius = 4 titleLabel.font = .systemFont(ofSize: 15, weight: .medium) titleLabel.textColor = UIColor(hexStr: "#26364A") titleLabel.numberOfLines = 1 contentLabel.font = .systemFont(ofSize: 13) contentLabel.textColor = UIColor(hexStr: "#AAAAAA") contentLabel.numberOfLines = 1 timeLabel.font = .systemFont(ofSize: 12) timeLabel.textColor = UIColor(hexStr: "#BBBBBB") timeLabel.textAlignment = .right separatorView.backgroundColor = UIColor(hexStr: "#EEEEEE") contentView.addSubview(avatarView) contentView.addSubview(defaultIconView) contentView.addSubview(unreadDotView) contentView.addSubview(titleLabel) contentView.addSubview(contentLabel) contentView.addSubview(timeLabel) contentView.addSubview(separatorView) [avatarView, defaultIconView, unreadDotView, titleLabel, contentLabel, timeLabel, separatorView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ avatarView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 26), avatarView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), avatarView.widthAnchor.constraint(equalToConstant: 42), avatarView.heightAnchor.constraint(equalToConstant: 42), defaultIconView.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor), defaultIconView.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor), defaultIconView.widthAnchor.constraint(equalTo: avatarView.widthAnchor), defaultIconView.heightAnchor.constraint(equalTo: avatarView.heightAnchor), unreadDotView.centerXAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: -2), unreadDotView.centerYAnchor.constraint(equalTo: avatarView.topAnchor, constant: 2), unreadDotView.widthAnchor.constraint(equalToConstant: 8), unreadDotView.heightAnchor.constraint(equalToConstant: 8), timeLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), timeLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15), timeLabel.widthAnchor.constraint(equalToConstant: 46), titleLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 12), titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: timeLabel.leadingAnchor, constant: -8), titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11), contentLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), contentLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), contentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5), separatorView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), separatorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), separatorView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), separatorView.heightAnchor.constraint(equalToConstant: 0.5) ]) } private static func previewText(for message: MailboxMessage) -> String { guard message.category == .interaction, message.content.contains("]"), let bracketIndex = message.content.lastIndex(of: "[") else { return message.content } return String(message.content[.. String { guard value.count >= 16 else { return value } let startIndex = value.index(value.startIndex, offsetBy: 11) let endIndex = value.index(startIndex, offsetBy: 5, limitedBy: value.endIndex) ?? value.endIndex return String(value[startIndex..