diff --git a/suixinkan/Features/Home/Models/LocationModels.swift b/suixinkan/Features/Home/Models/LocationModels.swift index 61c5a11..73cc4d3 100644 --- a/suixinkan/Features/Home/Models/LocationModels.swift +++ b/suixinkan/Features/Home/Models/LocationModels.swift @@ -34,6 +34,37 @@ struct LocationReportResponse: Decodable, Sendable { case staffId = "staff_id" case expired case status + case storeUserId = "store_user_id" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + staffId = Self.decodeFlexibleString(from: container, forKey: .staffId) + expired = try container.decodeIfPresent(Int64.self, forKey: .expired) ?? 0 + status = try container.decodeIfPresent(Int.self, forKey: .status) ?? 0 + } + + init(staffId: String = "", expired: Int64 = 0, status: Int = 0) { + self.staffId = staffId + self.expired = expired + self.status = status + } + + private static func decodeFlexibleString( + from container: KeyedDecodingContainer, + forKey key: CodingKeys, + defaultValue: String = "" + ) -> String { + if let string = try? container.decode(String.self, forKey: key) { + return string + } + if let intValue = try? container.decode(Int.self, forKey: key) { + return String(intValue) + } + if let int64Value = try? container.decode(Int64.self, forKey: key) { + return String(int64Value) + } + return defaultValue } } diff --git a/suixinkan/UI/LocationReport/LocationReportViewController.swift b/suixinkan/UI/LocationReport/LocationReportViewController.swift index 8bdb688..488b7c8 100644 --- a/suixinkan/UI/LocationReport/LocationReportViewController.swift +++ b/suixinkan/UI/LocationReport/LocationReportViewController.swift @@ -82,8 +82,8 @@ final class LocationReportViewController: BaseViewController { 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() + self?.viewModel.startLocation() } mapContainer.onMapTap = { [weak self] coordinate in self?.viewModel.setMarkerLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) @@ -105,6 +105,7 @@ final class LocationReportViewController: BaseViewController { onlineButton.addTarget(self, action: #selector(onlineTapped), for: .touchUpInside) viewModel.restoreStateIfNeeded() + mapContainer.enableInitialUserLocationCentering() requestLocationIfNeeded() applyViewModel() } @@ -113,6 +114,7 @@ final class LocationReportViewController: BaseViewController { Task { do { try await LocationProvider.shared.ensureLocationPermission() + mapContainer.enableInitialUserLocationCentering() viewModel.startLocation() } catch { showToast((error as? LocalizedError)?.errorDescription ?? error.localizedDescription) diff --git a/suixinkan/UI/LocationReport/Views/LocationReportViews.swift b/suixinkan/UI/LocationReport/Views/LocationReportViews.swift index 2643693..4d5965c 100644 --- a/suixinkan/UI/LocationReport/Views/LocationReportViews.swift +++ b/suixinkan/UI/LocationReport/Views/LocationReportViews.swift @@ -294,6 +294,7 @@ final class LocationReportMapView: UIView, MAMapViewDelegate { let mapView: MAMapView private var markerAnnotation: MAPointAnnotation? + private var shouldCenterOnNextUserLocation = false override init(frame: CGRect) { _ = AMapBootstrap.configureIfNeeded() @@ -314,9 +315,14 @@ final class LocationReportMapView: UIView, MAMapViewDelegate { fatalError("init(coder:) has not been implemented") } + /// 进入页面后,在首次拿到用户位置时将地图居中(不持续跟随,便于手动拖动)。 + func enableInitialUserLocationCentering() { + shouldCenterOnNextUserLocation = true + centerOnUserLocationIfNeeded() + } + func centerOnUserLocation() { - let coordinate = mapView.userLocation.location?.coordinate - guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return } + guard let coordinate = validUserCoordinate else { return } setCenter(coordinate) } @@ -345,7 +351,25 @@ final class LocationReportMapView: UIView, MAMapViewDelegate { mapView.addAnnotation(annotation) } + func mapView(_ mapView: MAMapView!, didUpdate userLocation: MAUserLocation!, updatingLocation: Bool) { + guard updatingLocation else { return } + centerOnUserLocationIfNeeded() + } + func mapView(_ mapView: MAMapView!, didSingleTappedAt coordinate: CLLocationCoordinate2D) { onMapTap?(coordinate) } + + private var validUserCoordinate: CLLocationCoordinate2D? { + let coordinate = mapView.userLocation.location?.coordinate + guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return nil } + guard coordinate.latitude != 0 || coordinate.longitude != 0 else { return nil } + return coordinate + } + + private func centerOnUserLocationIfNeeded() { + guard shouldCenterOnNextUserLocation, let coordinate = validUserCoordinate else { return } + setCenter(coordinate, animated: false) + shouldCenterOnNextUserLocation = false + } } diff --git a/suixinkanTests/LocationReportServiceTests.swift b/suixinkanTests/LocationReportServiceTests.swift index 84fb99f..697db8b 100644 --- a/suixinkanTests/LocationReportServiceTests.swift +++ b/suixinkanTests/LocationReportServiceTests.swift @@ -26,6 +26,26 @@ final class LocationReportServiceTests: XCTestCase { super.tearDown() } + func testReportLocationDecodesNumericStaffId() async throws { + let reportJSON = """ + {"code":100000,"msg":"success","data":{"staff_id":732,"expired":1783403917,"status":1,"store_user_id":732}} + """.data(using: .utf8)! + let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [reportJSON]))) + let stateStore = HomeLocationStateStore(store: appStore) + let service = LocationReportService(appStore: appStore, locationStateStore: stateStore) + + let result = try await service.reportWithCoordinates( + api: api, + latitude: 32.429921061197916, + longitude: 119.44032958984376, + address: "测试地址", + type: 1 + ) + + XCTAssertTrue(result.shouldShowSuccessDialog) + XCTAssertTrue(stateStore.isOnline) + } + func testReportWithCoordinatesType2UsesProvidedLocation() async throws { let reportJSON = """ {"code":100000,"msg":"success","data":{"staff_id":"staff-1","expired":0,"status":1}}