优化举报摄影师页面
This commit is contained in:
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
|
||||
private let contentStack = UIStackView()
|
||||
private let descriptionView = UITextView()
|
||||
private let contactField = UITextField()
|
||||
private let submitButton = AppButton(title: "提交举报")
|
||||
private let submitButton = UIButton(type: .system)
|
||||
|
||||
init(
|
||||
homeViewModel: WildPhotographerReportHomeViewModel,
|
||||
@ -40,45 +40,50 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
|
||||
override func setupUI() {
|
||||
view.backgroundColor = AppColor.pageBackgroundSoft
|
||||
scrollView.keyboardDismissMode = .interactive
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = AppSpacing.sm
|
||||
|
||||
descriptionView.font = .app(.body)
|
||||
descriptionView.font = .systemFont(ofSize: 15)
|
||||
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.backgroundColor = .clear
|
||||
descriptionView.textContainerInset = UIEdgeInsets(top: 10, left: 8, bottom: 34, right: 8)
|
||||
descriptionView.delegate = self
|
||||
|
||||
contactField.placeholder = "微信号或手机号(选填)"
|
||||
contactField.placeholder = "请输入摄影师微信号或手机号(选填)"
|
||||
contactField.font = .app(.body)
|
||||
contactField.textColor = AppColor.textPrimary
|
||||
contactField.backgroundColor = AppColor.inputBackground
|
||||
contactField.layer.cornerRadius = AppRadius.md
|
||||
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)
|
||||
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)
|
||||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
||||
}
|
||||
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)
|
||||
make.edges.equalTo(scrollView.contentLayoutGuide).inset(UIEdgeInsets(top: 14, left: 18, bottom: 26, right: 18))
|
||||
make.width.equalTo(scrollView.frameLayoutGuide).offset(-36)
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,37 +119,68 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
|
||||
contentStack.removeArrangedSubview(view)
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
contentStack.addArrangedSubview(makeTipsCard())
|
||||
contentStack.addArrangedSubview(makeRealNameBanner())
|
||||
contentStack.addArrangedSubview(makeTypeCard())
|
||||
contentStack.addArrangedSubview(makeEvidenceCard())
|
||||
contentStack.addArrangedSubview(makeDescriptionCard())
|
||||
contentStack.addArrangedSubview(makeLocationCard())
|
||||
contentStack.addArrangedSubview(makeContactCard())
|
||||
contentStack.addArrangedSubview(makePaymentCard())
|
||||
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 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 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 card = sectionCard(index: 1, title: "举报类型")
|
||||
let row = UIStackView()
|
||||
row.axis = .vertical
|
||||
row.spacing = AppSpacing.xs
|
||||
row.axis = .horizontal
|
||||
row.spacing = 8
|
||||
row.distribution = .fillEqually
|
||||
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)
|
||||
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(38) }
|
||||
button.snp.makeConstraints { make in make.height.equalTo(72) }
|
||||
row.addArrangedSubview(button)
|
||||
}
|
||||
card.stack.addArrangedSubview(row)
|
||||
@ -153,84 +189,306 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
|
||||
|
||||
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)
|
||||
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
|
||||
card.stack.addArrangedSubview(descriptionView)
|
||||
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)
|
||||
}
|
||||
let count = WildReportUI.label("\(viewModel.description.count)/500", font: .app(.caption), color: AppColor.textTertiary)
|
||||
count.textAlignment = .right
|
||||
card.stack.addArrangedSubview(count)
|
||||
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 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)
|
||||
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)
|
||||
button.snp.makeConstraints { make in make.height.equalTo(42) }
|
||||
card.stack.addArrangedSubview(title)
|
||||
card.stack.addArrangedSubview(subtitle)
|
||||
card.stack.addArrangedSubview(button)
|
||||
|
||||
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: "联系方式(选填)")
|
||||
let card = sectionCard(index: 5, title: "摄影师微信号/手机号", optional: true)
|
||||
contactField.text = viewModel.contact
|
||||
card.stack.addArrangedSubview(contactField)
|
||||
contactField.snp.makeConstraints { make in make.height.equalTo(46) }
|
||||
contactField.snp.makeConstraints { make in make.height.equalTo(48) }
|
||||
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
|
||||
}
|
||||
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
|
||||
|
||||
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)
|
||||
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() }
|
||||
@ -247,6 +505,209 @@ extension WildPhotographerReportSubmitViewController: UITextViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// 提交页步骤标题,复刻参考页左侧蓝色竖条与选填标识。
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user