// // GroupMemberListView.swift // QuickLocation // // Created by 八条 on 2026/6/29. // import UIKit final class GroupMemberListView: UIView { private var sectionTitles: [String] = [] override init(frame: CGRect) { super.init(frame: frame) backgroundColor = UIColor(hexStr: "#F7F7F8") setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func updateSectionTitles(_ titles: [String]) { sectionTitles = titles } func updateSearchPlaceholder(isHidden: Bool) { searchPlaceholderStack.isHidden = isHidden } private func setupUI() { addSubview(navView) addSubview(searchField) addSubview(tableView) searchField.addSubview(searchPlaceholderStack) [navView, searchField, tableView, searchPlaceholderStack].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ navView.topAnchor.constraint(equalTo: topAnchor), navView.leadingAnchor.constraint(equalTo: leadingAnchor), navView.trailingAnchor.constraint(equalTo: trailingAnchor), navView.heightAnchor.constraint(equalToConstant: kNaviHeight), searchField.topAnchor.constraint(equalTo: navView.bottomAnchor, constant: 20), searchField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15), searchField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -15), searchField.heightAnchor.constraint(equalToConstant: 38), searchPlaceholderStack.centerXAnchor.constraint(equalTo: searchField.centerXAnchor), searchPlaceholderStack.centerYAnchor.constraint(equalTo: searchField.centerYAnchor), tableView.topAnchor.constraint(equalTo: searchField.bottomAnchor, constant: 20), tableView.leadingAnchor.constraint(equalTo: leadingAnchor), tableView.trailingAnchor.constraint(equalTo: trailingAnchor), tableView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) } lazy var navView: BaseNavigationView = BaseNavigationView(title: "圈子成员") lazy var searchField: UISearchTextField = { let textField = UISearchTextField() textField.backgroundColor = .white textField.borderStyle = .none textField.layer.cornerRadius = 12 textField.clipsToBounds = true textField.font = .systemFont(ofSize: 15, weight: .regular) textField.textColor = UIColor(hexStr: "#293445") textField.tintColor = UIColor(hexStr: "#293445") textField.returnKeyType = .search textField.clearButtonMode = .whileEditing textField.leftView = UIView(frame: .zero) textField.leftViewMode = .never return textField }() private lazy var searchPlaceholderStack: UIStackView = { let imageView = UIImageView(image: UIImage(named: "Group/remove_member_search")) imageView.contentMode = .scaleAspectFit imageView.widthAnchor.constraint(equalToConstant: 16).isActive = true imageView.heightAnchor.constraint(equalToConstant: 16).isActive = true let label = UILabel() label.text = "搜索" label.textColor = UIColor(hexStr: "#B0B0B5") label.font = .systemFont(ofSize: 15, weight: .regular) let stack = UIStackView(arrangedSubviews: [imageView, label]) stack.axis = .horizontal stack.alignment = .center stack.spacing = 6 stack.isUserInteractionEnabled = false return stack }() lazy var tableView: UITableView = { let view = UITableView(frame: .zero, style: .plain) view.backgroundColor = .clear view.separatorStyle = .none view.rowHeight = GroupMemberDirectoryCell.rowHeight view.estimatedRowHeight = GroupMemberDirectoryCell.rowHeight view.sectionHeaderHeight = 42 view.estimatedSectionHeaderHeight = 42 view.showsVerticalScrollIndicator = false view.keyboardDismissMode = .onDrag view.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: kSafeBottomMargin + 24, right: 0) if #available(iOS 15.0, *) { view.sectionHeaderTopPadding = 0 } view.register(GroupMemberDirectoryCell.self) return view }() } extension GroupMemberListView: UITableViewDelegate { func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { guard section < sectionTitles.count else { return nil } let container = UIView() let label = UILabel() label.text = sectionTitles[section] label.textColor = UIColor(hexStr: "#777A80") label.font = .systemFont(ofSize: 14, weight: .regular) container.addSubview(label) label.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16), label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -8) ]) return container } func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 42 } } final class GroupMemberDirectoryCell: UITableViewCell { static let rowHeight: CGFloat = 52 override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear contentView.backgroundColor = .clear setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(_ member: GroupMemberModel, showsSeparator: Bool, roundsTopCorners: Bool, roundsBottomCorners: Bool) { avatarView.image = member.userIcon nicknameLabel.text = member.nick_name.isEmpty ? "未知昵称" : member.nick_name separatorView.isHidden = !showsSeparator var corners: CACornerMask = [] if roundsTopCorners { corners.formUnion([.layerMinXMinYCorner, .layerMaxXMinYCorner]) } if roundsBottomCorners { corners.formUnion([.layerMinXMaxYCorner, .layerMaxXMaxYCorner]) } cardView.layer.maskedCorners = corners } private func setupUI() { contentView.addSubview(cardView) cardView.addSubview(avatarView) cardView.addSubview(nicknameLabel) cardView.addSubview(separatorView) [cardView, avatarView, nicknameLabel, separatorView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ cardView.topAnchor.constraint(equalTo: contentView.topAnchor), cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15), cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15), cardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), avatarView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16), avatarView.centerYAnchor.constraint(equalTo: cardView.centerYAnchor), avatarView.widthAnchor.constraint(equalToConstant: 38), avatarView.heightAnchor.constraint(equalToConstant: 38), nicknameLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 12), nicknameLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -16), nicknameLabel.centerYAnchor.constraint(equalTo: cardView.centerYAnchor), separatorView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 20), separatorView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -13), separatorView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor), separatorView.heightAnchor.constraint(equalToConstant: 0.5) ]) } private lazy var cardView: UIView = { let view = UIView() view.backgroundColor = .white view.layer.cornerRadius = 12 view.clipsToBounds = true return view }() private lazy var avatarView: UIImageView = { let view = UIImageView() view.backgroundColor = UIColor(hexStr: "#F1F2F4") view.contentMode = .scaleAspectFill view.layer.cornerRadius = 10 view.clipsToBounds = true return view }() private lazy var nicknameLabel: UILabel = { let label = UILabel() label.textColor = UIColor(hexStr: "#111111") label.font = .systemFont(ofSize: 16, weight: .regular) label.lineBreakMode = .byTruncatingTail return label }() private lazy var separatorView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#EDEEF0") return view }() } // This compact cell is shared by the map and member information pages. final class GroupMemberListCell: UICollectionViewCell { private static let sosBlinkKey = "sosBlink" override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func prepareForReuse() { super.prepareForReuse() updateSOSBlink(false) bubbleImageView.isHidden = true } func configure(model: GroupMemberModel, isCurrentUser: Bool, isSelected: Bool) { avatarImageView.image = model.userIcon nameLabel.text = model.nick_name nameLabel.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846") selectionImageView.isHidden = !isSelected vipBackgroundView.isHidden = model.level == 1 let isSOS = model.status == 1 updateSOSBlink(isSOS) bubbleImageView.isHidden = isSOS || model.status != 2 } private func updateSOSBlink(_ blinking: Bool) { sosImageView.layer.removeAnimation(forKey: Self.sosBlinkKey) sosImageView.isHidden = !blinking sosImageView.alpha = 1 guard blinking else { return } let animation = CABasicAnimation(keyPath: "opacity") animation.fromValue = 1 animation.toValue = 0.2 animation.duration = 0.6 animation.autoreverses = true animation.repeatCount = .infinity sosImageView.layer.add(animation, forKey: Self.sosBlinkKey) } private func setupUI() { contentView.addSubview(selectionImageView) contentView.addSubview(avatarImageView) contentView.addSubview(bubbleImageView) contentView.addSubview(sosImageView) contentView.addSubview(vipBackgroundView) contentView.addSubview(nameLabel) [selectionImageView, avatarImageView, bubbleImageView, sosImageView, vipBackgroundView, nameLabel].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ selectionImageView.topAnchor.constraint(equalTo: contentView.topAnchor), selectionImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), selectionImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), selectionImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 9), avatarImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), avatarImageView.widthAnchor.constraint(equalToConstant: 50), avatarImageView.heightAnchor.constraint(equalToConstant: 50), bubbleImageView.topAnchor.constraint(equalTo: avatarImageView.topAnchor), bubbleImageView.leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor), bubbleImageView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor), bubbleImageView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), sosImageView.topAnchor.constraint(equalTo: avatarImageView.topAnchor), sosImageView.leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor), sosImageView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor), sosImageView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), vipBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 7), vipBackgroundView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), nameLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 7), nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) ]) } private lazy var selectionImageView: UIImageView = { let view = UIImageView(image: UIImage(named: "GroupMemberList/selected_bg")) view.contentMode = .scaleAspectFill view.isHidden = true return view }() private lazy var avatarImageView: UIImageView = { let view = UIImageView() view.backgroundColor = UIColor(hexStr: "#F1F2F4") view.contentMode = .scaleAspectFill view.layer.cornerRadius = 25 view.clipsToBounds = true return view }() private lazy var bubbleImageView: UIImageView = { let view = UIImageView(image: UIImage(named: "GroupMemberList/bubble")) view.contentMode = .scaleAspectFit view.isHidden = true return view }() private lazy var sosImageView: UIImageView = { let view = UIImageView(image: UIImage(named: "GroupMemberList/sos")) view.contentMode = .scaleAspectFit view.isHidden = true return view }() private lazy var vipBackgroundView: UIImageView = { let view = UIImageView(image: UIImage(named: "GroupMemberList/vip")) view.isHidden = true return view }() private lazy var nameLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12, weight: .medium) label.textAlignment = .center label.lineBreakMode = .byTruncatingTail return label }() }