接入首页位置上报与在线倒计时,并优化上报交互体验。
持久化服务端 expired 过期时间戳以正确恢复倒计时,切换在线/离线与上报时展示定位 Loading,移除重复的成功 Alert,并将 Toast 调整为居中半透明样式。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -13,7 +13,6 @@ struct HomeMoreFunctionsView: View {
|
||||
@EnvironmentObject private var permissionContext: PermissionContext
|
||||
@EnvironmentObject private var appRouter: AppRouter
|
||||
@EnvironmentObject private var router: RouterPath
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@StateObject private var viewModel = HomeViewModel()
|
||||
@State private var commonUris: [String] = []
|
||||
@ -31,22 +30,18 @@ struct HomeMoreFunctionsView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
topBar
|
||||
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 26) {
|
||||
section(title: "常用应用", items: commonItems, isCommon: true)
|
||||
section(title: "更多功能", items: moreItems, isCommon: false)
|
||||
}
|
||||
.padding(.horizontal, AppMetrics.Spacing.mediumLarge)
|
||||
.padding(.top, AppMetrics.Spacing.xxLarge + 1)
|
||||
.padding(.bottom, AppMetrics.Spacing.xxLarge)
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 26) {
|
||||
section(title: "常用应用", items: commonItems, isCommon: true)
|
||||
section(title: "更多功能", items: moreItems, isCommon: false)
|
||||
}
|
||||
.background(Color(hex: 0xF5F5F5))
|
||||
.padding(.horizontal, AppMetrics.Spacing.mediumLarge)
|
||||
.padding(.top, AppMetrics.Spacing.medium)
|
||||
.padding(.bottom, AppMetrics.Spacing.xxLarge)
|
||||
}
|
||||
.background(Color(hex: 0xF5F5F5).ignoresSafeArea())
|
||||
.toolbar(.hidden, for: .navigationBar)
|
||||
.background(Color(hex: 0xF5F5F5))
|
||||
.navigationTitle("全部功能")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task {
|
||||
rebuildMenus()
|
||||
}
|
||||
@ -58,37 +53,6 @@ struct HomeMoreFunctionsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var topBar: some View {
|
||||
ZStack {
|
||||
Text("全部功能")
|
||||
.font(.system(size: AppMetrics.FontSize.title2 + 1, weight: .semibold))
|
||||
.foregroundStyle(Color(hex: 0x2C2C2C))
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
HStack {
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
|
||||
.foregroundStyle(Color(hex: 0x111827))
|
||||
.frame(width: AppMetrics.ControlSize.iconTapArea, height: AppMetrics.ControlSize.iconTapArea)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
|
||||
}
|
||||
.frame(height: 62)
|
||||
.background(.white)
|
||||
.overlay(alignment: .bottom) {
|
||||
Rectangle()
|
||||
.fill(Color.black.opacity(0.06))
|
||||
.frame(height: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
private func section(title: String, items: [HomeMenuItem], isCommon: Bool) -> some View {
|
||||
VStack(alignment: .leading, spacing: AppMetrics.Spacing.medium) {
|
||||
Text(title)
|
||||
|
||||
@ -16,14 +16,15 @@ struct HomeView: View {
|
||||
@EnvironmentObject private var router: RouterPath
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
@EnvironmentObject private var homeLocationViewModel: HomeLocationViewModel
|
||||
@Environment(\.globalLoading) private var globalLoading
|
||||
@Environment(\.accountSnapshotStore) private var snapshotStore
|
||||
|
||||
@StateObject private var viewModel = HomeViewModel()
|
||||
@State private var commonUris: [String] = []
|
||||
@State private var isOnline = false
|
||||
@State private var reminderMinutes = 0
|
||||
@State private var secondsUntilReport = 0
|
||||
@State private var showOnlineDialog = false
|
||||
@State private var showReminderDialog = false
|
||||
@State private var showReportSuccess = false
|
||||
|
||||
private let commonMenuStore = HomeCommonMenuStore()
|
||||
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
@ -46,14 +47,19 @@ struct HomeView: View {
|
||||
}
|
||||
|
||||
private var countdownDisplay: String {
|
||||
let hours = secondsUntilReport / 3_600
|
||||
let minutes = (secondsUntilReport % 3_600) / 60
|
||||
let seconds = secondsUntilReport % 60
|
||||
return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", seconds))"
|
||||
homeLocationViewModel.countdownDisplay
|
||||
}
|
||||
|
||||
private var reminderText: String {
|
||||
reminderMinutes == 0 ? "不提醒" : "提前\(reminderMinutes)分钟"
|
||||
homeLocationViewModel.reminderMinutes == 0 ? "不提醒" : "提前\(homeLocationViewModel.reminderMinutes)分钟"
|
||||
}
|
||||
|
||||
private var staffId: Int? {
|
||||
snapshotStore.load()?.businessUserId
|
||||
}
|
||||
|
||||
private var scenicId: Int? {
|
||||
accountContext.currentScenic?.id
|
||||
}
|
||||
|
||||
private var tileHeight: CGFloat {
|
||||
@ -102,10 +108,11 @@ struct HomeView: View {
|
||||
.toolbar(.hidden, for: .navigationBar)
|
||||
.task {
|
||||
rebuildMenusFromCurrentContext()
|
||||
homeLocationViewModel.restoreState()
|
||||
await homeLocationViewModel.checkLocationTimeout(staffId: staffId)
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
guard isOnline, secondsUntilReport > 0 else { return }
|
||||
secondsUntilReport -= 1
|
||||
homeLocationViewModel.tick()
|
||||
}
|
||||
.onChange(of: permissionContext.currentRole?.roleCode) { _ in
|
||||
rebuildMenusFromCurrentContext()
|
||||
@ -116,26 +123,58 @@ struct HomeView: View {
|
||||
.alert("切换在线状态", isPresented: $showOnlineDialog) {
|
||||
Button("取消", role: .cancel) {}
|
||||
Button("确定") {
|
||||
isOnline.toggle()
|
||||
if isOnline {
|
||||
secondsUntilReport = 7_200
|
||||
runLocationReportTask {
|
||||
await homeLocationViewModel.confirmOnlineToggle(
|
||||
staffId: staffId,
|
||||
scenicId: scenicId,
|
||||
toast: toastCenter
|
||||
)
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
Text(isOnline ? "是否确认切换为离线状态?离线后将暂停位置上报。" : "是否确认切换为在线状态?在线后将开始位置上报和计时。")
|
||||
Text(homeLocationViewModel.isOnline ? "是否确认切换为离线状态?离线后将暂停位置上报。" : "是否确认切换为在线状态?在线后将开始位置上报和计时。")
|
||||
}
|
||||
.confirmationDialog("提前提醒时间", isPresented: $showReminderDialog, titleVisibility: .visible) {
|
||||
ForEach([0, 5, 10, 15, 30], id: \.self) { minute in
|
||||
Button(minute == 0 ? "不提醒" : "\(minute)分钟") {
|
||||
reminderMinutes = minute
|
||||
homeLocationViewModel.updateReminderMinutes(minute)
|
||||
}
|
||||
}
|
||||
Button("取消", role: .cancel) {}
|
||||
}
|
||||
.alert("上报成功", isPresented: $showReportSuccess) {
|
||||
Button("知道了", role: .cancel) {}
|
||||
.alert("位置上报提醒", isPresented: $homeLocationViewModel.showReminderDialog) {
|
||||
Button("立即上报") {
|
||||
homeLocationViewModel.dismissReminderDialog()
|
||||
runLocationReportTask {
|
||||
await homeLocationViewModel.startImmediateReport(
|
||||
staffId: staffId,
|
||||
scenicId: scenicId,
|
||||
toast: toastCenter
|
||||
)
|
||||
}
|
||||
}
|
||||
Button("稍后", role: .cancel) {
|
||||
homeLocationViewModel.dismissReminderDialog()
|
||||
}
|
||||
} message: {
|
||||
Text("上报已成功,距离下次上报时间 \(countdownDisplay)。")
|
||||
Text(homeLocationViewModel.reminderDialogMessage)
|
||||
}
|
||||
.alert("位置上报超时", isPresented: $homeLocationViewModel.showTimeoutDialog) {
|
||||
Button("立即上报") {
|
||||
homeLocationViewModel.showTimeoutDialog = false
|
||||
runLocationReportTask {
|
||||
await homeLocationViewModel.startImmediateReport(
|
||||
staffId: staffId,
|
||||
scenicId: scenicId,
|
||||
toast: toastCenter
|
||||
)
|
||||
}
|
||||
}
|
||||
Button("取消", role: .cancel) {
|
||||
homeLocationViewModel.dismissTimeoutDialog()
|
||||
}
|
||||
} message: {
|
||||
Text("位置上报即将超时或已超时,请立即重新上报。")
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,12 +208,12 @@ struct HomeView: View {
|
||||
Button {
|
||||
showOnlineDialog = true
|
||||
} label: {
|
||||
Text(isOnline ? "在线" : "离线")
|
||||
Text(homeLocationViewModel.isOnline ? "在线" : "离线")
|
||||
.font(.system(size: AppMetrics.FontSize.caption, weight: .medium))
|
||||
.foregroundStyle(isOnline ? Color(hex: 0xF0FDF4) : Color(hex: 0x7B8EAA))
|
||||
.foregroundStyle(homeLocationViewModel.isOnline ? Color(hex: 0xF0FDF4) : Color(hex: 0x7B8EAA))
|
||||
.padding(.horizontal, AppMetrics.Spacing.small)
|
||||
.padding(.vertical, AppMetrics.Spacing.xxSmall + 2)
|
||||
.background(isOnline ? Color(hex: 0x22C55E) : Color(hex: 0xF4F4F4), in: RoundedRectangle(cornerRadius: 4))
|
||||
.background(homeLocationViewModel.isOnline ? Color(hex: 0x22C55E) : Color(hex: 0xF4F4F4), in: RoundedRectangle(cornerRadius: 4))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
@ -235,7 +274,13 @@ struct HomeView: View {
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
openRoute(HomeMenuRouter.resolve(uri: "location_report", title: "位置上报"))
|
||||
runLocationReportTask {
|
||||
await homeLocationViewModel.startImmediateReport(
|
||||
staffId: staffId,
|
||||
scenicId: scenicId,
|
||||
toast: toastCenter
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "hand.tap.fill")
|
||||
.font(.system(size: 38, weight: .semibold))
|
||||
@ -269,7 +314,7 @@ struct HomeView: View {
|
||||
openRoute(HomeMenuRouter.resolve(uri: "task_create", title: "提交任务"))
|
||||
}
|
||||
|
||||
quickAction(icon: isOnline ? "wifi" : "wifi.slash", title: isOnline ? "在线" : "离线", active: isOnline) {
|
||||
quickAction(icon: homeLocationViewModel.isOnline ? "wifi" : "wifi.slash", title: homeLocationViewModel.isOnline ? "在线" : "离线", active: homeLocationViewModel.isOnline) {
|
||||
showOnlineDialog = true
|
||||
}
|
||||
}
|
||||
@ -423,6 +468,15 @@ struct HomeView: View {
|
||||
topLevelPermissionURIs: permissionContext.topLevelPermissionURIs(for: roleCode)
|
||||
)
|
||||
}
|
||||
|
||||
/// 包裹需要 GPS 定位的位置上报操作,定位期间展示全局 Loading。
|
||||
private func runLocationReportTask(_ body: @escaping () async -> Void) {
|
||||
Task {
|
||||
await globalLoading.withLoading(message: "正在定位...") {
|
||||
await body()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@ -432,5 +486,8 @@ struct HomeView: View {
|
||||
.environmentObject(PermissionContext())
|
||||
.environmentObject(AppRouter())
|
||||
.environmentObject(RouterPath())
|
||||
.environmentObject(ToastCenter())
|
||||
.environmentObject(HomeLocationViewModel(api: LocationReportAPI(client: APIClient())))
|
||||
.environment(\.accountSnapshotStore, AccountSnapshotStore())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user