同时收紧各 ViewModel 内部 isLoading 可见性,避免不必要的视图刷新。 Co-authored-by: Cursor <cursoragent@cursor.com>
232 lines
7.4 KiB
Swift
232 lines
7.4 KiB
Swift
//
|
||
// 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?
|
||
private 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
|
||
}()
|
||
}
|