// // LocationReportViewModels.swift // suixinkan // // Created by Codex on 2026/6/24. // import Foundation import Combine /// 位置上报 ViewModel,负责页面内在线状态、坐标、倒计时和提交动作。 @MainActor final class LocationReportViewModel: ObservableObject { @Published var isOnline = false @Published var reminderMinutes = 30 @Published var secondsUntilReport = 0 @Published var currentCoordinate: LocationCoordinate? @Published var markedCoordinate: LocationCoordinate? @Published var currentAddress = "" @Published var markedAddress = "" @Published var lastReportText = "" @Published var errorMessage: String? @Published 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 final class LocationReportHistoryViewModel: ObservableObject { @Published var selectedType: LocationReportType = .all @Published var startDate: Date? @Published var endDate: Date? @Published var items: [LocationReportHistoryItem] = [] @Published var errorMessage: String? @Published var isLoading = false @Published var isLoadingMore = false @Published var total = 0 @Published 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 }() }