// // GroupMemberListView.swift // QuickLocation // // Created by 八条 on 2026/6/29. // import UIKit import RxSwift import RxCocoa class GroupMemberListView: UIView { var disposeBag = DisposeBag() func updateArrowVisibility() { let offsetX = collectionView.contentOffset.x let contentW = collectionView.contentSize.width let viewW = collectionView.bounds.width memberArrowLeft.isHidden = offsetX <= 0 memberArrowRight.isHidden = offsetX >= contentW - viewW } private func setupRx() { backBtn.rx.tap.subscribe(onNext: { _ in AppRouter.shared.popOrDismiss() }).disposed(by: disposeBag) collectionView.rx.contentOffset .subscribe(onNext: { [weak self] _ in self?.updateArrowVisibility() }) .disposed(by: disposeBag) // 选中日期变化时更新月份 selectedDate.subscribe(onNext: { [weak self] date in guard let self = self else { return } let fmt = DateFormatter() fmt.dateFormat = "MM月" self.monthLab.text = fmt.string(from: date) }).disposed(by: disposeBag) // 往前/往后翻 7 天 datePreviousBtn.rx.tap.subscribe(onNext: { [weak self] _ in guard let self = self else { return } let target = self.dateCollectionView.contentOffset.x - self.dateItemWidth * CGFloat(self.daysPerPage) self.dateCollectionView.setContentOffset(CGPoint(x: max(0, target), y: 0), animated: true) }).disposed(by: disposeBag) dateNextBtn.rx.tap.subscribe(onNext: { [weak self] _ in guard let self = self else { return } var target = self.dateCollectionView.contentOffset.x + self.dateItemWidth * CGFloat(self.daysPerPage) let maxOffset = CGFloat(self.dateItems.count) * self.dateItemWidth - self.dateCollectionView.bounds.width target = min(max(0, maxOffset), target) self.dateCollectionView.setContentOffset(CGPoint(x: target, y: 0), animated: true) }).disposed(by: disposeBag) } private func setupUI() { addSubview(navBgView) addSubview(navBarView) navBarView.addSubview(navTitleLabel) navBarView.addSubview(backBtn) addSubview(memberArrowLeft) addSubview(collectionView) addSubview(memberArrowRight) addSubview(reportView) navBgView.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(160/375) navBarView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) navTitleLabel.layoutChain .top(kStatusBarHeight + 12) .centerY(backBtn) .centerX() backBtn.layoutChain .centerY(navTitleLabel) .left(15) .width(24) .height(24) collectionView.layoutChain .topToBottomOfView(navBarView, offset: 0) .height(90) .edgesHorzontal(28) memberArrowLeft.layoutChain .rightToLeftOfView(collectionView, offset: -8) .height(14) .width(5) .centerY(collectionView) memberArrowRight.layoutChain .leftToRightOfView(collectionView, offset: 8) .height(14) .width(5) .centerY(collectionView) reportView.layoutChain .topToBottomOfView(collectionView, offset: 10) .edges(excludingEdge: .top) } lazy var navBgView: UIImageView = { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_2") iv.contentMode = .scaleAspectFill return iv }() lazy var navBarView: UIView = { let view = UIView() view.backgroundColor = .clear return view }() lazy var navTitleLabel: UILabel = { let label = UILabel() label.text = "圈子成员" label.font = .systemFont(ofSize: 18, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor label.textAlignment = .center return label }() lazy var backBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Common/back"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100) return btn }() // MARK: - 成员列表 lazy var memberArrowLeft: UIImageView = { let view = UIImageView() view.image = UIImage(named: "GroupMemberList/member_left") view.isHidden = true return view }() lazy var memberArrowRight: UIImageView = { let view = UIImageView() view.image = UIImage(named: "GroupMemberList/member_right") view.isHidden = true return view }() lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() let cvWidth = kScreenWidth - 56 layout.itemSize = CGSize(width: 65, height: 90) layout.minimumLineSpacing = 4 layout.scrollDirection = .horizontal let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = .clear cv.showsHorizontalScrollIndicator = false cv.register(GroupMemberListCell.self) return cv }() // MARK: - 报告 lazy var reportView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#F5FBFF") view.layer.cornerRadius = 20 view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] let monthView = UIView() monthView.backgroundColor = UIColor(hexStr: "#57C7FF") monthView.cornerRadius = 6 monthView.addSubview(monthLab) monthLab.layoutChain .edgesVertical(2) .edgesHorzontal(12) view.addSubview(monthView) monthView.layoutChain .top(15) .left(15) let totalLab = UILabel() totalLab.text = "本周总计里程" totalLab.font = .systemFont(ofSize: 10, weight: .medium) totalLab.textColor = UIColor(hexStr: "#333333") view.addSubview(totalLab) totalLab.layoutChain .leftToRightOfView(monthView, offset: 21) .centerY(monthView) view.addSubview(mileageLab) mileageLab.layoutChain .leftToRightOfView(totalLab, offset: 5) .centerY(monthView) let highLab = UILabel() highLab.text = "本周最高时速" highLab.font = .systemFont(ofSize: 10, weight: .medium) highLab.textColor = UIColor(hexStr: "#333333") view.addSubview(highLab) highLab.layoutChain .leftToRightOfView(mileageLab, offset: 23) .centerY(monthView) view.addSubview(speedLab) speedLab.layoutChain .leftToRightOfView(highLab, offset: 5) .centerY(monthView) view.addSubview(dateView) dateView.layoutChain .topToBottomOfView(monthView, offset: 10) .edgesHorzontal() view.addSubview(scrollView) scrollView.layoutChain .topToBottomOfView(dateView) .edges(excludingEdge: .top) return view }() lazy var monthLab: UILabel = { let label = UILabel() label.text = " " label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = UIColor(hexStr: "#0F2846") label.textAlignment = .center return label }() /// 里程 lazy var mileageLab: UILabel = { let label = UILabel() label.text = "0km" label.font = .systemFont(ofSize: 16, weight: .medium) label.textColor = UIColor(hexStr: "#16B3FF") return label }() /// 速度 lazy var speedLab: UILabel = { let label = UILabel() label.text = "0km/h" label.font = .systemFont(ofSize: 16, weight: .medium) label.textColor = UIColor(hexStr: "#16B3FF") return label }() // MARK: - 日期 private var dateItems: [DateItem] = [] let selectedDate = BehaviorRelay(value: Date()) private let daysPerPage = 7 /// 当前选中的成员是否是自己 var selectedMemberIsSelf = true { didSet { updateDateSelectability() } } struct DateItem { let date: Date let day: Int let isToday: Bool let isFuture: Bool let isSelectable: Bool } /// 根据 VIP 和成员关系计算最大可查询天数 private var maxSelectableDays: Int { switch AppContextManager.shared.vip { case 1: return 0 // 非会员不可点 case 2: return selectedMemberIsSelf ? 7 : 1 case 3: return selectedMemberIsSelf ? 30 : 14 default: return 0 } } /// 重新设置 dateItems 的 isSelectable private func updateDateSelectability() { let calendar = Calendar.current let today = Date() let maxDays = maxSelectableDays dateItems = dateItems.map { item in let daysAgo = calendar.dateComponents([.day], from: item.date, to: today).day ?? 0 return DateItem(date: item.date, day: item.day, isToday: item.isToday, isFuture: item.isFuture, isSelectable: maxDays > 0 && !item.isFuture && daysAgo <= maxDays - 1) } dateCollectionView.reloadData() } private func generateDateItems() { let calendar = Calendar.current let today = Date() // 31 天前 → 今天 → 之后 3 天 = 35 天,正好 5 页 × 7 天 let pastDays = 31 let futureDays = 3 let totalDays = pastDays + 1 + futureDays // 35 dateItems = (0.. today let daysAgo = calendar.dateComponents([.day], from: date, to: today).day ?? 0 return DateItem(date: date, day: day, isToday: calendar.isDateInToday(date), isFuture: isFuture, isSelectable: maxSelectableDays > 0 && !isFuture && daysAgo <= maxSelectableDays - 1) } } /// 日期 lazy var dateView: UIView = { let view = UIView() view.backgroundColor = .clear generateDateItems() view.addSubview(datePreviousBtn) view.addSubview(dateNextBtn) view.addSubview(dateCollectionView) datePreviousBtn.layoutChain .left(15) .edgesVertical(10) .width(20) .height(20) dateNextBtn.layoutChain .right(15) .centerY() .width(20) .height(20) dateCollectionView.layoutChain .leftToRightOfView(datePreviousBtn, offset: 5) .rightToLeftOfView(dateNextBtn, offset: -5) .edgesVertical() // 直接翻到最后一页(第 4 页,index 28 开始),今天在第 3 位 DispatchQueue.main.async { let page: CGFloat = 4 // 第 5 页,items 28-34 let offset = page * CGFloat(self.daysPerPage) * self.dateItemWidth self.dateCollectionView.contentOffset.x = offset } return view }() private var dateItemWidth: CGFloat { let available = kScreenWidth - 15 - 20 - 5 - 5 - 20 - 15 return floor(available / CGFloat(daysPerPage)) } lazy var dateCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.itemSize = CGSize(width: dateItemWidth, height: 40) layout.minimumLineSpacing = 0 layout.scrollDirection = .horizontal layout.sectionInset = .zero let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = .clear cv.showsHorizontalScrollIndicator = false cv.isPagingEnabled = true cv.register(DateCell.self) cv.dataSource = self cv.delegate = self return cv }() lazy var datePreviousBtn: UIButton = { let btn = UIButton() btn.setImage(UIImage(named: "GroupMemberList/date_left"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 0) return btn }() lazy var dateNextBtn: UIButton = { let btn = UIButton() btn.setImage(UIImage(named: "GroupMemberList/date_right"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 15, left: 0, bottom: 15, right: 15) return btn }() // MARK: - 驾驶分析 lazy var scrollView: UIScrollView = { let view = UIScrollView() view.backgroundColor = .clear view.showsVerticalScrollIndicator = false view.bounces = false view.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: kSafeBottomMargin, right: 0) let contentView = UIView() contentView.backgroundColor = .clear view.addSubview(contentView) contentView.layoutChain.edges().widthToView(view) contentView.addSubview(drivingAnalysisView) drivingAnalysisView.layoutChain .top(5) .edgesHorzontal(15) .height(249) .bottom(20) return view }() lazy var drivingAnalysisView: UIView = { let view = UIView() view.backgroundColor = .white view.cornerRadius = 10 let titleBg = UIImageView(image: UIImage(named: "GroupMemberList/title_bg")) view.addSubview(titleBg) let titleLab = UILabel() titleLab.text = "驾驶分析" titleLab.font = .systemFont(ofSize: 16, weight: .medium) view.addSubview(titleLab) titleLab.layoutChain .top(15) .centerX() titleBg.layoutChain .centerX() .bottomToView(titleLab, offset: 5) view.addSubview(drivingEventCV) drivingEventCV.layoutChain .topToBottomOfView(titleBg, offset: 20) .edgesHorzontal() .bottom(20) return view }() lazy var drivingEventCV: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.itemSize = CGSize(width: 66, height: 75) layout.minimumLineSpacing = 15 layout.scrollDirection = .vertical layout.sectionInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12) let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = .clear cv.showsHorizontalScrollIndicator = false cv.isScrollEnabled = false cv.register(DrivingEventCell.self) return cv }() override func layoutSubviews() { super.layoutSubviews() updateArrowVisibility() } override init(frame: CGRect) { super.init(frame: .zero) backgroundColor = .white setupUI() setupRx() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // MARK: - UICollectionViewDataSource & Delegate (日期) extension GroupMemberListView: UICollectionViewDataSource, UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { dateItems.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell: DateCell = collectionView.dequeueReusableCell(for: indexPath) let item = dateItems[indexPath.row] let isSel = Calendar.current.isDate(item.date, inSameDayAs: selectedDate.value) cell.configure(item: item, isSelected: isSel) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard collectionView == dateCollectionView else { return } let item = dateItems[indexPath.row] guard item.isSelectable else { return } let oldDate = selectedDate.value selectedDate.accept(item.date) if let oldRow = dateItems.firstIndex(where: { Calendar.current.isDate($0.date, inSameDayAs: oldDate) }), let oldCell = dateCollectionView.cellForItem(at: IndexPath(row: oldRow, section: 0)) as? DateCell { oldCell.configure(item: dateItems[oldRow], isSelected: false) } if let newCell = dateCollectionView.cellForItem(at: indexPath) as? DateCell { newCell.configure(item: item, isSelected: true) } } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { guard scrollView == dateCollectionView else { return } let targetX = targetContentOffset.pointee.x let idx = round(targetX / dateItemWidth) targetContentOffset.pointee.x = idx * dateItemWidth } } // MARK: - DateCell class DateCell: UICollectionViewCell { private let bgView: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#16B3FF") v.cornerRadius = 11 v.isHidden = true return v }() private let dayLab: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 14, weight: .medium) l.textAlignment = .center return l }() override init(frame: CGRect) { super.init(frame: frame) contentView.addSubview(bgView) contentView.addSubview(dayLab) bgView.layoutChain.centerX().centerY().width(36).height(22) dayLab.layoutChain.centerX().centerY() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(item: GroupMemberListView.DateItem, isSelected: Bool) { dayLab.text = "\(item.day)" let disabled = item.isFuture || !item.isSelectable bgView.isHidden = !isSelected || disabled dayLab.textColor = disabled ? UIColor(hexStr: "#D1D1D6") : isSelected ? .white : UIColor(hexStr: "#333333") } } // MARK: - GroupMemberListCell class GroupMemberListCell: UICollectionViewCell { func configure(model: GroupMemberModel, isCurrentUser: Bool, isSelected: Bool) { avaterImgView.image = model.userIcon vipIcon.image = model.vipIcon nameLab.text = model.nick_name nameLab.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846") selectedBgView.isHidden = !isSelected // 会员权益 if AppContextManager.shared.vip > 1, model.is_online { batteryInfoView.isHidden = model.battery.int == 0 // 电量 16是电池图标宽度,右边有电池造型需要减去 let batteryPercent = min(CGFloat(model.battery.int), 100) batteryView.layoutChain.width(CGFloat(16 - 1) * batteryPercent / 100.0) batteryLab.text = "\(model.battery)%" } } private func setupSubviews() { contentView.addSubview(selectedBgView) contentView.addSubview(avaterImgView) contentView.addSubview(vipIcon) contentView.addSubview(batteryInfoView) batteryInfoView.addSubview(cornerView) cornerView.addSubview(batteryView) cornerView.addSubview(batteryIcon) cornerView.addSubview(batteryLab) contentView.addSubview(nameLab) setupLayout() } private func setupLayout() { selectedBgView.layoutChain.edges() avaterImgView.layoutChain .top(11) .edgesHorzontal(10) .heightToWidth(1) batteryInfoView.layoutChain .leftToView(avaterImgView) .rightToView(avaterImgView) .bottomToView(avaterImgView) .height(12) cornerView.layoutChain.edges() batteryIcon.layoutChain .left(7) .centerY() .width(16) .height(8) batteryView.layoutChain .topToView(batteryIcon) .leftToView(batteryIcon, offset: -1) .bottomToView(batteryIcon) batteryLab.layoutChain .leftToRightOfView(batteryIcon, offset: 4) .right(5) .centerY() vipIcon.layoutChain .topToView(avaterImgView, offset: -8) .leftToView(avaterImgView, offset: -6) .width(25) .height(21) nameLab.layoutChain .topToBottomOfView(batteryInfoView, offset: 4) .edgesHorzontal() } lazy var selectedBgView: UIImageView = { let view = UIImageView(image: UIImage(named: "GroupMemberList/selected_bg")) view.contentMode = .scaleAspectFill view.isHidden = true return view }() lazy var avaterImgView: UIImageView = { let view = UIImageView() view.backgroundColor = .lightGray view.contentMode = .scaleAspectFill view.cornerRadius = 25 return view }() lazy var batteryInfoView: UIView = { let view = UIView() view.backgroundColor = .clear view.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.1).cgColor view.layer.shadowOffset = CGSize(width: 0, height: 2) view.layer.shadowOpacity = 1 view.layer.shadowRadius = 6 view.isHidden = true return view }() lazy var cornerView: UIView = { let view = UIView() view.backgroundColor = .white view.cornerRadius = 6 return view }() lazy var batteryView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#75E582") return view }() lazy var batteryIcon: UIImageView = { let view = UIImageView() view.backgroundColor = .clear view.image = UIImage(named: "Home/battery") return view }() lazy var batteryLab: UILabel = { let label = UILabel() label.textColor = UIColor(hexStr: "#D4D4D4") label.font = .systemFont(ofSize: 6, weight: .medium) return label }() lazy var vipIcon: UIImageView = { let view = UIImageView() return view }() lazy var nameLab: UILabel = { let label = UILabel() label.textColor = UIColor(hexStr: "#0F2846") label.font = .systemFont(ofSize: 12, weight: .medium) label.textAlignment = .center return label }() override func awakeFromNib() { super.awakeFromNib() // Initialization code } override init(frame: CGRect) { super.init(frame: frame) setupSubviews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // MARK: - DrivingEventCell class DrivingEventCell: UICollectionViewCell { func configure(_ item: DrivingEventItem) { iconView.image = UIImage(named: item.iconName) nameLab.text = "\(item.title)\n(\(item.count))" } private let bgView: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#F5FBFF") v.cornerRadius = 8 return v }() lazy var iconView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFill return view }() lazy var nameLab: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 12, weight: .regular) l.textAlignment = .center l.numberOfLines = 0 return l }() override init(frame: CGRect) { super.init(frame: frame) contentView.addSubview(bgView) contentView.addSubview(iconView) contentView.addSubview(nameLab) bgView.layoutChain .edges(excludingEdge: .top) .height(60) iconView.layoutChain .top() .centerX() .width(32) .heightToWidth(1) nameLab.layoutChain .topToBottomOfView(iconView, offset: 8) .edgesHorzontal() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }