// // GroupMemberListView.swift // QuickLocation // // Created by 八条 on 2026/6/29. // import UIKit import RxSwift import RxCocoa import SwiftDate 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() { 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 } self.monthLab.text = date.toFormat("MM月") }).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(navView) addSubview(memberArrowLeft) addSubview(collectionView) addSubview(memberArrowRight) addSubview(reportView) navBgView.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(160/375) navView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) collectionView.layoutChain .topToBottomOfView(navView, 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 navView: BaseNavigationView = { let nav = BaseNavigationView(title: "圈子成员") return nav }() // 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: - 未解锁会员遮罩 var tipsImg: UIImageView! lazy var unlockVipMaskView: UIView = { let view = UIView() view.backgroundColor = .clear view.isHidden = true let maskBg = UIImageView(image: UIImage(named: "GroupMemberList/mask_bg")) view.addSubview(maskBg) maskBg.layoutChain.edges() tipsImg = UIImageView(image: UIImage(named: "GroupMemberList/mask_tips")) view.addSubview(tipsImg) tipsImg.layoutChain .top(83) .centerX() .width(283) .height(194) let btn = UIButton() btn.setTitle("立即开通", for: .normal) btn.setTitleColor(.white, for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) btn.setBackgroundColor(UIColor(hexStr: "#FF7B24"), for: .normal) btn.cornerRadius = 25 btn.rx.tap.subscribe(onNext: { _ in AppRouter.push(Route.vipRecharge) }).disposed(by: disposeBag) view.addSubview(btn) btn.layoutChain .topToBottomOfView(tipsImg, offset: 23) .centerX() .width(224) .height(50) return view }() // 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) view.addSubview(unlockVipMaskView) unlockVipMaskView.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()) var isSelectedDateLocked: Bool { let today = Date() let calendar = Calendar.current let daysAgo = calendar.dateComponents([.day], from: selectedDate.value, to: today).day ?? 0 return maxSelectableDays > 0 && daysAgo > maxSelectableDays - 1 } 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 let isLocked: 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 let hasAccess = maxDays > 0 dateItems = dateItems.map { item in let daysAgo = calendar.dateComponents([.day], from: item.date, to: today).day ?? 0 let inFreeRange = daysAgo <= maxDays - 1 return DateItem(date: item.date, day: item.day, isToday: item.isToday, isFuture: item.isFuture, isSelectable: hasAccess && !item.isFuture && inFreeRange, isLocked: hasAccess && !item.isFuture && !inFreeRange) } dateCollectionView.reloadData() } func refreshVipState() { updateDateSelectability() selectedDate.accept(Date()) } 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 let hasAccess = maxSelectableDays > 0 let inFreeRange = hasAccess && daysAgo <= maxSelectableDays - 1 return DateItem(date: date, day: day, isToday: calendar.isDateInToday(date), isFuture: isFuture, isSelectable: hasAccess && !isFuture && inFreeRange, isLocked: hasAccess && !isFuture && !inFreeRange) } } /// 日期 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() DispatchQueue.main.async { self.scrollToToday() } 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) contentView.addSubview(recordView) recordView.layoutChain .topToBottomOfView(drivingAnalysisView, offset: 15) .edgesHorzontal(15) .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 }() // MARK: - 行程记录 lazy var recordView: 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(tableView) tableView.layoutChain .topToBottomOfView(titleBg, offset: 10) .edgesHorzontal() .bottom() view.addSubview(nodataLab) nodataLab.layoutChain.centerX().centerY() return view }() lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero, style: .plain) tableView.backgroundColor = .clear tableView.separatorStyle = .none tableView.estimatedRowHeight = 177 tableView.showsVerticalScrollIndicator = false tableView.isScrollEnabled = false tableView.register(ScheduleRecordCell.self) return tableView }() lazy var nodataLab: UILabel = { let l = UILabel() l.text = "暂无行程记录" l.font = .systemFont(ofSize: 12, weight: .regular) l.textColor = UIColor(hexStr: "#999999") return l }() /// 滚动报告区域到顶部 func scrollReportToTop() { scrollView.setContentOffset(.zero, animated: false) } /// 滚到今天所在页(第 5 页,items 28-34,今天在第 3 位) func scrollToToday() { let page: CGFloat = 4 let offset = page * CGFloat(daysPerPage) * dateItemWidth dateCollectionView.contentOffset.x = offset } func updateTableViewHeight(count: Int) { let h = CGFloat(count) * 179 tableView.layoutChain.height(h > 0 ? h : 179) nodataLab.isHidden = h > 0 } 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 } unlockVipMaskView.isHidden = true 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 { private static let sosBlinkKey = "sosBlink" func configure(model: GroupMemberModel, isCurrentUser: Bool, isSelected: Bool) { avaterImgView.image = model.userIcon // vipIcon.image = model.vipIcon vipBgImgView.isHidden = model.level == 1 nameLab.text = model.nick_name nameLab.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846") selectedBgView.isHidden = !isSelected let isSOS = model.status == 1 updateSOSBlink(isSOS) bubbleImgView.isHidden = isSOS || model.status != 2 // 会员权益 // 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 updateSOSBlink(_ blinking: Bool) { sosImgView.layer.removeAnimation(forKey: Self.sosBlinkKey) if blinking { sosImgView.isHidden = false sosImgView.alpha = 1 let anim = CABasicAnimation(keyPath: "opacity") anim.fromValue = 1.0 anim.toValue = 0.2 anim.duration = 0.6 anim.autoreverses = true anim.repeatCount = .infinity anim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) sosImgView.layer.add(anim, forKey: Self.sosBlinkKey) } else { sosImgView.isHidden = true sosImgView.alpha = 1 } } override func prepareForReuse() { super.prepareForReuse() updateSOSBlink(false) bubbleImgView.isHidden = true } private func setupSubviews() { contentView.addSubview(selectedBgView) contentView.addSubview(avaterImgView) contentView.addSubview(bubbleImgView) contentView.addSubview(sosImgView) contentView.addSubview(vipBgImgView) // 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(9) .edgesHorzontal(5.5) .heightToWidth(1) bubbleImgView.layoutChain .topToView(avaterImgView) .leftToView(avaterImgView) .rightToView(avaterImgView) .bottomToView(avaterImgView) sosImgView.layoutChain .topToView(avaterImgView) .leftToView(avaterImgView) .rightToView(avaterImgView) .bottomToView(avaterImgView) vipBgImgView.layoutChain .top(7) .centerX() 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: 8) .edgesHorzontal() } lazy var selectedBgView: UIImageView = { let view = UIImageView(image: UIImage(named: "GroupMemberList/selected_bg")) view.contentMode = .scaleAspectFill view.isHidden = true return view }() lazy var vipBgImgView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "GroupMemberList/vip") 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 bubbleImgView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "GroupMemberList/bubble") view.contentMode = .scaleAspectFit view.isHidden = true return view }() lazy var sosImgView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "GroupMemberList/sos") view.isHidden = true 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") } } // MARK: - ScheduleRecordCells class ScheduleRecordCell: UITableViewCell { func configure(_ model: ScheduleRecordModel) { distanceLab.text = String(format: "%.1fkm", model.distance_km) speedLab.text = String(format: "最高时速 %.0fkm/h", model.max_speed) scoreLab.text = model.score.string dayLab.text = model.start_time.isoStringToCustom(model.start_time, format: "dd") ?? "" yearMonthLab.text = model.start_time.isoStringToCustom(model.start_time, format: "yyyy.MM") ?? "" let startTime = model.start_time.isoStringToCustom(model.start_time, format: "HH:mm") ?? "" let endTime = model.end_time.isoStringToCustom(model.end_time, format: "HH:mm") ?? "" durationLab.text = "\(startTime)-\(endTime)(\(model.duration_minutes)分钟)" guard let start_address = model.start_address, let end_address = model.end_address else { return } startLab.text = start_address.street endLab.text = end_address.street } private func setupViews() { contentView.addSubview(dotView) contentView.addSubview(dayLab) contentView.addSubview(yearMonthLab) contentView.addSubview(distanceLab) contentView.addSubview(durationLab) contentView.addSubview(detailView) detailView.addSubview(headerBgView) detailView.addSubview(speedLab) detailView.addSubview(scroreTitleLab) detailView.addSubview(scoreLab) detailView.addSubview(unitLab) detailView.addSubview(startTitleLab) detailView.addSubview(startLab) detailView.addSubview(endTitleLab) detailView.addSubview(endLab) contentView.addSubview(lineView) dotView.layoutChain .left(15) .width(8) .heightToWidth(1) dayLab.layoutChain .top(15) .leftToRightOfView(dotView, offset: 8) dotView.layoutChain.centerY(dayLab) yearMonthLab.layoutChain .leftToRightOfView(dayLab, offset: 4) .bottomToView(dayLab, offset: -2) distanceLab.layoutChain .leftToRightOfView(yearMonthLab, offset: 8) .bottomToView(yearMonthLab) durationLab.layoutChain .centerY(distanceLab) .leftToRightOfView(distanceLab, offset: 6) detailView.layoutChain .topToBottomOfView(dayLab, offset: 15) .left(28) .right(15) .height(110) .bottom(15) headerBgView.layoutChain .edges(excludingEdge: .bottom) .height(38) speedLab.layoutChain .top(11) .left(15) unitLab.layoutChain .right(10) .centerY(speedLab) scoreLab.layoutChain .rightToLeftOfView(unitLab) .centerY(speedLab) scroreTitleLab.layoutChain .rightToLeftOfView(scoreLab, offset: -3) .centerY(speedLab) startTitleLab.layoutChain .topToBottomOfView(headerBgView, offset: 12) .left(15) startLab.layoutChain .leftToRightOfView(startTitleLab, offset: 2) .centerY(startTitleLab) endTitleLab.layoutChain .topToBottomOfView(startTitleLab, offset: 11) .leftToView(startTitleLab) endLab.layoutChain .leftToRightOfView(endTitleLab, offset: 2) .centerY(endTitleLab) lineView.layoutChain .height(0.5) .edgesHorzontal(15) .bottom() } lazy var dotView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#16B3FF") view.cornerRadius = 4 return view }() lazy var dayLab: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 20, weight: .bold) l.textColor = UIColor(hexStr: "#16B3FF") return l }() lazy var yearMonthLab: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 12, weight: .bold) l.textColor = UIColor(hexStr: "#66CDFF") return l }() lazy var distanceLab: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 12, weight: .medium) l.textColor = UIColor(hexStr: "#333333") return l }() lazy var durationLab: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 12, weight: .regular) l.textColor = UIColor(hexStr: "#333333") return l }() lazy var detailView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#F5FBFF") view.cornerRadius = 10 view.clipsToBounds = true return view }() lazy var headerBgView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#C4E8FF") return view }() lazy var speedLab: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 12, weight: .medium) l.textColor = UIColor(hexStr: "#333333") return l }() lazy var scroreTitleLab: UILabel = { let l = UILabel() l.text = "安全评分" l.font = .systemFont(ofSize: 10, weight: .medium) l.textColor = UIColor(hexStr: "#333333") return l }() lazy var scoreLab: UILabel = { let l = UILabel() l.text = "0" l.font = .systemFont(ofSize: 24, weight: .medium) l.textColor = UIColor(hexStr: "#16B3FF") return l }() lazy var unitLab: UILabel = { let l = UILabel() l.text = "分" l.font = .systemFont(ofSize: 12, weight: .medium) l.textColor = UIColor(hexStr: "#16B3FF") return l }() lazy var startTitleLab: UILabel = { let l = UILabel() l.text = "起点:" l.font = .systemFont(ofSize: 12, weight: .medium) l.textColor = UIColor(hexStr: "#666666") return l }() lazy var startLab: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 12, weight: .medium) l.textColor = UIColor(hexStr: "#333333") return l }() lazy var endTitleLab: UILabel = { let l = UILabel() l.text = "终点:" l.font = .systemFont(ofSize: 12, weight: .medium) l.textColor = UIColor(hexStr: "#666666") return l }() lazy var endLab: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 12, weight: .medium) l.textColor = UIColor(hexStr: "#333333") return l }() lazy var lineView: UIView = { let view = UIView() view.backgroundColor = ThemeManager.shared.color.lineColor return view }() override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear setupViews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func awakeFromNib() { super.awakeFromNib() // Initialization code } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) // Configure the view for the selected state } }