357 lines
15 KiB
Swift
357 lines
15 KiB
Swift
//
|
||
// 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
|
||
)
|
||
}
|
||
}
|