feat: add wild photographer report mock flow
This commit is contained in:
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user