Files
suixinkan_uikit/suixinkan/UI/WildPhotographerReport/WildReportRiskMapViewController.swift

238 lines
9.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// WildReportRiskMapViewController.swift
// suixinkan
//
import MapKit
import SnapKit
import UIKit
/// 线
final class WildReportRiskMapViewController: BaseViewController {
private let viewModel: WildReportRiskMapViewModel
private let mapView = MKMapView()
private let panel = WildReportCardView(spacing: AppSpacing.sm)
private var annotationMap: [String: WildReportMapMarker] = [:]
init(record: WildReportRecord, riskPoints: [WildReportRiskPoint]) {
self.viewModel = WildReportRiskMapViewModel(record: record, riskPoints: riskPoints)
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "附近风险地图"
}
override func setupUI() {
view.backgroundColor = AppColor.pageBackgroundSoft
mapView.delegate = self
mapView.showsCompass = false
mapView.showsScale = true
view.addSubview(mapView)
view.addSubview(panel)
addLegend()
applyMap()
rebuildPanel()
}
override func setupConstraints() {
mapView.snp.makeConstraints { make in
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
make.height.equalTo(340)
}
panel.snp.makeConstraints { make in
make.top.equalTo(mapView.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.bottom.lessThanOrEqualTo(view.safeAreaLayoutGuide).inset(AppSpacing.sm)
}
}
override func bindActions() {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in
self?.mapView.setRegion(self?.viewModel.region ?? MKCoordinateRegion(), animated: true)
self?.rebuildPanel()
}
}
viewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
}
private func applyMap() {
mapView.setRegion(viewModel.region, animated: false)
annotationMap.removeAll()
let annotations = viewModel.markers.map { marker in
annotationMap[marker.id] = marker
let annotation = WildReportMapAnnotation(marker: marker)
return annotation
}
mapView.addAnnotations(annotations)
}
private func addLegend() {
let legend = UIStackView()
legend.axis = .horizontal
legend.distribution = .fillEqually
legend.backgroundColor = UIColor.white.withAlphaComponent(0.94)
legend.layer.cornerRadius = AppRadius.md
legend.clipsToBounds = true
[
WildReportMapMarkerKind.selfLocation,
.wildPhotographer,
.processingClue,
.peerPhotographer
].forEach { kind in
let row = UIStackView()
row.axis = .horizontal
row.alignment = .center
row.spacing = 4
let dot = UIView()
dot.backgroundColor = kind.color
dot.layer.cornerRadius = 4
dot.snp.makeConstraints { make in make.size.equalTo(8) }
let label = WildReportUI.label(kind.title, font: .app(.caption), color: AppColor.textSecondary)
row.addArrangedSubview(dot)
row.addArrangedSubview(label)
legend.addArrangedSubview(row)
}
view.addSubview(legend)
legend.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.top.equalTo(view.safeAreaLayoutGuide).offset(AppSpacing.sm)
make.height.equalTo(36)
}
}
@MainActor
private func rebuildPanel() {
panel.stack.arrangedSubviews.forEach { view in
panel.stack.removeArrangedSubview(view)
view.removeFromSuperview()
}
guard let marker = viewModel.selectedMarker,
case .activeClue(let clue) = marker.detail else {
panel.stack.addArrangedSubview(WildReportUI.label("点击地图标记查看详情", font: .app(.body), color: AppColor.textSecondary))
return
}
let header = UIStackView()
header.axis = .horizontal
header.alignment = .center
let title = WildReportUI.label(clue.displayTitle, font: .app(.title), color: AppColor.textPrimary, lines: 0)
let badge = UILabel()
badge.text = clue.statusText
badge.font = .app(.captionMedium)
badge.textColor = marker.kind.color
badge.textAlignment = .center
badge.backgroundColor = marker.kind.color.withAlphaComponent(0.12)
badge.layer.cornerRadius = 12
badge.clipsToBounds = true
badge.snp.makeConstraints { make in
make.width.greaterThanOrEqualTo(74)
make.height.equalTo(24)
}
header.addArrangedSubview(title)
header.addArrangedSubview(UIView())
header.addArrangedSubview(badge)
panel.stack.addArrangedSubview(header)
if let nearest = clue.nearestAbnormalText {
panel.stack.addArrangedSubview(WildReportUI.label(nearest, font: .app(.bodyMedium), color: AppColor.danger, lines: 0))
}
if let name = clue.name {
panel.stack.addArrangedSubview(WildReportInfoRowView(title: "点位", value: name))
}
if let store = clue.storeName {
panel.stack.addArrangedSubview(WildReportInfoRowView(title: "门店", value: store))
}
if let distance = clue.distanceText ?? clue.distance {
panel.stack.addArrangedSubview(WildReportInfoRowView(title: "距离", value: distance))
}
if let updatedAt = clue.updatedAt {
panel.stack.addArrangedSubview(WildReportInfoRowView(title: "时间", value: updatedAt))
}
if let summary = clue.summary ?? clue.detail {
panel.stack.addArrangedSubview(WildReportUI.label(summary, font: .app(.body), color: AppColor.textSecondary, lines: 0))
}
clue.reportContents.prefix(2).forEach { content in
panel.stack.addArrangedSubview(WildReportUI.label("\(content.time) \(content.content)", font: .app(.caption), color: AppColor.textSecondary, lines: 0))
}
if let record = clue.record {
panel.stack.addArrangedSubview(WildReportInfoRowView(title: "处理人", value: record.handlerName))
panel.stack.addArrangedSubview(WildReportUI.label(record.processResult, font: .app(.body), color: AppColor.textSecondary, lines: 0))
}
let buttonRow = UIStackView()
buttonRow.axis = .horizontal
buttonRow.spacing = AppSpacing.sm
buttonRow.distribution = .fillEqually
let navButton = WildReportUI.iconButton(title: clue.actionText, imageName: "location.fill", style: .secondary)
navButton.addTarget(self, action: #selector(navigateTapped), for: .touchUpInside)
let shareButton = WildReportUI.iconButton(title: "分享", imageName: "square.and.arrow.up", style: .secondary)
shareButton.addTarget(self, action: #selector(shareTapped), for: .touchUpInside)
[navButton, shareButton].forEach { button in
button.snp.makeConstraints { make in make.height.equalTo(42) }
buttonRow.addArrangedSubview(button)
}
panel.stack.addArrangedSubview(buttonRow)
}
@objc private func navigateTapped() {
viewModel.navigateToSelectedMarker()
}
@objc private func shareTapped() {
viewModel.shareSelectedClue()
}
}
extension WildReportRiskMapViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? WildReportMapAnnotation else { return nil }
let identifier = "WildReportMapAnnotationView"
let view = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
view.annotation = annotation
if let markerView = view as? MKMarkerAnnotationView {
markerView.markerTintColor = annotation.marker.kind.color
markerView.glyphImage = UIImage(systemName: glyphName(for: annotation.marker.kind))
markerView.glyphTintColor = .white
markerView.canShowCallout = false
markerView.animatesWhenAdded = true
}
return view
}
func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) {
guard let annotation = annotation as? WildReportMapAnnotation else { return }
viewModel.selectMarker(annotation.marker)
}
private func glyphName(for kind: WildReportMapMarkerKind) -> String {
switch kind {
case .selfLocation: return "location.fill"
case .wildPhotographer: return "exclamationmark"
case .processingClue: return "clock.fill"
case .peerPhotographer: return "camera.fill"
}
}
}
/// MKAnnotation
final class WildReportMapAnnotation: NSObject, MKAnnotation {
let marker: WildReportMapMarker
var coordinate: CLLocationCoordinate2D { marker.coordinate }
var title: String? { marker.title }
init(marker: WildReportMapMarker) {
self.marker = marker
super.init()
}
}