feat: 更新素材管理和举报风险地图

This commit is contained in:
2026-07-09 12:20:02 +08:00
parent 9b92d81902
commit 42aca73588
42 changed files with 6164 additions and 1369 deletions

View File

@ -4,6 +4,7 @@
//
import Kingfisher
import MAMapKit
import SnapKit
import UIKit
@ -928,7 +929,7 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
}
viewModel.paymentScreenshotItems.forEach { item in
let tile = WildReportDetailMediaThumbnailView(reporterItem: item)
tile.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reporterMediaTapped(_:))))
tile.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(paymentMediaTapped(_:))))
tile.snp.makeConstraints { make in
make.size.equalTo(108)
}
@ -987,7 +988,8 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
section.addArrangedSubview(makeLocationHeader())
let mapPreview = WildReportDetailMapPreviewView(
scenicName: viewModel.scenicAreaName,
detailAddress: viewModel.detailAddress
detailAddress: viewModel.detailAddress,
coordinate: viewModel.mapCoordinate
)
section.addArrangedSubview(mapPreview)
mapPreview.snp.makeConstraints { make in
@ -1198,18 +1200,63 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
}
@objc private func shareTapped() { viewModel.shareReport() }
@objc private func previewTapped() { viewModel.showMediaPreview(kind: "图片/视频") }
@objc private func paymentPreviewTapped() { viewModel.showMediaPreview(kind: "支付截图") }
@objc private func previewTapped() { presentReporterMediaPreview(selectedKey: nil) }
@objc private func paymentPreviewTapped() { presentPaymentMediaPreview(selectedKey: nil) }
@objc private func locationTapped() { viewModel.showLocationPreview() }
@objc private func reporterMediaTapped(_ recognizer: UITapGestureRecognizer) {
guard let tile = recognizer.view as? WildReportDetailMediaThumbnailView else { return }
viewModel.showMediaPreview(kind: tile.previewKindText)
presentReporterMediaPreview(selectedKey: tile.previewKey)
}
@objc private func paymentMediaTapped(_ recognizer: UITapGestureRecognizer) {
guard let tile = recognizer.view as? WildReportDetailMediaThumbnailView else { return }
presentPaymentMediaPreview(selectedKey: tile.previewKey)
}
@objc private func completionMediaTapped(_ recognizer: UITapGestureRecognizer) {
guard let tile = recognizer.view as? WildReportDetailMediaThumbnailView else { return }
viewModel.showMediaPreview(kind: tile.previewKindText)
presentCompletionMediaPreview(selectedKey: tile.previewKey)
}
private func presentReporterMediaPreview(selectedKey: String?) {
presentMediaPreview(entries: previewEntries(for: viewModel.reporterMediaItems), selectedKey: selectedKey, emptyMessage: "当前举报材料暂无可预览文件")
}
private func presentPaymentMediaPreview(selectedKey: String?) {
presentMediaPreview(entries: previewEntries(for: viewModel.paymentScreenshotItems), selectedKey: selectedKey, emptyMessage: "当前支付截图暂无可预览文件")
}
private func presentCompletionMediaPreview(selectedKey: String?) {
let entries = viewModel.handlerInfo?.completionMedia.compactMap { item -> (key: String, item: MediaPreviewItem)? in
guard let key = item.previewKey,
let previewItem = MediaPreviewItem(wildReportCompletionItem: item)
else { return nil }
return (key, previewItem)
} ?? []
presentMediaPreview(entries: entries, selectedKey: selectedKey, emptyMessage: "当前处理凭证暂无可预览文件")
}
private func previewEntries(for mediaItems: [WildReportReporterMediaItem]) -> [(key: String, item: MediaPreviewItem)] {
mediaItems.compactMap { mediaItem in
guard let key = mediaItem.previewKey,
let previewItem = MediaPreviewItem(wildReportReporterItem: mediaItem)
else { return nil }
return (key, previewItem)
}
}
private func presentMediaPreview(
entries: [(key: String, item: MediaPreviewItem)],
selectedKey: String?,
emptyMessage: String
) {
guard !entries.isEmpty else {
showToast(emptyMessage)
return
}
let startIndex = selectedKey.flatMap { key in entries.firstIndex { $0.key == key } } ?? 0
presentMediaPreview(items: entries.map(\.item), startIndex: startIndex)
}
@objc private func supplementTapped() {
@ -1320,15 +1367,57 @@ private final class WildReportDetailInfoRowView: UIView {
}
}
private extension MediaPreviewItem {
init?(wildReportReporterItem item: WildReportReporterMediaItem) {
guard let urlText = item.previewKey else { return nil }
switch item {
case .image:
guard let previewItem = MediaPreviewItem.remoteImage(urlText) else { return nil }
self = previewItem
case .video:
guard let previewItem = MediaPreviewItem.remoteVideo(urlText) else { return nil }
self = previewItem
}
}
init?(wildReportCompletionItem item: WildReportCompletionMediaItem) {
guard let urlText = item.previewKey else { return nil }
switch item.kind {
case .image:
guard let previewItem = MediaPreviewItem.remoteImage(urlText) else { return nil }
self = previewItem
case .video:
guard let previewItem = MediaPreviewItem.remoteVideo(urlText) else { return nil }
self = previewItem
}
}
}
private extension WildReportReporterMediaItem {
var previewKey: String? {
guard let text = fileURL?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { return nil }
return text
}
}
private extension WildReportCompletionMediaItem {
var previewKey: String? {
let text = coverURL.trimmingCharacters(in: .whitespacesAndNewlines)
return text.isEmpty ? nil : text
}
}
///
private final class WildReportDetailMediaThumbnailView: UIView {
let previewKindText: String
let previewKey: String?
private let gradient = CAGradientLayer()
private let remoteImageView = UIImageView()
private let footerLabel = PaddingLabel(insets: UIEdgeInsets(top: 3, left: 8, bottom: 3, right: 8))
private let playView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
init(reporterItem: WildReportReporterMediaItem) {
previewKey = reporterItem.previewKey
switch reporterItem {
case .image(let index, let url, let fileName, let fileTypeText):
previewKindText = fileName?.isEmpty == false ? fileName! : "\(index + 1)张举报图片"
@ -1342,11 +1431,12 @@ private final class WildReportDetailMediaThumbnailView: UIView {
}
init(completionItem: WildReportCompletionMediaItem) {
previewKey = completionItem.previewKey
switch completionItem.kind {
case .image:
previewKindText = "处理凭证图片"
super.init(frame: .zero)
configure(kind: .image(index: 0), footerText: "凭证")
configure(kind: .image(index: 0), footerText: "凭证", imageURL: completionItem.coverURL)
case .video:
previewKindText = "处理凭证视频"
super.init(frame: .zero)
@ -1500,107 +1590,41 @@ private final class WildReportDetailMediaThumbnailView: UIView {
///
private final class WildReportDetailMapPreviewView: UIView {
private let scenicName: String
private let detailAddress: String
private let detailLabel = UILabel()
private let scenicLabel = UILabel()
private let pinView = UIImageView(image: UIImage(systemName: "mappin.circle.fill"))
private let mapView: MAMapView
init(scenicName: String, detailAddress: String) {
self.scenicName = scenicName
self.detailAddress = detailAddress
init(scenicName: String, detailAddress: String, coordinate: CLLocationCoordinate2D?) {
_ = AMapBootstrap.configureIfNeeded()
mapView = MAMapView(frame: .zero)
super.init(frame: .zero)
backgroundColor = UIColor(hex: 0xE1F4D8)
backgroundColor = AppColor.pageBackgroundSoft
layer.cornerRadius = 10
clipsToBounds = true
detailLabel.text = detailAddress.replacingOccurrences(of: "附近", with: "")
detailLabel.font = .systemFont(ofSize: 13, weight: .semibold)
detailLabel.textColor = UIColor(hex: 0x64748B)
detailLabel.textAlignment = .center
scenicLabel.text = scenicName
scenicLabel.font = .systemFont(ofSize: 13, weight: .semibold)
scenicLabel.textColor = UIColor(hex: 0x418C48)
scenicLabel.textAlignment = .center
pinView.tintColor = AppColor.danger
pinView.contentMode = .scaleAspectFit
addSubview(detailLabel)
addSubview(scenicLabel)
addSubview(pinView)
detailLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview().multipliedBy(0.94)
make.centerY.equalToSuperview().multipliedBy(0.64)
make.leading.greaterThanOrEqualToSuperview().offset(12)
make.trailing.lessThanOrEqualToSuperview().inset(12)
}
scenicLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview().multipliedBy(1.56)
make.centerY.equalToSuperview().multipliedBy(1.16)
make.leading.greaterThanOrEqualToSuperview().offset(12)
make.trailing.lessThanOrEqualToSuperview().inset(12)
}
pinView.snp.makeConstraints { make in
make.centerX.equalToSuperview().multipliedBy(1.06)
make.centerY.equalToSuperview().multipliedBy(1.08)
make.size.equalTo(36)
mapView.showsCompass = false
mapView.showsScale = false
mapView.isScrollEnabled = false
mapView.isZoomEnabled = false
mapView.isRotateEnabled = false
mapView.isRotateCameraEnabled = false
mapView.zoomLevel = 16
addSubview(mapView)
mapView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
guard let coordinate else { return }
let annotation = MAPointAnnotation()
annotation.coordinate = coordinate
annotation.title = scenicName
annotation.subtitle = detailAddress
mapView.addAnnotation(annotation)
mapView.setCenter(coordinate, animated: false)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
let colors = [UIColor(hex: 0xE1F4D8).cgColor, UIColor(hex: 0xBDE6C7).cgColor] as CFArray
let colorSpace = CGColorSpaceCreateDeviceRGB()
if let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: [0, 1]) {
context.drawLinearGradient(
gradient,
start: CGPoint(x: 0, y: 0),
end: CGPoint(x: rect.width, y: rect.height),
options: []
)
}
let dashedPath = UIBezierPath()
dashedPath.move(to: CGPoint(x: rect.width * 0.05, y: rect.height * 0.32))
dashedPath.addCurve(
to: CGPoint(x: rect.width * 0.95, y: rect.height * 0.22),
controlPoint1: CGPoint(x: rect.width * 0.28, y: rect.height * 0.62),
controlPoint2: CGPoint(x: rect.width * 0.56, y: rect.height * 0.02)
)
UIColor(hex: 0x65A6D7).withAlphaComponent(0.75).setStroke()
dashedPath.lineWidth = 1.4
dashedPath.setLineDash([5, 5], count: 2, phase: 0)
dashedPath.stroke()
let road = UIBezierPath()
road.move(to: CGPoint(x: rect.width * 0.00, y: rect.height * 0.74))
road.addCurve(
to: CGPoint(x: rect.width * 1.00, y: rect.height * 0.62),
controlPoint1: CGPoint(x: rect.width * 0.26, y: rect.height * 0.20),
controlPoint2: CGPoint(x: rect.width * 0.68, y: rect.height * 1.00)
)
UIColor.white.withAlphaComponent(0.88).setStroke()
road.lineWidth = 5
road.stroke()
UIColor(hex: 0x99C59C).setStroke()
road.lineWidth = 1
road.stroke()
let pinCenter = CGPoint(x: rect.width * 0.53, y: rect.height * 0.54)
UIColor(hex: 0xEF4444).withAlphaComponent(0.18).setFill()
context.fillEllipse(in: CGRect(x: pinCenter.x - 32, y: pinCenter.y - 32, width: 64, height: 64))
UIColor(hex: 0xEF4444).withAlphaComponent(0.22).setFill()
context.fillEllipse(in: CGRect(x: pinCenter.x - 22, y: pinCenter.y - 22, width: 44, height: 44))
}
}
/// 线