Add location report map page and history list aligned with Android.
Extract shared LocationReportService, wire location_report menu routing, and add unit tests for reporting cooldown and history pagination. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
177
suixinkan/UI/LocationReport/LocationReportViewController.swift
Normal file
177
suixinkan/UI/LocationReport/LocationReportViewController.swift
Normal file
@ -0,0 +1,177 @@
|
||||
//
|
||||
// LocationReportViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 位置上报独立页,对齐 Android `LocationReportScreen`。
|
||||
final class LocationReportViewController: BaseViewController {
|
||||
|
||||
private let viewModel: LocationReportViewModel
|
||||
private let homeAPI: HomeAPI
|
||||
|
||||
private let mapContainer = LocationReportMapView()
|
||||
private let mapControls = LocationReportMapControlStack()
|
||||
private let statusCard = LocationReportStatusCardView()
|
||||
private let actionCard = LocationReportActionCardView()
|
||||
private let markButton = LocationReportBottomActionView(title: "标记上报", symbol: "mappin.and.ellipse")
|
||||
private let historyButton = LocationReportBottomActionView(title: "历史记录", symbol: "clock")
|
||||
private let onlineButton = LocationReportBottomActionView(title: "离线", symbol: "power")
|
||||
private var hasCenteredMap = false
|
||||
|
||||
init(
|
||||
viewModel: LocationReportViewModel = LocationReportViewModel(),
|
||||
homeAPI: HomeAPI = NetworkServices.shared.homeAPI
|
||||
) {
|
||||
self.viewModel = viewModel
|
||||
self.homeAPI = homeAPI
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "位置上报"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = AppColor.pageBackground
|
||||
|
||||
let bottomStack = UIStackView(arrangedSubviews: [markButton, historyButton, onlineButton])
|
||||
bottomStack.axis = .horizontal
|
||||
bottomStack.spacing = 12
|
||||
bottomStack.distribution = .fillEqually
|
||||
|
||||
let panelStack = UIStackView(arrangedSubviews: [statusCard, actionCard, bottomStack])
|
||||
panelStack.axis = .vertical
|
||||
panelStack.spacing = 12
|
||||
|
||||
view.addSubview(mapContainer)
|
||||
view.addSubview(mapControls)
|
||||
view.addSubview(panelStack)
|
||||
|
||||
mapContainer.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||||
make.height.equalTo(view.snp.height).multipliedBy(0.45)
|
||||
}
|
||||
mapControls.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(mapContainer).offset(-16)
|
||||
make.centerY.equalTo(mapContainer)
|
||||
}
|
||||
panelStack.snp.makeConstraints { make in
|
||||
make.top.equalTo(mapContainer.snp.bottom).offset(-20)
|
||||
make.leading.trailing.equalToSuperview().inset(15)
|
||||
make.bottom.lessThanOrEqualTo(view.safeAreaLayoutGuide).offset(-12)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
viewModel.onStateChange = { [weak self] in
|
||||
Task { @MainActor in self?.applyViewModel() }
|
||||
}
|
||||
viewModel.onShowMessage = { [weak self] message in
|
||||
Task { @MainActor in self?.showToast(message) }
|
||||
}
|
||||
|
||||
mapControls.onZoomIn = { [weak self] in self?.mapContainer.zoomIn() }
|
||||
mapControls.onZoomOut = { [weak self] in self?.mapContainer.zoomOut() }
|
||||
mapControls.onRelocate = { [weak self] in
|
||||
self?.viewModel.startLocation()
|
||||
self?.mapContainer.centerOnUserLocation()
|
||||
}
|
||||
mapContainer.onMapTap = { [weak self] coordinate in
|
||||
self?.viewModel.setMarkerLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
|
||||
}
|
||||
|
||||
statusCard.onOnlineStatusTap = { [weak self] in self?.viewModel.showOnlineStatusSwitchDialog() }
|
||||
statusCard.onReminderTap = { [weak self] in
|
||||
guard let self else { return }
|
||||
LocationReportDialogPresenter.presentReminderPicker(from: self) { minutes in
|
||||
self.viewModel.updateReminderMinutes(minutes)
|
||||
}
|
||||
}
|
||||
actionCard.onReportTap = { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.reportLocation(api: self.homeAPI) }
|
||||
}
|
||||
markButton.addTarget(self, action: #selector(markTapped), for: .touchUpInside)
|
||||
historyButton.addTarget(self, action: #selector(historyTapped), for: .touchUpInside)
|
||||
onlineButton.addTarget(self, action: #selector(onlineTapped), for: .touchUpInside)
|
||||
|
||||
viewModel.restoreStateIfNeeded()
|
||||
requestLocationIfNeeded()
|
||||
applyViewModel()
|
||||
}
|
||||
|
||||
private func requestLocationIfNeeded() {
|
||||
let status = CLLocationManager.authorizationStatus()
|
||||
switch status {
|
||||
case .authorizedAlways, .authorizedWhenInUse:
|
||||
viewModel.startLocation()
|
||||
case .notDetermined:
|
||||
CLLocationManager().requestWhenInUseAuthorization()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
self?.viewModel.startLocation()
|
||||
}
|
||||
default:
|
||||
showToast("需要定位权限才能使用此功能")
|
||||
}
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
statusCard.apply(
|
||||
isOnline: viewModel.isOnline,
|
||||
countdown: viewModel.countdownDisplayText,
|
||||
reminderMinutes: viewModel.reminderMinutes
|
||||
)
|
||||
actionCard.setReporting(viewModel.isReporting)
|
||||
markButton.isSelected = viewModel.isMarking
|
||||
onlineButton.applyOnlineStyle(isOnline: viewModel.isOnline)
|
||||
|
||||
mapContainer.updateMarker(latitude: viewModel.markerLatitude, longitude: viewModel.markerLongitude)
|
||||
if !hasCenteredMap, let lat = viewModel.currentLatitude, let lng = viewModel.currentLongitude {
|
||||
mapContainer.setCenter(CLLocationCoordinate2D(latitude: lat, longitude: lng), animated: false)
|
||||
hasCenteredMap = true
|
||||
}
|
||||
|
||||
if viewModel.showOnlineStatusDialog {
|
||||
viewModel.hideOnlineStatusSwitchDialog()
|
||||
LocationReportDialogPresenter.presentOnlineStatusSwitch(
|
||||
from: self,
|
||||
isOnline: viewModel.isOnline
|
||||
) { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.switchOnlineStatus(api: self.homeAPI) }
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.showLocationReportSuccessDialog {
|
||||
let reportTime = viewModel.reportTimeText
|
||||
let countdown = viewModel.countdownDisplayText
|
||||
viewModel.hideLocationReportSuccessDialog()
|
||||
LocationReportDialogPresenter.presentReportSuccess(
|
||||
from: self,
|
||||
reportTime: reportTime,
|
||||
countdown: countdown
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func markTapped() {
|
||||
viewModel.toggleMarking()
|
||||
}
|
||||
|
||||
@objc private func historyTapped() {
|
||||
navigationController?.pushViewController(LocationReportHistoryViewController(), animated: true)
|
||||
}
|
||||
|
||||
@objc private func onlineTapped() {
|
||||
viewModel.showOnlineStatusSwitchDialog()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user