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>
This commit is contained in:
@ -0,0 +1,233 @@
|
||||
//
|
||||
// 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
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user