Decode numeric staff_id from the report API and auto-center the map when the first user location update arrives. Co-authored-by: Cursor <cursoragent@cursor.com>
176 lines
6.6 KiB
Swift
176 lines
6.6 KiB
Swift
//
|
||
// 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?.mapContainer.centerOnUserLocation()
|
||
self?.viewModel.startLocation()
|
||
}
|
||
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()
|
||
mapContainer.enableInitialUserLocationCentering()
|
||
requestLocationIfNeeded()
|
||
applyViewModel()
|
||
}
|
||
|
||
private func requestLocationIfNeeded() {
|
||
Task {
|
||
do {
|
||
try await LocationProvider.shared.ensureLocationPermission()
|
||
mapContainer.enableInitialUserLocationCentering()
|
||
viewModel.startLocation()
|
||
} catch {
|
||
showToast((error as? LocalizedError)?.errorDescription ?? error.localizedDescription)
|
||
}
|
||
}
|
||
}
|
||
|
||
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()
|
||
}
|
||
}
|