feat: add wild photographer report mock flow

This commit is contained in:
2026-07-08 12:06:13 +08:00
parent 87fba3d5d6
commit 8a65d3c104
14 changed files with 3542 additions and 0 deletions

View File

@ -0,0 +1,330 @@
//
// WildPhotographerReportSubmitViewController.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class WildPhotographerReportSubmitViewController: BaseViewController {
private let viewModel: WildPhotographerReportSubmitViewModel
private let homeViewModel: WildPhotographerReportHomeViewModel
private let scrollView = UIScrollView()
private let contentStack = UIStackView()
private let descriptionView = UITextView()
private let contactField = UITextField()
private let submitButton = AppButton(title: "提交举报")
init(
homeViewModel: WildPhotographerReportHomeViewModel,
locationProvider: any LocationProviding = LocationProvider.shared
) {
self.homeViewModel = homeViewModel
self.viewModel = WildPhotographerReportSubmitViewModel(
homeViewModel: homeViewModel,
locationProvider: locationProvider
)
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
scrollView.keyboardDismissMode = .interactive
contentStack.axis = .vertical
contentStack.spacing = AppSpacing.sm
descriptionView.font = .app(.body)
descriptionView.textColor = AppColor.textPrimary
descriptionView.backgroundColor = AppColor.inputBackground
descriptionView.layer.cornerRadius = AppRadius.md
descriptionView.textContainerInset = UIEdgeInsets(top: 12, left: 10, bottom: 12, right: 10)
descriptionView.delegate = self
contactField.placeholder = "微信号或手机号(选填)"
contactField.font = .app(.body)
contactField.textColor = AppColor.textPrimary
contactField.backgroundColor = AppColor.inputBackground
contactField.layer.cornerRadius = AppRadius.md
contactField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
contactField.leftViewMode = .always
contactField.addTarget(self, action: #selector(contactChanged), for: .editingChanged)
submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)
view.addSubview(scrollView)
scrollView.addSubview(contentStack)
view.addSubview(submitButton)
rebuildContent()
}
override func setupConstraints() {
submitButton.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(AppSpacing.sm)
}
scrollView.snp.makeConstraints { make in
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
make.bottom.equalTo(submitButton.snp.top).offset(-AppSpacing.sm)
}
contentStack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: AppSpacing.sm, left: AppSpacing.md, bottom: AppSpacing.lg, right: AppSpacing.md))
make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.md * 2)
}
}
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) }
}
viewModel.onSubmitSuccess = { [weak self] result in
Task { @MainActor in
guard let self else { return }
self.navigationController?.pushViewController(
WildPhotographerReportSuccessViewController(
result: result,
homeViewModel: self.homeViewModel
),
animated: true
)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel.loadInitialLocationIfNeeded()
}
@MainActor
private func rebuildContent() {
contentStack.arrangedSubviews.forEach { view in
contentStack.removeArrangedSubview(view)
view.removeFromSuperview()
}
contentStack.addArrangedSubview(makeTipsCard())
contentStack.addArrangedSubview(makeTypeCard())
contentStack.addArrangedSubview(makeEvidenceCard())
contentStack.addArrangedSubview(makeDescriptionCard())
contentStack.addArrangedSubview(makeLocationCard())
contentStack.addArrangedSubview(makeContactCard())
contentStack.addArrangedSubview(makePaymentCard())
}
private func makeTipsCard() -> UIView {
let card = WildReportCardView(spacing: AppSpacing.xs)
card.stack.addArrangedSubview(WildReportUI.label("实名登录后提交,景区公安将接收并处理", font: .app(.bodyMedium), color: AppColor.textPrimary, lines: 0))
card.stack.addArrangedSubview(WildReportUI.label("请描述摄影师位置、外貌特征和具体行为。", font: .app(.caption), color: AppColor.textSecondary, lines: 0))
return card
}
private func makeTypeCard() -> UIView {
let card = sectionCard(index: 1, title: "选择举报类型")
let row = UIStackView()
row.axis = .vertical
row.spacing = AppSpacing.xs
WildReportType.allCases.forEach { type in
let button = UIButton(type: .system)
button.contentHorizontalAlignment = .left
button.titleLabel?.font = .app(.bodyMedium)
button.setTitle(" \(type.rawValue)", for: .normal)
button.setImage(UIImage(systemName: type == viewModel.selectedReportType ? "largecircle.fill.circle" : "circle"), for: .normal)
button.tintColor = AppColor.primary
button.setTitleColor(AppColor.textPrimary, for: .normal)
button.addAction(UIAction { [weak self] _ in self?.viewModel.selectReportType(type) }, for: .touchUpInside)
button.snp.makeConstraints { make in make.height.equalTo(38) }
row.addArrangedSubview(button)
}
card.stack.addArrangedSubview(row)
return card
}
private func makeEvidenceCard() -> UIView {
let card = sectionCard(index: 2, title: "上传现场证据")
let buttonRow = UIStackView()
buttonRow.axis = .horizontal
buttonRow.spacing = AppSpacing.sm
buttonRow.distribution = .fillEqually
let imageButton = WildReportUI.iconButton(title: "添加图片", imageName: "photo", style: .secondary)
imageButton.addTarget(self, action: #selector(addImageTapped), for: .touchUpInside)
let videoButton = WildReportUI.iconButton(title: "添加视频", imageName: "video", style: .secondary)
videoButton.addTarget(self, action: #selector(addVideoTapped), for: .touchUpInside)
[imageButton, videoButton].forEach { button in
button.snp.makeConstraints { make in make.height.equalTo(44) }
buttonRow.addArrangedSubview(button)
}
card.stack.addArrangedSubview(buttonRow)
addAttachments(viewModel.images + viewModel.videos, to: card)
return card
}
private func makeDescriptionCard() -> UIView {
let card = sectionCard(index: 3, title: "举报说明")
descriptionView.text = viewModel.description
card.stack.addArrangedSubview(descriptionView)
descriptionView.snp.makeConstraints { make in
make.height.equalTo(132)
}
let count = WildReportUI.label("\(viewModel.description.count)/500", font: .app(.caption), color: AppColor.textTertiary)
count.textAlignment = .right
card.stack.addArrangedSubview(count)
return card
}
private func makeLocationCard() -> UIView {
let card = sectionCard(index: 4, title: "举报位置")
let title = WildReportUI.label(viewModel.locationTitleText, font: .app(.bodyMedium), color: AppColor.textPrimary, lines: 0)
let subtitle = WildReportUI.label(viewModel.locationSubtitleText, font: .app(.caption), color: AppColor.textSecondary, lines: 0)
let button = WildReportUI.iconButton(title: viewModel.isUpdatingLocation ? "定位中" : "更新定位", imageName: "location.fill", style: .secondary)
button.isEnabled = !viewModel.isUpdatingLocation
button.addTarget(self, action: #selector(refreshLocationTapped), for: .touchUpInside)
button.snp.makeConstraints { make in make.height.equalTo(42) }
card.stack.addArrangedSubview(title)
card.stack.addArrangedSubview(subtitle)
card.stack.addArrangedSubview(button)
return card
}
private func makeContactCard() -> UIView {
let card = sectionCard(index: 5, title: "联系方式(选填)")
contactField.text = viewModel.contact
card.stack.addArrangedSubview(contactField)
contactField.snp.makeConstraints { make in make.height.equalTo(46) }
return card
}
private func makePaymentCard() -> UIView {
let card = sectionCard(index: 6, title: "支付截图(选填)")
card.stack.addArrangedSubview(WildReportUI.label("上传线下微信/支付宝支付截图,最多 3 张。", font: .app(.caption), color: AppColor.textSecondary, lines: 0))
let button = WildReportUI.iconButton(title: "添加截图", imageName: "creditcard", style: .secondary)
button.addTarget(self, action: #selector(addPaymentTapped), for: .touchUpInside)
button.snp.makeConstraints { make in make.height.equalTo(42) }
card.stack.addArrangedSubview(button)
addAttachments(viewModel.paymentImages, to: card)
return card
}
private func sectionCard(index: Int, title: String) -> WildReportCardView {
let card = WildReportCardView(spacing: AppSpacing.sm)
card.stack.addArrangedSubview(WildReportUI.label("\(index). \(title)", font: .app(.title), color: AppColor.textPrimary))
return card
}
private func addAttachments(_ attachments: [WildReportAttachment], to card: WildReportCardView) {
guard !attachments.isEmpty else { return }
attachments.forEach { attachment in
let chip = WildReportAttachmentChipView(attachment: attachment)
chip.onRemove = { [weak self] in
self?.viewModel.removeAttachment(attachment)
}
card.stack.addArrangedSubview(chip)
}
}
@objc private func addImageTapped() { viewModel.addImage() }
@objc private func addVideoTapped() { viewModel.addVideo() }
@objc private func addPaymentTapped() { viewModel.addPaymentImage() }
@objc private func refreshLocationTapped() { viewModel.refreshCurrentLocation() }
@objc private func submitTapped() { viewModel.submitReport() }
@objc private func contactChanged() { viewModel.updateContact(contactField.text ?? "") }
}
extension WildPhotographerReportSubmitViewController: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
viewModel.updateDescription(textView.text)
}
}
///
final class WildPhotographerReportSuccessViewController: BaseViewController {
private let viewModel: WildPhotographerReportSuccessViewModel
private let homeViewModel: WildPhotographerReportHomeViewModel
private let stack = UIStackView()
init(result: WildReportSubmitResult, homeViewModel: WildPhotographerReportHomeViewModel) {
self.viewModel = WildPhotographerReportSuccessViewModel(result: result)
self.homeViewModel = homeViewModel
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
stack.axis = .vertical
stack.spacing = AppSpacing.sm
view.addSubview(stack)
let hero = WildReportCardView(spacing: AppSpacing.md)
hero.stack.alignment = .center
let icon = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
icon.tintColor = AppColor.success
icon.snp.makeConstraints { make in make.size.equalTo(58) }
hero.stack.addArrangedSubview(icon)
hero.stack.addArrangedSubview(WildReportUI.label("举报已提交", font: .app(.metric), color: AppColor.textPrimary))
let desc = WildReportUI.label("景区公安已收到线索,将尽快前往现场核实处理", font: .app(.body), color: AppColor.textSecondary, lines: 0)
desc.textAlignment = .center
hero.stack.addArrangedSubview(desc)
stack.addArrangedSubview(hero)
let info = WildReportCardView(spacing: AppSpacing.sm)
info.stack.addArrangedSubview(WildReportInfoRowView(title: "举报编号", value: viewModel.result.record.id))
info.stack.addArrangedSubview(WildReportInfoRowView(title: "上传材料", value: viewModel.uploadSummary))
info.stack.addArrangedSubview(WildReportInfoRowView(title: "提交时间", value: viewModel.result.record.submitTime))
stack.addArrangedSubview(info)
let progress = WildReportCardView(spacing: AppSpacing.sm)
progress.stack.addArrangedSubview(WildReportUI.label("处理进度", font: .app(.title), color: AppColor.textPrimary))
viewModel.progressSteps.enumerated().forEach { index, title in
progress.stack.addArrangedSubview(WildReportUI.label("\(index + 1). \(title)", font: .app(.body), color: AppColor.textSecondary))
}
stack.addArrangedSubview(progress)
let detailButton = AppButton(title: "查看处理进度")
detailButton.addTarget(self, action: #selector(detailTapped), for: .touchUpInside)
let homeButton = AppButton(title: "返回首页", style: .secondary)
homeButton.addTarget(self, action: #selector(homeTapped), for: .touchUpInside)
stack.addArrangedSubview(detailButton)
stack.addArrangedSubview(homeButton)
}
override func setupConstraints() {
stack.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(AppSpacing.md)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
}
}
@objc private func detailTapped() {
navigationController?.pushViewController(
WildPhotographerReportDetailViewController(record: viewModel.result.record, homeViewModel: homeViewModel),
animated: true
)
}
@objc private func homeTapped() {
navigationController?.popToViewController(
navigationController?.viewControllers.first(where: { $0 is WildPhotographerReportHomeViewController }) ?? self,
animated: true
)
}
}