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,356 @@
//
// WildPhotographerReportListViewController.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class WildPhotographerReportListViewController: BaseViewController {
private let viewModel: WildPhotographerReportListViewModel
private let segmentedControl = UISegmentedControl(items: WildReportListFilter.allCases.map(\.rawValue))
private let tableView = UITableView(frame: .zero, style: .plain)
init(homeViewModel: WildPhotographerReportHomeViewModel) {
self.viewModel = WildPhotographerReportListViewModel(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
segmentedControl.selectedSegmentIndex = 0
segmentedControl.addTarget(self, action: #selector(filterChanged), for: .valueChanged)
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.delegate = self
tableView.dataSource = self
tableView.register(WildReportListCell.self, forCellReuseIdentifier: WildReportListCell.reuseIdentifier)
tableView.contentInset = UIEdgeInsets(top: AppSpacing.sm, left: 0, bottom: AppSpacing.lg, right: 0)
view.addSubview(segmentedControl)
view.addSubview(tableView)
}
override func setupConstraints() {
segmentedControl.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(36)
}
tableView.snp.makeConstraints { make in
make.top.equalTo(segmentedControl.snp.bottom).offset(AppSpacing.xs)
make.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide)
}
}
override func bindActions() {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.tableView.reloadData() }
}
viewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
}
@objc private func filterChanged() {
let filter = WildReportListFilter.allCases[segmentedControl.selectedSegmentIndex]
viewModel.selectFilter(filter)
}
}
extension WildPhotographerReportListViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let count = viewModel.filteredReports.count
tableView.backgroundView = count == 0 ? WildReportEmptyView(text: "暂无相关举报记录") : nil
return count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: WildReportListCell.reuseIdentifier, for: indexPath) as! WildReportListCell
let record = viewModel.filteredReports[indexPath.row]
cell.apply(record: record)
cell.onShare = { [weak self] in self?.viewModel.shareReport(record) }
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let record = viewModel.filteredReports[indexPath.row]
navigationController?.pushViewController(
WildPhotographerReportDetailViewController(record: record, homeViewModel: viewModel.homeViewModel),
animated: true
)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
UITableView.automaticDimension
}
}
/// Cell
final class WildReportListCell: UITableViewCell {
static let reuseIdentifier = "WildReportListCell"
private let card = WildReportCardView(spacing: AppSpacing.sm)
private let titleLabel = UILabel()
private let statusView = WildReportStatusView()
private let typeLabel = UILabel()
private let locationLabel = UILabel()
private let timeLabel = UILabel()
private let descLabel = UILabel()
private let materialLabel = UILabel()
var onShare: (() -> Void)?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.addSubview(card)
card.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: AppSpacing.xs, left: AppSpacing.md, bottom: AppSpacing.xs, right: AppSpacing.md))
}
let header = UIStackView()
header.axis = .horizontal
header.alignment = .center
header.spacing = AppSpacing.sm
titleLabel.font = .app(.title)
titleLabel.textColor = AppColor.textPrimary
header.addArrangedSubview(titleLabel)
header.addArrangedSubview(UIView())
statusView.snp.makeConstraints { make in
make.width.equalTo(64)
make.height.equalTo(24)
}
header.addArrangedSubview(statusView)
[typeLabel, locationLabel, timeLabel, descLabel, materialLabel].forEach { label in
label.font = .app(.body)
label.textColor = AppColor.textSecondary
label.numberOfLines = 0
}
let shareButton = UIButton(type: .system)
shareButton.setTitle("分享", for: .normal)
shareButton.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal)
shareButton.titleLabel?.font = .app(.captionMedium)
shareButton.addTarget(self, action: #selector(shareTapped), for: .touchUpInside)
card.stack.addArrangedSubview(header)
card.stack.addArrangedSubview(typeLabel)
card.stack.addArrangedSubview(locationLabel)
card.stack.addArrangedSubview(timeLabel)
card.stack.addArrangedSubview(descLabel)
card.stack.addArrangedSubview(materialLabel)
card.stack.addArrangedSubview(shareButton)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(record: WildReportRecord) {
titleLabel.text = record.title
statusView.apply(status: record.status)
typeLabel.text = "举报类型:\(record.reportType.rawValue)"
locationLabel.text = "位置:\(record.location)"
timeLabel.text = "提交时间:\(record.submitTime)"
descLabel.text = record.description
materialLabel.text = "材料:\(record.imageCount)张图片 \(record.videoCount)段视频"
}
@objc private func shareTapped() {
onShare?()
}
}
///
final class WildPhotographerReportDetailViewController: BaseViewController {
private let viewModel: WildPhotographerReportDetailViewModel
private let homeViewModel: WildPhotographerReportHomeViewModel
private let scrollView = UIScrollView()
private let stack = UIStackView()
init(record: WildReportRecord, homeViewModel: WildPhotographerReportHomeViewModel) {
self.viewModel = WildPhotographerReportDetailViewModel(record: record, homeViewModel: homeViewModel)
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 = "举报详情"
navigationItem.rightBarButtonItem = UIBarButtonItem(
image: UIImage(systemName: "square.and.arrow.up"),
style: .plain,
target: self,
action: #selector(shareTapped)
)
}
override func setupUI() {
view.backgroundColor = AppColor.pageBackgroundSoft
stack.axis = .vertical
stack.spacing = AppSpacing.sm
view.addSubview(scrollView)
scrollView.addSubview(stack)
rebuildContent()
}
override func setupConstraints() {
scrollView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
stack.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.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
homeViewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.rebuildContent() }
}
}
@MainActor
private func rebuildContent() {
stack.arrangedSubviews.forEach { view in
stack.removeArrangedSubview(view)
view.removeFromSuperview()
}
stack.addArrangedSubview(makeHeaderCard())
stack.addArrangedSubview(makeProgressCard())
stack.addArrangedSubview(makeEvidenceCard())
if viewModel.showHandlerInfo {
stack.addArrangedSubview(makeHandlerCard())
}
stack.addArrangedSubview(makeSupplementCard())
stack.addArrangedSubview(makeActionButtons())
}
private func makeHeaderCard() -> UIView {
let card = WildReportCardView(spacing: AppSpacing.sm)
let header = UIStackView()
header.axis = .horizontal
header.alignment = .center
header.addArrangedSubview(WildReportUI.label(viewModel.heroTitle, font: .app(.metric), color: AppColor.textPrimary))
header.addArrangedSubview(UIView())
let status = WildReportStatusView()
status.apply(status: viewModel.record.status)
status.snp.makeConstraints { make in
make.width.equalTo(64)
make.height.equalTo(24)
}
header.addArrangedSubview(status)
card.stack.addArrangedSubview(header)
card.stack.addArrangedSubview(WildReportInfoRowView(title: "举报编号", value: viewModel.record.id))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "景区", value: viewModel.scenicAreaName))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "位置", value: viewModel.detailAddress))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "联系方式", value: viewModel.contactText))
return card
}
private func makeProgressCard() -> UIView {
let card = WildReportCardView(spacing: AppSpacing.sm)
card.stack.addArrangedSubview(WildReportUI.label("处理进度", font: .app(.title), color: AppColor.textPrimary))
viewModel.progressSteps.forEach { step in
let prefix: String
switch step.state {
case .completed: prefix = ""
case .current: prefix = ""
case .pending: prefix = ""
}
card.stack.addArrangedSubview(WildReportUI.label("\(prefix) \(step.title) \(step.timeText)", font: .app(.body), color: AppColor.textSecondary, lines: 0))
}
return card
}
private func makeEvidenceCard() -> UIView {
let card = WildReportCardView(spacing: AppSpacing.sm)
card.stack.addArrangedSubview(WildReportUI.label("举报证据", font: .app(.title), color: AppColor.textPrimary))
card.stack.addArrangedSubview(WildReportUI.label(viewModel.descriptionText, font: .app(.body), color: AppColor.textSecondary, lines: 0))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "举报类型", value: viewModel.record.reportType.rawValue))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "举报人材料", value: viewModel.reporterMediaSummary))
let previewButton = WildReportUI.iconButton(title: "预览材料", imageName: "photo.on.rectangle", style: .secondary)
previewButton.addTarget(self, action: #selector(previewTapped), for: .touchUpInside)
card.stack.addArrangedSubview(previewButton)
return card
}
private func makeHandlerCard() -> UIView {
let card = WildReportCardView(spacing: AppSpacing.sm)
guard let info = viewModel.handlerInfo else { return card }
card.stack.addArrangedSubview(WildReportUI.label("处理信息", font: .app(.title), color: AppColor.textPrimary))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "处理人", value: info.handlerName))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "处理电话", value: info.maskedHandlerPhone))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "处理时间", value: info.processedAt))
card.stack.addArrangedSubview(WildReportUI.label(info.processOpinion, font: .app(.body), color: AppColor.textSecondary, lines: 0))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "处理凭证", value: info.completionMediaSummary))
return card
}
private func makeSupplementCard() -> UIView {
let card = WildReportCardView(spacing: AppSpacing.sm)
card.stack.addArrangedSubview(WildReportUI.label("补充证据记录", font: .app(.title), color: AppColor.textPrimary))
let items = viewModel.supplementaryEvidences
if items.isEmpty {
card.stack.addArrangedSubview(WildReportUI.label("暂无补充证据", font: .app(.body), color: AppColor.textSecondary))
} else {
items.enumerated().forEach { index, item in
card.stack.addArrangedSubview(WildReportUI.label("\(items.count - index)次补充 · \(item.submitTime)", font: .app(.bodyMedium), color: AppColor.textPrimary, lines: 0))
card.stack.addArrangedSubview(WildReportUI.label(item.summaryText, font: .app(.body), color: AppColor.textSecondary, lines: 0))
}
}
return card
}
private func makeActionButtons() -> UIView {
let row = UIStackView()
row.axis = .vertical
row.spacing = AppSpacing.sm
let supplement = AppButton(title: "补充证据")
supplement.addTarget(self, action: #selector(supplementTapped), for: .touchUpInside)
let map = AppButton(title: "查看附近风险地图", style: .secondary)
map.addTarget(self, action: #selector(mapTapped), for: .touchUpInside)
row.addArrangedSubview(supplement)
row.addArrangedSubview(map)
return row
}
@objc private func shareTapped() { viewModel.shareReport() }
@objc private func previewTapped() { viewModel.showMediaPreview(kind: "图片/视频") }
@objc private func supplementTapped() {
navigationController?.pushViewController(
WildReportSupplementEvidenceViewController(reportID: viewModel.record.id, homeViewModel: homeViewModel),
animated: true
)
}
@objc private func mapTapped() {
navigationController?.pushViewController(
WildReportRiskMapViewController(record: viewModel.record, riskPoints: homeViewModel.riskPoints),
animated: true
)
}
}