Files
suixinkan_ios_new/suixinkan/Features/LocationReport/ViewModels/LocationReportViewModels.swift
汉秋 0d4b63d273 Add PunchPoint and LocationReport modules with home routing.
Migrate check-in point management and location reporting from placeholders to full MVVM flows, including foreground location support and unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:08:41 +08:00

234 lines
7.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// LocationReportViewModels.swift
// suixinkan
//
// Created by Codex on 2026/6/24.
//
import Foundation
import Observation
/// ViewModel线
@MainActor
@Observable
final class LocationReportViewModel {
var isOnline = false
var reminderMinutes = 30
var secondsUntilReport = 0
var currentCoordinate: LocationCoordinate?
var markedCoordinate: LocationCoordinate?
var currentAddress = ""
var markedAddress = ""
var lastReportText = ""
var errorMessage: String?
var isSubmitting = false
///
var countdownText: String {
guard secondsUntilReport > 0 else { return "可立即上报" }
let hours = secondsUntilReport / 3600
let minutes = (secondsUntilReport % 3600) / 60
return "\(hours)小时\(minutes)分钟后可再次提醒"
}
///
func applyCurrentLocation(latitude: Double, longitude: Double, address: String) {
currentCoordinate = LocationCoordinate(latitude: latitude, longitude: longitude)
currentAddress = address
}
///
func applyMarkedLocation(latitude: Double, longitude: Double, address: String) {
markedCoordinate = LocationCoordinate(latitude: latitude, longitude: longitude)
markedAddress = address
}
/// 线线
func setOnline(
_ value: Bool,
staffId: Int?,
scenicId: Int?,
api: any LocationReportServing
) async -> Bool {
isOnline = value
guard value else { return true }
return await submit(type: .onlineStatus, staffId: staffId, scenicId: scenicId, api: api)
}
///
func submit(
type: LocationReportType,
staffId: Int?,
scenicId: Int?,
api: any LocationReportServing
) async -> Bool {
guard !isSubmitting else { return false }
guard let staffId, staffId > 0 else {
errorMessage = "缺少上报人员"
return false
}
guard let scenicId, scenicId > 0 else {
errorMessage = "缺少当前景区"
return false
}
let source = reportSource(for: type)
guard let coordinate = source.coordinate else {
errorMessage = "请先获取或选择位置"
return false
}
let address = source.address.trimmingCharacters(in: .whitespacesAndNewlines)
guard !address.isEmpty else {
errorMessage = "请填写位置地址"
return false
}
isSubmitting = true
errorMessage = nil
defer { isSubmitting = false }
do {
let response = try await api.reportLocation(
staffId: staffId,
latitude: coordinate.latitude,
longitude: coordinate.longitude,
address: address,
type: type,
scenicId: scenicId
)
secondsUntilReport = response.expired > 0 ? response.expired : 7200
lastReportText = LocationReportViewModel.displayFormatter.string(from: Date())
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
func tick() {
guard secondsUntilReport > 0 else { return }
secondsUntilReport -= 1
}
///
private func reportSource(for type: LocationReportType) -> (coordinate: LocationCoordinate?, address: String) {
switch type {
case .marked:
(markedCoordinate ?? currentCoordinate, markedAddress.isEmpty ? currentAddress : markedAddress)
case .all, .immediate, .onlineStatus:
(currentCoordinate, currentAddress)
}
}
private static let displayFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter
}()
}
/// ViewModel
@MainActor
@Observable
final class LocationReportHistoryViewModel {
var selectedType: LocationReportType = .all
var startDate: Date?
var endDate: Date?
var items: [LocationReportHistoryItem] = []
var errorMessage: String?
var isLoading = false
var isLoadingMore = false
var total = 0
private var page = 1
private let pageSize = 20
///
var hasMore: Bool {
items.count < total
}
///
func reload(staffId: Int?, api: any LocationReportServing) async {
guard let staffId, staffId > 0 else {
reset()
errorMessage = "缺少上报人员"
return
}
page = 1
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
let payload = try await api.locationReportList(
staffId: staffId,
page: page,
pageSize: pageSize,
type: selectedType,
startDate: dateString(startDate),
endDate: dateString(endDate)
)
items = payload.list
total = payload.total
} catch {
items = []
total = 0
errorMessage = error.localizedDescription
}
}
///
func loadMore(staffId: Int?, api: any LocationReportServing) async {
guard let staffId, staffId > 0, hasMore, !isLoading, !isLoadingMore else { return }
isLoadingMore = true
let nextPage = page + 1
defer { isLoadingMore = false }
do {
let payload = try await api.locationReportList(
staffId: staffId,
page: nextPage,
pageSize: pageSize,
type: selectedType,
startDate: dateString(startDate),
endDate: dateString(endDate)
)
page = nextPage
items.append(contentsOf: payload.list)
total = payload.total
} catch {
errorMessage = error.localizedDescription
}
}
///
func selectType(_ type: LocationReportType, staffId: Int?, api: any LocationReportServing) async {
guard selectedType != type else { return }
selectedType = type
await reload(staffId: staffId, api: api)
}
///
func reset() {
items = []
total = 0
page = 1
isLoading = false
isLoadingMore = false
}
/// yyyy-MM-dd
private func dateString(_ date: Date?) -> String? {
guard let date else { return nil }
return Self.dateFormatter.string(from: date)
}
private static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
}