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

@ -328,6 +328,19 @@ enum WildReportLocalMediaLoader {
}
}
extension MediaPreviewItem {
///
init?(wildReportAttachment attachment: WildReportAttachment) {
guard let url = attachment.localFileURL else { return nil }
switch attachment.kind {
case .image, .payment:
self = .localImage(url: url)
case .video:
self = .localVideo(url: url)
}
}
}
/// tile
final class WildReportAttachmentTileView: UIControl {
private let gradientLayer = CAGradientLayer()

View File

@ -9,11 +9,16 @@ import UIKit
///
final class WildPhotographerReportHomeViewController: BaseViewController {
private let viewModel: WildPhotographerReportHomeViewModel
private let api: any WildPhotographerReportServing
private let scrollView = UIScrollView()
private let contentStack = UIStackView()
init(viewModel: WildPhotographerReportHomeViewModel = WildPhotographerReportHomeViewModel()) {
init(
viewModel: WildPhotographerReportHomeViewModel = WildPhotographerReportHomeViewModel(),
api: any WildPhotographerReportServing = NetworkServices.shared.wildPhotographerReportAPI
) {
self.viewModel = viewModel
self.api = api
super.init(nibName: nil, bundle: nil)
}
@ -25,20 +30,6 @@ final class WildPhotographerReportHomeViewController: BaseViewController {
override func setupNavigationBar() {
title = "举报摄影师"
navigationItem.largeTitleDisplayMode = .never
let backButton = UIButton(type: .system)
backButton.backgroundColor = .white
backButton.tintColor = AppColor.textPrimary
backButton.setImage(UIImage(systemName: "chevron.left"), for: .normal)
backButton.layer.cornerRadius = 25
backButton.layer.shadowColor = UIColor.black.cgColor
backButton.layer.shadowOpacity = 0.05
backButton.layer.shadowOffset = CGSize(width: 0, height: 6)
backButton.layer.shadowRadius = 16
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
backButton.snp.makeConstraints { make in
make.size.equalTo(50)
}
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton)
}
override func setupUI() {
@ -68,6 +59,16 @@ final class WildPhotographerReportHomeViewController: BaseViewController {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.rebuildContent() }
}
viewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
}
override func viewDidLoad() {
super.viewDidLoad()
Task {
await viewModel.loadReportCopy(api: api)
}
}
@MainActor
@ -81,18 +82,16 @@ final class WildPhotographerReportHomeViewController: BaseViewController {
case .normal:
contentStack.addArrangedSubview(makeHeroCard())
contentStack.addArrangedSubview(makeRiskMapEntry())
contentStack.addArrangedSubview(makeNoticeCard())
if viewModel.shouldShowNoticeModule {
contentStack.addArrangedSubview(makeNoticeCard())
}
contentStack.addArrangedSubview(makeTrustCard())
case .loading:
contentStack.addArrangedSubview(makeStateCard(icon: "clock", title: "加载中", message: "正在加载举报服务"))
case .empty:
contentStack.addArrangedSubview(makeStateCard(icon: "exclamationmark.shield", title: viewModel.emptyTitle, message: viewModel.emptyMessage, buttonTitle: "恢复演示数据") { [weak self] in
self?.viewModel.restoreDemoData()
})
contentStack.addArrangedSubview(makeStateCard(icon: "exclamationmark.shield", title: viewModel.emptyTitle, message: viewModel.emptyMessage))
case .error:
contentStack.addArrangedSubview(makeStateCard(icon: "wifi.exclamationmark", title: "加载失败", message: "举报服务暂时不可用,请稍后重试。", buttonTitle: "重新加载") { [weak self] in
self?.viewModel.retryLoadAfterError()
})
contentStack.addArrangedSubview(makeStateCard(icon: "wifi.exclamationmark", title: "加载失败", message: "举报服务暂时不可用,请稍后重试。"))
}
}
@ -172,31 +171,28 @@ final class WildPhotographerReportHomeViewController: BaseViewController {
headerIcon.tintColor = AppColor.primary
headerIcon.snp.makeConstraints { make in make.size.equalTo(24) }
let title = WildReportUI.label("举报须知", font: .systemFont(ofSize: 18, weight: .bold), color: AppColor.textPrimary)
let ruleButton = UIButton(type: .system)
ruleButton.setTitle("举报规则", for: .normal)
ruleButton.setImage(UIImage(systemName: "shield.lefthalf.filled"), for: .normal)
ruleButton.titleLabel?.font = .systemFont(ofSize: 15, weight: .bold)
ruleButton.tintColor = AppColor.primary
ruleButton.setTitleColor(AppColor.primary, for: .normal)
ruleButton.backgroundColor = AppColor.primaryLight
ruleButton.layer.cornerRadius = 18
ruleButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
ruleButton.addTarget(self, action: #selector(ruleTapped), for: .touchUpInside)
ruleButton.snp.makeConstraints { make in
make.height.equalTo(36)
}
header.addArrangedSubview(headerIcon)
header.addArrangedSubview(title)
header.addArrangedSubview(UIView())
header.addArrangedSubview(ruleButton)
if viewModel.shouldShowRuleButton {
let ruleButton = UIButton(type: .system)
ruleButton.setTitle("举报规则", for: .normal)
ruleButton.setImage(UIImage(systemName: "shield.lefthalf.filled"), for: .normal)
ruleButton.titleLabel?.font = .systemFont(ofSize: 15, weight: .bold)
ruleButton.tintColor = AppColor.primary
ruleButton.setTitleColor(AppColor.primary, for: .normal)
ruleButton.backgroundColor = AppColor.primaryLight
ruleButton.layer.cornerRadius = 18
ruleButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
ruleButton.addTarget(self, action: #selector(ruleTapped), for: .touchUpInside)
ruleButton.snp.makeConstraints { make in
make.height.equalTo(36)
}
header.addArrangedSubview(ruleButton)
}
let items = [
"需实名登录",
"请尽量上传清晰证据",
"恶意举报将被追责"
]
card.stack.addArrangedSubview(header)
items.forEach { item in
viewModel.noticeItems.forEach { item in
card.stack.addArrangedSubview(makeBullet(item))
}
return card
@ -206,7 +202,7 @@ final class WildPhotographerReportHomeViewController: BaseViewController {
let badges = UIStackView()
badges.axis = .horizontal
badges.distribution = .fillEqually
badges.spacing = AppSpacing.sm
badges.spacing = AppSpacing.xs
[
("实名", "身份核验 真实可靠", "person.crop.circle.badge.checkmark", AppColor.primary, AppColor.primaryLight),
("安全", "数据加密 隐私保护", "checkmark.shield.fill", AppColor.success, AppColor.successBackground),
@ -308,19 +304,14 @@ final class WildPhotographerReportHomeViewController: BaseViewController {
}
@objc private func mapTapped() {
let record = viewModel.reports.first ?? WildPhotographerReportMockStore.seedReports[0]
navigationController?.pushViewController(
WildReportRiskMapViewController(record: record, riskPoints: viewModel.riskPoints),
WildReportRiskMapViewController(),
animated: true
)
}
@objc private func ruleTapped() {
navigationController?.pushViewController(WildReportRuleViewController(), animated: true)
}
@objc private func backTapped() {
navigationController?.popViewController(animated: true)
navigationController?.pushViewController(WildReportRuleViewController(ruleGroups: viewModel.ruleGroups), animated: true)
}
}
@ -455,7 +446,19 @@ private final class WildReportTrustItemView: UIView {
///
final class WildReportRuleViewController: BaseViewController {
private let scrollView = UIScrollView()
private let stack = UIStackView()
private let ruleGroups: [WildReportRuleGroup]
init(ruleGroups: [WildReportRuleGroup]) {
self.ruleGroups = ruleGroups
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "举报规则"
@ -463,24 +466,32 @@ final class WildReportRuleViewController: BaseViewController {
override func setupUI() {
view.backgroundColor = AppColor.pageBackgroundSoft
scrollView.alwaysBounceVertical = true
scrollView.showsVerticalScrollIndicator = false
stack.axis = .vertical
stack.spacing = AppSpacing.sm
view.addSubview(stack)
addSection(title: "可举报行为", items: ["疑似打野摄影师主动揽客", "未佩戴工牌开展摄影服务", "引导游客线下付款或私下交易"])
addSection(title: "材料要求", items: ["尽量提供现场图片、视频、位置和文字说明", "联系方式可选填,用于景区工作人员核实"])
addSection(title: "处理说明", items: ["举报提交后景区公安会接收线索", "处理进度可在“我的举报”中查看", "请勿提交虚假或恶意举报"])
view.addSubview(scrollView)
scrollView.addSubview(stack)
ruleGroups.forEach { group in
addSection(title: group.title, items: group.items)
}
}
override func setupConstraints() {
scrollView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
stack.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(AppSpacing.md)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.edges.equalTo(scrollView.contentLayoutGuide).inset(AppSpacing.md)
make.width.equalTo(scrollView.frameLayoutGuide).offset(-AppSpacing.md * 2)
}
}
private func addSection(title: String, items: [String]) {
let card = WildReportCardView(spacing: AppSpacing.sm)
card.stack.addArrangedSubview(WildReportUI.label(title, font: .app(.title), color: AppColor.textPrimary))
if !title.isEmpty {
card.stack.addArrangedSubview(WildReportUI.label(title, font: .app(.title), color: AppColor.textPrimary))
}
items.forEach { item in
card.stack.addArrangedSubview(WildReportUI.label("· \(item)", font: .app(.body), color: AppColor.textSecondary, lines: 0))
}

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

View File

@ -4,7 +4,6 @@
//
import PhotosUI
import QuickLook
import SnapKit
import UIKit
@ -20,7 +19,6 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
private let contactField = UITextField()
private let submitButton = UIButton(type: .system)
private var pickerTarget: WildReportPickerTarget = .image
private var previewURL: URL?
private weak var activeInputView: UIView?
init(
@ -213,7 +211,12 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
grid.axis = .vertical
grid.spacing = 8
let types = viewModel.reportTypes.isEmpty ? WildReportType.fallbackOptions : viewModel.reportTypes
let types = viewModel.reportTypes
if types.isEmpty, !viewModel.isLoadingReportTypes {
let empty = WildReportUI.label("暂无可选举报类型,请稍后重试", font: .systemFont(ofSize: 13), color: AppColor.textSecondary)
card.stack.addArrangedSubview(empty)
return card
}
stride(from: 0, to: types.count, by: 3).forEach { startIndex in
let rowTypes = Array(types[startIndex ..< min(startIndex + 3, types.count)])
let row = UIStackView()
@ -223,7 +226,7 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
rowTypes.forEach { type in
let button = WildReportTypeOptionButton()
button.apply(type: type, isSelected: type == viewModel.selectedReportType)
button.apply(type: type, isSelected: viewModel.selectedReportType == type)
button.addAction(UIAction { [weak self] _ in self?.viewModel.selectReportType(type) }, for: .touchUpInside)
button.snp.makeConstraints { make in make.height.equalTo(72) }
row.addArrangedSubview(button)
@ -537,14 +540,11 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
}
private func previewAttachment(_ attachment: WildReportAttachment) {
guard let url = attachment.localFileURL else {
guard let item = MediaPreviewItem(wildReportAttachment: attachment) else {
showToast("当前附件暂无本地预览")
return
}
previewURL = url
let preview = QLPreviewController()
preview.dataSource = self
present(preview, animated: true)
presentMediaPreview(items: [item], startIndex: 0)
}
@objc private func refreshLocationTapped() { viewModel.refreshCurrentLocation() }
@ -631,16 +631,6 @@ extension WildPhotographerReportSubmitViewController: PHPickerViewControllerDele
}
}
extension WildPhotographerReportSubmitViewController: QLPreviewControllerDataSource {
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
previewURL == nil ? 0 : 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
previewURL! as NSURL
}
}
extension WildPhotographerReportSubmitViewController: UITextViewDelegate {
func textViewDidBeginEditing(_ textView: UITextView) {
activeInputView = textView

View File

@ -25,18 +25,16 @@ final class WildReportRiskMapViewController: BaseViewController {
private let detailContainerView = UIView()
private let scrollView = UIScrollView()
private let contentStack = UIStackView()
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
private var annotationMap: [String: WildReportMapMarker] = [:]
private var isShowingRiskMapLoading = false
init(
record: WildReportRecord,
riskPoints: [WildReportRiskPoint],
api: any WildPhotographerReportServing = NetworkServices.shared.wildPhotographerReportAPI,
locationProvider: any LocationProviding = LocationProvider.shared,
scenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId },
scenicNameProvider: @escaping () -> String = { AppStore.shared.currentScenicName }
) {
self.viewModel = WildReportRiskMapViewModel(record: record, riskPoints: riskPoints)
self.viewModel = WildReportRiskMapViewModel()
self.api = api
self.locationProvider = locationProvider
self.scenicIdProvider = scenicIdProvider
@ -61,6 +59,13 @@ final class WildReportRiskMapViewController: BaseViewController {
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true {
setRiskMapLoadingVisible(false)
}
}
override func setupNavigationBar() {
title = "附近风险地图"
}
@ -184,6 +189,7 @@ final class WildReportRiskMapViewController: BaseViewController {
@MainActor
private func refreshContent(animated: Bool) {
setRiskMapLoadingVisible(viewModel.isLoading)
scenicNameLabel.text = viewModel.scenicAreaName
syncMapAnnotations()
mapView.setRegion(viewModel.region, animated: animated)
@ -192,6 +198,13 @@ final class WildReportRiskMapViewController: BaseViewController {
rebuildPanel()
}
@MainActor
private func setRiskMapLoadingVisible(_ isVisible: Bool) {
guard isShowingRiskMapLoading != isVisible else { return }
isShowingRiskMapLoading = isVisible
isVisible ? showLoading() : hideLoading()
}
private func syncMapAnnotations() {
let oldAnnotations = mapView.annotations.compactMap { $0 as? WildReportMapAnnotation }
mapView.removeAnnotations(oldAnnotations)
@ -234,9 +247,6 @@ final class WildReportRiskMapViewController: BaseViewController {
view.removeFromSuperview()
}
if viewModel.isLoading {
contentStack.addArrangedSubview(makeLoadingView(text: "正在加载风险点位..."))
}
if let message = viewModel.errorMessage, !viewModel.isLoading {
contentStack.addArrangedSubview(makeStateCard(icon: "exclamationmark.triangle.fill", text: message))
}
@ -401,15 +411,6 @@ final class WildReportRiskMapViewController: BaseViewController {
return row
}
private func makeLoadingView(text: String) -> UIView {
let card = WildReportCardView(spacing: 12, inset: 16)
card.stack.alignment = .center
loadingIndicator.startAnimating()
card.stack.addArrangedSubview(loadingIndicator)
card.stack.addArrangedSubview(WildReportUI.label(text, font: .systemFont(ofSize: 14), color: AppColor.textSecondary))
return card
}
private func makeInlineLoading(text: String) -> UIView {
let row = UIStackView()
row.axis = .horizontal
@ -464,7 +465,7 @@ final class WildReportRiskMapViewController: BaseViewController {
}
private func presentImagePreview(url: String) {
present(WildReportRiskImagePreviewController(url: url), animated: true)
presentImagePreview(imageURLs: [url], startIndex: 0)
}
@objc private func navigateTapped() {
@ -711,46 +712,3 @@ private final class WildReportRiskImageTile: UIView {
onTap?()
}
}
///
private final class WildReportRiskImagePreviewController: UIViewController {
private let url: String
private let imageView = UIImageView()
init(url: String) {
self.url = url
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .fullScreen
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
imageView.contentMode = .scaleAspectFit
view.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
if let imageURL = URL(string: url) {
imageView.kf.setImage(with: imageURL)
}
let closeButton = UIButton(type: .system)
closeButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
closeButton.tintColor = .white
closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
view.addSubview(closeButton)
closeButton.snp.makeConstraints { make in
make.top.trailing.equalTo(view.safeAreaLayoutGuide).inset(18)
make.size.equalTo(36)
}
}
@objc private func closeTapped() {
dismiss(animated: true)
}
}

View File

@ -4,7 +4,6 @@
//
import PhotosUI
import QuickLook
import SnapKit
import UIKit
@ -23,7 +22,6 @@ final class WildReportSupplementEvidenceViewController: BaseViewController {
private let submitButton = UIButton(type: .system)
private var pickerTarget: WildReportPickerTarget = .image
private var previewURL: URL?
private weak var activeInputView: UIView?
init(
@ -322,14 +320,11 @@ final class WildReportSupplementEvidenceViewController: BaseViewController {
}
private func previewAttachment(_ attachment: WildReportAttachment) {
guard let url = attachment.localFileURL else {
guard let item = MediaPreviewItem(wildReportAttachment: attachment) else {
showToast("当前附件暂无本地预览")
return
}
previewURL = url
let preview = QLPreviewController()
preview.dataSource = self
present(preview, animated: true)
presentMediaPreview(items: [item], startIndex: 0)
}
@objc private func submitTapped() {
@ -420,16 +415,6 @@ extension WildReportSupplementEvidenceViewController: PHPickerViewControllerDele
}
}
extension WildReportSupplementEvidenceViewController: QLPreviewControllerDataSource {
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
previewURL == nil ? 0 : 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
previewURL! as NSURL
}
}
extension WildReportSupplementEvidenceViewController: UITextViewDelegate {
func textViewDidBeginEditing(_ textView: UITextView) {
activeInputView = textView