完善任务流程、键盘适配与页面交互
This commit is contained in:
@ -21,7 +21,7 @@ final class WildReportRiskMapViewController: BaseViewController {
|
||||
private let mapView = MKMapView()
|
||||
private let scenicCapsule = UIView()
|
||||
private let scenicNameLabel = UILabel()
|
||||
private let locateButton = UIButton(type: .system)
|
||||
private let mapControls = LocationReportMapControlStack()
|
||||
private let legendBar = UIStackView()
|
||||
private let detailContainerView = UIView()
|
||||
private let scrollView = UIScrollView()
|
||||
@ -88,7 +88,7 @@ final class WildReportRiskMapViewController: BaseViewController {
|
||||
mapContainerView.addSubview(mapView)
|
||||
|
||||
configureScenicCapsule()
|
||||
configureLocateButton()
|
||||
configureMapControls()
|
||||
configureLegendBar()
|
||||
configureDetailArea()
|
||||
refreshContent(animated: false)
|
||||
@ -108,9 +108,8 @@ final class WildReportRiskMapViewController: BaseViewController {
|
||||
make.top.leading.equalToSuperview().inset(12)
|
||||
make.height.equalTo(34)
|
||||
}
|
||||
locateButton.snp.makeConstraints { make in
|
||||
mapControls.snp.makeConstraints { make in
|
||||
make.top.trailing.equalToSuperview().inset(12)
|
||||
make.size.equalTo(44)
|
||||
}
|
||||
detailContainerView.snp.makeConstraints { make in
|
||||
make.height.greaterThanOrEqualTo(180)
|
||||
@ -154,18 +153,21 @@ final class WildReportRiskMapViewController: BaseViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func configureLocateButton() {
|
||||
locateButton.backgroundColor = UIColor.white.withAlphaComponent(0.96)
|
||||
locateButton.tintColor = AppColor.primary
|
||||
locateButton.setImage(UIImage(systemName: "location.fill"), for: .normal)
|
||||
locateButton.layer.cornerRadius = 22
|
||||
locateButton.layer.shadowColor = UIColor.black.cgColor
|
||||
locateButton.layer.shadowOpacity = 0.1
|
||||
locateButton.layer.shadowRadius = 8
|
||||
locateButton.layer.shadowOffset = CGSize(width: 0, height: 4)
|
||||
locateButton.accessibilityLabel = "回到我的位置"
|
||||
locateButton.addTarget(self, action: #selector(locateButtonTapped), for: .touchUpInside)
|
||||
mapContainerView.addSubview(locateButton)
|
||||
private func configureMapControls() {
|
||||
mapControls.layer.shadowColor = UIColor.black.cgColor
|
||||
mapControls.layer.shadowOpacity = 0.1
|
||||
mapControls.layer.shadowRadius = 8
|
||||
mapControls.layer.shadowOffset = CGSize(width: 0, height: 4)
|
||||
mapControls.onZoomIn = { [weak self] in
|
||||
self?.zoomMap(by: 0.5)
|
||||
}
|
||||
mapControls.onZoomOut = { [weak self] in
|
||||
self?.zoomMap(by: 2)
|
||||
}
|
||||
mapControls.onRelocate = { [weak self] in
|
||||
self?.focusCurrentLocation()
|
||||
}
|
||||
mapContainerView.addSubview(mapControls)
|
||||
}
|
||||
|
||||
private func configureLegendBar() {
|
||||
@ -226,8 +228,7 @@ final class WildReportRiskMapViewController: BaseViewController {
|
||||
@MainActor
|
||||
private func updateLocateButtonState() {
|
||||
let hasCurrentLocation = viewModel.markers.contains { $0.kind == .selfLocation }
|
||||
locateButton.isEnabled = hasCurrentLocation
|
||||
locateButton.alpha = hasCurrentLocation ? 1 : 0.46
|
||||
mapControls.setRelocateEnabled(hasCurrentLocation)
|
||||
}
|
||||
|
||||
private func syncMapAnnotations() {
|
||||
@ -325,6 +326,10 @@ final class WildReportRiskMapViewController: BaseViewController {
|
||||
}
|
||||
|
||||
private func makeRadarHeader(clue: WildReportRadarActiveClue) -> UIView {
|
||||
let container = UIStackView()
|
||||
container.axis = .vertical
|
||||
container.spacing = 6
|
||||
|
||||
let row = UIStackView()
|
||||
row.axis = .horizontal
|
||||
row.alignment = .top
|
||||
@ -348,21 +353,18 @@ final class WildReportRiskMapViewController: BaseViewController {
|
||||
let textStack = UIStackView()
|
||||
textStack.axis = .vertical
|
||||
textStack.spacing = 6
|
||||
var reportMetaText: String?
|
||||
if isReportClue(clue) {
|
||||
let title = WildReportUI.label("线索 ID:\(clue.id)", font: .systemFont(ofSize: 16, weight: .bold), color: AppColor.textPrimary, lines: 2)
|
||||
textStack.addArrangedSubview(title)
|
||||
let metaRow = UIStackView()
|
||||
metaRow.axis = .horizontal
|
||||
metaRow.spacing = 12
|
||||
var metaParts: [String] = []
|
||||
if let distance = clue.distanceText ?? clue.distance {
|
||||
metaRow.addArrangedSubview(WildReportUI.label("距离:距离您\(distance)", font: .systemFont(ofSize: 12), color: AppColor.textSecondary))
|
||||
metaParts.append("距离:距离您\(distance)")
|
||||
}
|
||||
if let updatedAt = clue.updatedAt {
|
||||
metaRow.addArrangedSubview(WildReportUI.label("时间:\(updatedAt)", font: .systemFont(ofSize: 12), color: AppColor.textSecondary))
|
||||
}
|
||||
if !metaRow.arrangedSubviews.isEmpty {
|
||||
textStack.addArrangedSubview(metaRow)
|
||||
metaParts.append("时间:\(updatedAt)")
|
||||
}
|
||||
reportMetaText = metaParts.isEmpty ? nil : metaParts.joined(separator: " ")
|
||||
} else {
|
||||
let title = WildReportUI.label(clue.displayTitle, font: .systemFont(ofSize: 16, weight: .bold), color: AppColor.textPrimary, lines: 2)
|
||||
textStack.addArrangedSubview(title)
|
||||
@ -380,7 +382,24 @@ final class WildReportRiskMapViewController: BaseViewController {
|
||||
row.addArrangedSubview(badge)
|
||||
textStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
badge.setContentHuggingPriority(.required, for: .horizontal)
|
||||
return row
|
||||
container.addArrangedSubview(row)
|
||||
|
||||
if let reportMetaText {
|
||||
let metaRow = UIStackView()
|
||||
metaRow.axis = .horizontal
|
||||
metaRow.spacing = 0
|
||||
let spacer = UIView()
|
||||
spacer.snp.makeConstraints { make in
|
||||
make.width.equalTo(56)
|
||||
}
|
||||
let metaLabel = WildReportUI.label(reportMetaText, font: .systemFont(ofSize: 12), color: AppColor.textSecondary)
|
||||
metaLabel.textAlignment = .right
|
||||
metaLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
metaRow.addArrangedSubview(spacer)
|
||||
metaRow.addArrangedSubview(metaLabel)
|
||||
container.addArrangedSubview(metaRow)
|
||||
}
|
||||
return container
|
||||
}
|
||||
|
||||
private func addPhotographerProfile(for clue: WildReportRadarActiveClue, to stack: UIStackView) {
|
||||
@ -888,22 +907,134 @@ final class WildReportRiskMapViewController: BaseViewController {
|
||||
return
|
||||
}
|
||||
guard let target = viewModel.navigateToSelectedMarker() else { return }
|
||||
let item = MKMapItem(placemark: MKPlacemark(coordinate: target.coordinate))
|
||||
item.name = target.title
|
||||
item.openInMaps(launchOptions: [
|
||||
MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving
|
||||
])
|
||||
presentNavigationOptions(for: target)
|
||||
}
|
||||
|
||||
@objc private func shareTapped() {
|
||||
viewModel.shareSelectedClue()
|
||||
showLoading()
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
let payload = await self.viewModel.shareSelectedClue(api: self.api)
|
||||
await MainActor.run {
|
||||
self.hideLoading()
|
||||
guard let payload else { return }
|
||||
WeChatShareService.shareMiniProgram(payload, previewImage: UIImage(named: "report_share_cover")) { [weak self] result in
|
||||
self?.showShareResult(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func locateButtonTapped() {
|
||||
private func presentNavigationOptions(for target: WildReportMapMarker) {
|
||||
let options = availableNavigationOptions(for: target)
|
||||
let alert = UIAlertController(title: "选择导航软件", message: target.title, preferredStyle: .actionSheet)
|
||||
options.forEach { option in
|
||||
alert.addAction(UIAlertAction(title: option.title, style: .default) { [weak self] _ in
|
||||
self?.openNavigation(option)
|
||||
})
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
if let popover = alert.popoverPresentationController {
|
||||
popover.sourceView = view
|
||||
popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY - 80, width: 1, height: 1)
|
||||
}
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func availableNavigationOptions(for target: WildReportMapMarker) -> [WildReportNavigationOption] {
|
||||
[
|
||||
WildReportNavigationOption(title: "Apple 地图", url: navigationURL(for: .apple, target: target)),
|
||||
WildReportNavigationOption(title: "高德地图", url: navigationURL(for: .amap, target: target)),
|
||||
WildReportNavigationOption(title: "百度地图", url: navigationURL(for: .baidu, target: target)),
|
||||
WildReportNavigationOption(title: "腾讯地图", url: navigationURL(for: .tencent, target: target)),
|
||||
WildReportNavigationOption(title: "Google Maps", url: navigationURL(for: .google, target: target))
|
||||
].compactMap { option in
|
||||
guard option.url != nil else { return nil }
|
||||
return option
|
||||
}
|
||||
}
|
||||
|
||||
private func openNavigation(_ option: WildReportNavigationOption) {
|
||||
guard let url = option.url else { return }
|
||||
UIApplication.shared.open(url) { [weak self] success in
|
||||
guard !success else { return }
|
||||
Task { @MainActor in
|
||||
self?.showToast("未安装\(option.title)或无法打开")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func navigationURL(for app: WildReportNavigationApp, target: WildReportMapMarker) -> URL? {
|
||||
let latitude = target.coordinate.latitude
|
||||
let longitude = target.coordinate.longitude
|
||||
let name = encodedNavigationText(target.title)
|
||||
switch app {
|
||||
case .apple:
|
||||
return URL(string: "http://maps.apple.com/?daddr=\(latitude),\(longitude)&dirflg=d&q=\(name)")
|
||||
case .amap:
|
||||
return URL(string: "iosamap://path?sourceApplication=suixinkan&dlat=\(latitude)&dlon=\(longitude)&dname=\(name)&dev=0&t=0")
|
||||
case .baidu:
|
||||
return URL(string: "baidumap://map/direction?destination=latlng:\(latitude),\(longitude)|name:\(name)&mode=driving&coord_type=wgs84")
|
||||
case .tencent:
|
||||
return URL(string: "qqmap://map/routeplan?type=drive&tocoord=\(latitude),\(longitude)&to=\(name)&referer=suixinkan")
|
||||
case .google:
|
||||
return URL(string: "comgooglemaps://?daddr=\(latitude),\(longitude)&directionsmode=driving")
|
||||
}
|
||||
}
|
||||
|
||||
private func encodedNavigationText(_ text: String) -> String {
|
||||
var allowed = CharacterSet.urlQueryAllowed
|
||||
allowed.remove(charactersIn: "&=?")
|
||||
return text.addingPercentEncoding(withAllowedCharacters: allowed) ?? text
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func showShareResult(_ result: WeChatShareResult) {
|
||||
switch result {
|
||||
case .success:
|
||||
showToast("分享成功")
|
||||
case .cancelled:
|
||||
showToast("已取消分享")
|
||||
case .notInstalled:
|
||||
showToast("请先安装微信")
|
||||
case .unsupported:
|
||||
showToast("当前微信版本不支持分享")
|
||||
case .invalidPayload(let message):
|
||||
showToast(message)
|
||||
case .sendFailed:
|
||||
showToast("微信分享发送失败")
|
||||
case .unknown:
|
||||
showToast("微信分享失败")
|
||||
}
|
||||
}
|
||||
|
||||
private func zoomMap(by multiplier: CLLocationDegrees) {
|
||||
let visibleRegion = mapView.region
|
||||
let span = MKCoordinateSpan(
|
||||
latitudeDelta: min(max(visibleRegion.span.latitudeDelta * multiplier, 0.001), 180),
|
||||
longitudeDelta: min(max(visibleRegion.span.longitudeDelta * multiplier, 0.001), 180)
|
||||
)
|
||||
mapView.setRegion(MKCoordinateRegion(center: visibleRegion.center, span: span), animated: true)
|
||||
}
|
||||
|
||||
private func focusCurrentLocation() {
|
||||
viewModel.focusCurrentLocation()
|
||||
}
|
||||
}
|
||||
|
||||
private struct WildReportNavigationOption {
|
||||
let title: String
|
||||
let url: URL?
|
||||
}
|
||||
|
||||
private enum WildReportNavigationApp {
|
||||
case apple
|
||||
case amap
|
||||
case baidu
|
||||
case tencent
|
||||
case google
|
||||
}
|
||||
|
||||
extension WildReportRiskMapViewController: MKMapViewDelegate {
|
||||
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
|
||||
guard let annotation = annotation as? WildReportMapAnnotation else { return nil }
|
||||
@ -1130,6 +1261,8 @@ private final class WildReportRiskLegendView: UIView {
|
||||
|
||||
/// 风险地图状态胶囊。
|
||||
private final class WildReportRiskStatusBadge: UILabel {
|
||||
private let contentInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
|
||||
|
||||
init(text: String, color: UIColor) {
|
||||
super.init(frame: .zero)
|
||||
self.text = text
|
||||
@ -1142,13 +1275,26 @@ private final class WildReportRiskStatusBadge: UILabel {
|
||||
numberOfLines = 1
|
||||
adjustsFontSizeToFitWidth = true
|
||||
minimumScaleFactor = 0.8
|
||||
setContentHuggingPriority(.required, for: .horizontal)
|
||||
setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
snp.makeConstraints { make in
|
||||
make.height.equalTo(24)
|
||||
make.width.greaterThanOrEqualTo(64)
|
||||
make.width.lessThanOrEqualTo(96)
|
||||
make.width.greaterThanOrEqualTo(52)
|
||||
}
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
let size = super.intrinsicContentSize
|
||||
return CGSize(
|
||||
width: size.width + contentInsets.left + contentInsets.right,
|
||||
height: max(24, size.height + contentInsets.top + contentInsets.bottom)
|
||||
)
|
||||
}
|
||||
|
||||
override func drawText(in rect: CGRect) {
|
||||
super.drawText(in: rect.inset(by: contentInsets))
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
|
||||
Reference in New Issue
Block a user