Files
suixinkan_uikit/suixinkan/UI/WildPhotographerReport/WildPhotographerReportHomeViewController.swift

490 lines
20 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// WildPhotographerReportHomeViewController.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class WildPhotographerReportHomeViewController: BaseViewController {
private let viewModel: WildPhotographerReportHomeViewModel
private let scrollView = UIScrollView()
private let contentStack = UIStackView()
init(viewModel: WildPhotographerReportHomeViewModel = WildPhotographerReportHomeViewModel()) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "举报摄影师"
navigationItem.largeTitleDisplayMode = .never
let backButton = UIButton(type: .system)
backButton.backgroundColor = .white
backButton.tintColor = AppColor.textPrimary
backButton.setImage(UIImage(systemName: "chevron.left"), for: .normal)
backButton.layer.cornerRadius = 25
backButton.layer.shadowColor = UIColor.black.cgColor
backButton.layer.shadowOpacity = 0.05
backButton.layer.shadowOffset = CGSize(width: 0, height: 6)
backButton.layer.shadowRadius = 16
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
backButton.snp.makeConstraints { make in
make.size.equalTo(50)
}
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton)
}
override func setupUI() {
view.backgroundColor = AppColor.pageBackgroundSoft
scrollView.alwaysBounceVertical = true
scrollView.showsVerticalScrollIndicator = false
scrollView.contentInsetAdjustmentBehavior = .never
contentStack.axis = .vertical
contentStack.spacing = 14
view.addSubview(scrollView)
scrollView.addSubview(contentStack)
rebuildContent()
}
override func setupConstraints() {
scrollView.snp.makeConstraints { make in
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
make.bottom.equalToSuperview()
}
contentStack.snp.makeConstraints { make in
make.edges.equalTo(scrollView.contentLayoutGuide).inset(UIEdgeInsets(top: AppSpacing.md, left: 20, bottom: AppSpacing.md, right: 20))
make.width.equalTo(scrollView.frameLayoutGuide).offset(-40)
}
}
override func bindActions() {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.rebuildContent() }
}
}
@MainActor
private func rebuildContent() {
contentStack.arrangedSubviews.forEach { view in
contentStack.removeArrangedSubview(view)
view.removeFromSuperview()
}
switch viewModel.pageState {
case .normal:
contentStack.addArrangedSubview(makeHeroCard())
contentStack.addArrangedSubview(makeRiskMapEntry())
contentStack.addArrangedSubview(makeNoticeCard())
contentStack.addArrangedSubview(makeTrustCard())
case .loading:
contentStack.addArrangedSubview(makeStateCard(icon: "clock", title: "加载中", message: "正在加载举报服务"))
case .empty:
contentStack.addArrangedSubview(makeStateCard(icon: "exclamationmark.shield", title: viewModel.emptyTitle, message: viewModel.emptyMessage, buttonTitle: "恢复演示数据") { [weak self] in
self?.viewModel.restoreDemoData()
})
case .error:
contentStack.addArrangedSubview(makeStateCard(icon: "wifi.exclamationmark", title: "加载失败", message: "举报服务暂时不可用,请稍后重试。", buttonTitle: "重新加载") { [weak self] in
self?.viewModel.retryLoadAfterError()
})
}
}
private func makeHeroCard() -> UIView {
let card = WildReportCardView(spacing: 22, inset: 22)
card.backgroundColor = UIColor(hex: 0xFBFDFF)
card.layer.cornerRadius = 26
card.layer.borderColor = UIColor(hex: 0xBFD7FF).cgColor
let topRow = UIStackView()
topRow.axis = .horizontal
topRow.alignment = .top
topRow.spacing = AppSpacing.md
let titleStack = UIStackView()
titleStack.axis = .vertical
titleStack.spacing = 10
let title = WildReportUI.label("实名举报", font: .systemFont(ofSize: 30, weight: .heavy), color: AppColor.textPrimary)
let subtitle = WildReportUI.label("摄影师", font: .systemFont(ofSize: 30, weight: .heavy), color: AppColor.primary)
titleStack.addArrangedSubview(title)
titleStack.addArrangedSubview(subtitle)
topRow.addArrangedSubview(titleStack)
topRow.addArrangedSubview(UIView())
topRow.addArrangedSubview(WildReportHeroIllustrationView())
let desc = WildReportUI.label("可上传图片、视频、文字和位置,景区公安将接收并现场处理。", font: .systemFont(ofSize: 17, weight: .regular), color: UIColor(hex: 0x687484), lines: 0)
desc.setContentCompressionResistancePriority(.required, for: .vertical)
let buttonRow = UIStackView()
buttonRow.axis = .horizontal
buttonRow.spacing = 14
buttonRow.distribution = .fillEqually
let submitButton = WildReportUI.iconButton(title: "立即举报", imageName: "checkmark.shield.fill")
submitButton.titleLabel?.font = .systemFont(ofSize: 18, weight: .bold)
submitButton.layer.cornerRadius = 14
submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)
let listButton = WildReportUI.iconButton(title: "我的举报", imageName: "doc.text.fill", style: .secondary)
listButton.titleLabel?.font = .systemFont(ofSize: 18, weight: .bold)
listButton.layer.cornerRadius = 14
listButton.backgroundColor = .white
listButton.layer.borderWidth = 1
listButton.layer.borderColor = UIColor(hex: 0xBFD7FF).cgColor
listButton.addTarget(self, action: #selector(listTapped), for: .touchUpInside)
[submitButton, listButton].forEach { button in
button.snp.makeConstraints { make in make.height.equalTo(58) }
buttonRow.addArrangedSubview(button)
}
card.stack.addArrangedSubview(topRow)
card.stack.addArrangedSubview(desc)
card.stack.addArrangedSubview(buttonRow)
return card
}
private func makeRiskMapEntry() -> UIView {
let card = WildReportCardView(spacing: 0, inset: 18)
card.layer.cornerRadius = 20
let row = makeEntryRow(icon: "map.fill", title: "附近风险地图", subtitle: "查看附近摄影师风险点位")
let button = UIButton(type: .system)
button.addTarget(self, action: #selector(mapTapped), for: .touchUpInside)
card.stack.addArrangedSubview(row)
card.addSubview(button)
button.snp.makeConstraints { make in make.edges.equalToSuperview() }
return card
}
private func makeNoticeCard() -> UIView {
let card = WildReportCardView(spacing: 18, inset: 18)
card.layer.cornerRadius = 20
let header = UIStackView()
header.axis = .horizontal
header.alignment = .center
header.spacing = AppSpacing.sm
let headerIcon = UIImageView(image: UIImage(systemName: "list.clipboard.fill"))
headerIcon.tintColor = AppColor.primary
headerIcon.snp.makeConstraints { make in make.size.equalTo(24) }
let title = WildReportUI.label("举报须知", font: .systemFont(ofSize: 18, weight: .bold), color: AppColor.textPrimary)
let ruleButton = UIButton(type: .system)
ruleButton.setTitle("举报规则", for: .normal)
ruleButton.setImage(UIImage(systemName: "shield.lefthalf.filled"), for: .normal)
ruleButton.titleLabel?.font = .systemFont(ofSize: 15, weight: .bold)
ruleButton.tintColor = AppColor.primary
ruleButton.setTitleColor(AppColor.primary, for: .normal)
ruleButton.backgroundColor = AppColor.primaryLight
ruleButton.layer.cornerRadius = 18
ruleButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
ruleButton.addTarget(self, action: #selector(ruleTapped), for: .touchUpInside)
ruleButton.snp.makeConstraints { make in
make.height.equalTo(36)
}
header.addArrangedSubview(headerIcon)
header.addArrangedSubview(title)
header.addArrangedSubview(UIView())
header.addArrangedSubview(ruleButton)
let items = [
"需实名登录",
"请尽量上传清晰证据",
"恶意举报将被追责"
]
card.stack.addArrangedSubview(header)
items.forEach { item in
card.stack.addArrangedSubview(makeBullet(item))
}
return card
}
private func makeTrustCard() -> UIView {
let badges = UIStackView()
badges.axis = .horizontal
badges.distribution = .fillEqually
badges.spacing = AppSpacing.sm
[
("实名", "身份核验 真实可靠", "person.crop.circle.badge.checkmark", AppColor.primary, AppColor.primaryLight),
("安全", "数据加密 隐私保护", "checkmark.shield.fill", AppColor.success, AppColor.successBackground),
("景区公安处理", "专业受理 依法处置", "person.text.rectangle.fill", AppColor.primary, AppColor.primaryLight)
].forEach { title, subtitle, icon, tint, bg in
badges.addArrangedSubview(WildReportTrustItemView(title: title, subtitle: subtitle, iconName: icon, tintColor: tint, backgroundColor: bg))
}
return badges
}
private func makeEntryRow(icon: String, title: String, subtitle: String) -> UIView {
let row = UIStackView()
row.axis = .horizontal
row.alignment = .center
row.spacing = 18
let iconWrap = UIView()
iconWrap.backgroundColor = AppColor.primaryLight
iconWrap.layer.cornerRadius = 18
let image = UIImageView(image: UIImage(systemName: icon))
image.tintColor = AppColor.primary
iconWrap.addSubview(image)
image.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(34)
}
iconWrap.snp.makeConstraints { make in make.size.equalTo(64) }
let textStack = UIStackView()
textStack.axis = .vertical
textStack.spacing = 6
textStack.addArrangedSubview(WildReportUI.label(title, font: .systemFont(ofSize: 19, weight: .bold), color: AppColor.textPrimary))
textStack.addArrangedSubview(WildReportUI.label(subtitle, font: .systemFont(ofSize: 15, weight: .regular), color: AppColor.textSecondary))
let chevron = UIImageView(image: UIImage(systemName: "chevron.right"))
chevron.tintColor = AppColor.textTertiary
row.addArrangedSubview(iconWrap)
row.addArrangedSubview(textStack)
row.addArrangedSubview(UIView())
row.addArrangedSubview(chevron)
return row
}
private func makeBullet(_ text: String) -> UIView {
let row = UIStackView()
row.axis = .horizontal
row.alignment = .center
row.spacing = 14
let dot = UIView()
dot.backgroundColor = AppColor.primary
dot.layer.cornerRadius = 4
dot.snp.makeConstraints { make in
make.size.equalTo(8)
}
let label = WildReportUI.label(text, font: .systemFont(ofSize: 16, weight: .regular), color: UIColor(hex: 0x667085), lines: 0)
row.addArrangedSubview(dot)
row.addArrangedSubview(label)
return row
}
private func makeStateCard(
icon: String,
title: String,
message: String,
buttonTitle: String? = nil,
action: (() -> Void)? = nil
) -> UIView {
let card = WildReportCardView(spacing: AppSpacing.md)
card.stack.alignment = .center
let image = UIImageView(image: UIImage(systemName: icon))
image.tintColor = AppColor.primary
image.snp.makeConstraints { make in make.size.equalTo(48) }
let titleLabel = WildReportUI.label(title, font: .app(.metric), color: AppColor.textPrimary)
let messageLabel = WildReportUI.label(message, font: .app(.body), color: AppColor.textSecondary, lines: 0)
messageLabel.textAlignment = .center
card.stack.addArrangedSubview(image)
card.stack.addArrangedSubview(titleLabel)
card.stack.addArrangedSubview(messageLabel)
if let buttonTitle {
let button = AppButton(title: buttonTitle)
button.addAction(UIAction { _ in action?() }, for: .touchUpInside)
card.stack.addArrangedSubview(button)
button.snp.makeConstraints { make in make.width.equalTo(180) }
}
return card
}
@objc private func submitTapped() {
navigationController?.pushViewController(
WildPhotographerReportSubmitViewController(homeViewModel: viewModel),
animated: true
)
}
@objc private func listTapped() {
navigationController?.pushViewController(
WildPhotographerReportListViewController(homeViewModel: viewModel),
animated: true
)
}
@objc private func mapTapped() {
let record = viewModel.reports.first ?? WildPhotographerReportMockStore.seedReports[0]
navigationController?.pushViewController(
WildReportRiskMapViewController(record: record, riskPoints: viewModel.riskPoints),
animated: true
)
}
@objc private func ruleTapped() {
navigationController?.pushViewController(WildReportRuleViewController(), animated: true)
}
@objc private func backTapped() {
navigationController?.popViewController(animated: true)
}
}
/// Hero
private final class WildReportHeroIllustrationView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
let ring = UIView()
ring.backgroundColor = UIColor(hex: 0xD9EAFF)
ring.layer.cornerRadius = 58
let center = UIView()
center.backgroundColor = .white
center.layer.cornerRadius = 28
center.layer.shadowColor = UIColor.black.cgColor
center.layer.shadowOpacity = 0.04
center.layer.shadowRadius = 10
center.layer.shadowOffset = CGSize(width: 0, height: 4)
let shield = UIImageView(image: UIImage(systemName: "exclamationmark.shield.fill"))
shield.tintColor = AppColor.primary
shield.contentMode = .scaleAspectFit
let cameraBadge = UIView()
cameraBadge.backgroundColor = UIColor(hex: 0xE8F3FF)
cameraBadge.layer.cornerRadius = 14
let camera = UIImageView(image: UIImage(systemName: "camera.fill"))
camera.tintColor = UIColor(hex: 0x195591)
camera.contentMode = .scaleAspectFit
addSubview(ring)
addSubview(center)
center.addSubview(shield)
addSubview(cameraBadge)
cameraBadge.addSubview(camera)
ring.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(116)
}
center.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(88)
}
shield.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(52)
}
cameraBadge.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-8)
make.bottom.equalToSuperview().offset(-10)
make.size.equalTo(38)
}
camera.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(22)
}
snp.makeConstraints { make in
make.size.equalTo(116)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
private final class WildReportTrustItemView: UIView {
init(title: String, subtitle: String, iconName: String, tintColor: UIColor, backgroundColor: UIColor) {
super.init(frame: .zero)
self.backgroundColor = .white
layer.cornerRadius = 16
layer.borderWidth = 1
layer.borderColor = AppColor.cardOutline.cgColor
let iconWrap = UIView()
iconWrap.backgroundColor = backgroundColor
iconWrap.layer.cornerRadius = 22
let icon = UIImageView(image: UIImage(systemName: iconName))
icon.tintColor = tintColor
icon.contentMode = .scaleAspectFit
iconWrap.addSubview(icon)
icon.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(26)
}
let titleLabel = WildReportUI.label(title, font: .systemFont(ofSize: 16, weight: .bold), color: AppColor.textPrimary)
titleLabel.textAlignment = .center
titleLabel.adjustsFontSizeToFitWidth = true
titleLabel.minimumScaleFactor = 0.78
titleLabel.lineBreakMode = .byClipping
titleLabel.setContentCompressionResistancePriority(.required, for: .vertical)
let subtitleLabel = WildReportUI.label(subtitle, font: .systemFont(ofSize: 12, weight: .regular), color: AppColor.textSecondary)
subtitleLabel.textAlignment = .center
subtitleLabel.adjustsFontSizeToFitWidth = true
subtitleLabel.minimumScaleFactor = 0.74
subtitleLabel.lineBreakMode = .byClipping
subtitleLabel.setContentCompressionResistancePriority(.required, for: .vertical)
addSubview(iconWrap)
addSubview(titleLabel)
addSubview(subtitleLabel)
iconWrap.snp.makeConstraints { make in
make.top.equalToSuperview().offset(14)
make.centerX.equalToSuperview()
make.size.equalTo(44)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(iconWrap.snp.bottom).offset(8)
make.leading.trailing.equalToSuperview().inset(6)
make.height.equalTo(20)
}
subtitleLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(4)
make.leading.trailing.equalToSuperview().inset(6)
make.height.equalTo(16)
}
snp.makeConstraints { make in
make.height.equalTo(118)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
final class WildReportRuleViewController: BaseViewController {
private let stack = UIStackView()
override func setupNavigationBar() {
title = "举报规则"
}
override func setupUI() {
view.backgroundColor = AppColor.pageBackgroundSoft
stack.axis = .vertical
stack.spacing = AppSpacing.sm
view.addSubview(stack)
addSection(title: "可举报行为", items: ["疑似打野摄影师主动揽客", "未佩戴工牌开展摄影服务", "引导游客线下付款或私下交易"])
addSection(title: "材料要求", items: ["尽量提供现场图片、视频、位置和文字说明", "联系方式可选填,用于景区工作人员核实"])
addSection(title: "处理说明", items: ["举报提交后景区公安会接收线索", "处理进度可在“我的举报”中查看", "请勿提交虚假或恶意举报"])
}
override func setupConstraints() {
stack.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(AppSpacing.md)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
}
}
private func addSection(title: String, items: [String]) {
let card = WildReportCardView(spacing: AppSpacing.sm)
card.stack.addArrangedSubview(WildReportUI.label(title, font: .app(.title), color: AppColor.textPrimary))
items.forEach { item in
card.stack.addArrangedSubview(WildReportUI.label("· \(item)", font: .app(.body), color: AppColor.textSecondary, lines: 0))
}
stack.addArrangedSubview(card)
}
}