// // TodayTrackDetailView.swift // QuickLocation // import UIKit import AMapNaviKit final class TodayTrackDetailView: UIView { lazy var headerBgImg: UIImageView = { let view = UIImageView() view.image = UIImage(named: "Home/HistoryTrack/header_bg") return view }() let mapView: MAMapView = { let mv = MAMapView() mv.zoomLevel = 14 mv.showsUserLocation = false mv.showsCompass = false mv.showsScale = false return mv }() let backBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Common/back"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 40) return btn }() let titleLab: UILabel = { let lab = UILabel() lab.text = "历史轨迹" lab.font = .systemFont(ofSize: 17, weight: .semibold) lab.textColor = UIColor(hexStr: "#293445") lab.textAlignment = .center return lab }() lazy var memberCV: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.itemSize = CGSize(width: 61, height: 90) layout.minimumLineSpacing = 12 let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = .white cv.showsHorizontalScrollIndicator = false cv.contentInset = UIEdgeInsets(top: 3, left: 6, bottom: 0, right: 6) cv.register(GroupMemberListCell.self) cv.cornerRadius = 16 return cv }() let playBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Home/HistoryTrack/play"), for: .normal) btn.setImage(UIImage(named: "Home/HistoryTrack/pause"), for: .selected) return btn }() let progressSlider: HistoryTrackSlider = { let slider = HistoryTrackSlider() slider.minimumValue = 0 slider.maximumValue = 1 slider.value = 0 slider.minimumTrackTintColor = UIColor(hexStr: "#16B3FF") slider.maximumTrackTintColor = UIColor(hexStr: "#D9EEFF") let thumb = UIImage(named: "Home/HistoryTrack/slider_thumb") slider.setThumbImage(thumb, for: .normal) slider.setThumbImage(thumb, for: .highlighted) return slider }() private let playbackBar: UIView = { let v = UIView() v.backgroundColor = UIColor.white.withAlphaComponent(0.96) v.cornerRadius = 12 return v }() let sheetView: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#FAFAFA") v.layer.cornerRadius = 30 v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] v.clipsToBounds = true return v }() let handleView: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#E5E7EB") v.cornerRadius = 2.5 return v }() let datePrevBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Home/HistoryTrack/date_prev"), for: .normal) return btn }() let dateNextBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Home/HistoryTrack/date_next"), for: .normal) return btn }() private let dateBar: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#ECEFF3") v.cornerRadius = 16 return v }() lazy var dateCV: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.minimumLineSpacing = 0 layout.minimumInteritemSpacing = 0 layout.sectionInset = .zero let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = .clear cv.showsHorizontalScrollIndicator = false cv.isPagingEnabled = true cv.register(TodayTrackDateCell.self) return cv }() lazy var timelineTV: UITableView = { let tv = UITableView(frame: .zero, style: .plain) tv.separatorStyle = .none tv.backgroundColor = .clear tv.showsVerticalScrollIndicator = false tv.register(TodayTrackTripCell.self) tv.rowHeight = UITableView.automaticDimension tv.estimatedRowHeight = 128 return tv }() let emptyLab: UILabel = { let lab = UILabel() lab.text = "暂无轨迹" lab.font = .systemFont(ofSize: 14, weight: .medium) lab.textColor = UIColor(hexStr: "#9CA3AF") lab.textAlignment = .center lab.isHidden = true return lab }() private(set) var sheetHeightConstraint: NSLayoutConstraint? override init(frame: CGRect) { super.init(frame: frame) backgroundColor = UIColor(hexStr: "#F5F7FA") setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupUI() { addSubview(mapView) mapView.layoutChain.edges() addSubview(headerBgImg) headerBgImg.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(160/375) addSubview(backBtn) backBtn.layoutChain .top(kStatusBarHeight + 6) .left(15) .width(42) .height(42) addSubview(titleLab) titleLab.layoutChain .centerY(backBtn) .centerX() addSubview(memberCV) memberCV.layoutChain .topToBottomOfView(backBtn, offset: 12) .edgesHorzontal(15) .height(100) addSubview(sheetView) let sheetH = sheetView.heightAnchor.constraint(equalToConstant: 200 + kSafeBottomMargin) sheetH.isActive = true sheetHeightConstraint = sheetH sheetView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ sheetView.leadingAnchor.constraint(equalTo: leadingAnchor), sheetView.trailingAnchor.constraint(equalTo: trailingAnchor), sheetView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) addSubview(playbackBar) playbackBar.layoutChain .bottomToTopOfView(sheetView, offset: -12) .edgesHorzontal(15) .height(35) playbackBar.addSubview(playBtn) playbackBar.addSubview(progressSlider) playBtn.layoutChain .left(12) .centerY() .width(28) .height(28) progressSlider.layoutChain .leftToRightOfView(playBtn, offset: 10) .right(16) .centerY() .height(28) sheetView.addSubview(handleView) handleView.layoutChain .top(8) .centerX() .width(36) .height(5) sheetView.addSubview(dateBar) dateBar.layoutChain .topToBottomOfView(handleView, offset: 10) .edgesHorzontal(10) .height(32) dateBar.addSubview(datePrevBtn) dateBar.addSubview(dateNextBtn) dateBar.addSubview(dateCV) datePrevBtn.layoutChain .left(4) .centerY() .width(28) .height(28) dateNextBtn.layoutChain .right(4) .centerY() .width(28) .height(28) dateCV.layoutChain .leftToRightOfView(datePrevBtn, offset: 0) .rightToLeftOfView(dateNextBtn, offset: 0) .top() .bottom() sheetView.addSubview(timelineTV) timelineTV.layoutChain .topToBottomOfView(dateBar, offset: 12) .edgesHorzontal(10) .bottom() sheetView.addSubview(emptyLab) emptyLab.layoutChain .centerX() .topToBottomOfView(dateBar, offset: 40) } func cleanupMap() { mapView.removeAnnotations(mapView.annotations) mapView.removeOverlays(mapView.overlays) mapView.delegate = nil } func dateItemSize() -> CGSize { let width = dateCV.bounds.width let height = dateCV.bounds.height let itemWidth = width > 0 ? width / 5 : 50 let itemHeight = height > 0 ? height : 32 return CGSize(width: itemWidth, height: itemHeight) } func scrollDates(direction: CGFloat) { let page = dateCV.bounds.width guard page > 0 else { return } let offset = dateCV.contentOffset.x + direction * page let maxX = max(0, dateCV.contentSize.width - page) let x = min(max(0, offset), maxX) dateCV.setContentOffset(CGPoint(x: x, y: 0), animated: true) } func scrollDatesToEnd(animated: Bool) { dateCV.layoutIfNeeded() let page = dateCV.bounds.width let maxX = max(0, dateCV.contentSize.width - page) dateCV.setContentOffset(CGPoint(x: maxX, y: 0), animated: animated) } } final class HistoryTrackSlider: UISlider { override func trackRect(forBounds bounds: CGRect) -> CGRect { var rect = super.trackRect(forBounds: bounds) rect.size.height = 4 rect.origin.y = (bounds.height - 4) / 2 return rect } } // MARK: - Date cell final class TodayTrackDateCell: UICollectionViewCell { private let titleLab = UILabel() private let selectedBg = UIView() override init(frame: CGRect) { super.init(frame: frame) contentView.backgroundColor = .clear selectedBg.cornerRadius = 12 titleLab.font = .systemFont(ofSize: 12, weight: .medium) titleLab.textAlignment = .center titleLab.adjustsFontSizeToFitWidth = true titleLab.minimumScaleFactor = 0.8 titleLab.lineBreakMode = .byClipping contentView.addSubview(selectedBg) selectedBg.addSubview(titleLab) selectedBg.layoutChain.edgesVertical(4).edgesHorzontal() titleLab.layoutChain.edges() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(title: String, selected: Bool) { titleLab.text = title titleLab.textColor = selected ? UIColor(hexStr: "#293445") : UIColor(hexStr: "#767676") selectedBg.backgroundColor = selected ? .white : .clear } } // MARK: - Trip cell final class TodayTrackTripCell: UITableViewCell { var onViewTapped: (() -> Void)? private let marker = UIImageView() private let dot = UIView() private let lineTop = UIView() private let lineBottom = UIView() private let card = UIView() private let minutesValue = UILabel() private let minutesUnit = UILabel() private let distanceValue = UILabel() private let distanceUnit = UILabel() private let divider = UIView() private let viewBtn = UIButton(type: .custom) private let startPrefix = UILabel() private let startLab = UILabel() private let routeLine = UIView() private let endPrefix = UILabel() private let endLab = UILabel() override init(style: UITableViewCell.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(trip: ScheduleRecordModel, isFirst: Bool, isLast: Bool) { if isFirst { marker.image = UIImage(named: "Home/HistoryTrack/start") marker.isHidden = false dot.isHidden = true } else if isLast { marker.image = UIImage(named: "Home/HistoryTrack/end") marker.isHidden = false dot.isHidden = true } else { marker.image = nil marker.isHidden = true dot.isHidden = false } lineTop.isHidden = isFirst lineBottom.isHidden = isLast minutesValue.text = "\(max(0, trip.duration_minutes))" distanceValue.text = Self.distanceText(trip.distance_km) let startTime = trip.start_time.isoStringToCustom(trip.start_time, format: "HH:mm") ?? "" let endTime = trip.end_time.isoStringToCustom(trip.end_time, format: "HH:mm") ?? "" startLab.text = Self.placeText(trip.start_address, time: startTime) endLab.text = Self.placeText(trip.end_address, time: endTime) } private func setupUI() { lineTop.backgroundColor = UIColor(hexStr: "#D1D5DB") lineBottom.backgroundColor = UIColor(hexStr: "#D1D5DB") marker.contentMode = .scaleAspectFit dot.backgroundColor = UIColor(hexStr: "#293445") dot.cornerRadius = 4 dot.isHidden = true contentView.addSubview(lineTop) contentView.addSubview(lineBottom) contentView.addSubview(marker) contentView.addSubview(dot) marker.layoutChain .centerY() .left(4) .width(20) .height(20) dot.layoutChain .centerX(marker) .centerY(marker) .width(8) .height(8) lineTop.layoutChain .top() .centerX(marker) .width(2) .bottomToTopOfView(marker, offset: 0) lineBottom.layoutChain .topToBottomOfView(marker, offset: 0) .centerX(marker) .width(2) .bottom() card.backgroundColor = .white card.cornerRadius = 16 contentView.addSubview(card) card.layoutChain .top(4) .leftToRightOfView(marker, offset: 10) .right(6) .bottom(8) minutesValue.font = .systemFont(ofSize: 20, weight: .semibold) minutesValue.textColor = UIColor(hexStr: "#16B3FF") minutesUnit.text = "分钟" minutesUnit.font = .systemFont(ofSize: 12, weight: .medium) minutesUnit.textColor = UIColor(hexStr: "#16B3FF") distanceValue.font = .systemFont(ofSize: 20, weight: .semibold) distanceValue.textColor = UIColor(hexStr: "#16B3FF") distanceUnit.text = "公里" distanceUnit.font = .systemFont(ofSize: 12, weight: .medium) distanceUnit.textColor = UIColor(hexStr: "#16B3FF") divider.backgroundColor = UIColor(hexStr: "#D9EEFF") viewBtn.setTitle("查看", for: .normal) viewBtn.setTitleColor(UIColor(hexStr: "#9CA3AF"), for: .normal) viewBtn.titleLabel?.font = .systemFont(ofSize: 13, weight: .medium) viewBtn.addTarget(self, action: #selector(viewTapped), for: .touchUpInside) startPrefix.text = "起点:" startPrefix.font = .systemFont(ofSize: 13, weight: .medium) startPrefix.textColor = UIColor(hexStr: "#9CA3AF") startLab.font = .systemFont(ofSize: 13, weight: .medium) startLab.textColor = UIColor(hexStr: "#293445") startLab.numberOfLines = 2 routeLine.backgroundColor = UIColor(hexStr: "#D4F3FF") routeLine.cornerRadius = 8 endPrefix.text = "终点:" endPrefix.font = .systemFont(ofSize: 13, weight: .medium) endPrefix.textColor = UIColor(hexStr: "#9CA3AF") endLab.font = .systemFont(ofSize: 13, weight: .medium) endLab.textColor = UIColor(hexStr: "#293445") endLab.numberOfLines = 2 [minutesValue, minutesUnit, divider, distanceValue, distanceUnit, viewBtn, startPrefix, startLab, routeLine, endPrefix, endLab].forEach { card.addSubview($0) } minutesValue.layoutChain.top(14).left(14) minutesUnit.layoutChain.leftToRightOfView(minutesValue, offset: 4).centerY(minutesValue) divider.layoutChain .leftToRightOfView(minutesUnit, offset: 10) .centerY(minutesValue) .width(1) .height(16) distanceValue.layoutChain.leftToRightOfView(divider, offset: 10).centerY(minutesValue) distanceUnit.layoutChain.leftToRightOfView(distanceValue, offset: 4).centerY(minutesValue) viewBtn.layoutChain.right(8).centerY(minutesValue).width(44).height(28) startPrefix.layoutChain .topToBottomOfView(minutesValue, offset: 14) .left(14) startLab.layoutChain .topToView(startPrefix) .leftToRightOfView(startPrefix, offset: 0) .right(12) routeLine.layoutChain .topToBottomOfView(startLab, offset: 4) .centerX(startLab) .width(3) .height(10) endPrefix.layoutChain .topToBottomOfView(routeLine, offset: 4) .leftToView(startPrefix) endLab.layoutChain .topToView(endPrefix) .leftToRightOfView(endPrefix, offset: 0) .right(12) .bottom(14) } @objc private func viewTapped() { onViewTapped?() } private static func distanceText(_ km: Double) -> String { if km < 0.05 { return "0.0" } if abs(km - km.rounded()) < 0.05 { return "\(Int(km.rounded()))" } return String(format: "%.1f", km) } private static func placeText(_ address: TripAddress?, time: String) -> String { let district = address?.district ?? "" let street = address?.street ?? "" var place = "" if !district.isEmpty, !street.isEmpty { place = "\(district) \(street)" } else if !street.isEmpty { place = street } else if !district.isEmpty { place = district } else { place = address?.formatted_address ?? "" } if time.isEmpty { return place } if place.isEmpty { return time } return "\(place) \(time)" } }