// // GroupView.swift // QuickLocation // // Created by 八条 on 2026/5/29. // import UIKit import RxSwift import RxCocoa import SDCycleScrollView import OpenIMSDK import SwiftDate // MARK: - PanScrollView(参考 PanScrollView,允许嵌套手势同时识别) class PanScrollView: UIScrollView { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } } class GroupView: UIView { var disposeBag = DisposeBag() /// 0 = 圈子,1 = 行程 private(set) var currentTopTab: Int = 0 /// 当前是否吸顶 private var isSticky = false /// 内部 tableview 是否可滚动 private var isSubCanScroll = false var stickThreshold: CGFloat { // segment 在 listContainer 内,吸顶以 listContainer 顶部为准 return listContainer.dl.y } // MARK: - Setup private func setupUI() { addSubview(navBgView) addSubview(topBar) topBar.addSubview(circleTabLabel) topBar.addSubview(itineraryTabLabel) topBar.addSubview(messageBtn) addSubview(circlePage) circlePage.addSubview(mainScrollView) mainScrollView.addSubview(contentView) contentView.addSubview(actionButtonsView) actionButtonsView.addSubview(createGroupBtn) actionButtonsView.addSubview(joinGroupBtn) contentView.addSubview(listContainer) listContainer.addSubview(segmentView) segmentView.addSubview(joinedTabLabel) segmentView.addSubview(createdTabLabel) segmentView.addSubview(tabIndicator) listContainer.addSubview(segmentScrollView) segmentScrollView.addSubview(segmentContentView) // 全部圈子(joined) | 我的圈子(created) segmentContentView.addSubview(joinedTableView) segmentContentView.addSubview(createdTableView) // 保留绑定入口,设计无轮播/热门时隐藏 contentView.addSubview(cycleScrollView) contentView.addSubview(hotGroupTitleLabel) contentView.addSubview(hotGroupsCollectionView) cycleScrollView.isHidden = true hotGroupTitleLabel.isHidden = true hotGroupsCollectionView.isHidden = true addSubview(itineraryPage) itineraryPage.isHidden = true navBgView.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(160/375) topBar.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) circleTabLabel.layoutChain .left(20) .bottom(10) .height(28) itineraryTabLabel.layoutChain .leftToRightOfView(circleTabLabel, offset: 20) .centerY(circleTabLabel) .height(28) messageBtn.layoutChain .right(16) .centerY(circleTabLabel) .width(28) .height(28) circlePage.layoutChain .topToBottomOfView(topBar) .edges(excludingEdge: .top) itineraryPage.layoutChain .topToBottomOfView(topBar) .edges(excludingEdge: .top) mainScrollView.layoutChain .edges() contentView.layoutChain .edges() .widthToView(mainScrollView) actionButtonsView.layoutChain .top(12) .edgesHorzontal(16) .height(108) createGroupBtn.layoutChain .left() .edgesVertical() .widthToView(joinGroupBtn) joinGroupBtn.layoutChain .topToView(createGroupBtn) .bottomToView(createGroupBtn) .leftToRightOfView(createGroupBtn, offset: 12) .right() .widthToView(createGroupBtn) listContainer.layoutChain .topToBottomOfView(actionButtonsView, offset: 16) .edgesHorzontal() .bottom() let labelWidth = (UIScreen.main.bounds.width - 60) / 2 segmentView.layoutChain .top() .edgesHorzontal() .height(44) joinedTabLabel.layoutChain .left(30).centerY() .width(labelWidth) createdTabLabel.layoutChain .right(30).centerY() .width(labelWidth) tabIndicator.layoutChain .topToBottomOfView(joinedTabLabel, offset: 4) .centerX(joinedTabLabel) .width(19).height(4) segmentScrollView.layoutChain .topToBottomOfView(segmentView) .edgesHorzontal() .bottom() .height(kScreenHeight - kNaviHeight - 44 - 12 - 108 - 16) segmentContentView.layoutChain .edges() .heightToView(segmentScrollView) let svWidth = UIScreen.main.bounds.width joinedTableView.layoutChain .top().bottom().left() .width(svWidth) createdTableView.layoutChain .top().bottom() .leftToRightOfView(joinedTableView) .width(svWidth) // 离屏占位,避免影响布局 cycleScrollView.layoutChain .top(0).left(0).width(0).height(0) hotGroupTitleLabel.layoutChain .top(0).left(0).width(0).height(0) hotGroupsCollectionView.layoutChain .top(0).left(0).width(0).height(0) configureActionCard( createGroupBtn, bgColor: UIColor(hexStr: "#D6F3FF"), title: "创建圈子", subtitle: "和朋友一起定位玩" ) configureActionCard( joinGroupBtn, bgColor: UIColor(hexStr: "#E8F8C8"), title: "加入圈子", subtitle: "输入邀请码快速加入" ) } private func configureActionCard(_ card: UIView, bgColor: UIColor, title: String, subtitle: String) { card.backgroundColor = bgColor card.cornerRadius = 16 card.clipsToBounds = true let titleLab = UILabel() titleLab.text = title titleLab.font = .systemFont(ofSize: 16, weight: .bold) titleLab.textColor = UIColor(hexStr: "#0F2846") let subLab = UILabel() subLab.text = subtitle subLab.font = .systemFont(ofSize: 11, weight: .medium) subLab.textColor = UIColor(hexStr: "#6B7A90") let cta = UILabel() cta.text = "立即体验" cta.font = .systemFont(ofSize: 12, weight: .semibold) cta.textColor = UIColor(hexStr: "#0F2846") cta.textAlignment = .center cta.backgroundColor = .white cta.cornerRadius = 12 cta.clipsToBounds = true card.addSubview(titleLab) card.addSubview(subLab) card.addSubview(cta) titleLab.layoutChain .top(16) .left(14) .right(14) subLab.layoutChain .topToBottomOfView(titleLab, offset: 6) .left(14) .right(14) cta.layoutChain .left(14) .bottom(14) .width(72) .height(24) } override func layoutSubviews() { super.layoutSubviews() segmentContentView.layoutChain.width(bounds.width * 2) listContainer.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] listContainer.layer.cornerRadius = 20 listContainer.clipsToBounds = true } // MARK: - Nav lazy var navBgView: UIImageView = { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_1") iv.contentMode = .scaleAspectFill return iv }() // MARK: - Top Bar lazy var topBar: UIView = { let v = UIView() v.backgroundColor = .clear return v }() lazy var circleTabLabel: UILabel = { let label = UILabel() label.text = "圈子" label.font = FontManager.youSheBiaoTiHei(32) label.textColor = UIColor(hexStr: "#0F2846") label.isUserInteractionEnabled = true return label }() lazy var itineraryTabLabel: UILabel = { let label = UILabel() label.text = "行程" label.font = FontManager.youSheBiaoTiHei(24) label.textColor = UIColor(hexStr: "#0F2846").withAlphaComponent(0.45) label.isUserInteractionEnabled = true return label }() lazy var messageBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Home/message"), for: .normal) btn.isHidden = true return btn }() lazy var navView: BaseNavigationView = { let nav = BaseNavigationView(title: "圈子") nav.isHidden = true return nav }() lazy var scanBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Group/scan"), for: .normal) btn.isHidden = true return btn }() // MARK: - Pages lazy var circlePage: UIView = { let v = UIView() v.backgroundColor = .clear return v }() lazy var itineraryPage: GroupItineraryView = { let v = GroupItineraryView() return v }() // MARK: - Main Scroll lazy var mainScrollView: PanScrollView = { let sv = PanScrollView() sv.backgroundColor = .clear sv.delegate = self sv.showsVerticalScrollIndicator = false sv.bounces = false return sv }() lazy var contentView: UIView = { let v = UIView() v.backgroundColor = .clear return v }() // MARK: - 轮播图(隐藏保留绑定) lazy var cycleScrollView: SDCycleScrollView = { let view = SDCycleScrollView(frame: .zero) view.backgroundColor = .lightGray view.scrollDirection = .horizontal view.showPageControl = true view.bannerImageViewContentMode = .scaleAspectFill view.autoScrollTimeInterval = 5 view.currentPageDotColor = UIColor(hexStr: "#16B3FF") view.pageDotColor = UIColor(hexStr: "#7AD6FF").withAlphaComponent(0.4) view.pageControlDotSize = CGSize(width: 8, height: 8) view.clipsToBounds = false view.pageControlBottomOffset = -34 return view }() // MARK: - 创建 / 加入 圈子(色卡,整卡可点) private lazy var actionButtonsView: UIView = { let v = UIView() v.backgroundColor = .clear return v }() lazy var createGroupBtn: UIView = { let v = UIView() v.isUserInteractionEnabled = true return v }() lazy var joinGroupBtn: UIView = { let v = UIView() v.isUserInteractionEnabled = true return v }() // MARK: - 热门酷圈(隐藏保留绑定) private lazy var hotGroupTitleLabel: UILabel = { let label = UILabel() label.text = "热门酷圈" label.font = .systemFont(ofSize: 16, weight: .semibold) label.textColor = ThemeManager.shared.color.titleAuxColor return label }() private(set) lazy var hotGroupsCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.itemSize = CGSize(width: 75, height: 88) layout.minimumLineSpacing = 10 layout.sectionInset = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 18) let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = .clear cv.showsHorizontalScrollIndicator = false cv.register(HotGroupCell.self, forCellWithReuseIdentifier: HotGroupCell.reuseId) return cv }() // MARK: - List container private lazy var listContainer: UIView = { let v = UIView() v.backgroundColor = .white return v }() // MARK: - Segment(0 全部圈子=joined,1 我的圈子=created) private(set) lazy var segmentView: UIView = { let v = UIView() v.backgroundColor = .white let line = UIView() line.backgroundColor = UIColor(hexStr: "#B5B5B5").withAlphaComponent(0.12) // v.addSubview(line) // line.layoutChain // .edgesHorzontal() // .height(1) // .bottom(4) return v }() /// 「全部圈子」→ joined 列表(index 0) private(set) lazy var joinedTabLabel: UILabel = { let label = UILabel() label.text = "全部圈子" label.font = .systemFont(ofSize: 16, weight: .heavy) label.textColor = UIColor(hexStr: "#2A3648") label.textAlignment = .center label.isUserInteractionEnabled = true return label }() /// 「我的圈子」→ created 列表(index 1) private(set) lazy var createdTabLabel: UILabel = { let label = UILabel() label.text = "我的圈子" label.font = .systemFont(ofSize: 16, weight: .medium) label.textColor = UIColor(hexStr: "#2A3648") label.textAlignment = .center label.isUserInteractionEnabled = true return label }() private lazy var tabIndicator: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#85D8FF") v.layer.cornerRadius = 2 return v }() // MARK: - 横向滚动容器 (PanScrollView) private(set) lazy var segmentScrollView: PanScrollView = { let sv = PanScrollView() sv.isPagingEnabled = true sv.showsHorizontalScrollIndicator = false sv.bounces = false sv.delegate = self return sv }() private lazy var segmentContentView: UIView = { let v = UIView() v.backgroundColor = .clear return v }() // MARK: - 两个列表 private(set) lazy var createdTableView: UITableView = { let tv = UITableView(frame: .zero, style: .plain) tv.backgroundColor = .clear tv.separatorStyle = .none tv.showsVerticalScrollIndicator = false tv.register(CircleGroupCell.self) tv.estimatedRowHeight = 70 tv.bounces = true tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0) return tv }() private(set) lazy var joinedTableView: UITableView = { let tv = UITableView(frame: .zero, style: .plain) tv.backgroundColor = .clear tv.separatorStyle = .none tv.showsVerticalScrollIndicator = false tv.register(CircleGroupCell.self) tv.estimatedRowHeight = 70 tv.bounces = true tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0) return tv }() // MARK: - Segment var currentSegmentIndex: Int = 0 /// index: 0=全部圈子(joined),1=我的圈子(created) func selectSegment(at index: Int) { currentSegmentIndex = index let isAll = index == 0 UIView.animate(withDuration: 0.2) { self.joinedTabLabel.textColor = isAll ? UIColor(hexStr: "#2A3648") : UIColor(hexStr: "#2A3648", alpha: 0.5) self.joinedTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .heavy : .medium) self.createdTabLabel.textColor = isAll ? UIColor(hexStr: "#2A3648", alpha: 0.5) : UIColor(hexStr: "#2A3648") self.createdTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .medium : .heavy) self.tabIndicator.center.x = isAll ? self.joinedTabLabel.center.x : self.createdTabLabel.center.x self.layoutIfNeeded() } } func selectTopTab(at index: Int) { guard index == 0 || index == 1 else { return } currentTopTab = index let isCircle = index == 0 circlePage.isHidden = !isCircle itineraryPage.isHidden = isCircle circleTabLabel.font = FontManager.youSheBiaoTiHei(isCircle ? 32 : 24) itineraryTabLabel.font = FontManager.youSheBiaoTiHei(isCircle ? 24 : 32) circleTabLabel.textColor = isCircle ? UIColor(hexStr: "#353B4F") : UIColor(hexStr: "#2A3648", alpha: 0.5) itineraryTabLabel.textColor = isCircle ? UIColor(hexStr: "#2A3648", alpha: 0.5) : UIColor(hexStr: "#353B4F") } override init(frame: CGRect) { super.init(frame: .zero) backgroundColor = UIColor(hexStr: "#FAFAFA") setupUI() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // MARK: - UIScrollViewDelegate(参考 PanView + PanSubItemView 通知模式) extension GroupView: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView == mainScrollView { handleMainScroll(scrollView) } else if scrollView == segmentScrollView { let page = Int(round(scrollView.contentOffset.x / scrollView.bounds.width)) if page != currentSegmentIndex, page >= 0, page < 2 { currentSegmentIndex = page selectSegment(at: page) } } } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { guard scrollView != mainScrollView, scrollView != segmentScrollView else { return } if scrollView.contentOffset.y > 0 { isSubCanScroll = true } } /// 由 GroupViewController 通过 rx.didScroll 转发调用 func handleTableViewScroll(_ scrollView: UIScrollView) { if isSubCanScroll { if scrollView.contentOffset.y <= 0 { isSubCanScroll = false isSticky = false scrollView.contentOffset.y = 0 } } else { scrollView.contentOffset.y = 0 } } // MARK: - 主 scroll 逻辑 private func handleMainScroll(_ scrollView: UIScrollView) { if isSubCanScroll { scrollView.contentOffset.y = stickThreshold isSticky = true selectSegment(at: currentSegmentIndex) } else { let offsetY = scrollView.contentOffset.y if offsetY >= stickThreshold { scrollView.contentOffset.y = stickThreshold isSticky = true isSubCanScroll = true selectSegment(at: currentSegmentIndex) } } } } // MARK: - HotGroupCell final class HotGroupCell: UICollectionViewCell { static let reuseId = "HotGroupCell" private let iconView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.layer.cornerRadius = 10 iv.clipsToBounds = true iv.backgroundColor = .clear return iv }() private let nameLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor label.textAlignment = .center return label }() private let countLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .medium) label.textColor = ThemeManager.shared.color.contentColor label.textAlignment = .center return label }() override init(frame: CGRect) { super.init(frame: frame) contentView.addSubview(iconView) contentView.addSubview(nameLabel) contentView.addSubview(countLabel) iconView.layoutChain .top().centerX() .width(50).height(50) nameLabel.layoutChain .topToBottomOfView(iconView, offset: 6) .edgesHorzontal() countLabel.layoutChain .topToBottomOfView(nameLabel, offset: 2) .edgesHorzontal() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(_ model: GroupInfoModel) { iconView.image = model.groupIcon nameLabel.text = model.name countLabel.text = "共\(model.people_no)个成员" } } // MARK: - CircleGroupCell final class CircleGroupCell: UITableViewCell { private let iconView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.cornerRadius = 20 iv.clipsToBounds = true iv.backgroundColor = UIColor(hexStr: "#F0F0F0") return iv }() private let nameLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor return label }() private let countLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .medium) label.textColor = UIColor(hexStr: "#999999") return label }() lazy var unreadBadgeView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#FF383C") view.cornerRadius = 7 view.isHidden = true return view }() private let unreadBadge: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .medium) label.textColor = .white return label }() private let timeLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .regular) label.textColor = UIColor(hexStr: "#999999") label.textAlignment = .right return label }() private let dividerLine: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#F0F0F0") return v }() override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .white contentView.addSubview(iconView) contentView.addSubview(nameLabel) contentView.addSubview(countLabel) contentView.addSubview(unreadBadgeView) unreadBadgeView.addSubview(unreadBadge) contentView.addSubview(timeLabel) contentView.addSubview(dividerLine) iconView.layoutChain .left(15).edgesVertical(15) .width(40).height(40) nameLabel.layoutChain .topToView(iconView, offset: 4) .leftToRightOfView(iconView, offset: 12) .right(90) countLabel.layoutChain .topToBottomOfView(nameLabel, offset: 4) .leftToView(nameLabel) unreadBadgeView.layoutChain .centerY(nameLabel) .right(15) unreadBadge.layoutChain .edgesHorzontal(8) .edgesVertical(1) timeLabel.layoutChain .right(15) .centerY(countLabel) dividerLine.layoutChain .edgesHorzontal(15) .bottom() .height(0.5) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(_ model: GroupCellData) { let g = model.groupInfo iconView.image = model.localIcon nameLabel.text = g.groupName ?? "" countLabel.text = "共\(g.memberCount)个成员" // 未读消息数 if model.unreadCount > 0 { unreadBadgeView.isHidden = false unreadBadge.text = model.unreadCount > 99 ? "99+" : "\(model.unreadCount)" } else { unreadBadgeView.isHidden = true } // 最后消息时间 if model.lastMsgTime > 0 { let date = Date(timeIntervalSince1970: TimeInterval(model.lastMsgTime) / 1000) timeLabel.text = date.formatForIM() } else { timeLabel.text = "" } } } // MARK: - Date IM format private extension Date { func formatForIM() -> String { if isToday { return toFormat("HH:mm") } if isYesterday { return "昨天" } if Calendar.current.isDate(self, equalTo: Date(), toGranularity: .year) { return toFormat("MM/dd") } return toFormat("yyyy/MM/dd") } }