// // LocationReportViews.swift // suixinkan // import CoreLocation import SnapKit import UIKit /// 地图缩放/定位控件。 final class LocationReportMapControlStack: UIView { var onZoomIn: (() -> Void)? var onZoomOut: (() -> Void)? var onRelocate: (() -> Void)? override init(frame: CGRect) { super.init(frame: frame) let zoomIn = makeButton(symbol: "plus", action: #selector(zoomInTapped)) let zoomOut = makeButton(symbol: "minus", action: #selector(zoomOutTapped)) let relocate = makeButton(symbol: "location.fill", action: #selector(relocateTapped)) let zoomStack = UIStackView(arrangedSubviews: [zoomIn, zoomOut]) zoomStack.axis = .vertical zoomStack.spacing = 0 zoomStack.backgroundColor = .white zoomStack.layer.cornerRadius = 12 zoomStack.clipsToBounds = true relocate.backgroundColor = .white relocate.layer.cornerRadius = 12 let stack = UIStackView(arrangedSubviews: [zoomStack, relocate]) stack.axis = .vertical stack.spacing = 12 addSubview(stack) stack.snp.makeConstraints { make in make.edges.equalToSuperview() } [zoomIn, zoomOut, relocate].forEach { button in button.snp.makeConstraints { make in make.size.equalTo(44) } } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func makeButton(symbol: String, action: Selector) -> UIButton { let button = UIButton(type: .system) button.setImage(UIImage(systemName: symbol), for: .normal) button.tintColor = AppColor.primary button.addTarget(self, action: action, for: .touchUpInside) return button } @objc private func zoomInTapped() { onZoomIn?() } @objc private func zoomOutTapped() { onZoomOut?() } @objc private func relocateTapped() { onRelocate?() } } /// 状态信息卡片。 final class LocationReportStatusCardView: UIView { var onOnlineStatusTap: (() -> Void)? var onReminderTap: (() -> Void)? private let onlineBadge = UILabel() private let countdownLabel = UILabel() private let reminderLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .white layer.cornerRadius = 10 let statusTitle = makeTitle("当前状态") onlineBadge.font = .systemFont(ofSize: 12, weight: .medium) onlineBadge.textAlignment = .center onlineBadge.layer.cornerRadius = 4 onlineBadge.clipsToBounds = true onlineBadge.isUserInteractionEnabled = true onlineBadge.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onlineTapped))) let countdownTitle = makeTitle("距离下次上报时间") countdownLabel.font = .systemFont(ofSize: 16, weight: .medium) countdownLabel.textColor = AppColor.primary let reminderTitle = makeTitle("提醒设置") reminderLabel.font = .systemFont(ofSize: 16, weight: .medium) reminderLabel.textColor = AppColor.primary reminderLabel.isUserInteractionEnabled = true reminderLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reminderTapped))) let stack = UIStackView() stack.axis = .vertical stack.spacing = 12 stack.addArrangedSubview(makeRow(left: statusTitle, right: onlineBadge)) stack.addArrangedSubview(makeRow(left: countdownTitle, right: countdownLabel)) stack.addArrangedSubview(makeRow(left: reminderTitle, right: reminderLabel)) addSubview(stack) stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(15) } onlineBadge.snp.makeConstraints { make in make.width.greaterThanOrEqualTo(52) make.height.equalTo(28) } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func apply(isOnline: Bool, countdown: String, reminderMinutes: Int) { onlineBadge.text = isOnline ? "在线" : "离线" onlineBadge.textColor = isOnline ? AppColor.online : UIColor(hex: 0x7B8EAA) onlineBadge.backgroundColor = isOnline ? UIColor(hex: 0xF0FDF4) : AppColor.inputBackground countdownLabel.text = countdown reminderLabel.text = reminderMinutes == 0 ? "不提醒" : "提前\(reminderMinutes)分钟" } @objc private func onlineTapped() { onOnlineStatusTap?() } @objc private func reminderTapped() { onReminderTap?() } private func makeTitle(_ text: String) -> UILabel { let label = UILabel() label.text = text label.font = .systemFont(ofSize: 14) label.textColor = AppColor.textPrimary return label } private func makeRow(left: UIView, right: UIView) -> UIView { let row = UIStackView(arrangedSubviews: [left, right]) row.axis = .horizontal row.distribution = .equalSpacing row.alignment = .center return row } } /// 立即上报卡片。 final class LocationReportActionCardView: UIView { var onReportTap: (() -> Void)? private let reportButton = UIButton(type: .system) override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .white layer.cornerRadius = 10 let icon = UIImageView(image: UIImage(systemName: "arrow.up")) icon.tintColor = AppColor.primary let title = UILabel() title.text = "立即上报" title.font = .systemFont(ofSize: 16, weight: .bold) title.textColor = AppColor.textPrimary let subtitle = UILabel() subtitle.text = "您已进入打卡范围" subtitle.font = .systemFont(ofSize: 12) subtitle.textColor = AppColor.textTertiary let textStack = UIStackView(arrangedSubviews: [title, subtitle]) textStack.axis = .vertical textStack.spacing = 4 reportButton.backgroundColor = AppColor.primary reportButton.tintColor = .white reportButton.setImage(UIImage(systemName: "location.fill"), for: .normal) reportButton.layer.cornerRadius = 36 reportButton.addTarget(self, action: #selector(reportTapped), for: .touchUpInside) addSubview(icon) addSubview(textStack) addSubview(reportButton) icon.snp.makeConstraints { make in make.leading.equalToSuperview().offset(15) make.centerY.equalToSuperview() make.size.equalTo(24) } textStack.snp.makeConstraints { make in make.leading.equalTo(icon.snp.trailing).offset(8) make.centerY.equalToSuperview() } reportButton.snp.makeConstraints { make in make.trailing.equalToSuperview().offset(-15) make.centerY.equalToSuperview() make.size.equalTo(72) } snp.makeConstraints { make in make.height.equalTo(96) } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setReporting(_ reporting: Bool) { reportButton.isEnabled = !reporting reportButton.alpha = reporting ? 0.6 : 1 } @objc private func reportTapped() { onReportTap?() } } /// 底部快捷操作按钮。 final class LocationReportBottomActionView: UIControl { private let iconView = UIImageView() private let titleLabel = UILabel() init(title: String, symbol: String) { super.init(frame: .zero) backgroundColor = .white layer.cornerRadius = 10 iconView.image = UIImage(systemName: symbol) iconView.tintColor = AppColor.primary iconView.contentMode = .scaleAspectFit titleLabel.text = title titleLabel.font = .systemFont(ofSize: 12) titleLabel.textColor = AppColor.textPrimary titleLabel.textAlignment = .center let stack = UIStackView(arrangedSubviews: [iconView, titleLabel]) stack.axis = .vertical stack.spacing = 4 stack.alignment = .center stack.isUserInteractionEnabled = false addSubview(stack) stack.snp.makeConstraints { make in make.center.equalToSuperview() } iconView.snp.makeConstraints { make in make.size.equalTo(24) } snp.makeConstraints { make in make.height.equalTo(80) } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override var isSelected: Bool { didSet { applySelectedStyle() } } func applyOnlineStyle(isOnline: Bool) { if isOnline { backgroundColor = AppColor.online iconView.tintColor = .white titleLabel.text = "在线" titleLabel.textColor = .white } else { backgroundColor = .white iconView.tintColor = UIColor(hex: 0x7B8EAA) titleLabel.text = "离线" titleLabel.textColor = UIColor(hex: 0x7B8EAA) } } private func applySelectedStyle() { guard titleLabel.text == "标记上报" else { return } if isSelected { backgroundColor = AppColor.online iconView.tintColor = UIColor(hex: 0xF0FDF4) titleLabel.textColor = UIColor(hex: 0xF0FDF4) } else { backgroundColor = .white iconView.tintColor = AppColor.primary titleLabel.textColor = AppColor.textPrimary } } } /// 地图容器,封装高德 `MAMapView`。 final class LocationReportMapView: UIView, MAMapViewDelegate { var onMapTap: ((CLLocationCoordinate2D) -> Void)? let mapView: MAMapView private var markerAnnotation: MAPointAnnotation? private var shouldCenterOnNextUserLocation = false override init(frame: CGRect) { _ = AMapBootstrap.configureIfNeeded() mapView = MAMapView(frame: .zero) super.init(frame: frame) mapView.delegate = self mapView.showsUserLocation = true mapView.showsCompass = false mapView.zoomLevel = 15 addSubview(mapView) mapView.snp.makeConstraints { make in make.edges.equalToSuperview() } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } /// 进入页面后,在首次拿到用户位置时将地图居中(不持续跟随,便于手动拖动)。 func enableInitialUserLocationCentering() { shouldCenterOnNextUserLocation = true centerOnUserLocationIfNeeded() } func centerOnUserLocation() { guard let coordinate = validUserCoordinate else { return } setCenter(coordinate) } func setCenter(_ coordinate: CLLocationCoordinate2D, animated: Bool = true) { mapView.setCenter(coordinate, animated: animated) mapView.setZoomLevel(15, animated: animated) } func zoomIn() { mapView.setZoomLevel(mapView.zoomLevel + 1, animated: true) } func zoomOut() { mapView.setZoomLevel(mapView.zoomLevel - 1, animated: true) } func updateMarker(latitude: Double?, longitude: Double?) { if let annotation = markerAnnotation { mapView.removeAnnotation(annotation) markerAnnotation = nil } guard let latitude, let longitude else { return } let annotation = MAPointAnnotation() annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) markerAnnotation = annotation mapView.addAnnotation(annotation) } func mapView(_ mapView: MAMapView!, didUpdate userLocation: MAUserLocation!, updatingLocation: Bool) { guard updatingLocation else { return } centerOnUserLocationIfNeeded() } func mapView(_ mapView: MAMapView!, didSingleTappedAt coordinate: CLLocationCoordinate2D) { onMapTap?(coordinate) } private var validUserCoordinate: CLLocationCoordinate2D? { let coordinate = mapView.userLocation.location?.coordinate guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return nil } guard coordinate.latitude != 0 || coordinate.longitude != 0 else { return nil } return coordinate } private func centerOnUserLocationIfNeeded() { guard shouldCenterOnNextUserLocation, let coordinate = validUserCoordinate else { return } setCenter(coordinate, animated: false) shouldCenterOnNextUserLocation = false } }