From 9b92d81902cb2fbc076fbfb2d637a1df422a0aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=89=E7=A7=8B?= <497055328@qq.com> Date: Thu, 9 Jul 2026 11:11:21 +0800 Subject: [PATCH] fix: optimize report detail evidence UI --- .../UI/Home/AllFunctionsViewController.swift | 39 +- ...PhotographerReportListViewController.swift | 412 +++++++++++++----- 2 files changed, 340 insertions(+), 111 deletions(-) diff --git a/suixinkan/UI/Home/AllFunctionsViewController.swift b/suixinkan/UI/Home/AllFunctionsViewController.swift index 602a730..2177bda 100644 --- a/suixinkan/UI/Home/AllFunctionsViewController.swift +++ b/suixinkan/UI/Home/AllFunctionsViewController.swift @@ -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 collectionView: UICollectionView - private var dataSource: UICollectionViewDiffableDataSource! + private var dataSource: UICollectionViewDiffableDataSource! private var hasAppliedInitialSnapshot = false init() { @@ -104,30 +114,35 @@ final class AllFunctionsViewController: BaseViewController { @MainActor private func applyViewModel(animated: Bool) { - var snapshot = NSDiffableDataSourceSnapshot() + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.common, .more]) - snapshot.appendItems(viewModel.commonMenus, toSection: .common) - snapshot.appendItems(viewModel.moreMenus, toSection: .more) + snapshot.appendItems( + 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) hasAppliedInitialSnapshot = true } - private func makeDataSource() -> UICollectionViewDiffableDataSource { - let dataSource = UICollectionViewDiffableDataSource( + private func makeDataSource() -> UICollectionViewDiffableDataSource { + let dataSource = UICollectionViewDiffableDataSource( collectionView: collectionView - ) { [weak self] collectionView, indexPath, menu in + ) { [weak self] collectionView, indexPath, item in let cell = collectionView.dequeueReusableCell( withReuseIdentifier: AllFunctionMenuCell.reuseIdentifier, for: indexPath ) as! AllFunctionMenuCell - guard let section = AllFunctionSection(rawValue: indexPath.section) else { return cell } - cell.apply(menu: menu, actionStyle: section.actionStyle) + cell.apply(menu: item.menu, actionStyle: item.actionStyle) cell.onActionTap = { - switch section { + switch item.section { case .common: - self?.viewModel.removeFromCommon(menu) + self?.viewModel.removeFromCommon(item.menu) case .more: - self?.viewModel.addToCommon(menu) + self?.viewModel.addToCommon(item.menu) } } return cell diff --git a/suixinkan/UI/WildPhotographerReport/WildPhotographerReportListViewController.swift b/suixinkan/UI/WildPhotographerReport/WildPhotographerReportListViewController.swift index 5213d84..3cb2445 100644 --- a/suixinkan/UI/WildPhotographerReport/WildPhotographerReportListViewController.swift +++ b/suixinkan/UI/WildPhotographerReport/WildPhotographerReportListViewController.swift @@ -52,17 +52,23 @@ final class WildPhotographerReportListViewController: BaseViewController { } } + view.addSubview(filterTabBar) view.addSubview(scrollView) scrollView.addSubview(stack) reloadContent(animated: false) } 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 - 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 - make.top.equalTo(scrollView.contentLayoutGuide).offset(14) + make.top.equalTo(scrollView.contentLayoutGuide) make.leading.equalTo(scrollView.contentLayoutGuide).offset(18) make.trailing.equalTo(scrollView.contentLayoutGuide).inset(18) make.bottom.equalTo(scrollView.contentLayoutGuide).inset(24) @@ -104,14 +110,13 @@ final class WildPhotographerReportListViewController: BaseViewController { } private func rebuildContent() { + filterTabBar.apply(selectedFilter: viewModel.selectedFilter) + stack.arrangedSubviews.forEach { view in stack.removeArrangedSubview(view) view.removeFromSuperview() } - filterTabBar.apply(selectedFilter: viewModel.selectedFilter) - stack.addArrangedSubview(filterTabBar) - let reports = viewModel.filteredReports if reports.isEmpty { stack.addArrangedSubview(viewModel.isLoading ? WildReportListLoadingView(text: "正在加载举报记录...") : WildReportListEmptyView()) @@ -327,6 +332,8 @@ private final class WildReportListCardView: UIControl, UIGestureRecognizerDelega status.backgroundColor = statusColor.withAlphaComponent(0.12) status.layer.cornerRadius = 12 status.clipsToBounds = true + status.setContentHuggingPriority(.required, for: .horizontal) + status.setContentCompressionResistancePriority(.required, for: .horizontal) status.snp.makeConstraints { make in make.height.equalTo(24) } @@ -348,6 +355,7 @@ private final class WildReportListCardView: UIControl, UIGestureRecognizerDelega } private func makeScenicPill() -> UIView { + let wrapper = UIView() let label = PaddingLabel(insets: UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)) label.text = scenicName 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.layer.cornerRadius = 11 label.clipsToBounds = true + label.setContentHuggingPriority(.required, for: .horizontal) + label.setContentCompressionResistancePriority(.required, for: .horizontal) + wrapper.addSubview(label) label.snp.makeConstraints { make in + make.top.bottom.leading.equalToSuperview() + make.trailing.lessThanOrEqualToSuperview() make.height.equalTo(22) } - return label + return wrapper } private func makeMetaRows() -> UIView { @@ -811,11 +824,10 @@ final class WildPhotographerReportDetailViewController: BaseViewController { let section = UIStackView() section.axis = .vertical section.spacing = 12 - section.addArrangedSubview(makeSectionHeader( + section.addArrangedSubview(makeCompactMediaSectionHeader( iconName: "photo.on.rectangle.angled", title: "举报人材料", - subtitle: "\(viewModel.imageCountText)图片 \(viewModel.videoCountText)视频", - chevron: true, + summary: viewModel.reporterMediaSummary, action: #selector(previewTapped) )) @@ -844,15 +856,63 @@ final class WildPhotographerReportDetailViewController: BaseViewController { 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 { let section = UIStackView() section.axis = .vertical section.spacing = 12 - section.addArrangedSubview(makeSectionHeader( + section.addArrangedSubview(makeCompactMediaSectionHeader( iconName: "creditcard.fill", title: "支付截图", - subtitle: viewModel.paymentScreenshotSummary, - chevron: true, + summary: viewModel.paymentScreenshotSummary, action: #selector(paymentPreviewTapped) )) @@ -924,14 +984,11 @@ final class WildPhotographerReportDetailViewController: BaseViewController { 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(makeLocationHeader()) + let mapPreview = WildReportDetailMapPreviewView( + scenicName: viewModel.scenicAreaName, + detailAddress: viewModel.detailAddress + ) section.addArrangedSubview(mapPreview) mapPreview.snp.makeConstraints { make in make.height.equalTo(98) @@ -939,7 +996,66 @@ final class WildPhotographerReportDetailViewController: BaseViewController { 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() if let action { control.addTarget(self, action: action, for: .touchUpInside) @@ -958,12 +1074,21 @@ final class WildPhotographerReportDetailViewController: BaseViewController { 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) + if !subtitle.isEmpty { + 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 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")) chevronView.tintColor = AppColor.textTertiary chevronView.isHidden = !chevron @@ -981,10 +1106,18 @@ final class WildPhotographerReportDetailViewController: BaseViewController { make.trailing.centerY.equalToSuperview() 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 make.leading.equalTo(iconBox.snp.trailing).offset(12) make.centerY.equalToSuperview() - make.trailing.equalTo(chevronView.snp.leading).offset(-10) + if trailingText?.isEmpty == false { + make.trailing.lessThanOrEqualTo(trailingLabel.snp.leading).offset(-10) + } else { + make.trailing.equalTo(chevronView.snp.leading).offset(-10) + } } control.snp.makeConstraints { make in make.height.greaterThanOrEqualTo(50) @@ -993,6 +1126,7 @@ final class WildPhotographerReportDetailViewController: BaseViewController { } private func makeScenicPill() -> UIView { + let wrapper = UIView() let pill = UIStackView() pill.axis = .horizontal pill.alignment = .center @@ -1002,6 +1136,8 @@ final class WildPhotographerReportDetailViewController: BaseViewController { pill.backgroundColor = AppColor.primaryLight pill.layer.cornerRadius = 13 pill.clipsToBounds = true + pill.setContentHuggingPriority(.required, for: .horizontal) + pill.setContentCompressionResistancePriority(.required, for: .horizontal) let icon = UIImageView(image: UIImage(systemName: "mappin.circle.fill")) icon.tintColor = AppColor.primary @@ -1011,7 +1147,12 @@ final class WildPhotographerReportDetailViewController: BaseViewController { let label = WildReportUI.label(viewModel.scenicAreaName, font: .app(.captionMedium), color: AppColor.primary) pill.addArrangedSubview(icon) 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 { @@ -1090,7 +1231,7 @@ final class WildPhotographerReportDetailViewController: BaseViewController { @objc private func mapTapped() { navigationController?.pushViewController( - WildReportRiskMapViewController(record: viewModel.record, riskPoints: homeViewModel.riskPoints), + WildReportRiskMapViewController(), animated: true ) } @@ -1140,7 +1281,6 @@ private final class WildReportDetailInfoRowView: UIView { 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) @@ -1185,7 +1325,6 @@ private final class WildReportDetailMediaThumbnailView: UIView { let previewKindText: String private let gradient = CAGradientLayer() private let remoteImageView = UIImageView() - 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")) @@ -1194,11 +1333,11 @@ private final class WildReportDetailMediaThumbnailView: UIView { case .image(let index, let url, let fileName, let fileTypeText): previewKindText = fileName?.isEmpty == false ? fileName! : "第\(index + 1)张举报图片" 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): previewKindText = fileName?.isEmpty == false ? fileName! : "第\(index + 1)段举报视频" 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 } - 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 - layer.cornerRadius = 14 + layer.cornerRadius = 10 gradient.colors = kind.gradientColors gradient.startPoint = CGPoint(x: 0, y: 0) gradient.endPoint = CGPoint(x: 1, y: 1) @@ -1243,11 +1382,9 @@ private final class WildReportDetailMediaThumbnailView: UIView { } } - iconView.image = UIImage(systemName: kind.iconName) - iconView.tintColor = UIColor.white.withAlphaComponent(0.92) - iconView.contentMode = .scaleAspectFit - iconView.isHidden = imageURL != nil - addSubview(iconView) + if imageURL == nil { + addPlaceholderArtwork(for: kind) + } footerLabel.text = footerText footerLabel.font = .systemFont(ofSize: 11, weight: .bold) @@ -1255,59 +1392,107 @@ private final class WildReportDetailMediaThumbnailView: UIView { footerLabel.backgroundColor = UIColor.black.withAlphaComponent(0.24) footerLabel.layer.cornerRadius = 9 footerLabel.clipsToBounds = true + footerLabel.isHidden = !showsFooter 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 { + 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 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.font = .systemFont(ofSize: 14, weight: .semibold) durationLabel.textColor = .white - durationLabel.backgroundColor = UIColor.black.withAlphaComponent(0.34) - durationLabel.layer.cornerRadius = 8 - durationLabel.clipsToBounds = true + durationLabel.backgroundColor = .clear + durationLabel.layer.shadowColor = UIColor.black.cgColor + durationLabel.layer.shadowOpacity = 0.25 + durationLabel.layer.shadowRadius = 4 + durationLabel.layer.shadowOffset = CGSize(width: 0, height: 2) addSubview(durationLabel) - playView.snp.makeConstraints { make in + playCircle.snp.makeConstraints { make in 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 - 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 { case image(index: Int) case video(index: Int, duration: String) - var iconName: String { + var artworkIndex: Int { switch self { - case .image(let index): - return index.isMultiple(of: 2) ? "person.crop.rectangle.fill" : "camera.fill" - case .video: - return "video.fill" + case .image(let index), .video(let index, _): + return index } } 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] + switch index { + case 1: + 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: - 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 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")) - init(locationText: String) { + init(scenicName: String, detailAddress: String) { + self.scenicName = scenicName + self.detailAddress = detailAddress super.init(frame: .zero) - backgroundColor = UIColor(hex: 0xEAF7EA) - layer.cornerRadius = 14 + backgroundColor = UIColor(hex: 0xE1F4D8) + layer.cornerRadius = 10 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 + detailLabel.text = detailAddress.replacingOccurrences(of: "附近", with: "") + detailLabel.font = .systemFont(ofSize: 13, weight: .semibold) + detailLabel.textColor = UIColor(hex: 0x64748B) + detailLabel.textAlignment = .center + + scenicLabel.text = scenicName + scenicLabel.font = .systemFont(ofSize: 13, weight: .semibold) + scenicLabel.textColor = UIColor(hex: 0x418C48) + scenicLabel.textAlignment = .center + pinView.tintColor = AppColor.danger pinView.contentMode = .scaleAspectFit - addSubview(locationLabel) + addSubview(detailLabel) + addSubview(scenicLabel) addSubview(pinView) - locationLabel.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(12) - make.bottom.equalToSuperview().inset(10) + detailLabel.snp.makeConstraints { make in + make.centerX.equalToSuperview().multipliedBy(0.94) + 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.height.equalTo(24) } pinView.snp.makeConstraints { make in - make.centerX.equalToSuperview().offset(34) - make.centerY.equalToSuperview().offset(-2) - make.size.equalTo(32) + make.centerX.equalToSuperview().multipliedBy(1.06) + make.centerY.equalToSuperview().multipliedBy(1.08) + make.size.equalTo(36) } } @@ -1358,34 +1558,48 @@ private final class WildReportDetailMapPreviewView: UIView { 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 colors = [UIColor(hex: 0xE1F4D8).cgColor, UIColor(hex: 0xBDE6C7).cgColor] as CFArray + 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() - 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( - 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) + to: CGPoint(x: rect.width * 1.00, y: rect.height * 0.62), + controlPoint1: CGPoint(x: rect.width * 0.26, y: rect.height * 0.20), + controlPoint2: CGPoint(x: rect.width * 0.68, y: rect.height * 1.00) ) - UIColor.white.withAlphaComponent(0.86).setStroke() - road.lineWidth = 10 + UIColor.white.withAlphaComponent(0.88).setStroke() + road.lineWidth = 5 road.stroke() - UIColor(hex: 0xBFDDBD).setStroke() - road.lineWidth = 2 + UIColor(hex: 0x99C59C).setStroke() + road.lineWidth = 1 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() + let pinCenter = CGPoint(x: rect.width * 0.53, y: rect.height * 0.54) + UIColor(hex: 0xEF4444).withAlphaComponent(0.18).setFill() + context.fillEllipse(in: CGRect(x: pinCenter.x - 32, y: pinCenter.y - 32, width: 64, height: 64)) + UIColor(hex: 0xEF4444).withAlphaComponent(0.22).setFill() + context.fillEllipse(in: CGRect(x: pinCenter.x - 22, y: pinCenter.y - 22, width: 44, height: 44)) } }