490 lines
20 KiB
Swift
490 lines
20 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|