// // WildPhotographerReportHomeViewController.swift // suixinkan // import SnapKit import UIKit /// 举报摄影师首页,提供立即举报、我的举报、风险地图和规则入口。 final class WildPhotographerReportHomeViewController: BaseViewController { private let viewModel: WildPhotographerReportHomeViewModel private let api: any WildPhotographerReportServing private let scrollView = UIScrollView() private let contentStack = UIStackView() init( viewModel: WildPhotographerReportHomeViewModel = WildPhotographerReportHomeViewModel(), api: any WildPhotographerReportServing = NetworkServices.shared.wildPhotographerReportAPI ) { self.viewModel = viewModel self.api = api 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 } 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() } } viewModel.onShowMessage = { [weak self] message in Task { @MainActor in self?.showToast(message) } } } override func viewDidLoad() { super.viewDidLoad() Task { await viewModel.loadReportCopy(api: api) } } @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()) if viewModel.shouldShowNoticeModule { 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)) case .error: contentStack.addArrangedSubview(makeStateCard(icon: "wifi.exclamationmark", title: "加载失败", message: "举报服务暂时不可用,请稍后重试。")) } } 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) header.addArrangedSubview(headerIcon) header.addArrangedSubview(title) header.addArrangedSubview(UIView()) if viewModel.shouldShowRuleButton { 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.setConfigurationContentInsets( NSDirectionalEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12) ) ruleButton.addTarget(self, action: #selector(ruleTapped), for: .touchUpInside) ruleButton.snp.makeConstraints { make in make.height.equalTo(36) } header.addArrangedSubview(ruleButton) } card.stack.addArrangedSubview(header) viewModel.noticeItems.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.xs [ ("实名", "身份核验 真实可靠", "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() { navigationController?.pushViewController( WildReportRiskMapViewController(), animated: true ) } @objc private func ruleTapped() { navigationController?.pushViewController(WildReportRuleViewController(ruleGroups: viewModel.ruleGroups), 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 scrollView = UIScrollView() private let stack = UIStackView() private let ruleGroups: [WildReportRuleGroup] init(ruleGroups: [WildReportRuleGroup]) { self.ruleGroups = ruleGroups 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.alwaysBounceVertical = true scrollView.showsVerticalScrollIndicator = false stack.axis = .vertical stack.spacing = AppSpacing.sm view.addSubview(scrollView) scrollView.addSubview(stack) ruleGroups.forEach { group in addSection(title: group.title, items: group.items) } } override func setupConstraints() { scrollView.snp.makeConstraints { make in make.edges.equalTo(view.safeAreaLayoutGuide) } stack.snp.makeConstraints { make in make.edges.equalTo(scrollView.contentLayoutGuide).inset(AppSpacing.md) make.width.equalTo(scrollView.frameLayoutGuide).offset(-AppSpacing.md * 2) } } private func addSection(title: String, items: [String]) { let card = WildReportCardView(spacing: AppSpacing.sm) if !title.isEmpty { 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) } }