fix: optimize report detail evidence UI

This commit is contained in:
2026-07-09 11:11:21 +08:00
parent 939295d74a
commit 9b92d81902
2 changed files with 340 additions and 111 deletions

View File

@ -30,10 +30,20 @@ final class AllFunctionsViewController: BaseViewController {
} }
} }
///
private struct AllFunctionListItem: Hashable {
let section: AllFunctionSection
let menu: HomeMenuItem
var actionStyle: AllFunctionMenuCell.ActionStyle {
section.actionStyle
}
}
private let viewModel = AllFunctionsViewModel() private let viewModel = AllFunctionsViewModel()
private let collectionView: UICollectionView private let collectionView: UICollectionView
private var dataSource: UICollectionViewDiffableDataSource<AllFunctionSection, HomeMenuItem>! private var dataSource: UICollectionViewDiffableDataSource<AllFunctionSection, AllFunctionListItem>!
private var hasAppliedInitialSnapshot = false private var hasAppliedInitialSnapshot = false
init() { init() {
@ -104,30 +114,35 @@ final class AllFunctionsViewController: BaseViewController {
@MainActor @MainActor
private func applyViewModel(animated: Bool) { private func applyViewModel(animated: Bool) {
var snapshot = NSDiffableDataSourceSnapshot<AllFunctionSection, HomeMenuItem>() var snapshot = NSDiffableDataSourceSnapshot<AllFunctionSection, AllFunctionListItem>()
snapshot.appendSections([.common, .more]) snapshot.appendSections([.common, .more])
snapshot.appendItems(viewModel.commonMenus, toSection: .common) snapshot.appendItems(
snapshot.appendItems(viewModel.moreMenus, toSection: .more) viewModel.commonMenus.map { AllFunctionListItem(section: .common, menu: $0) },
toSection: .common
)
snapshot.appendItems(
viewModel.moreMenus.map { AllFunctionListItem(section: .more, menu: $0) },
toSection: .more
)
dataSource.apply(snapshot, animatingDifferences: animated) dataSource.apply(snapshot, animatingDifferences: animated)
hasAppliedInitialSnapshot = true hasAppliedInitialSnapshot = true
} }
private func makeDataSource() -> UICollectionViewDiffableDataSource<AllFunctionSection, HomeMenuItem> { private func makeDataSource() -> UICollectionViewDiffableDataSource<AllFunctionSection, AllFunctionListItem> {
let dataSource = UICollectionViewDiffableDataSource<AllFunctionSection, HomeMenuItem>( let dataSource = UICollectionViewDiffableDataSource<AllFunctionSection, AllFunctionListItem>(
collectionView: collectionView collectionView: collectionView
) { [weak self] collectionView, indexPath, menu in ) { [weak self] collectionView, indexPath, item in
let cell = collectionView.dequeueReusableCell( let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: AllFunctionMenuCell.reuseIdentifier, withReuseIdentifier: AllFunctionMenuCell.reuseIdentifier,
for: indexPath for: indexPath
) as! AllFunctionMenuCell ) as! AllFunctionMenuCell
guard let section = AllFunctionSection(rawValue: indexPath.section) else { return cell } cell.apply(menu: item.menu, actionStyle: item.actionStyle)
cell.apply(menu: menu, actionStyle: section.actionStyle)
cell.onActionTap = { cell.onActionTap = {
switch section { switch item.section {
case .common: case .common:
self?.viewModel.removeFromCommon(menu) self?.viewModel.removeFromCommon(item.menu)
case .more: case .more:
self?.viewModel.addToCommon(menu) self?.viewModel.addToCommon(item.menu)
} }
} }
return cell return cell

View File

@ -52,17 +52,23 @@ final class WildPhotographerReportListViewController: BaseViewController {
} }
} }
view.addSubview(filterTabBar)
view.addSubview(scrollView) view.addSubview(scrollView)
scrollView.addSubview(stack) scrollView.addSubview(stack)
reloadContent(animated: false) reloadContent(animated: false)
} }
override func setupConstraints() { override func setupConstraints() {
filterTabBar.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(14)
make.leading.trailing.equalToSuperview().inset(18)
}
scrollView.snp.makeConstraints { make in scrollView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide) make.top.equalTo(filterTabBar.snp.bottom).offset(14)
make.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide)
} }
stack.snp.makeConstraints { make in stack.snp.makeConstraints { make in
make.top.equalTo(scrollView.contentLayoutGuide).offset(14) make.top.equalTo(scrollView.contentLayoutGuide)
make.leading.equalTo(scrollView.contentLayoutGuide).offset(18) make.leading.equalTo(scrollView.contentLayoutGuide).offset(18)
make.trailing.equalTo(scrollView.contentLayoutGuide).inset(18) make.trailing.equalTo(scrollView.contentLayoutGuide).inset(18)
make.bottom.equalTo(scrollView.contentLayoutGuide).inset(24) make.bottom.equalTo(scrollView.contentLayoutGuide).inset(24)
@ -104,14 +110,13 @@ final class WildPhotographerReportListViewController: BaseViewController {
} }
private func rebuildContent() { private func rebuildContent() {
filterTabBar.apply(selectedFilter: viewModel.selectedFilter)
stack.arrangedSubviews.forEach { view in stack.arrangedSubviews.forEach { view in
stack.removeArrangedSubview(view) stack.removeArrangedSubview(view)
view.removeFromSuperview() view.removeFromSuperview()
} }
filterTabBar.apply(selectedFilter: viewModel.selectedFilter)
stack.addArrangedSubview(filterTabBar)
let reports = viewModel.filteredReports let reports = viewModel.filteredReports
if reports.isEmpty { if reports.isEmpty {
stack.addArrangedSubview(viewModel.isLoading ? WildReportListLoadingView(text: "正在加载举报记录...") : WildReportListEmptyView()) stack.addArrangedSubview(viewModel.isLoading ? WildReportListLoadingView(text: "正在加载举报记录...") : WildReportListEmptyView())
@ -327,6 +332,8 @@ private final class WildReportListCardView: UIControl, UIGestureRecognizerDelega
status.backgroundColor = statusColor.withAlphaComponent(0.12) status.backgroundColor = statusColor.withAlphaComponent(0.12)
status.layer.cornerRadius = 12 status.layer.cornerRadius = 12
status.clipsToBounds = true status.clipsToBounds = true
status.setContentHuggingPriority(.required, for: .horizontal)
status.setContentCompressionResistancePriority(.required, for: .horizontal)
status.snp.makeConstraints { make in status.snp.makeConstraints { make in
make.height.equalTo(24) make.height.equalTo(24)
} }
@ -348,6 +355,7 @@ private final class WildReportListCardView: UIControl, UIGestureRecognizerDelega
} }
private func makeScenicPill() -> UIView { private func makeScenicPill() -> UIView {
let wrapper = UIView()
let label = PaddingLabel(insets: UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)) let label = PaddingLabel(insets: UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8))
label.text = scenicName label.text = scenicName
label.font = .systemFont(ofSize: 12, weight: .semibold) label.font = .systemFont(ofSize: 12, weight: .semibold)
@ -355,10 +363,15 @@ private final class WildReportListCardView: UIControl, UIGestureRecognizerDelega
label.backgroundColor = AppColor.primary.withAlphaComponent(0.10) label.backgroundColor = AppColor.primary.withAlphaComponent(0.10)
label.layer.cornerRadius = 11 label.layer.cornerRadius = 11
label.clipsToBounds = true label.clipsToBounds = true
label.setContentHuggingPriority(.required, for: .horizontal)
label.setContentCompressionResistancePriority(.required, for: .horizontal)
wrapper.addSubview(label)
label.snp.makeConstraints { make in label.snp.makeConstraints { make in
make.top.bottom.leading.equalToSuperview()
make.trailing.lessThanOrEqualToSuperview()
make.height.equalTo(22) make.height.equalTo(22)
} }
return label return wrapper
} }
private func makeMetaRows() -> UIView { private func makeMetaRows() -> UIView {
@ -811,11 +824,10 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
let section = UIStackView() let section = UIStackView()
section.axis = .vertical section.axis = .vertical
section.spacing = 12 section.spacing = 12
section.addArrangedSubview(makeSectionHeader( section.addArrangedSubview(makeCompactMediaSectionHeader(
iconName: "photo.on.rectangle.angled", iconName: "photo.on.rectangle.angled",
title: "举报人材料", title: "举报人材料",
subtitle: "\(viewModel.imageCountText)图片 \(viewModel.videoCountText)视频", summary: viewModel.reporterMediaSummary,
chevron: true,
action: #selector(previewTapped) action: #selector(previewTapped)
)) ))
@ -844,15 +856,63 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
return section return section
} }
private func makeCompactMediaSectionHeader(
iconName: String,
title: String,
summary: String,
action: Selector
) -> UIView {
let control = UIControl()
control.addTarget(self, action: action, for: .touchUpInside)
let row = UIStackView()
row.axis = .horizontal
row.alignment = .center
row.spacing = 8
let icon = UIImageView(image: UIImage(systemName: iconName))
icon.tintColor = AppColor.primary
icon.contentMode = .scaleAspectFit
icon.snp.makeConstraints { make in
make.size.equalTo(16)
}
let titleLabel = WildReportUI.label(title, font: .systemFont(ofSize: 15, weight: .bold), color: AppColor.textPrimary)
let spacer = UIView()
let summaryLabel = WildReportUI.label(summary, font: .systemFont(ofSize: 13, weight: .medium), color: AppColor.textSecondary)
summaryLabel.setContentHuggingPriority(.required, for: .horizontal)
summaryLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
let chevron = UIImageView(image: UIImage(systemName: "chevron.right"))
chevron.tintColor = UIColor(hex: 0xB6BECA)
chevron.contentMode = .scaleAspectFit
chevron.snp.makeConstraints { make in
make.size.equalTo(14)
}
row.addArrangedSubview(icon)
row.addArrangedSubview(titleLabel)
row.addArrangedSubview(spacer)
row.addArrangedSubview(summaryLabel)
row.addArrangedSubview(chevron)
control.addSubview(row)
row.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
control.snp.makeConstraints { make in
make.height.equalTo(22)
}
return control
}
private func makePaymentScreenshotSection() -> UIView { private func makePaymentScreenshotSection() -> UIView {
let section = UIStackView() let section = UIStackView()
section.axis = .vertical section.axis = .vertical
section.spacing = 12 section.spacing = 12
section.addArrangedSubview(makeSectionHeader( section.addArrangedSubview(makeCompactMediaSectionHeader(
iconName: "creditcard.fill", iconName: "creditcard.fill",
title: "支付截图", title: "支付截图",
subtitle: viewModel.paymentScreenshotSummary, summary: viewModel.paymentScreenshotSummary,
chevron: true,
action: #selector(paymentPreviewTapped) action: #selector(paymentPreviewTapped)
)) ))
@ -924,14 +984,11 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
let section = UIStackView() let section = UIStackView()
section.axis = .vertical section.axis = .vertical
section.spacing = 12 section.spacing = 12
section.addArrangedSubview(makeSectionHeader( section.addArrangedSubview(makeLocationHeader())
iconName: "mappin.circle.fill", let mapPreview = WildReportDetailMapPreviewView(
title: "位置", scenicName: viewModel.scenicAreaName,
subtitle: viewModel.detailAddress, detailAddress: viewModel.detailAddress
chevron: true, )
action: #selector(locationTapped)
))
let mapPreview = WildReportDetailMapPreviewView(locationText: viewModel.detailAddress)
section.addArrangedSubview(mapPreview) section.addArrangedSubview(mapPreview)
mapPreview.snp.makeConstraints { make in mapPreview.snp.makeConstraints { make in
make.height.equalTo(98) make.height.equalTo(98)
@ -939,7 +996,66 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
return section return section
} }
private func makeSectionHeader(iconName: String, title: String, subtitle: String, chevron: Bool, action: Selector?) -> UIView { private func makeLocationHeader() -> UIView {
let control = UIControl()
control.addTarget(self, action: #selector(locationTapped), for: .touchUpInside)
let row = UIStackView()
row.axis = .horizontal
row.alignment = .center
row.spacing = 14
let iconBox = UIView()
iconBox.backgroundColor = AppColor.primaryLight
iconBox.layer.cornerRadius = 12
let icon = UIImageView(image: UIImage(systemName: "mappin.circle.fill"))
icon.tintColor = AppColor.primary
icon.contentMode = .scaleAspectFit
iconBox.addSubview(icon)
iconBox.snp.makeConstraints { make in
make.size.equalTo(50)
}
icon.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(22)
}
let titleLabel = WildReportUI.label("位置", font: .systemFont(ofSize: 20, weight: .bold), color: AppColor.textPrimary)
let spacer = UIView()
let valueLabel = WildReportUI.label(viewModel.detailAddress, font: .systemFont(ofSize: 18, weight: .medium), color: AppColor.textSecondary)
valueLabel.lineBreakMode = .byTruncatingTail
valueLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
let chevron = UIImageView(image: UIImage(systemName: "chevron.right"))
chevron.tintColor = UIColor(hex: 0xB6BECA)
chevron.contentMode = .scaleAspectFit
chevron.snp.makeConstraints { make in
make.size.equalTo(16)
}
row.addArrangedSubview(iconBox)
row.addArrangedSubview(titleLabel)
row.addArrangedSubview(spacer)
row.addArrangedSubview(valueLabel)
row.addArrangedSubview(chevron)
control.addSubview(row)
row.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
control.snp.makeConstraints { make in
make.height.equalTo(50)
}
return control
}
private func makeSectionHeader(
iconName: String,
title: String,
subtitle: String,
trailingText: String? = nil,
chevron: Bool,
action: Selector?
) -> UIView {
let control = UIControl() let control = UIControl()
if let action { if let action {
control.addTarget(self, action: action, for: .touchUpInside) control.addTarget(self, action: action, for: .touchUpInside)
@ -958,12 +1074,21 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
textStack.axis = .vertical textStack.axis = .vertical
textStack.spacing = 4 textStack.spacing = 4
textStack.addArrangedSubview(WildReportUI.label(title, font: .systemFont(ofSize: 20, weight: .bold), color: AppColor.textPrimary)) textStack.addArrangedSubview(WildReportUI.label(title, font: .systemFont(ofSize: 20, weight: .bold), color: AppColor.textPrimary))
if !subtitle.isEmpty {
let subtitleLabel = WildReportUI.label(subtitle, font: .systemFont(ofSize: 18, weight: .medium), color: AppColor.textSecondary) let subtitleLabel = WildReportUI.label(subtitle, font: .systemFont(ofSize: 18, weight: .medium), color: AppColor.textSecondary)
subtitleLabel.adjustsFontSizeToFitWidth = true subtitleLabel.adjustsFontSizeToFitWidth = true
subtitleLabel.minimumScaleFactor = 0.8 subtitleLabel.minimumScaleFactor = 0.8
textStack.addArrangedSubview(subtitleLabel) textStack.addArrangedSubview(subtitleLabel)
}
control.addSubview(textStack) control.addSubview(textStack)
let trailingLabel = WildReportUI.label(trailingText ?? "", font: .systemFont(ofSize: 15, weight: .semibold), color: AppColor.textSecondary)
trailingLabel.textAlignment = .right
trailingLabel.setContentHuggingPriority(.required, for: .horizontal)
trailingLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
trailingLabel.isHidden = trailingText?.isEmpty != false
control.addSubview(trailingLabel)
let chevronView = UIImageView(image: UIImage(systemName: "chevron.right")) let chevronView = UIImageView(image: UIImage(systemName: "chevron.right"))
chevronView.tintColor = AppColor.textTertiary chevronView.tintColor = AppColor.textTertiary
chevronView.isHidden = !chevron chevronView.isHidden = !chevron
@ -981,11 +1106,19 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
make.trailing.centerY.equalToSuperview() make.trailing.centerY.equalToSuperview()
make.size.equalTo(18) make.size.equalTo(18)
} }
trailingLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalTo(chevronView.snp.leading).offset(-8)
}
textStack.snp.makeConstraints { make in textStack.snp.makeConstraints { make in
make.leading.equalTo(iconBox.snp.trailing).offset(12) make.leading.equalTo(iconBox.snp.trailing).offset(12)
make.centerY.equalToSuperview() make.centerY.equalToSuperview()
if trailingText?.isEmpty == false {
make.trailing.lessThanOrEqualTo(trailingLabel.snp.leading).offset(-10)
} else {
make.trailing.equalTo(chevronView.snp.leading).offset(-10) make.trailing.equalTo(chevronView.snp.leading).offset(-10)
} }
}
control.snp.makeConstraints { make in control.snp.makeConstraints { make in
make.height.greaterThanOrEqualTo(50) make.height.greaterThanOrEqualTo(50)
} }
@ -993,6 +1126,7 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
} }
private func makeScenicPill() -> UIView { private func makeScenicPill() -> UIView {
let wrapper = UIView()
let pill = UIStackView() let pill = UIStackView()
pill.axis = .horizontal pill.axis = .horizontal
pill.alignment = .center pill.alignment = .center
@ -1002,6 +1136,8 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
pill.backgroundColor = AppColor.primaryLight pill.backgroundColor = AppColor.primaryLight
pill.layer.cornerRadius = 13 pill.layer.cornerRadius = 13
pill.clipsToBounds = true pill.clipsToBounds = true
pill.setContentHuggingPriority(.required, for: .horizontal)
pill.setContentCompressionResistancePriority(.required, for: .horizontal)
let icon = UIImageView(image: UIImage(systemName: "mappin.circle.fill")) let icon = UIImageView(image: UIImage(systemName: "mappin.circle.fill"))
icon.tintColor = AppColor.primary icon.tintColor = AppColor.primary
@ -1011,7 +1147,12 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
let label = WildReportUI.label(viewModel.scenicAreaName, font: .app(.captionMedium), color: AppColor.primary) let label = WildReportUI.label(viewModel.scenicAreaName, font: .app(.captionMedium), color: AppColor.primary)
pill.addArrangedSubview(icon) pill.addArrangedSubview(icon)
pill.addArrangedSubview(label) pill.addArrangedSubview(label)
return pill wrapper.addSubview(pill)
pill.snp.makeConstraints { make in
make.top.bottom.leading.equalToSuperview()
make.trailing.lessThanOrEqualToSuperview()
}
return wrapper
} }
private func makeBottomButton(title: String, iconName: String, isPrimary: Bool) -> UIButton { private func makeBottomButton(title: String, iconName: String, isPrimary: Bool) -> UIButton {
@ -1090,7 +1231,7 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
@objc private func mapTapped() { @objc private func mapTapped() {
navigationController?.pushViewController( navigationController?.pushViewController(
WildReportRiskMapViewController(record: viewModel.record, riskPoints: homeViewModel.riskPoints), WildReportRiskMapViewController(),
animated: true animated: true
) )
} }
@ -1140,7 +1281,6 @@ private final class WildReportDetailInfoRowView: UIView {
let titleLabel = WildReportUI.label(title, font: .app(.captionMedium), color: AppColor.textSecondary) 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) 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) valueLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
addSubview(titleLabel) addSubview(titleLabel)
@ -1185,7 +1325,6 @@ private final class WildReportDetailMediaThumbnailView: UIView {
let previewKindText: String let previewKindText: String
private let gradient = CAGradientLayer() private let gradient = CAGradientLayer()
private let remoteImageView = UIImageView() private let remoteImageView = UIImageView()
private let iconView = UIImageView()
private let footerLabel = PaddingLabel(insets: UIEdgeInsets(top: 3, left: 8, bottom: 3, right: 8)) private let footerLabel = PaddingLabel(insets: UIEdgeInsets(top: 3, left: 8, bottom: 3, right: 8))
private let playView = UIImageView(image: UIImage(systemName: "play.circle.fill")) private let playView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
@ -1194,11 +1333,11 @@ private final class WildReportDetailMediaThumbnailView: UIView {
case .image(let index, let url, let fileName, let fileTypeText): case .image(let index, let url, let fileName, let fileTypeText):
previewKindText = fileName?.isEmpty == false ? fileName! : "\(index + 1)张举报图片" previewKindText = fileName?.isEmpty == false ? fileName! : "\(index + 1)张举报图片"
super.init(frame: .zero) super.init(frame: .zero)
configure(kind: .image(index: index), footerText: fileTypeText ?? "图片 \(index + 1)", imageURL: url) configure(kind: .image(index: index), footerText: fileTypeText ?? "图片 \(index + 1)", imageURL: url, showsFooter: false)
case .video(let index, _, let fileName, let fileTypeText): case .video(let index, _, let fileName, let fileTypeText):
previewKindText = fileName?.isEmpty == false ? fileName! : "\(index + 1)段举报视频" previewKindText = fileName?.isEmpty == false ? fileName! : "\(index + 1)段举报视频"
super.init(frame: .zero) super.init(frame: .zero)
configure(kind: .video(index: index, duration: fileTypeText ?? "视频"), footerText: fileTypeText ?? "视频 \(index + 1)") configure(kind: .video(index: index, duration: fileTypeText ?? "00:32"), footerText: fileTypeText ?? "视频 \(index + 1)", showsFooter: false)
} }
} }
@ -1225,9 +1364,9 @@ private final class WildReportDetailMediaThumbnailView: UIView {
gradient.frame = bounds gradient.frame = bounds
} }
private func configure(kind: Kind, footerText: String, imageURL: String? = nil) { private func configure(kind: Kind, footerText: String, imageURL: String? = nil, showsFooter: Bool = true) {
clipsToBounds = true clipsToBounds = true
layer.cornerRadius = 14 layer.cornerRadius = 10
gradient.colors = kind.gradientColors gradient.colors = kind.gradientColors
gradient.startPoint = CGPoint(x: 0, y: 0) gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 1) gradient.endPoint = CGPoint(x: 1, y: 1)
@ -1243,11 +1382,9 @@ private final class WildReportDetailMediaThumbnailView: UIView {
} }
} }
iconView.image = UIImage(systemName: kind.iconName) if imageURL == nil {
iconView.tintColor = UIColor.white.withAlphaComponent(0.92) addPlaceholderArtwork(for: kind)
iconView.contentMode = .scaleAspectFit }
iconView.isHidden = imageURL != nil
addSubview(iconView)
footerLabel.text = footerText footerLabel.text = footerText
footerLabel.font = .systemFont(ofSize: 11, weight: .bold) footerLabel.font = .systemFont(ofSize: 11, weight: .bold)
@ -1255,59 +1392,107 @@ private final class WildReportDetailMediaThumbnailView: UIView {
footerLabel.backgroundColor = UIColor.black.withAlphaComponent(0.24) footerLabel.backgroundColor = UIColor.black.withAlphaComponent(0.24)
footerLabel.layer.cornerRadius = 9 footerLabel.layer.cornerRadius = 9
footerLabel.clipsToBounds = true footerLabel.clipsToBounds = true
footerLabel.isHidden = !showsFooter
addSubview(footerLabel) addSubview(footerLabel)
iconView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(36)
}
footerLabel.snp.makeConstraints { make in footerLabel.snp.makeConstraints { make in
make.leading.bottom.equalToSuperview().inset(8) make.leading.bottom.equalToSuperview().inset(8)
} }
if case .video(_, let duration) = kind { if case .video(_, let duration) = kind {
let playCircle = UIView()
playCircle.backgroundColor = UIColor.black.withAlphaComponent(0.08)
playCircle.layer.cornerRadius = 21
playCircle.layer.borderWidth = 2.5
playCircle.layer.borderColor = UIColor.white.cgColor
addSubview(playCircle)
playView.image = UIImage(systemName: "play.fill")
playView.tintColor = .white playView.tintColor = .white
addSubview(playView) addSubview(playView)
let durationLabel = PaddingLabel(insets: UIEdgeInsets(top: 2, left: 6, bottom: 2, right: 6)) let durationLabel = PaddingLabel(insets: UIEdgeInsets(top: 2, left: 6, bottom: 2, right: 6))
durationLabel.text = duration durationLabel.text = duration
durationLabel.font = .systemFont(ofSize: 10, weight: .bold) durationLabel.font = .systemFont(ofSize: 14, weight: .semibold)
durationLabel.textColor = .white durationLabel.textColor = .white
durationLabel.backgroundColor = UIColor.black.withAlphaComponent(0.34) durationLabel.backgroundColor = .clear
durationLabel.layer.cornerRadius = 8 durationLabel.layer.shadowColor = UIColor.black.cgColor
durationLabel.clipsToBounds = true durationLabel.layer.shadowOpacity = 0.25
durationLabel.layer.shadowRadius = 4
durationLabel.layer.shadowOffset = CGSize(width: 0, height: 2)
addSubview(durationLabel) addSubview(durationLabel)
playView.snp.makeConstraints { make in playCircle.snp.makeConstraints { make in
make.center.equalToSuperview() make.center.equalToSuperview()
make.size.equalTo(34) make.size.equalTo(42)
}
playView.snp.makeConstraints { make in
make.centerX.equalToSuperview().offset(2)
make.centerY.equalToSuperview()
make.size.equalTo(18)
} }
durationLabel.snp.makeConstraints { make in durationLabel.snp.makeConstraints { make in
make.trailing.top.equalToSuperview().inset(8) make.trailing.bottom.equalToSuperview().inset(10)
} }
} }
} }
private func addPlaceholderArtwork(for kind: Kind) {
let index = min(kind.artworkIndex, 2)
let mountain = UIImageView(image: UIImage(systemName: "mountain.2.fill"))
mountain.tintColor = UIColor.white.withAlphaComponent(0.32)
mountain.contentMode = .scaleAspectFit
addSubview(mountain)
let camera = UIImageView(image: UIImage(systemName: "camera.fill"))
camera.tintColor = UIColor.white.withAlphaComponent(0.90)
camera.contentMode = .scaleAspectFit
addSubview(camera)
let person = UIImageView(image: UIImage(systemName: "person.fill"))
person.tintColor = UIColor(hex: 0x101827).withAlphaComponent(0.78)
person.contentMode = .scaleAspectFit
addSubview(person)
mountain.snp.makeConstraints { make in
make.width.height.equalTo(52)
make.centerX.equalToSuperview().offset(-16)
make.centerY.equalToSuperview().offset(-8)
}
camera.snp.makeConstraints { make in
make.width.height.equalTo(22)
make.trailing.equalToSuperview().inset(10)
make.centerY.equalToSuperview().offset(-18)
}
person.snp.makeConstraints { make in
make.width.height.equalTo(44)
make.trailing.equalToSuperview().inset(24 - index * 12)
make.bottom.equalToSuperview().offset(10)
}
}
/// ///
private enum Kind { private enum Kind {
case image(index: Int) case image(index: Int)
case video(index: Int, duration: String) case video(index: Int, duration: String)
var iconName: String { var artworkIndex: Int {
switch self { switch self {
case .image(let index): case .image(let index), .video(let index, _):
return index.isMultiple(of: 2) ? "person.crop.rectangle.fill" : "camera.fill" return index
case .video:
return "video.fill"
} }
} }
var gradientColors: [CGColor] { var gradientColors: [CGColor] {
switch self { switch self {
case .image(let index): case .image(let index):
return index.isMultiple(of: 2) switch index {
? [UIColor(hex: 0x75C7FF).cgColor, UIColor(hex: 0x0073FF).cgColor] case 1:
: [UIColor(hex: 0xA6E3B8).cgColor, UIColor(hex: 0x10B981).cgColor] return [UIColor(hex: 0xD7E8F8).cgColor, UIColor(hex: 0x7E9CB4).cgColor]
case 2:
return [UIColor(hex: 0xBFD9EE).cgColor, UIColor(hex: 0x435B70).cgColor]
default:
return [UIColor(hex: 0xC7DFEC).cgColor, UIColor(hex: 0x2D3D4B).cgColor]
}
case .video: case .video:
return [UIColor(hex: 0x8CA8FF).cgColor, UIColor(hex: 0x4F46E5).cgColor] return [UIColor(hex: 0xBFD9EE).cgColor, UIColor(hex: 0x435B70).cgColor]
} }
} }
} }
@ -1315,37 +1500,52 @@ private final class WildReportDetailMediaThumbnailView: UIView {
/// ///
private final class WildReportDetailMapPreviewView: UIView { private final class WildReportDetailMapPreviewView: UIView {
private let locationLabel = UILabel() private let scenicName: String
private let detailAddress: String
private let detailLabel = UILabel()
private let scenicLabel = UILabel()
private let pinView = UIImageView(image: UIImage(systemName: "mappin.circle.fill")) private let pinView = UIImageView(image: UIImage(systemName: "mappin.circle.fill"))
init(locationText: String) { init(scenicName: String, detailAddress: String) {
self.scenicName = scenicName
self.detailAddress = detailAddress
super.init(frame: .zero) super.init(frame: .zero)
backgroundColor = UIColor(hex: 0xEAF7EA) backgroundColor = UIColor(hex: 0xE1F4D8)
layer.cornerRadius = 14 layer.cornerRadius = 10
clipsToBounds = true clipsToBounds = true
locationLabel.text = locationText detailLabel.text = detailAddress.replacingOccurrences(of: "附近", with: "")
locationLabel.font = .systemFont(ofSize: 12, weight: .bold) detailLabel.font = .systemFont(ofSize: 13, weight: .semibold)
locationLabel.textColor = AppColor.textPrimary detailLabel.textColor = UIColor(hex: 0x64748B)
locationLabel.backgroundColor = UIColor.white.withAlphaComponent(0.86) detailLabel.textAlignment = .center
locationLabel.layer.cornerRadius = 10
locationLabel.clipsToBounds = true scenicLabel.text = scenicName
locationLabel.textAlignment = .center scenicLabel.font = .systemFont(ofSize: 13, weight: .semibold)
scenicLabel.textColor = UIColor(hex: 0x418C48)
scenicLabel.textAlignment = .center
pinView.tintColor = AppColor.danger pinView.tintColor = AppColor.danger
pinView.contentMode = .scaleAspectFit pinView.contentMode = .scaleAspectFit
addSubview(locationLabel) addSubview(detailLabel)
addSubview(scenicLabel)
addSubview(pinView) addSubview(pinView)
locationLabel.snp.makeConstraints { make in detailLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(12) make.centerX.equalToSuperview().multipliedBy(0.94)
make.bottom.equalToSuperview().inset(10) make.centerY.equalToSuperview().multipliedBy(0.64)
make.leading.greaterThanOrEqualToSuperview().offset(12)
make.trailing.lessThanOrEqualToSuperview().inset(12)
}
scenicLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview().multipliedBy(1.56)
make.centerY.equalToSuperview().multipliedBy(1.16)
make.leading.greaterThanOrEqualToSuperview().offset(12)
make.trailing.lessThanOrEqualToSuperview().inset(12) make.trailing.lessThanOrEqualToSuperview().inset(12)
make.height.equalTo(24)
} }
pinView.snp.makeConstraints { make in pinView.snp.makeConstraints { make in
make.centerX.equalToSuperview().offset(34) make.centerX.equalToSuperview().multipliedBy(1.06)
make.centerY.equalToSuperview().offset(-2) make.centerY.equalToSuperview().multipliedBy(1.08)
make.size.equalTo(32) make.size.equalTo(36)
} }
} }
@ -1358,34 +1558,48 @@ private final class WildReportDetailMapPreviewView: UIView {
super.draw(rect) super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return } guard let context = UIGraphicsGetCurrentContext() else { return }
context.setFillColor(UIColor(hex: 0xD9F1D8).cgColor) let colors = [UIColor(hex: 0xE1F4D8).cgColor, UIColor(hex: 0xBDE6C7).cgColor] as CFArray
context.fill(CGRect(x: rect.width * 0.56, y: 0, width: rect.width * 0.44, height: rect.height)) let colorSpace = CGColorSpaceCreateDeviceRGB()
if let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: [0, 1]) {
context.drawLinearGradient(
gradient,
start: CGPoint(x: 0, y: 0),
end: CGPoint(x: rect.width, y: rect.height),
options: []
)
}
let dashedPath = UIBezierPath()
dashedPath.move(to: CGPoint(x: rect.width * 0.05, y: rect.height * 0.32))
dashedPath.addCurve(
to: CGPoint(x: rect.width * 0.95, y: rect.height * 0.22),
controlPoint1: CGPoint(x: rect.width * 0.28, y: rect.height * 0.62),
controlPoint2: CGPoint(x: rect.width * 0.56, y: rect.height * 0.02)
)
UIColor(hex: 0x65A6D7).withAlphaComponent(0.75).setStroke()
dashedPath.lineWidth = 1.4
dashedPath.setLineDash([5, 5], count: 2, phase: 0)
dashedPath.stroke()
let road = UIBezierPath() let road = UIBezierPath()
road.move(to: CGPoint(x: 0, y: rect.height * 0.62)) road.move(to: CGPoint(x: rect.width * 0.00, y: rect.height * 0.74))
road.addCurve( road.addCurve(
to: CGPoint(x: rect.width, y: rect.height * 0.24), to: CGPoint(x: rect.width * 1.00, y: rect.height * 0.62),
controlPoint1: CGPoint(x: rect.width * 0.28, y: rect.height * 0.26), controlPoint1: CGPoint(x: rect.width * 0.26, y: rect.height * 0.20),
controlPoint2: CGPoint(x: rect.width * 0.64, y: rect.height * 0.88) controlPoint2: CGPoint(x: rect.width * 0.68, y: rect.height * 1.00)
) )
UIColor.white.withAlphaComponent(0.86).setStroke() UIColor.white.withAlphaComponent(0.88).setStroke()
road.lineWidth = 10 road.lineWidth = 5
road.stroke() road.stroke()
UIColor(hex: 0xBFDDBD).setStroke() UIColor(hex: 0x99C59C).setStroke()
road.lineWidth = 2 road.lineWidth = 1
road.stroke() road.stroke()
let path = UIBezierPath() let pinCenter = CGPoint(x: rect.width * 0.53, y: rect.height * 0.54)
path.move(to: CGPoint(x: rect.width * 0.16, y: rect.height * 0.24)) UIColor(hex: 0xEF4444).withAlphaComponent(0.18).setFill()
path.addCurve( context.fillEllipse(in: CGRect(x: pinCenter.x - 32, y: pinCenter.y - 32, width: 64, height: 64))
to: CGPoint(x: rect.width * 0.76, y: rect.height * 0.70), UIColor(hex: 0xEF4444).withAlphaComponent(0.22).setFill()
controlPoint1: CGPoint(x: rect.width * 0.38, y: rect.height * 0.34), context.fillEllipse(in: CGRect(x: pinCenter.x - 22, y: pinCenter.y - 22, width: 44, height: 44))
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()
} }
} }