792 lines
31 KiB
Swift
792 lines
31 KiB
Swift
//
|
||
// 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 = UIButton(type: .system)
|
||
|
||
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
|
||
scrollView.showsVerticalScrollIndicator = false
|
||
contentStack.axis = .vertical
|
||
contentStack.spacing = AppSpacing.sm
|
||
|
||
descriptionView.font = .systemFont(ofSize: 15)
|
||
descriptionView.textColor = AppColor.textPrimary
|
||
descriptionView.backgroundColor = .clear
|
||
descriptionView.textContainerInset = UIEdgeInsets(top: 10, left: 8, bottom: 34, right: 8)
|
||
descriptionView.delegate = self
|
||
|
||
contactField.placeholder = "请输入摄影师微信号或手机号(选填)"
|
||
contactField.font = .app(.body)
|
||
contactField.textColor = AppColor.textPrimary
|
||
contactField.backgroundColor = UIColor(hex: 0xFBFCFE)
|
||
contactField.layer.cornerRadius = 10
|
||
contactField.layer.borderWidth = 1
|
||
contactField.layer.borderColor = UIColor(hex: 0xCDD5E1).cgColor
|
||
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.setTitle("提交举报", for: .normal)
|
||
submitButton.titleLabel?.font = .systemFont(ofSize: 22, weight: .bold)
|
||
submitButton.setTitleColor(.white, for: .normal)
|
||
submitButton.backgroundColor = AppColor.primary
|
||
submitButton.layer.cornerRadius = 14
|
||
submitButton.layer.shadowColor = AppColor.primary.cgColor
|
||
submitButton.layer.shadowOpacity = 0.22
|
||
submitButton.layer.shadowRadius = 12
|
||
submitButton.layer.shadowOffset = CGSize(width: 0, height: 6)
|
||
submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)
|
||
|
||
view.addSubview(scrollView)
|
||
scrollView.addSubview(contentStack)
|
||
rebuildContent()
|
||
}
|
||
|
||
override func setupConstraints() {
|
||
scrollView.snp.makeConstraints { make in
|
||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
||
}
|
||
contentStack.snp.makeConstraints { make in
|
||
make.edges.equalTo(scrollView.contentLayoutGuide).inset(UIEdgeInsets(top: 14, left: 18, bottom: 26, right: 18))
|
||
make.width.equalTo(scrollView.frameLayoutGuide).offset(-36)
|
||
}
|
||
}
|
||
|
||
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(makeRealNameBanner())
|
||
contentStack.addArrangedSubview(makeTypeCard())
|
||
contentStack.addArrangedSubview(makeEvidenceCard())
|
||
contentStack.addArrangedSubview(makeDescriptionCard())
|
||
contentStack.addArrangedSubview(makeLocationCard())
|
||
contentStack.addArrangedSubview(makeContactCard())
|
||
let paymentCard = makePaymentCard()
|
||
contentStack.addArrangedSubview(paymentCard)
|
||
contentStack.setCustomSpacing(16, after: paymentCard)
|
||
contentStack.addArrangedSubview(submitButton)
|
||
submitButton.snp.remakeConstraints { make in
|
||
make.height.equalTo(56)
|
||
}
|
||
}
|
||
|
||
private func makeRealNameBanner() -> UIView {
|
||
let banner = UIView()
|
||
banner.backgroundColor = UIColor(hex: 0xF6FAFF)
|
||
banner.layer.cornerRadius = 16
|
||
banner.layer.borderWidth = 1
|
||
banner.layer.borderColor = UIColor(hex: 0xBFD7FF).cgColor
|
||
|
||
let iconWrap = UIView()
|
||
iconWrap.backgroundColor = AppColor.primary.withAlphaComponent(0.10)
|
||
iconWrap.layer.cornerRadius = 17
|
||
let icon = UIImageView(image: UIImage(systemName: "checkmark.shield.fill"))
|
||
icon.tintColor = AppColor.primary
|
||
iconWrap.addSubview(icon)
|
||
icon.snp.makeConstraints { make in
|
||
make.center.equalToSuperview()
|
||
make.size.equalTo(22)
|
||
}
|
||
|
||
let message = UILabel()
|
||
message.numberOfLines = 0
|
||
message.attributedText = realNameBannerText()
|
||
|
||
banner.addSubview(iconWrap)
|
||
banner.addSubview(message)
|
||
iconWrap.snp.makeConstraints { make in
|
||
make.top.leading.equalToSuperview().offset(16)
|
||
make.size.equalTo(34)
|
||
make.bottom.lessThanOrEqualToSuperview().inset(16)
|
||
}
|
||
message.snp.makeConstraints { make in
|
||
make.top.trailing.bottom.equalToSuperview().inset(16)
|
||
make.leading.equalTo(iconWrap.snp.trailing).offset(12)
|
||
}
|
||
return banner
|
||
}
|
||
|
||
private func makeTypeCard() -> UIView {
|
||
let card = sectionCard(index: 1, title: "举报类型")
|
||
let row = UIStackView()
|
||
row.axis = .horizontal
|
||
row.spacing = 8
|
||
row.distribution = .fillEqually
|
||
WildReportType.allCases.forEach { type in
|
||
let button = WildReportTypeOptionButton()
|
||
button.apply(type: type, isSelected: type == viewModel.selectedReportType)
|
||
button.addAction(UIAction { [weak self] _ in self?.viewModel.selectReportType(type) }, for: .touchUpInside)
|
||
button.snp.makeConstraints { make in make.height.equalTo(72) }
|
||
row.addArrangedSubview(button)
|
||
}
|
||
card.stack.addArrangedSubview(row)
|
||
return card
|
||
}
|
||
|
||
private func makeEvidenceCard() -> UIView {
|
||
let card = sectionCard(index: 2, title: "上传现场证据")
|
||
card.stack.addArrangedSubview(makeEvidenceUploadBlock(
|
||
icon: "photo.fill",
|
||
title: "上传图片",
|
||
subtitle: "支持 JPG、PNG 格式,最多 9 张",
|
||
attachments: viewModel.images,
|
||
maxCount: 9,
|
||
kind: .image
|
||
))
|
||
card.stack.addArrangedSubview(makeDivider())
|
||
card.stack.addArrangedSubview(makeEvidenceUploadBlock(
|
||
icon: "video.fill",
|
||
title: "上传视频",
|
||
subtitle: "支持 MP4 格式,时长不超过 60 秒",
|
||
attachments: viewModel.videos,
|
||
maxCount: 3,
|
||
kind: .video
|
||
))
|
||
return card
|
||
}
|
||
|
||
private func makeDescriptionCard() -> UIView {
|
||
let card = sectionCard(index: 3, title: "举报说明")
|
||
descriptionView.text = viewModel.description
|
||
let inputContainer = UIView()
|
||
inputContainer.backgroundColor = UIColor(hex: 0xFBFCFE)
|
||
inputContainer.layer.cornerRadius = 12
|
||
inputContainer.layer.borderWidth = 1
|
||
inputContainer.layer.borderColor = UIColor(hex: 0xCDD5E1).cgColor
|
||
|
||
let placeholder = UILabel()
|
||
placeholder.text = "请描述摄影师的位置、特征、行为..."
|
||
placeholder.font = .systemFont(ofSize: 15)
|
||
placeholder.textColor = UIColor(hex: 0x9CA3AF)
|
||
placeholder.isHidden = !viewModel.description.isEmpty
|
||
|
||
let count = WildReportUI.label("\(viewModel.description.count)/500", font: .systemFont(ofSize: 14), color: UIColor(hex: 0x6B7280))
|
||
count.textAlignment = .right
|
||
|
||
inputContainer.addSubview(descriptionView)
|
||
inputContainer.addSubview(placeholder)
|
||
inputContainer.addSubview(count)
|
||
descriptionView.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
make.height.equalTo(132)
|
||
}
|
||
placeholder.snp.makeConstraints { make in
|
||
make.top.equalToSuperview().offset(18)
|
||
make.leading.equalToSuperview().offset(14)
|
||
make.trailing.lessThanOrEqualToSuperview().inset(14)
|
||
}
|
||
count.snp.makeConstraints { make in
|
||
make.trailing.bottom.equalToSuperview().inset(12)
|
||
}
|
||
card.stack.addArrangedSubview(inputContainer)
|
||
return card
|
||
}
|
||
|
||
private func makeLocationCard() -> UIView {
|
||
let card = sectionCard(index: 4, title: "举报位置")
|
||
let container = UIView()
|
||
container.backgroundColor = UIColor(hex: 0xF6FAFF)
|
||
container.layer.cornerRadius = 12
|
||
container.layer.borderWidth = 1
|
||
container.layer.borderColor = UIColor(hex: 0xC8DDFF).cgColor
|
||
|
||
let iconWrap = UIView()
|
||
iconWrap.backgroundColor = AppColor.primary.withAlphaComponent(0.10)
|
||
iconWrap.layer.cornerRadius = 21
|
||
let icon = UIImageView(image: UIImage(systemName: "mappin.circle.fill"))
|
||
icon.tintColor = AppColor.primary
|
||
iconWrap.addSubview(icon)
|
||
icon.snp.makeConstraints { make in
|
||
make.center.equalToSuperview()
|
||
make.size.equalTo(30)
|
||
}
|
||
|
||
let textStack = UIStackView()
|
||
textStack.axis = .vertical
|
||
textStack.spacing = 5
|
||
let title = WildReportUI.label(viewModel.locationTitleText, font: .systemFont(ofSize: 15, weight: .semibold), color: AppColor.textPrimary, lines: 2)
|
||
title.adjustsFontSizeToFitWidth = true
|
||
title.minimumScaleFactor = 0.78
|
||
let subtitle = WildReportUI.label(viewModel.locationSubtitleText, font: .systemFont(ofSize: 12), color: AppColor.textSecondary, lines: 0)
|
||
textStack.addArrangedSubview(title)
|
||
textStack.addArrangedSubview(subtitle)
|
||
|
||
let button = UIButton(type: .system)
|
||
button.setTitle(viewModel.isUpdatingLocation ? "定位中" : "更新定位", for: .normal)
|
||
button.titleLabel?.font = .systemFont(ofSize: 13, weight: .semibold)
|
||
button.setTitleColor(AppColor.primary, for: .normal)
|
||
button.backgroundColor = .white
|
||
button.layer.cornerRadius = 17
|
||
button.layer.borderWidth = 1
|
||
button.layer.borderColor = AppColor.primary.cgColor
|
||
button.isEnabled = !viewModel.isUpdatingLocation
|
||
button.alpha = viewModel.isUpdatingLocation ? 0.6 : 1
|
||
button.addTarget(self, action: #selector(refreshLocationTapped), for: .touchUpInside)
|
||
|
||
container.addSubview(iconWrap)
|
||
container.addSubview(textStack)
|
||
container.addSubview(button)
|
||
iconWrap.snp.makeConstraints { make in
|
||
make.leading.top.bottom.equalToSuperview().inset(12)
|
||
make.size.equalTo(42)
|
||
}
|
||
button.snp.makeConstraints { make in
|
||
make.trailing.equalToSuperview().inset(12)
|
||
make.centerY.equalToSuperview()
|
||
make.width.equalTo(82)
|
||
make.height.equalTo(34)
|
||
}
|
||
textStack.snp.makeConstraints { make in
|
||
make.leading.equalTo(iconWrap.snp.trailing).offset(12)
|
||
make.trailing.equalTo(button.snp.leading).offset(-8)
|
||
make.centerY.equalToSuperview()
|
||
make.top.greaterThanOrEqualToSuperview().offset(12)
|
||
make.bottom.lessThanOrEqualToSuperview().inset(12)
|
||
}
|
||
card.stack.addArrangedSubview(container)
|
||
return card
|
||
}
|
||
|
||
private func makeContactCard() -> UIView {
|
||
let card = sectionCard(index: 5, title: "摄影师微信号/手机号", optional: true)
|
||
contactField.text = viewModel.contact
|
||
card.stack.addArrangedSubview(contactField)
|
||
contactField.snp.makeConstraints { make in make.height.equalTo(48) }
|
||
return card
|
||
}
|
||
|
||
private func makePaymentCard() -> UIView {
|
||
let card = sectionCard(index: 6, title: "线下微信/支付宝支付截图", optional: true)
|
||
let row = UIView()
|
||
row.backgroundColor = UIColor(hex: 0xFBFCFE)
|
||
row.layer.cornerRadius = 12
|
||
row.layer.borderWidth = 1
|
||
row.layer.borderColor = UIColor(hex: 0xCDD5E1).cgColor
|
||
|
||
let iconWrap = UIView()
|
||
iconWrap.backgroundColor = AppColor.primaryLight
|
||
iconWrap.layer.cornerRadius = 12
|
||
let icon = UIImageView(image: UIImage(systemName: "creditcard.fill"))
|
||
icon.tintColor = AppColor.primary
|
||
iconWrap.addSubview(icon)
|
||
icon.snp.makeConstraints { make in
|
||
make.center.equalToSuperview()
|
||
make.size.equalTo(24)
|
||
}
|
||
|
||
let textStack = UIStackView()
|
||
textStack.axis = .vertical
|
||
textStack.spacing = 4
|
||
textStack.addArrangedSubview(WildReportUI.label("上传线下微信/支付宝支付截图(选填)", font: .systemFont(ofSize: 15, weight: .semibold), color: AppColor.textPrimary, lines: 0))
|
||
textStack.addArrangedSubview(WildReportUI.label("支持 JPG、PNG 格式,最多 3 张", font: .systemFont(ofSize: 12), color: AppColor.textSecondary))
|
||
|
||
let trailingView: UIView
|
||
if let first = viewModel.paymentImages.first {
|
||
trailingView = WildReportAttachmentTileView(attachment: first) { [weak self] in
|
||
self?.viewModel.removeAttachment(first)
|
||
}
|
||
} else {
|
||
trailingView = WildReportAddAttachmentTileView { [weak self] in
|
||
self?.viewModel.addPaymentImage()
|
||
}
|
||
}
|
||
|
||
row.addSubview(iconWrap)
|
||
row.addSubview(textStack)
|
||
row.addSubview(trailingView)
|
||
iconWrap.snp.makeConstraints { make in
|
||
make.leading.top.bottom.equalToSuperview().inset(12)
|
||
make.size.equalTo(44)
|
||
}
|
||
trailingView.snp.makeConstraints { make in
|
||
make.trailing.equalToSuperview().inset(12)
|
||
make.centerY.equalToSuperview()
|
||
make.size.equalTo(58)
|
||
}
|
||
textStack.snp.makeConstraints { make in
|
||
make.leading.equalTo(iconWrap.snp.trailing).offset(12)
|
||
make.trailing.lessThanOrEqualTo(trailingView.snp.leading).offset(-10)
|
||
make.centerY.equalToSuperview()
|
||
make.top.greaterThanOrEqualToSuperview().offset(12)
|
||
make.bottom.lessThanOrEqualToSuperview().inset(12)
|
||
}
|
||
card.stack.addArrangedSubview(row)
|
||
return card
|
||
}
|
||
|
||
private func sectionCard(index: Int, title: String, optional: Bool = false) -> WildReportCardView {
|
||
let card = WildReportCardView(spacing: 18, inset: 16)
|
||
card.layer.cornerRadius = 16
|
||
card.stack.addArrangedSubview(WildReportSectionTitleView(index: index, title: title, optional: optional))
|
||
return card
|
||
}
|
||
|
||
private func makeEvidenceUploadBlock(
|
||
icon: String,
|
||
title: String,
|
||
subtitle: String,
|
||
attachments: [WildReportAttachment],
|
||
maxCount: Int,
|
||
kind: WildReportAttachmentKind
|
||
) -> UIView {
|
||
let block = UIStackView()
|
||
block.axis = .vertical
|
||
block.spacing = 12
|
||
|
||
let header = UIStackView()
|
||
header.axis = .horizontal
|
||
header.alignment = .center
|
||
header.spacing = 12
|
||
|
||
let iconWrap = UIView()
|
||
iconWrap.backgroundColor = AppColor.primaryLight
|
||
iconWrap.layer.cornerRadius = 12
|
||
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(22)
|
||
}
|
||
iconWrap.snp.makeConstraints { make in
|
||
make.size.equalTo(42)
|
||
}
|
||
|
||
let textStack = UIStackView()
|
||
textStack.axis = .vertical
|
||
textStack.spacing = 3
|
||
textStack.addArrangedSubview(WildReportUI.label(title, font: .systemFont(ofSize: 16, weight: .bold), color: AppColor.textPrimary))
|
||
textStack.addArrangedSubview(WildReportUI.label(subtitle, font: .systemFont(ofSize: 13), color: AppColor.textSecondary, lines: 0))
|
||
|
||
header.addArrangedSubview(iconWrap)
|
||
header.addArrangedSubview(textStack)
|
||
header.addArrangedSubview(UIView())
|
||
block.addArrangedSubview(header)
|
||
|
||
let scroll = UIScrollView()
|
||
scroll.showsHorizontalScrollIndicator = false
|
||
let tiles = UIStackView()
|
||
tiles.axis = .horizontal
|
||
tiles.spacing = 10
|
||
scroll.addSubview(tiles)
|
||
tiles.snp.makeConstraints { make in
|
||
make.edges.equalTo(scroll.contentLayoutGuide).inset(UIEdgeInsets(top: 2, left: 0, bottom: 2, right: 0))
|
||
make.height.equalTo(scroll.frameLayoutGuide).offset(-4)
|
||
}
|
||
attachments.forEach { attachment in
|
||
tiles.addArrangedSubview(WildReportAttachmentTileView(attachment: attachment) { [weak self] in
|
||
self?.viewModel.removeAttachment(attachment)
|
||
})
|
||
}
|
||
let addCount = max(0, min(3, maxCount - attachments.count))
|
||
for _ in 0..<addCount {
|
||
let addTile = WildReportAddAttachmentTileView { [weak self] in
|
||
switch kind {
|
||
case .image:
|
||
self?.viewModel.addImage()
|
||
case .video:
|
||
self?.viewModel.addVideo()
|
||
case .payment:
|
||
self?.viewModel.addPaymentImage()
|
||
}
|
||
}
|
||
tiles.addArrangedSubview(addTile)
|
||
}
|
||
scroll.snp.makeConstraints { make in
|
||
make.height.equalTo(62)
|
||
}
|
||
block.addArrangedSubview(scroll)
|
||
return block
|
||
}
|
||
|
||
private func makeDivider() -> UIView {
|
||
let divider = UIView()
|
||
divider.backgroundColor = AppColor.border
|
||
divider.snp.makeConstraints { make in
|
||
make.height.equalTo(1)
|
||
}
|
||
return divider
|
||
}
|
||
|
||
private func realNameBannerText() -> NSAttributedString {
|
||
let text = NSMutableAttributedString()
|
||
[
|
||
("实名登录", AppColor.primary, UIFont.systemFont(ofSize: 16, weight: .bold)),
|
||
("后提交,", AppColor.textPrimary, UIFont.systemFont(ofSize: 16, weight: .medium)),
|
||
("景区公安", AppColor.primary, UIFont.systemFont(ofSize: 16, weight: .bold)),
|
||
("将接收并处理", AppColor.textPrimary, UIFont.systemFont(ofSize: 16, weight: .medium))
|
||
].forEach { value, color, font in
|
||
text.append(NSAttributedString(string: value, attributes: [
|
||
.foregroundColor: color,
|
||
.font: font
|
||
]))
|
||
}
|
||
let paragraph = NSMutableParagraphStyle()
|
||
paragraph.lineSpacing = 4
|
||
text.addAttribute(.paragraphStyle, value: paragraph, range: NSRange(location: 0, length: text.length))
|
||
return text
|
||
}
|
||
|
||
@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)
|
||
}
|
||
}
|
||
|
||
/// 提交页步骤标题,复刻参考页左侧蓝色竖条与选填标识。
|
||
private final class WildReportSectionTitleView: UIView {
|
||
init(index: Int, title: String, optional: Bool) {
|
||
super.init(frame: .zero)
|
||
let bar = UIView()
|
||
bar.backgroundColor = AppColor.primary
|
||
bar.layer.cornerRadius = 2
|
||
|
||
let label = UILabel()
|
||
label.font = .systemFont(ofSize: 17, weight: .bold)
|
||
label.textColor = AppColor.textPrimary
|
||
label.text = "\(index). \(title)"
|
||
|
||
let optionalLabel = UILabel()
|
||
optionalLabel.font = .systemFont(ofSize: 14)
|
||
optionalLabel.textColor = AppColor.textSecondary
|
||
optionalLabel.text = "(选填)"
|
||
optionalLabel.isHidden = !optional
|
||
|
||
addSubview(bar)
|
||
addSubview(label)
|
||
addSubview(optionalLabel)
|
||
bar.snp.makeConstraints { make in
|
||
make.leading.equalToSuperview()
|
||
make.centerY.equalToSuperview()
|
||
make.width.equalTo(4)
|
||
make.height.equalTo(20)
|
||
}
|
||
label.snp.makeConstraints { make in
|
||
make.leading.equalTo(bar.snp.trailing).offset(8)
|
||
make.top.bottom.equalToSuperview()
|
||
}
|
||
optionalLabel.snp.makeConstraints { make in
|
||
make.leading.equalTo(label.snp.trailing)
|
||
make.centerY.equalTo(label)
|
||
make.trailing.lessThanOrEqualToSuperview()
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
}
|
||
|
||
/// 提交页举报类型三列选择按钮。
|
||
private final class WildReportTypeOptionButton: UIControl {
|
||
private let iconView = UIImageView()
|
||
private let titleLabel = UILabel()
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
layer.cornerRadius = 12
|
||
layer.borderWidth = 1
|
||
|
||
let stack = UIStackView()
|
||
stack.axis = .vertical
|
||
stack.alignment = .center
|
||
stack.spacing = 6
|
||
stack.isUserInteractionEnabled = false
|
||
|
||
iconView.contentMode = .scaleAspectFit
|
||
titleLabel.font = .systemFont(ofSize: 13, weight: .bold)
|
||
titleLabel.textAlignment = .center
|
||
titleLabel.adjustsFontSizeToFitWidth = true
|
||
titleLabel.minimumScaleFactor = 0.8
|
||
titleLabel.lineBreakMode = .byClipping
|
||
|
||
addSubview(stack)
|
||
stack.addArrangedSubview(iconView)
|
||
stack.addArrangedSubview(titleLabel)
|
||
iconView.snp.makeConstraints { make in
|
||
make.size.equalTo(22)
|
||
}
|
||
stack.snp.makeConstraints { make in
|
||
make.center.equalToSuperview()
|
||
make.leading.trailing.equalToSuperview().inset(6)
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
func apply(type: WildReportType, isSelected: Bool) {
|
||
iconView.image = UIImage(systemName: type.iconName)
|
||
titleLabel.text = type.rawValue
|
||
backgroundColor = isSelected ? AppColor.primary : UIColor(hex: 0xF6FAFF)
|
||
layer.borderColor = (isSelected ? AppColor.primary : UIColor(hex: 0xC8DDFF)).cgColor
|
||
iconView.tintColor = isSelected ? .white : AppColor.textPrimary
|
||
titleLabel.textColor = isSelected ? .white : AppColor.textPrimary
|
||
}
|
||
}
|
||
|
||
/// 提交页附件缩略 tile,使用渐变和删除角标模拟本地 Mock 附件。
|
||
private final class WildReportAttachmentTileView: UIControl {
|
||
private let gradientLayer = CAGradientLayer()
|
||
private let removeAction: () -> Void
|
||
|
||
init(attachment: WildReportAttachment, onRemove: @escaping () -> Void) {
|
||
self.removeAction = onRemove
|
||
super.init(frame: .zero)
|
||
layer.cornerRadius = 10
|
||
clipsToBounds = false
|
||
|
||
gradientLayer.cornerRadius = 10
|
||
let colors: [UIColor] = attachment.kind == .video
|
||
? [UIColor(hex: 0xB7D8FF), UIColor(hex: 0x4C8FEF)]
|
||
: [UIColor(hex: 0xCFE6FF), UIColor(hex: 0x7EB4FF)]
|
||
gradientLayer.colors = colors.map(\.cgColor)
|
||
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
|
||
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
|
||
layer.insertSublayer(gradientLayer, at: 0)
|
||
|
||
let icon = UIImageView(image: UIImage(systemName: attachment.kind == .video ? "play.circle.fill" : "mountain.2.fill"))
|
||
icon.tintColor = UIColor.white.withAlphaComponent(0.95)
|
||
icon.contentMode = .scaleAspectFit
|
||
addSubview(icon)
|
||
icon.snp.makeConstraints { make in
|
||
make.center.equalToSuperview()
|
||
make.size.equalTo(attachment.kind == .video ? 30 : 26)
|
||
}
|
||
|
||
let removeButton = UIButton(type: .system)
|
||
removeButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
|
||
removeButton.tintColor = UIColor(hex: 0x8D93A1)
|
||
removeButton.backgroundColor = .white
|
||
removeButton.layer.cornerRadius = 9
|
||
removeButton.addTarget(self, action: #selector(removeTapped), for: .touchUpInside)
|
||
addSubview(removeButton)
|
||
removeButton.snp.makeConstraints { make in
|
||
make.top.trailing.equalToSuperview().inset(-6)
|
||
make.size.equalTo(18)
|
||
}
|
||
|
||
snp.makeConstraints { make in
|
||
make.size.equalTo(58)
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
gradientLayer.frame = bounds
|
||
}
|
||
|
||
@objc private func removeTapped() {
|
||
removeAction()
|
||
}
|
||
}
|
||
|
||
/// 提交页添加附件 tile,白底虚线加号。
|
||
private final class WildReportAddAttachmentTileView: UIControl {
|
||
private let tapAction: () -> Void
|
||
private let borderLayer = CAShapeLayer()
|
||
|
||
init(action: @escaping () -> Void) {
|
||
self.tapAction = action
|
||
super.init(frame: .zero)
|
||
backgroundColor = .white
|
||
layer.cornerRadius = 10
|
||
|
||
borderLayer.strokeColor = UIColor(hex: 0xCDD5E1).cgColor
|
||
borderLayer.fillColor = UIColor.clear.cgColor
|
||
borderLayer.lineWidth = 1
|
||
borderLayer.lineDashPattern = [5, 4]
|
||
layer.addSublayer(borderLayer)
|
||
|
||
let icon = UIImageView(image: UIImage(systemName: "plus"))
|
||
icon.tintColor = UIColor(hex: 0x8D93A1)
|
||
icon.contentMode = .scaleAspectFit
|
||
addSubview(icon)
|
||
icon.snp.makeConstraints { make in
|
||
make.center.equalToSuperview()
|
||
make.size.equalTo(30)
|
||
}
|
||
|
||
addTarget(self, action: #selector(tapped), for: .touchUpInside)
|
||
snp.makeConstraints { make in
|
||
make.size.equalTo(58)
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
borderLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
|
||
}
|
||
|
||
@objc private func tapped() {
|
||
tapAction()
|
||
}
|
||
}
|
||
|
||
/// 举报成功页面,展示编号、上传摘要和处理进度。
|
||
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
|
||
)
|
||
}
|
||
}
|