// // 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 = AppButton(title: "提交举报") 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 contentStack.axis = .vertical contentStack.spacing = AppSpacing.sm descriptionView.font = .app(.body) descriptionView.textColor = AppColor.textPrimary descriptionView.backgroundColor = AppColor.inputBackground descriptionView.layer.cornerRadius = AppRadius.md descriptionView.textContainerInset = UIEdgeInsets(top: 12, left: 10, bottom: 12, right: 10) descriptionView.delegate = self contactField.placeholder = "微信号或手机号(选填)" contactField.font = .app(.body) contactField.textColor = AppColor.textPrimary contactField.backgroundColor = AppColor.inputBackground contactField.layer.cornerRadius = AppRadius.md 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.addTarget(self, action: #selector(submitTapped), for: .touchUpInside) view.addSubview(scrollView) scrollView.addSubview(contentStack) view.addSubview(submitButton) rebuildContent() } override func setupConstraints() { submitButton.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(AppSpacing.md) make.bottom.equalTo(view.safeAreaLayoutGuide).inset(AppSpacing.sm) } scrollView.snp.makeConstraints { make in make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide) make.bottom.equalTo(submitButton.snp.top).offset(-AppSpacing.sm) } contentStack.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.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(makeTipsCard()) contentStack.addArrangedSubview(makeTypeCard()) contentStack.addArrangedSubview(makeEvidenceCard()) contentStack.addArrangedSubview(makeDescriptionCard()) contentStack.addArrangedSubview(makeLocationCard()) contentStack.addArrangedSubview(makeContactCard()) contentStack.addArrangedSubview(makePaymentCard()) } private func makeTipsCard() -> UIView { let card = WildReportCardView(spacing: AppSpacing.xs) card.stack.addArrangedSubview(WildReportUI.label("实名登录后提交,景区公安将接收并处理", font: .app(.bodyMedium), color: AppColor.textPrimary, lines: 0)) card.stack.addArrangedSubview(WildReportUI.label("请描述摄影师位置、外貌特征和具体行为。", font: .app(.caption), color: AppColor.textSecondary, lines: 0)) return card } private func makeTypeCard() -> UIView { let card = sectionCard(index: 1, title: "选择举报类型") let row = UIStackView() row.axis = .vertical row.spacing = AppSpacing.xs WildReportType.allCases.forEach { type in let button = UIButton(type: .system) button.contentHorizontalAlignment = .left button.titleLabel?.font = .app(.bodyMedium) button.setTitle(" \(type.rawValue)", for: .normal) button.setImage(UIImage(systemName: type == viewModel.selectedReportType ? "largecircle.fill.circle" : "circle"), for: .normal) button.tintColor = AppColor.primary button.setTitleColor(AppColor.textPrimary, for: .normal) button.addAction(UIAction { [weak self] _ in self?.viewModel.selectReportType(type) }, for: .touchUpInside) button.snp.makeConstraints { make in make.height.equalTo(38) } row.addArrangedSubview(button) } card.stack.addArrangedSubview(row) return card } private func makeEvidenceCard() -> UIView { let card = sectionCard(index: 2, title: "上传现场证据") let buttonRow = UIStackView() buttonRow.axis = .horizontal buttonRow.spacing = AppSpacing.sm buttonRow.distribution = .fillEqually let imageButton = WildReportUI.iconButton(title: "添加图片", imageName: "photo", style: .secondary) imageButton.addTarget(self, action: #selector(addImageTapped), for: .touchUpInside) let videoButton = WildReportUI.iconButton(title: "添加视频", imageName: "video", style: .secondary) videoButton.addTarget(self, action: #selector(addVideoTapped), for: .touchUpInside) [imageButton, videoButton].forEach { button in button.snp.makeConstraints { make in make.height.equalTo(44) } buttonRow.addArrangedSubview(button) } card.stack.addArrangedSubview(buttonRow) addAttachments(viewModel.images + viewModel.videos, to: card) return card } private func makeDescriptionCard() -> UIView { let card = sectionCard(index: 3, title: "举报说明") descriptionView.text = viewModel.description card.stack.addArrangedSubview(descriptionView) descriptionView.snp.makeConstraints { make in make.height.equalTo(132) } let count = WildReportUI.label("\(viewModel.description.count)/500", font: .app(.caption), color: AppColor.textTertiary) count.textAlignment = .right card.stack.addArrangedSubview(count) return card } private func makeLocationCard() -> UIView { let card = sectionCard(index: 4, title: "举报位置") let title = WildReportUI.label(viewModel.locationTitleText, font: .app(.bodyMedium), color: AppColor.textPrimary, lines: 0) let subtitle = WildReportUI.label(viewModel.locationSubtitleText, font: .app(.caption), color: AppColor.textSecondary, lines: 0) let button = WildReportUI.iconButton(title: viewModel.isUpdatingLocation ? "定位中" : "更新定位", imageName: "location.fill", style: .secondary) button.isEnabled = !viewModel.isUpdatingLocation button.addTarget(self, action: #selector(refreshLocationTapped), for: .touchUpInside) button.snp.makeConstraints { make in make.height.equalTo(42) } card.stack.addArrangedSubview(title) card.stack.addArrangedSubview(subtitle) card.stack.addArrangedSubview(button) return card } private func makeContactCard() -> UIView { let card = sectionCard(index: 5, title: "联系方式(选填)") contactField.text = viewModel.contact card.stack.addArrangedSubview(contactField) contactField.snp.makeConstraints { make in make.height.equalTo(46) } return card } private func makePaymentCard() -> UIView { let card = sectionCard(index: 6, title: "支付截图(选填)") card.stack.addArrangedSubview(WildReportUI.label("上传线下微信/支付宝支付截图,最多 3 张。", font: .app(.caption), color: AppColor.textSecondary, lines: 0)) let button = WildReportUI.iconButton(title: "添加截图", imageName: "creditcard", style: .secondary) button.addTarget(self, action: #selector(addPaymentTapped), for: .touchUpInside) button.snp.makeConstraints { make in make.height.equalTo(42) } card.stack.addArrangedSubview(button) addAttachments(viewModel.paymentImages, to: card) return card } private func sectionCard(index: Int, title: String) -> WildReportCardView { let card = WildReportCardView(spacing: AppSpacing.sm) card.stack.addArrangedSubview(WildReportUI.label("\(index). \(title)", font: .app(.title), color: AppColor.textPrimary)) return card } private func addAttachments(_ attachments: [WildReportAttachment], to card: WildReportCardView) { guard !attachments.isEmpty else { return } attachments.forEach { attachment in let chip = WildReportAttachmentChipView(attachment: attachment) chip.onRemove = { [weak self] in self?.viewModel.removeAttachment(attachment) } card.stack.addArrangedSubview(chip) } } @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) } } /// 举报成功页面,展示编号、上传摘要和处理进度。 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 ) } }