feat: 更新素材管理和举报风险地图
This commit is contained in:
@ -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()
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
/// 举报详情页处理进度时间线单行。
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user