238 lines
9.4 KiB
Swift
238 lines
9.4 KiB
Swift
//
|
||
// 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()
|
||
}
|
||
}
|