接入首页位置上报与在线倒计时,并优化上报交互体验。

持久化服务端 expired 过期时间戳以正确恢复倒计时,切换在线/离线与上报时展示定位 Loading,移除重复的成功 Alert,并将 Toast 调整为居中半透明样式。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 15:59:59 +08:00
parent d9f897038d
commit 560b52fcc8
19 changed files with 1028 additions and 204 deletions

View File

@ -11,6 +11,7 @@ import SwiftUI
/// 线
struct LocationReportView: View {
@EnvironmentObject private var accountContext: AccountContext
@EnvironmentObject private var homeLocationViewModel: HomeLocationViewModel
@Environment(\.accountSnapshotStore) private var snapshotStore
@Environment(\.locationReportAPI) private var locationReportAPI
@EnvironmentObject private var router: RouterPath
@ -19,6 +20,8 @@ struct LocationReportView: View {
@StateObject private var viewModel = LocationReportViewModel()
@State private var locationProvider = ForegroundLocationProvider()
@State private var showOnlineDialog = false
@State private var showReminderDialog = false
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
@ -46,11 +49,36 @@ struct LocationReportView: View {
.accessibilityLabel("上报历史")
}
}
.onReceive(timer) { _ in viewModel.tick() }
.onReceive(timer) { _ in homeLocationViewModel.tick() }
.task {
homeLocationViewModel.restoreState()
guard viewModel.currentCoordinate == nil else { return }
await locateCurrent()
}
.alert("切换在线状态", isPresented: $showOnlineDialog) {
Button("取消", role: .cancel) {}
Button("确定") {
Task {
await globalLoading.withLoading(message: "正在定位...") {
await homeLocationViewModel.confirmOnlineToggle(
staffId: staffId,
scenicId: scenicId,
toast: toastCenter
)
}
}
}
} message: {
Text(homeLocationViewModel.isOnline ? "是否确认切换为离线状态?离线后将暂停位置上报。" : "是否确认切换为在线状态?在线后将开始位置上报和计时。")
}
.confirmationDialog("提前提醒时间", isPresented: $showReminderDialog, titleVisibility: .visible) {
ForEach([0, 5, 10, 15, 30], id: \.self) { minute in
Button(minute == 0 ? "不提醒" : "\(minute)分钟") {
homeLocationViewModel.updateReminderMinutes(minute)
}
}
Button("取消", role: .cancel) {}
}
}
/// 线
@ -58,24 +86,28 @@ struct LocationReportView: View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
HStack {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xSmall) {
Text(viewModel.isOnline ? "当前在线" : "当前离线")
Text(homeLocationViewModel.isOnline ? "当前在线" : "当前离线")
.font(.system(size: AppMetrics.FontSize.title3, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
Text(viewModel.countdownText)
Text(countdownText)
.font(.system(size: AppMetrics.FontSize.subheadline))
.foregroundStyle(AppDesign.textSecondary)
}
Spacer()
Toggle("", isOn: Binding(
get: { viewModel.isOnline },
set: { value in
Task { await toggleOnline(value) }
}
))
.labelsHidden()
Button {
showOnlineDialog = true
} label: {
Text(homeLocationViewModel.isOnline ? "在线" : "离线")
.font(.system(size: AppMetrics.FontSize.caption, weight: .medium))
.foregroundStyle(homeLocationViewModel.isOnline ? Color(hex: 0xF0FDF4) : Color(hex: 0x7B8EAA))
.padding(.horizontal, AppMetrics.Spacing.small)
.padding(.vertical, AppMetrics.Spacing.xxSmall + 2)
.background(homeLocationViewModel.isOnline ? Color(hex: 0x22C55E) : Color(hex: 0xF4F4F4), in: RoundedRectangle(cornerRadius: 4))
}
.buttonStyle(.plain)
}
if !viewModel.lastReportText.isEmpty {
Text("上次上报:\(viewModel.lastReportText)")
if !homeLocationViewModel.reportTimeText.isEmpty {
Text("上次上报:\(homeLocationViewModel.reportTimeText)")
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.placeholder)
}
@ -84,6 +116,13 @@ struct LocationReportView: View {
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
private var countdownText: String {
guard homeLocationViewModel.secondsUntilReport > 0 else { return "可立即上报" }
let hours = homeLocationViewModel.secondsUntilReport / 3600
let minutes = (homeLocationViewModel.secondsUntilReport % 3600) / 60
return "\(hours)小时\(minutes)分钟后可再次提醒"
}
///
private var currentLocationSection: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
@ -149,8 +188,14 @@ struct LocationReportView: View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
Text("提醒设置")
.font(.system(size: AppMetrics.FontSize.title3, weight: .semibold))
Stepper("提前 \(viewModel.reminderMinutes) 分钟提醒", value: $viewModel.reminderMinutes, in: 5...120, step: 5)
.font(.system(size: AppMetrics.FontSize.subheadline))
Button {
showReminderDialog = true
} label: {
Text(homeLocationViewModel.reminderMinutes == 0 ? "不提醒" : "提前 \(homeLocationViewModel.reminderMinutes) 分钟提醒")
.font(.system(size: AppMetrics.FontSize.subheadline))
.foregroundStyle(AppDesign.primary)
}
.buttonStyle(.plain)
}
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
@ -177,7 +222,7 @@ struct LocationReportView: View {
}
.buttonStyle(.bordered)
}
.disabled(viewModel.isSubmitting)
.disabled(viewModel.isSubmitting || homeLocationViewModel.isReporting)
}
///
@ -211,20 +256,16 @@ struct LocationReportView: View {
}
}
/// 线
private func toggleOnline(_ value: Bool) async {
let success = await globalLoading.withOptionalLoading(value) {
await viewModel.setOnline(value, staffId: staffId, scenicId: scenicId, api: locationReportAPI)
}
if !success {
toastCenter.show(viewModel.errorMessage ?? "状态上报失败")
}
}
///
private func submit(type: LocationReportType) async {
let success = await globalLoading.withLoading {
await viewModel.submit(type: type, staffId: staffId, scenicId: scenicId, api: locationReportAPI)
await viewModel.submit(
type: type,
staffId: staffId,
scenicId: scenicId,
homeLocationViewModel: homeLocationViewModel,
toast: toastCenter
)
}
toastCenter.show(success ? "上报成功" : (viewModel.errorMessage ?? "上报失败"))
}