// // 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? = nil ) { self.viewModel = viewModel self.homeAPI = homeAPI ?? NetworkServices.shared.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() } }