优化举报摄影师页面

This commit is contained in:
2026-07-08 14:02:44 +08:00
parent ccd63380cf
commit 8ca8bcf948
3 changed files with 1357 additions and 168 deletions

View File

@ -182,6 +182,8 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
private let homeViewModel: WildPhotographerReportHomeViewModel
private let scrollView = UIScrollView()
private let stack = UIStackView()
private let bottomBar = UIView()
private let bottomButtonRow = UIStackView()
init(record: WildReportRecord, homeViewModel: WildPhotographerReportHomeViewModel) {
self.viewModel = WildPhotographerReportDetailViewModel(record: record, homeViewModel: homeViewModel)
@ -196,30 +198,64 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
override func setupNavigationBar() {
title = "举报详情"
navigationItem.rightBarButtonItem = UIBarButtonItem(
image: UIImage(systemName: "square.and.arrow.up"),
style: .plain,
target: self,
action: #selector(shareTapped)
)
}
override func setupUI() {
view.backgroundColor = AppColor.pageBackgroundSoft
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = true
stack.axis = .vertical
stack.spacing = AppSpacing.sm
stack.spacing = 12
bottomBar.backgroundColor = UIColor.white.withAlphaComponent(0.96)
bottomButtonRow.axis = .horizontal
bottomButtonRow.alignment = .fill
bottomButtonRow.distribution = .fillEqually
bottomButtonRow.spacing = 12
let supplementButton = makeBottomButton(title: "补充证据", iconName: "doc.badge.plus", isPrimary: false)
supplementButton.addTarget(self, action: #selector(supplementTapped), for: .touchUpInside)
let mapButton = makeBottomButton(title: "查看附近风险地图", iconName: "map.fill", isPrimary: true)
mapButton.addTarget(self, action: #selector(mapTapped), for: .touchUpInside)
bottomButtonRow.addArrangedSubview(supplementButton)
bottomButtonRow.addArrangedSubview(mapButton)
view.addSubview(scrollView)
view.addSubview(bottomBar)
scrollView.addSubview(stack)
bottomBar.addSubview(bottomButtonRow)
let topBorder = UIView()
topBorder.backgroundColor = AppColor.cardOutline
bottomBar.addSubview(topBorder)
topBorder.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(0.5)
}
rebuildContent()
}
override func setupConstraints() {
scrollView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
make.bottom.equalToSuperview()
}
stack.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)
make.top.equalTo(scrollView.contentLayoutGuide).offset(14)
make.leading.equalTo(scrollView.contentLayoutGuide).offset(18)
make.trailing.equalTo(scrollView.contentLayoutGuide).inset(18)
make.bottom.equalTo(scrollView.contentLayoutGuide).inset(120)
make.width.equalTo(scrollView.frameLayoutGuide).offset(-36)
}
bottomBar.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
}
bottomButtonRow.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10)
make.leading.trailing.equalToSuperview().inset(16)
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(12)
make.height.equalTo(52)
}
}
@ -238,107 +274,405 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
stack.removeArrangedSubview(view)
view.removeFromSuperview()
}
stack.addArrangedSubview(makeHeaderCard())
stack.addArrangedSubview(makeProgressCard())
stack.addArrangedSubview(makeEvidenceCard())
if viewModel.showHandlerInfo {
stack.addArrangedSubview(makeHandlerCard())
stack.addArrangedSubview(makeSummaryCard())
stack.addArrangedSubview(makeReportInfoCard())
if viewModel.showHandlerInfo, let info = viewModel.handlerInfo {
stack.addArrangedSubview(makeHandlerCard(info: info))
}
stack.addArrangedSubview(makeSupplementCard())
stack.addArrangedSubview(makeActionButtons())
stack.addArrangedSubview(makeEvidenceCard())
if !viewModel.supplementaryEvidences.isEmpty {
stack.addArrangedSubview(makeSupplementCard())
}
stack.addArrangedSubview(makeTimelineCard())
}
private func makeHeaderCard() -> UIView {
let card = WildReportCardView(spacing: AppSpacing.sm)
let header = UIStackView()
header.axis = .horizontal
header.alignment = .center
header.addArrangedSubview(WildReportUI.label(viewModel.heroTitle, font: .app(.metric), color: AppColor.textPrimary))
header.addArrangedSubview(UIView())
let status = WildReportStatusView()
status.apply(status: viewModel.record.status)
status.snp.makeConstraints { make in
make.width.equalTo(64)
make.height.equalTo(24)
private func makeSummaryCard() -> UIView {
let card = WildReportCardView(spacing: 20, inset: 18)
card.layer.cornerRadius = 18
let topRow = UIStackView()
topRow.axis = .horizontal
topRow.alignment = .center
topRow.spacing = 14
let iconBox = UIView()
iconBox.backgroundColor = AppColor.primary
iconBox.layer.cornerRadius = 16
let icon = UIImageView(image: UIImage(systemName: "exclamationmark.shield.fill"))
icon.tintColor = .white
icon.contentMode = .scaleAspectFit
iconBox.addSubview(icon)
icon.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(30)
}
header.addArrangedSubview(status)
card.stack.addArrangedSubview(header)
card.stack.addArrangedSubview(WildReportInfoRowView(title: "举报编号", value: viewModel.record.id))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "景区", value: viewModel.scenicAreaName))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "位置", value: viewModel.detailAddress))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "联系方式", value: viewModel.contactText))
iconBox.snp.makeConstraints { make in
make.size.equalTo(62)
}
let titleStack = UIStackView()
titleStack.axis = .vertical
titleStack.spacing = 8
let titleLabel = WildReportUI.label(viewModel.heroTitle, font: .systemFont(ofSize: 23, weight: .heavy), color: AppColor.textPrimary, lines: 1)
titleLabel.adjustsFontSizeToFitWidth = true
titleLabel.minimumScaleFactor = 0.82
titleStack.addArrangedSubview(titleLabel)
titleStack.addArrangedSubview(makeScenicPill())
let shareButton = WildReportDetailIconTextButton(title: "分享", iconName: "square.and.arrow.up")
shareButton.addTarget(self, action: #selector(shareTapped), for: .touchUpInside)
topRow.addArrangedSubview(iconBox)
topRow.addArrangedSubview(titleStack)
topRow.addArrangedSubview(shareButton)
titleStack.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
shareButton.snp.makeConstraints { make in
make.size.equalTo(48)
}
card.stack.addArrangedSubview(topRow)
card.stack.addArrangedSubview(makeDivider())
let bottomRow = UIStackView()
bottomRow.axis = .horizontal
bottomRow.alignment = .center
bottomRow.spacing = 12
let idStack = UIStackView()
idStack.axis = .vertical
idStack.spacing = 4
idStack.addArrangedSubview(WildReportUI.label("举报编号", font: .app(.body), color: AppColor.textSecondary))
idStack.addArrangedSubview(WildReportUI.label(viewModel.record.id, font: .systemFont(ofSize: 19, weight: .semibold), color: AppColor.textPrimary))
let statusLabel = PaddingLabel(insets: UIEdgeInsets(top: 0, left: 14, bottom: 0, right: 14))
statusLabel.text = viewModel.displayStatusText
statusLabel.font = .systemFont(ofSize: 16, weight: .bold)
statusLabel.textColor = viewModel.record.status.displayColor
statusLabel.backgroundColor = viewModel.record.status.displayColor.withAlphaComponent(0.12)
statusLabel.textAlignment = .center
statusLabel.layer.cornerRadius = 19
statusLabel.clipsToBounds = true
statusLabel.snp.makeConstraints { make in
make.height.equalTo(38)
}
bottomRow.addArrangedSubview(idStack)
bottomRow.addArrangedSubview(UIView())
bottomRow.addArrangedSubview(statusLabel)
card.stack.addArrangedSubview(bottomRow)
return card
}
private func makeProgressCard() -> UIView {
let card = WildReportCardView(spacing: AppSpacing.sm)
card.stack.addArrangedSubview(WildReportUI.label("处理进度", font: .app(.title), color: AppColor.textPrimary))
viewModel.progressSteps.forEach { step in
let prefix: String
switch step.state {
case .completed: prefix = ""
case .current: prefix = ""
case .pending: prefix = ""
}
card.stack.addArrangedSubview(WildReportUI.label("\(prefix) \(step.title) \(step.timeText)", font: .app(.body), color: AppColor.textSecondary, lines: 0))
}
private func makeReportInfoCard() -> UIView {
let card = WildReportCardView(spacing: 0, inset: 0)
card.layer.cornerRadius = 18
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "举报类型", value: viewModel.record.reportType.rawValue))
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "举报说明", value: viewModel.descriptionText, multiline: true))
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "微信号/手机号", value: viewModel.contactText))
return card
}
private func makeEvidenceCard() -> UIView {
let card = WildReportCardView(spacing: AppSpacing.sm)
card.stack.addArrangedSubview(WildReportUI.label("举报证据", font: .app(.title), color: AppColor.textPrimary))
card.stack.addArrangedSubview(WildReportUI.label(viewModel.descriptionText, font: .app(.body), color: AppColor.textSecondary, lines: 0))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "举报类型", value: viewModel.record.reportType.rawValue))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "举报人材料", value: viewModel.reporterMediaSummary))
let previewButton = WildReportUI.iconButton(title: "预览材料", imageName: "photo.on.rectangle", style: .secondary)
previewButton.addTarget(self, action: #selector(previewTapped), for: .touchUpInside)
card.stack.addArrangedSubview(previewButton)
let card = WildReportCardView(spacing: 16, inset: 18)
card.layer.cornerRadius = 18
if !viewModel.reporterMediaItems.isEmpty {
card.stack.addArrangedSubview(makeReporterMaterialsSection())
card.stack.addArrangedSubview(makeDivider())
}
card.stack.addArrangedSubview(makeLocationSection())
return card
}
private func makeHandlerCard() -> UIView {
let card = WildReportCardView(spacing: AppSpacing.sm)
guard let info = viewModel.handlerInfo else { return card }
card.stack.addArrangedSubview(WildReportUI.label("处理信息", font: .app(.title), color: AppColor.textPrimary))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "处理人", value: info.handlerName))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "处理电话", value: info.maskedHandlerPhone))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "处理时间", value: info.processedAt))
card.stack.addArrangedSubview(WildReportUI.label(info.processOpinion, font: .app(.body), color: AppColor.textSecondary, lines: 0))
card.stack.addArrangedSubview(WildReportInfoRowView(title: "处理凭证", value: info.completionMediaSummary))
private func makeHandlerCard(info: WildReportHandlerInfo) -> UIView {
let card = WildReportCardView(spacing: 0, inset: 0)
card.layer.cornerRadius = 18
let title = WildReportUI.label("处理信息", font: .systemFont(ofSize: 18, weight: .bold), color: AppColor.textPrimary)
let titleWrap = UIView()
titleWrap.addSubview(title)
title.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 18, left: 18, bottom: 10, right: 18))
}
card.stack.addArrangedSubview(titleWrap)
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "处理人", value: info.handlerName))
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "处理人电话", value: info.maskedHandlerPhone, trailingIconName: "phone.fill"))
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "处理时间", value: info.processingAt))
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "处理完成时间", value: info.processedAt))
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(WildReportDetailInfoRowView(title: "处理意见", value: info.processOpinion, multiline: true))
if !info.completionMedia.isEmpty {
card.stack.addArrangedSubview(makeInsetDivider())
card.stack.addArrangedSubview(makeCompletionMediaSection(info: info))
}
return card
}
private func makeSupplementCard() -> UIView {
let card = WildReportCardView(spacing: AppSpacing.sm)
card.stack.addArrangedSubview(WildReportUI.label("补充证据记录", font: .app(.title), color: AppColor.textPrimary))
let card = WildReportCardView(spacing: 14, inset: 18)
card.layer.cornerRadius = 18
card.stack.addArrangedSubview(WildReportUI.label("补充证据记录", font: .systemFont(ofSize: 18, weight: .bold), color: AppColor.textPrimary))
let items = viewModel.supplementaryEvidences
if items.isEmpty {
card.stack.addArrangedSubview(WildReportUI.label("暂无补充证据", font: .app(.body), color: AppColor.textSecondary))
} else {
items.enumerated().forEach { index, item in
card.stack.addArrangedSubview(WildReportUI.label("\(items.count - index)次补充 · \(item.submitTime)", font: .app(.bodyMedium), color: AppColor.textPrimary, lines: 0))
card.stack.addArrangedSubview(WildReportUI.label(item.summaryText, font: .app(.body), color: AppColor.textSecondary, lines: 0))
items.enumerated().forEach { index, item in
let itemStack = UIStackView()
itemStack.axis = .vertical
itemStack.spacing = 8
let header = UIStackView()
header.axis = .horizontal
header.alignment = .center
header.addArrangedSubview(WildReportUI.label(item.submitTime, font: .app(.captionMedium), color: AppColor.textSecondary))
header.addArrangedSubview(UIView())
header.addArrangedSubview(WildReportUI.label("\(items.count - index)次补充", font: .app(.captionMedium), color: AppColor.primary))
itemStack.addArrangedSubview(header)
itemStack.addArrangedSubview(WildReportUI.label(item.summaryText, font: .app(.bodyMedium), color: AppColor.textPrimary, lines: 0))
card.stack.addArrangedSubview(itemStack)
if index < items.count - 1 {
card.stack.addArrangedSubview(makeDivider())
}
}
return card
}
private func makeActionButtons() -> UIView {
let row = UIStackView()
row.axis = .vertical
row.spacing = AppSpacing.sm
let supplement = AppButton(title: "补充证据")
supplement.addTarget(self, action: #selector(supplementTapped), for: .touchUpInside)
let map = AppButton(title: "查看附近风险地图", style: .secondary)
map.addTarget(self, action: #selector(mapTapped), for: .touchUpInside)
row.addArrangedSubview(supplement)
row.addArrangedSubview(map)
return row
private func makeTimelineCard() -> UIView {
let card = WildReportCardView(spacing: 14, inset: 18)
card.layer.cornerRadius = 18
card.stack.addArrangedSubview(WildReportUI.label("处理进度", font: .systemFont(ofSize: 18, weight: .bold), color: AppColor.textPrimary))
viewModel.progressSteps.enumerated().forEach { index, step in
card.stack.addArrangedSubview(WildReportDetailTimelineRowView(step: step, isLast: index == viewModel.progressSteps.count - 1))
}
return card
}
private func makeReporterMaterialsSection() -> UIView {
let section = UIStackView()
section.axis = .vertical
section.spacing = 12
section.addArrangedSubview(makeSectionHeader(
iconName: "photo.on.rectangle.angled",
title: "举报人材料",
subtitle: "\(viewModel.imageCountText)图片 \(viewModel.videoCountText)视频",
chevron: true,
action: #selector(previewTapped)
))
let mediaScroll = UIScrollView()
mediaScroll.showsHorizontalScrollIndicator = false
let mediaStack = UIStackView()
mediaStack.axis = .horizontal
mediaStack.spacing = 10
mediaScroll.addSubview(mediaStack)
mediaStack.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.height.equalTo(mediaScroll.frameLayoutGuide)
}
viewModel.reporterMediaItems.forEach { item in
let tile = WildReportDetailMediaThumbnailView(reporterItem: item)
tile.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reporterMediaTapped(_:))))
tile.snp.makeConstraints { make in
make.size.equalTo(108)
}
mediaStack.addArrangedSubview(tile)
}
mediaScroll.snp.makeConstraints { make in
make.height.equalTo(108)
}
section.addArrangedSubview(mediaScroll)
return section
}
private func makeCompletionMediaSection(info: WildReportHandlerInfo) -> UIView {
let section = UIStackView()
section.axis = .vertical
section.spacing = 12
section.layoutMargins = UIEdgeInsets(top: 14, left: 18, bottom: 16, right: 18)
section.isLayoutMarginsRelativeArrangement = true
section.addArrangedSubview(makeSectionHeader(
iconName: "photo.on.rectangle.angled",
title: "处理凭证",
subtitle: info.completionMediaSummary,
chevron: false,
action: nil
))
let mediaScroll = UIScrollView()
mediaScroll.showsHorizontalScrollIndicator = false
let mediaStack = UIStackView()
mediaStack.axis = .horizontal
mediaStack.spacing = 10
mediaScroll.addSubview(mediaStack)
mediaStack.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.height.equalTo(mediaScroll.frameLayoutGuide)
}
info.completionMedia.forEach { item in
let tile = WildReportDetailMediaThumbnailView(completionItem: item)
tile.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(completionMediaTapped(_:))))
tile.snp.makeConstraints { make in
make.size.equalTo(108)
}
mediaStack.addArrangedSubview(tile)
}
mediaScroll.snp.makeConstraints { make in
make.height.equalTo(108)
}
section.addArrangedSubview(mediaScroll)
return section
}
private func makeLocationSection() -> UIView {
let section = UIStackView()
section.axis = .vertical
section.spacing = 12
section.addArrangedSubview(makeSectionHeader(
iconName: "mappin.circle.fill",
title: "位置",
subtitle: viewModel.detailAddress,
chevron: true,
action: #selector(locationTapped)
))
let mapPreview = WildReportDetailMapPreviewView(locationText: viewModel.detailAddress)
section.addArrangedSubview(mapPreview)
mapPreview.snp.makeConstraints { make in
make.height.equalTo(98)
}
return section
}
private func makeSectionHeader(iconName: String, title: String, subtitle: String, chevron: Bool, action: Selector?) -> UIView {
let control = UIControl()
if let action {
control.addTarget(self, action: action, for: .touchUpInside)
}
let iconBox = UIView()
iconBox.backgroundColor = AppColor.primaryLight
iconBox.layer.cornerRadius = 14
let icon = UIImageView(image: UIImage(systemName: iconName))
icon.tintColor = AppColor.primary
icon.contentMode = .scaleAspectFit
iconBox.addSubview(icon)
control.addSubview(iconBox)
let textStack = UIStackView()
textStack.axis = .vertical
textStack.spacing = 4
textStack.addArrangedSubview(WildReportUI.label(title, font: .systemFont(ofSize: 20, weight: .bold), color: AppColor.textPrimary))
let subtitleLabel = WildReportUI.label(subtitle, font: .systemFont(ofSize: 18, weight: .medium), color: AppColor.textSecondary)
subtitleLabel.adjustsFontSizeToFitWidth = true
subtitleLabel.minimumScaleFactor = 0.8
textStack.addArrangedSubview(subtitleLabel)
control.addSubview(textStack)
let chevronView = UIImageView(image: UIImage(systemName: "chevron.right"))
chevronView.tintColor = AppColor.textTertiary
chevronView.isHidden = !chevron
control.addSubview(chevronView)
iconBox.snp.makeConstraints { make in
make.leading.top.bottom.equalToSuperview()
make.size.equalTo(50)
}
icon.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(24)
}
chevronView.snp.makeConstraints { make in
make.trailing.centerY.equalToSuperview()
make.size.equalTo(18)
}
textStack.snp.makeConstraints { make in
make.leading.equalTo(iconBox.snp.trailing).offset(12)
make.centerY.equalToSuperview()
make.trailing.equalTo(chevronView.snp.leading).offset(-10)
}
control.snp.makeConstraints { make in
make.height.greaterThanOrEqualTo(50)
}
return control
}
private func makeScenicPill() -> UIView {
let pill = UIStackView()
pill.axis = .horizontal
pill.alignment = .center
pill.spacing = 4
pill.layoutMargins = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 10)
pill.isLayoutMarginsRelativeArrangement = true
pill.backgroundColor = AppColor.primaryLight
pill.layer.cornerRadius = 13
pill.clipsToBounds = true
let icon = UIImageView(image: UIImage(systemName: "mappin.circle.fill"))
icon.tintColor = AppColor.primary
icon.snp.makeConstraints { make in
make.size.equalTo(14)
}
let label = WildReportUI.label(viewModel.scenicAreaName, font: .app(.captionMedium), color: AppColor.primary)
pill.addArrangedSubview(icon)
pill.addArrangedSubview(label)
return pill
}
private func makeBottomButton(title: String, iconName: String, isPrimary: Bool) -> UIButton {
let button = UIButton(type: .system)
button.backgroundColor = isPrimary ? AppColor.primary : .white
button.layer.cornerRadius = 14
button.layer.borderWidth = isPrimary ? 0 : 1.2
button.layer.borderColor = AppColor.primary.cgColor
button.tintColor = isPrimary ? .white : AppColor.primary
button.setTitleColor(isPrimary ? .white : AppColor.primary, for: .normal)
button.setTitle(title, for: .normal)
button.setImage(UIImage(systemName: iconName), for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.titleLabel?.minimumScaleFactor = 0.82
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -4, bottom: 0, right: 4)
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
return button
}
private func makeDivider() -> UIView {
let divider = UIView()
divider.backgroundColor = AppColor.cardOutline
divider.snp.makeConstraints { make in
make.height.equalTo(1)
}
return divider
}
private func makeInsetDivider() -> UIView {
let wrapper = UIView()
let divider = UIView()
divider.backgroundColor = AppColor.cardOutline
wrapper.addSubview(divider)
wrapper.snp.makeConstraints { make in
make.height.equalTo(1)
}
divider.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(18)
make.top.bottom.equalToSuperview()
}
return wrapper
}
@objc private func shareTapped() { viewModel.shareReport() }
@objc private func previewTapped() { viewModel.showMediaPreview(kind: "图片/视频") }
@objc private func locationTapped() { viewModel.showLocationPreview() }
@objc private func reporterMediaTapped(_ recognizer: UITapGestureRecognizer) {
guard let tile = recognizer.view as? WildReportDetailMediaThumbnailView else { return }
viewModel.showMediaPreview(kind: tile.previewKindText)
}
@objc private func completionMediaTapped(_ recognizer: UITapGestureRecognizer) {
guard let tile = recognizer.view as? WildReportDetailMediaThumbnailView else { return }
viewModel.showMediaPreview(kind: tile.previewKindText)
}
@objc private func supplementTapped() {
navigationController?.pushViewController(
@ -354,3 +688,390 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
)
}
}
/// Summary
private final class WildReportDetailIconTextButton: UIControl {
private let iconView = UIImageView()
private let titleLabel = UILabel()
init(title: String, iconName: String) {
super.init(frame: .zero)
backgroundColor = AppColor.primaryLight
layer.cornerRadius = 14
iconView.image = UIImage(systemName: iconName)
iconView.tintColor = AppColor.primary
iconView.contentMode = .scaleAspectFit
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 10, weight: .bold)
titleLabel.textColor = AppColor.primary
titleLabel.textAlignment = .center
addSubview(iconView)
addSubview(titleLabel)
iconView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(8)
make.centerX.equalToSuperview()
make.size.equalTo(18)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(iconView.snp.bottom).offset(2)
make.leading.trailing.equalToSuperview().inset(4)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
private final class WildReportDetailInfoRowView: UIView {
init(title: String, value: String, multiline: Bool = false, trailingIconName: String? = nil) {
super.init(frame: .zero)
let titleLabel = WildReportUI.label(title, font: .app(.captionMedium), color: AppColor.textSecondary)
let valueLabel = WildReportUI.label(value, font: .app(.subtitle), color: AppColor.textPrimary, lines: multiline ? 0 : 1)
valueLabel.textAlignment = multiline ? .left : .right
valueLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
addSubview(titleLabel)
addSubview(valueLabel)
if let trailingIconName {
let icon = UIImageView(image: UIImage(systemName: trailingIconName))
icon.tintColor = AppColor.primary
icon.contentMode = .scaleAspectFit
addSubview(icon)
icon.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(18)
make.centerY.equalTo(valueLabel)
make.size.equalTo(16)
}
valueLabel.snp.makeConstraints { make in
make.trailing.equalTo(icon.snp.leading).offset(-6)
}
}
titleLabel.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview().inset(UIEdgeInsets(top: 14, left: 18, bottom: 0, right: 18))
}
valueLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(7)
make.leading.equalToSuperview().offset(18)
if trailingIconName == nil {
make.trailing.equalToSuperview().inset(18)
}
make.bottom.equalToSuperview().inset(14)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
private final class WildReportDetailMediaThumbnailView: UIView {
let previewKindText: String
private let gradient = CAGradientLayer()
private let iconView = UIImageView()
private let footerLabel = PaddingLabel(insets: UIEdgeInsets(top: 3, left: 8, bottom: 3, right: 8))
private let playView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
init(reporterItem: WildReportReporterMediaItem) {
switch reporterItem {
case .image(let index):
previewKindText = "\(index + 1)张举报图片"
super.init(frame: .zero)
configure(kind: .image(index: index), footerText: "图片 \(index + 1)")
case .video(let index):
previewKindText = "\(index + 1)段举报视频"
super.init(frame: .zero)
configure(kind: .video(index: index, duration: "00:32"), footerText: "视频 \(index + 1)")
}
}
init(completionItem: WildReportCompletionMediaItem) {
switch completionItem.kind {
case .image:
previewKindText = "处理凭证图片"
super.init(frame: .zero)
configure(kind: .image(index: 0), footerText: "凭证")
case .video:
previewKindText = "处理凭证视频"
super.init(frame: .zero)
configure(kind: .video(index: 0, duration: completionItem.duration ?? "00:32"), footerText: completionItem.duration ?? "视频")
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
gradient.frame = bounds
}
private func configure(kind: Kind, footerText: String) {
clipsToBounds = true
layer.cornerRadius = 14
gradient.colors = kind.gradientColors
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 1)
layer.insertSublayer(gradient, at: 0)
iconView.image = UIImage(systemName: kind.iconName)
iconView.tintColor = UIColor.white.withAlphaComponent(0.92)
iconView.contentMode = .scaleAspectFit
addSubview(iconView)
footerLabel.text = footerText
footerLabel.font = .systemFont(ofSize: 11, weight: .bold)
footerLabel.textColor = .white
footerLabel.backgroundColor = UIColor.black.withAlphaComponent(0.24)
footerLabel.layer.cornerRadius = 9
footerLabel.clipsToBounds = true
addSubview(footerLabel)
iconView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(36)
}
footerLabel.snp.makeConstraints { make in
make.leading.bottom.equalToSuperview().inset(8)
}
if case .video(_, let duration) = kind {
playView.tintColor = .white
addSubview(playView)
let durationLabel = PaddingLabel(insets: UIEdgeInsets(top: 2, left: 6, bottom: 2, right: 6))
durationLabel.text = duration
durationLabel.font = .systemFont(ofSize: 10, weight: .bold)
durationLabel.textColor = .white
durationLabel.backgroundColor = UIColor.black.withAlphaComponent(0.34)
durationLabel.layer.cornerRadius = 8
durationLabel.clipsToBounds = true
addSubview(durationLabel)
playView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(34)
}
durationLabel.snp.makeConstraints { make in
make.trailing.top.equalToSuperview().inset(8)
}
}
}
///
private enum Kind {
case image(index: Int)
case video(index: Int, duration: String)
var iconName: String {
switch self {
case .image(let index):
return index.isMultiple(of: 2) ? "person.crop.rectangle.fill" : "camera.fill"
case .video:
return "video.fill"
}
}
var gradientColors: [CGColor] {
switch self {
case .image(let index):
return index.isMultiple(of: 2)
? [UIColor(hex: 0x75C7FF).cgColor, UIColor(hex: 0x0073FF).cgColor]
: [UIColor(hex: 0xA6E3B8).cgColor, UIColor(hex: 0x10B981).cgColor]
case .video:
return [UIColor(hex: 0x8CA8FF).cgColor, UIColor(hex: 0x4F46E5).cgColor]
}
}
}
}
///
private final class WildReportDetailMapPreviewView: UIView {
private let locationLabel = UILabel()
private let pinView = UIImageView(image: UIImage(systemName: "mappin.circle.fill"))
init(locationText: String) {
super.init(frame: .zero)
backgroundColor = UIColor(hex: 0xEAF7EA)
layer.cornerRadius = 14
clipsToBounds = true
locationLabel.text = locationText
locationLabel.font = .systemFont(ofSize: 12, weight: .bold)
locationLabel.textColor = AppColor.textPrimary
locationLabel.backgroundColor = UIColor.white.withAlphaComponent(0.86)
locationLabel.layer.cornerRadius = 10
locationLabel.clipsToBounds = true
locationLabel.textAlignment = .center
pinView.tintColor = AppColor.danger
pinView.contentMode = .scaleAspectFit
addSubview(locationLabel)
addSubview(pinView)
locationLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(12)
make.bottom.equalToSuperview().inset(10)
make.trailing.lessThanOrEqualToSuperview().inset(12)
make.height.equalTo(24)
}
pinView.snp.makeConstraints { make in
make.centerX.equalToSuperview().offset(34)
make.centerY.equalToSuperview().offset(-2)
make.size.equalTo(32)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
context.setFillColor(UIColor(hex: 0xD9F1D8).cgColor)
context.fill(CGRect(x: rect.width * 0.56, y: 0, width: rect.width * 0.44, height: rect.height))
let road = UIBezierPath()
road.move(to: CGPoint(x: 0, y: rect.height * 0.62))
road.addCurve(
to: CGPoint(x: rect.width, y: rect.height * 0.24),
controlPoint1: CGPoint(x: rect.width * 0.28, y: rect.height * 0.26),
controlPoint2: CGPoint(x: rect.width * 0.64, y: rect.height * 0.88)
)
UIColor.white.withAlphaComponent(0.86).setStroke()
road.lineWidth = 10
road.stroke()
UIColor(hex: 0xBFDDBD).setStroke()
road.lineWidth = 2
road.stroke()
let path = UIBezierPath()
path.move(to: CGPoint(x: rect.width * 0.16, y: rect.height * 0.24))
path.addCurve(
to: CGPoint(x: rect.width * 0.76, y: rect.height * 0.70),
controlPoint1: CGPoint(x: rect.width * 0.38, y: rect.height * 0.34),
controlPoint2: CGPoint(x: rect.width * 0.48, y: rect.height * 0.76)
)
AppColor.primary.setStroke()
path.lineWidth = 2
path.setLineDash([6, 5], count: 2, phase: 0)
path.stroke()
}
}
/// 线
private final class WildReportDetailTimelineRowView: UIView {
init(step: WildReportProgressStep, isLast: Bool) {
super.init(frame: .zero)
let leftColumn = UIView()
let node = WildReportDetailTimelineNodeView(state: step.state)
let line = UIView()
line.backgroundColor = step.state == .pending ? AppColor.cardOutline : AppColor.primary.withAlphaComponent(0.45)
line.isHidden = isLast
leftColumn.addSubview(line)
leftColumn.addSubview(node)
addSubview(leftColumn)
let textStack = UIStackView()
textStack.axis = .vertical
textStack.spacing = 5
let isPending = step.state == .pending
let title = WildReportUI.label(
step.title,
font: .systemFont(ofSize: 16, weight: step.state == .current ? .bold : .semibold),
color: isPending ? AppColor.textTertiary : AppColor.textPrimary
)
textStack.addArrangedSubview(title)
if !isPending || !step.timeText.isEmpty {
textStack.addArrangedSubview(WildReportUI.label(
step.timeText,
font: .app(.captionMedium),
color: step.state == .current ? AppColor.primary : AppColor.textSecondary,
lines: 0
))
}
addSubview(textStack)
leftColumn.snp.makeConstraints { make in
make.leading.top.bottom.equalToSuperview()
make.width.equalTo(24)
}
node.snp.makeConstraints { make in
make.top.centerX.equalToSuperview()
make.size.equalTo(24)
}
line.snp.makeConstraints { make in
make.top.equalTo(node.snp.bottom).offset(4)
make.centerX.equalTo(node)
make.bottom.equalToSuperview().inset(-8)
make.width.equalTo(2)
}
textStack.snp.makeConstraints { make in
make.top.equalToSuperview().offset(1)
make.leading.equalTo(leftColumn.snp.trailing).offset(12)
make.trailing.equalToSuperview()
make.bottom.equalToSuperview().inset(16)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
/// 线
private final class WildReportDetailTimelineNodeView: UIView {
private let state: WildReportProgressStepState
init(state: WildReportProgressStepState) {
self.state = state
super.init(frame: .zero)
backgroundColor = .clear
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let circle = UIBezierPath(ovalIn: rect.insetBy(dx: 1.5, dy: 1.5))
switch state {
case .completed:
AppColor.primary.setFill()
circle.fill()
let check = UIBezierPath()
check.move(to: CGPoint(x: rect.width * 0.30, y: rect.height * 0.52))
check.addLine(to: CGPoint(x: rect.width * 0.45, y: rect.height * 0.66))
check.addLine(to: CGPoint(x: rect.width * 0.72, y: rect.height * 0.36))
UIColor.white.setStroke()
check.lineWidth = 2.2
check.lineCapStyle = .round
check.lineJoinStyle = .round
check.stroke()
case .current:
AppColor.primary.setStroke()
circle.lineWidth = 2
circle.stroke()
AppColor.primary.setFill()
UIBezierPath(ovalIn: rect.insetBy(dx: 7, dy: 7)).fill()
case .pending:
AppColor.cardOutline.setStroke()
circle.lineWidth = 2
circle.stroke()
}
}
}