修复离线路径误弹位置上报超时窗,并优化提醒设置与定位 Loading 展示。
离线或倒计时未进入提醒窗口时不再弹出超时提醒;全局 Loading 支持按需展示定位文案,提前提醒选项抽成共用组件。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -44,6 +44,7 @@ Home 模块负责登录后的首页工作台,包括当前景区展示、工作
|
||||
首页工作状态区域由 `HomeLocationViewModel` 驱动,与 `LocationReport` 详情页共享同一实例(在 `RootView` 注入)。
|
||||
|
||||
- 在线/离线切换、立即上报、2 小时倒计时和提前提醒均走真实 API。
|
||||
- 「位置上报超时」弹窗仅在本地在线且倒计时进入提前提醒窗口时弹出;离线状态不弹窗。
|
||||
- 本地状态通过 `LocationStateStore` 持久化在线标志、过期时间戳、上次上报时间和提醒分钟数。
|
||||
- 首页大按钮直接触发 GPS 上报;菜单 `location_report` 仍进入详情页。
|
||||
- 非 `homeMinimalTopRoles` 角色才展示工作状态卡片。
|
||||
|
||||
@ -61,6 +61,8 @@ final class HomeLocationViewModel: ObservableObject {
|
||||
isOnline = false
|
||||
secondsUntilReport = 0
|
||||
expireTimestamp = nil
|
||||
showTimeoutDialog = false
|
||||
timeoutDialogTriggered = false
|
||||
return
|
||||
}
|
||||
|
||||
@ -156,31 +158,13 @@ final class HomeLocationViewModel: ObservableObject {
|
||||
await reportLocation(type: .immediate, staffId: staffId!, scenicId: scenicId!, toast: toast)
|
||||
}
|
||||
|
||||
/// 调用 detail 接口判断是否需要弹出超时提醒。
|
||||
/// 根据本地在线状态和倒计时判断是否需要弹出超时提醒。
|
||||
func checkLocationTimeout(staffId: Int?) async {
|
||||
guard reminderMinutes > 0 else {
|
||||
guard isOnline, reminderMinutes > 0, secondsUntilReport > 0 else {
|
||||
showTimeoutDialog = false
|
||||
return
|
||||
}
|
||||
guard let staffId, staffId > 0 else {
|
||||
showTimeoutDialog = false
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let detail = try await api.locationDetail(staffId: staffId)
|
||||
if detail.expireTime <= 0 {
|
||||
showTimeoutDialog = true
|
||||
return
|
||||
}
|
||||
|
||||
let currentMillis = Date().timeIntervalSince1970 * 1000
|
||||
let expireMillis = Double(detail.expireTime) * 1000
|
||||
let thresholdMillis = expireMillis - Double(reminderMinutes * 60 * 1000)
|
||||
showTimeoutDialog = currentMillis >= thresholdMillis
|
||||
} catch {
|
||||
showTimeoutDialog = true
|
||||
}
|
||||
evaluateReminderThreshold(remainingSeconds: secondsUntilReport, forceDialog: true)
|
||||
}
|
||||
|
||||
/// 关闭超时对话框并将用户置为离线。
|
||||
@ -313,6 +297,8 @@ final class HomeLocationViewModel: ObservableObject {
|
||||
secondsUntilReport = 0
|
||||
lastReportAnchorTime = nil
|
||||
expireTimestamp = nil
|
||||
showTimeoutDialog = false
|
||||
timeoutDialogTriggered = false
|
||||
if clearPersisted {
|
||||
store.saveOnlineStatus(false)
|
||||
store.clearLastReportTime()
|
||||
@ -344,9 +330,9 @@ final class HomeLocationViewModel: ObservableObject {
|
||||
return false
|
||||
}
|
||||
|
||||
/// 判断是否到达提前提醒阈值。
|
||||
/// 判断是否到达提前提醒阈值(仅在线且倒计时进入提醒窗口时触发)。
|
||||
private func evaluateReminderThreshold(remainingSeconds: Int, forceDialog: Bool) {
|
||||
guard reminderMinutes > 0 else { return }
|
||||
guard isOnline, reminderMinutes > 0 else { return }
|
||||
let thresholdSeconds = reminderMinutes * 60
|
||||
guard remainingSeconds <= thresholdSeconds else { return }
|
||||
|
||||
|
||||
@ -24,7 +24,6 @@ struct HomeView: View {
|
||||
@StateObject private var viewModel = HomeViewModel()
|
||||
@State private var commonUris: [String] = []
|
||||
@State private var showOnlineDialog = false
|
||||
@State private var showReminderDialog = false
|
||||
|
||||
private let commonMenuStore = HomeCommonMenuStore()
|
||||
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
@ -134,14 +133,6 @@ struct HomeView: View {
|
||||
} 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) {}
|
||||
}
|
||||
.alert("位置上报提醒", isPresented: $homeLocationViewModel.showReminderDialog) {
|
||||
Button("立即上报") {
|
||||
homeLocationViewModel.dismissReminderDialog()
|
||||
@ -232,9 +223,7 @@ struct HomeView: View {
|
||||
|
||||
Spacer(minLength: AppMetrics.Spacing.xxSmall)
|
||||
|
||||
Button {
|
||||
showReminderDialog = true
|
||||
} label: {
|
||||
ReminderTimeOptionsMenu(onSelect: { homeLocationViewModel.updateReminderMinutes($0) }) {
|
||||
Label {
|
||||
Text(reminderText)
|
||||
.font(.system(size: AppMetrics.FontSize.body, weight: .medium))
|
||||
@ -472,7 +461,7 @@ struct HomeView: View {
|
||||
/// 包裹需要 GPS 定位的位置上报操作,定位期间展示全局 Loading。
|
||||
private func runLocationReportTask(_ body: @escaping () async -> Void) {
|
||||
Task {
|
||||
await globalLoading.withLoading(message: "正在定位...") {
|
||||
await globalLoading.withLoading(message: "获取定位中", showsMessage: true) {
|
||||
await body()
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,6 @@ 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 {
|
||||
@ -59,7 +58,7 @@ struct LocationReportView: View {
|
||||
Button("取消", role: .cancel) {}
|
||||
Button("确定") {
|
||||
Task {
|
||||
await globalLoading.withLoading(message: "正在定位...") {
|
||||
await globalLoading.withLoading(message: "获取定位中", showsMessage: true) {
|
||||
await homeLocationViewModel.confirmOnlineToggle(
|
||||
staffId: staffId,
|
||||
scenicId: scenicId,
|
||||
@ -71,14 +70,6 @@ struct LocationReportView: View {
|
||||
} 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) {}
|
||||
}
|
||||
}
|
||||
|
||||
/// 在线状态和倒计时区域。
|
||||
@ -188,9 +179,7 @@ struct LocationReportView: View {
|
||||
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
|
||||
Text("提醒设置")
|
||||
.font(.system(size: AppMetrics.FontSize.title3, weight: .semibold))
|
||||
Button {
|
||||
showReminderDialog = true
|
||||
} label: {
|
||||
ReminderTimeOptionsMenu(onSelect: { homeLocationViewModel.updateReminderMinutes($0) }) {
|
||||
Text(homeLocationViewModel.reminderMinutes == 0 ? "不提醒" : "提前 \(homeLocationViewModel.reminderMinutes) 分钟提醒")
|
||||
.font(.system(size: AppMetrics.FontSize.subheadline))
|
||||
.foregroundStyle(AppDesign.primary)
|
||||
|
||||
@ -80,6 +80,9 @@ struct SourcePersonRow: View {
|
||||
}
|
||||
|
||||
struct SourceLeadContent: View {
|
||||
/// 带客单缩略图固定三列等宽网格。
|
||||
private static let leadImageColumns = Array(repeating: GridItem(.flexible()), count: 3)
|
||||
|
||||
let lead: OrderSourceLead
|
||||
var compact = false
|
||||
var enablePhoneCall = true
|
||||
@ -123,14 +126,15 @@ struct SourceLeadContent: View {
|
||||
.background(OrderSourceColors.imageEmptyBg, in: RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
} else {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(Array(lead.images.prefix(3).enumerated()), id: \.offset) { index, imageUrl in
|
||||
leadThumbnail(imageUrl: imageUrl, index: index)
|
||||
}
|
||||
ForEach(0..<max(0, 3 - min(lead.images.count, 3)), id: \.self) { _ in
|
||||
Color.clear
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
LazyVGrid(columns: Self.leadImageColumns, spacing: 8) {
|
||||
ForEach(0..<3, id: \.self) { index in
|
||||
if index < min(lead.images.count, 3) {
|
||||
leadThumbnail(imageUrl: lead.images[index], index: index)
|
||||
} else {
|
||||
Color.clear
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -138,21 +142,28 @@ struct SourceLeadContent: View {
|
||||
}
|
||||
|
||||
private func leadThumbnail(imageUrl: String, index: Int) -> some View {
|
||||
Group {
|
||||
if let url = URL(string: imageUrl), !imageUrl.isEmpty {
|
||||
KFImage(url)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
} else {
|
||||
OrderSourceColors.imageEmptyBg
|
||||
Color.clear
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
.overlay {
|
||||
leadThumbnailImage(imageUrl: imageUrl)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
onImageTap(lead.images, index)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
onImageTap(lead.images, index)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func leadThumbnailImage(imageUrl: String) -> some View {
|
||||
if let url = URL(string: imageUrl), !imageUrl.isEmpty {
|
||||
KFImage(url)
|
||||
.placeholder { OrderSourceColors.imageEmptyBg }
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
} else {
|
||||
OrderSourceColors.imageEmptyBg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,19 +62,6 @@ struct ProfileView: View {
|
||||
await updatePassword(password)
|
||||
}
|
||||
}
|
||||
.confirmationDialog("确认退出当前账号?", isPresented: $showsLogoutConfirm, titleVisibility: .visible) {
|
||||
Button("退出登录", role: .destructive) {
|
||||
authSessionCoordinator.logout(
|
||||
appSession: appSession,
|
||||
accountContext: accountContext,
|
||||
permissionContext: permissionContext,
|
||||
scenicSpotContext: scenicSpotContext,
|
||||
appRouter: appRouter,
|
||||
toastCenter: toastCenter
|
||||
)
|
||||
}
|
||||
Button("取消", role: .cancel) {}
|
||||
}
|
||||
.onChange(of: viewModel.isEditingProfile) { isEditing in
|
||||
guard isEditing else { return }
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
||||
@ -371,6 +358,19 @@ struct ProfileView: View {
|
||||
.shadow(color: Color(hex: 0xC8D7EA, alpha: 0.24), radius: 14, x: 0, y: 7)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.confirmationDialog("确认退出当前账号?", isPresented: $showsLogoutConfirm, titleVisibility: .visible) {
|
||||
Button("退出登录", role: .destructive) {
|
||||
authSessionCoordinator.logout(
|
||||
appSession: appSession,
|
||||
accountContext: accountContext,
|
||||
permissionContext: permissionContext,
|
||||
scenicSpotContext: scenicSpotContext,
|
||||
appRouter: appRouter,
|
||||
toastCenter: toastCenter
|
||||
)
|
||||
}
|
||||
Button("取消", role: .cancel) {}
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造资料信息行,统一左右布局和点击区域。
|
||||
|
||||
Reference in New Issue
Block a user