Implement report supplement evidence flow

This commit is contained in:
2026-07-08 17:23:28 +08:00
parent 290a01e699
commit fd5dc2bbe6
9 changed files with 1906 additions and 421 deletions

View File

@ -3,6 +3,7 @@
// suixinkan
//
import Kingfisher
import SnapKit
import UIKit
@ -136,7 +137,7 @@ final class WildPhotographerReportListViewController: BaseViewController {
private func openDetail(_ record: WildReportRecord) {
navigationController?.pushViewController(
WildPhotographerReportDetailViewController(record: record, homeViewModel: viewModel.homeViewModel),
WildPhotographerReportDetailViewController(record: record, homeViewModel: viewModel.homeViewModel, api: api),
animated: true
)
}
@ -514,14 +515,20 @@ private final class WildReportListLoadingView: UIView {
final class WildPhotographerReportDetailViewController: BaseViewController {
private let viewModel: WildPhotographerReportDetailViewModel
private let homeViewModel: WildPhotographerReportHomeViewModel
private let api: any WildPhotographerReportServing
private let scrollView = UIScrollView()
private let stack = UIStackView()
private let bottomBar = UIView()
private let bottomButtonRow = UIStackView()
init(record: WildReportRecord, homeViewModel: WildPhotographerReportHomeViewModel) {
init(
record: WildReportRecord,
homeViewModel: WildPhotographerReportHomeViewModel,
api: any WildPhotographerReportServing = NetworkServices.shared.wildPhotographerReportAPI
) {
self.viewModel = WildPhotographerReportDetailViewModel(record: record, homeViewModel: homeViewModel)
self.homeViewModel = homeViewModel
self.api = api
super.init(nibName: nil, bundle: nil)
}
@ -593,7 +600,17 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
}
}
override func viewDidLoad() {
super.viewDidLoad()
Task {
await viewModel.loadDetailIfNeeded(api: api)
}
}
override func bindActions() {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.rebuildContent() }
}
viewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
@ -705,6 +722,10 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "举报说明", value: viewModel.descriptionText, multiline: true))
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "微信号/手机号", value: viewModel.contactText))
if !viewModel.handleRemarkText.isEmpty {
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "处理备注", value: viewModel.handleRemarkText, multiline: true))
}
return card
}
@ -715,6 +736,10 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
card.stack.addArrangedSubview(makeReporterMaterialsSection())
card.stack.addArrangedSubview(makeDivider())
}
if !viewModel.paymentScreenshotItems.isEmpty {
card.stack.addArrangedSubview(makePaymentScreenshotSection())
card.stack.addArrangedSubview(makeDivider())
}
card.stack.addArrangedSubview(makeLocationSection())
return card
}
@ -819,6 +844,43 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
return section
}
private func makePaymentScreenshotSection() -> UIView {
let section = UIStackView()
section.axis = .vertical
section.spacing = 12
section.addArrangedSubview(makeSectionHeader(
iconName: "creditcard.fill",
title: "支付截图",
subtitle: viewModel.paymentScreenshotSummary,
chevron: true,
action: #selector(paymentPreviewTapped)
))
let mediaScroll = UIScrollView()
mediaScroll.showsHorizontalScrollIndicator = false
let mediaStack = UIStackView()
mediaStack.axis = .horizontal
mediaStack.spacing = 10
mediaScroll.addSubview(mediaStack)
mediaStack.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.height.equalTo(mediaScroll.frameLayoutGuide)
}
viewModel.paymentScreenshotItems.forEach { item in
let tile = WildReportDetailMediaThumbnailView(reporterItem: item)
tile.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reporterMediaTapped(_:))))
tile.snp.makeConstraints { make in
make.size.equalTo(108)
}
mediaStack.addArrangedSubview(tile)
}
mediaScroll.snp.makeConstraints { make in
make.height.equalTo(108)
}
section.addArrangedSubview(mediaScroll)
return section
}
private func makeCompletionMediaSection(info: WildReportHandlerInfo) -> UIView {
let section = UIStackView()
section.axis = .vertical
@ -996,6 +1058,7 @@ 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 locationTapped() { viewModel.showLocationPreview() }
@objc private func reporterMediaTapped(_ recognizer: UITapGestureRecognizer) {
@ -1010,7 +1073,17 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
@objc private func supplementTapped() {
navigationController?.pushViewController(
WildReportSupplementEvidenceViewController(reportID: viewModel.record.id, homeViewModel: homeViewModel),
WildReportSupplementEvidenceViewController(
record: viewModel.record,
homeViewModel: homeViewModel,
api: api,
onSupplementSuccess: { [weak self] in
guard let self else { return }
Task {
await self.viewModel.loadDetailIfNeeded(api: self.api, force: true)
}
}
),
animated: true
)
}
@ -1107,24 +1180,25 @@ private final class WildReportDetailInfoRowView: UIView {
}
}
///
///
private final class WildReportDetailMediaThumbnailView: UIView {
let previewKindText: String
private let gradient = CAGradientLayer()
private let remoteImageView = UIImageView()
private let iconView = 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) {
switch reporterItem {
case .image(let index):
previewKindText = "\(index + 1)张举报图片"
case .image(let index, let url, let fileName, let fileTypeText):
previewKindText = fileName?.isEmpty == false ? fileName! : "\(index + 1)张举报图片"
super.init(frame: .zero)
configure(kind: .image(index: index), footerText: "图片 \(index + 1)")
case .video(let index):
previewKindText = "\(index + 1)段举报视频"
configure(kind: .image(index: index), footerText: fileTypeText ?? "图片 \(index + 1)", imageURL: url)
case .video(let index, _, let fileName, let fileTypeText):
previewKindText = fileName?.isEmpty == false ? fileName! : "\(index + 1)段举报视频"
super.init(frame: .zero)
configure(kind: .video(index: index, duration: "00:32"), footerText: "视频 \(index + 1)")
configure(kind: .video(index: index, duration: fileTypeText ?? "视频"), footerText: fileTypeText ?? "视频 \(index + 1)")
}
}
@ -1151,7 +1225,7 @@ private final class WildReportDetailMediaThumbnailView: UIView {
gradient.frame = bounds
}
private func configure(kind: Kind, footerText: String) {
private func configure(kind: Kind, footerText: String, imageURL: String? = nil) {
clipsToBounds = true
layer.cornerRadius = 14
gradient.colors = kind.gradientColors
@ -1159,9 +1233,20 @@ private final class WildReportDetailMediaThumbnailView: UIView {
gradient.endPoint = CGPoint(x: 1, y: 1)
layer.insertSublayer(gradient, at: 0)
if let imageURL, let url = URL(string: imageURL) {
remoteImageView.contentMode = .scaleAspectFill
remoteImageView.clipsToBounds = true
remoteImageView.kf.setImage(with: url, placeholder: UIImage(systemName: "photo"))
addSubview(remoteImageView)
remoteImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
iconView.image = UIImage(systemName: kind.iconName)
iconView.tintColor = UIColor.white.withAlphaComponent(0.92)
iconView.contentMode = .scaleAspectFit
iconView.isHidden = imageURL != nil
addSubview(iconView)
footerLabel.text = footerText