// // 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() } }