// // HomeViewModel.swift // suixinkan // import Foundation /// 首页 ViewModel,对齐 Android `HomeViewModel` 的权限、门店与位置逻辑。 final class HomeViewModel { /// 首页位置上报卡片固定提示文案。 static let locationReportPromptText = "您已进入打卡范围" private(set) var currentScenicName = "" private(set) var currentAppRole: AppRoleCode? private(set) var commonMenus: [HomeMenuItem] = [] private(set) var storeItem: StoreItem? private(set) var showPermissionDialog = false private(set) var showScenicDialog = false private(set) var showLocationTimeoutDialog = false private(set) var showOnlineStatusDialog = false private(set) var showLocationReportSuccessDialog = false private(set) var isMinimalTopRole = false private(set) var locationInfoText = HomeViewModel.locationReportPromptText private(set) var reportTimeText = "" private(set) var isLocationReporting = false private(set) var needsPermissionReload = false var onStateChange: (() -> Void)? var onShowMessage: ((String) -> Void)? let locationStateStore: HomeLocationStateStore let locationReportService: LocationReportService private let appStore: AppStore private let commonMenuStore: HomeCommonMenuStore private let locationProvider: any LocationProviding private var timeoutDialogTriggered = false private var dismissedLocationTimeoutDialog = false init( appStore: AppStore = .shared, locationStateStore: HomeLocationStateStore = .shared, commonMenuStore: HomeCommonMenuStore = HomeCommonMenuStore(), locationProvider: any LocationProviding = LocationProvider.shared ) { self.appStore = appStore self.locationStateStore = locationStateStore self.locationProvider = locationProvider self.locationReportService = LocationReportService( appStore: appStore, locationStateStore: locationStateStore, locationProvider: locationProvider ) self.commonMenuStore = commonMenuStore self.locationStateStore.observe(self) { [weak self] in self?.notifyStateChange() } } deinit { locationStateStore.removeObserver(self) } var isOnline: Bool { locationStateStore.isOnline } var countdownDisplayText: String { locationStateStore.countdownDisplayText } var reminderMinutes: Int { locationStateStore.reminderMinutes } /// 首次进入首页时加载权限并恢复在线状态。 func initialize(api: HomeAPI) async { locationStateStore.restoreStateIfNeeded() refreshLocalDisplayState() await loadPermissions(api: api, force: true) } /// 账号切换后标记需要重新拉取权限。 func markNeedsPermissionReload() { needsPermissionReload = true } /// 按需重新拉取权限。 func reloadIfNeeded(api: HomeAPI) async { locationStateStore.restoreStateIfNeeded() guard needsPermissionReload else { refreshLocalDisplayState() rebuildCommonMenus() await loadStoreListIfNeeded(api: api) notifyStateChange() return } needsPermissionReload = false await loadPermissions(api: api, force: true) } /// 拉取 role-permission 并刷新菜单。 func loadPermissions(api: HomeAPI, force: Bool = false) async { if !force, !appStore.permissions.permissionItems().isEmpty { rebuildCommonMenus() await loadStoreListIfNeeded(api: api) notifyStateChange() return } do { let rolePermissionList = try await api.rolePermissions() if rolePermissionList.isEmpty { appStore.permissions.savePermissionItems([]) showPermissionDialog = true rebuildCommonMenus() notifyStateChange() return } appStore.permissions.saveRolePermissionList(rolePermissionList) let matched = appStore.permissions.matchRolePermissionItem(in: rolePermissionList) ?? rolePermissionList[0] if !matched.role.name.isEmpty { appStore.session.roleName = matched.role.name } appStore.permissions.saveRoleScenicList(matched.scenic) let permissions = matched.role.permission.map(HomePermissionItem.init) appStore.permissions.savePermissionItems(permissions) showPermissionDialog = false refreshLocalDisplayState() rebuildCommonMenus() await loadStoreListIfNeeded(api: api) notifyStateChange() } catch is CancellationError { return } catch { appStore.permissions.savePermissionItems([]) showPermissionDialog = true rebuildCommonMenus() onShowMessage?(error.localizedDescription) notifyStateChange() } } /// 店铺管理员加载当前景区门店。 func loadStoreListIfNeeded(api: HomeAPI) async { guard currentAppRole == .storeAdmin else { storeItem = nil notifyStateChange() return } guard appStore.session.currentScenicId > 0 else { storeItem = nil notifyStateChange() return } do { let response = try await api.storeList() let scenicId = appStore.session.currentScenicId let savedStoreId = appStore.session.currentStoreId let savedMatch = savedStoreId > 0 ? response.list.first { $0.id == savedStoreId } : nil let matched = savedMatch ?? response.list.first { $0.scenicId == scenicId } storeItem = matched if let matched { appStore.session.currentStoreId = matched.id } notifyStateChange() } catch is CancellationError { return } catch { storeItem = nil notifyStateChange() } } /// 评估弹窗展示优先级:权限 > 景区 > 位置超时。 func evaluateDialogs(api: HomeAPI) async { if showPermissionDialog { showScenicDialog = false return } let hasScenic = appStore.session.currentScenicId > 0 showScenicDialog = !hasScenic if showScenicDialog || dismissedLocationTimeoutDialog { showLocationTimeoutDialog = false notifyStateChange() return } guard !isMinimalTopRole else { showLocationTimeoutDialog = false notifyStateChange() return } try? await Task.sleep(nanoseconds: 500_000_000) await checkLocationTimeout(api: api) } /// 检查位置上报是否接近超时。 func checkLocationTimeout(api: HomeAPI) async { guard reminderMinutes > 0 else { showLocationTimeoutDialog = false notifyStateChange() return } let staffId = appStore.session.userId.trimmingCharacters(in: .whitespacesAndNewlines) guard !staffId.isEmpty else { showLocationTimeoutDialog = true notifyStateChange() return } do { let detail = try await api.locationDetail(staffId: staffId) if detail.expireTime <= 0 { showLocationTimeoutDialog = true notifyStateChange() return } let currentMillis = Int64(Date().timeIntervalSince1970 * 1000) let expireMillis = detail.expireTime * 1000 let threshold = expireMillis - Int64(reminderMinutes * 60 * 1000) showLocationTimeoutDialog = currentMillis >= threshold notifyStateChange() } catch is CancellationError { return } catch { showLocationTimeoutDialog = true notifyStateChange() } } /// 刷新景区名与角色展示状态,对齐 Android `initCurrentScenicName`。 func refreshLocalDisplayState() { let scenicId = appStore.session.currentScenicId let scenicName = appStore.session.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines) if scenicId > 0, !scenicName.isEmpty { currentScenicName = scenicName } else { currentScenicName = "" appStore.session.currentScenicId = 0 appStore.session.currentScenicName = "" } currentAppRole = appStore.session.currentAppRole isMinimalTopRole = currentAppRole?.usesHomeMinimalTopLayout ?? false } /// 重建常用应用列表。 func rebuildCommonMenus() { let permissions = appStore.permissions.permissionItems() let allMenus = HomeMenuCatalog.visibleMenus(from: permissions) let accountScope = appStore.session.accountCachePrefix let roleCode = appStore.session.roleCode let savedURIs = commonMenuStore.savedCommonURIs(accountScope: accountScope, roleCode: roleCode) if savedURIs.isEmpty, !permissions.isEmpty { let defaultURIs = HomeCommonMenuStore.defaultCommonURIs(from: permissions) commonMenuStore.saveCommonURIs(defaultURIs, accountScope: accountScope, roleCode: roleCode) } commonMenus = commonMenuStore.commonMenus( from: allMenus, permissions: permissions, accountScope: accountScope, roleCode: roleCode ) } func hidePermissionDialog() { showPermissionDialog = false notifyStateChange() } func hideScenicDialog() { showScenicDialog = false notifyStateChange() } func showOnlineStatusSwitchDialog() { showOnlineStatusDialog = true notifyStateChange() } func hideOnlineStatusSwitchDialog() { showOnlineStatusDialog = false notifyStateChange() } func hideLocationTimeoutDialog() { showLocationTimeoutDialog = false dismissedLocationTimeoutDialog = true timeoutDialogTriggered = false locationStateStore.updateOnlineStatus(false) notifyStateChange() } func hideLocationReportSuccessDialog() { showLocationReportSuccessDialog = false notifyStateChange() } func updateReminderMinutes(_ minutes: Int) { locationStateStore.updateReminderMinutes(minutes) notifyStateChange() } /// 切换在线/离线并触发位置上报。 func switchOnlineStatus(api: HomeAPI) async { hideOnlineStatusSwitchDialog() isLocationReporting = true notifyStateChange() defer { isLocationReporting = locationReportService.isReporting notifyStateChange() } do { let goingOnline = !isOnline if let result = try await locationReportService.switchOnlineStatus(api: api) { applyReportSuccess(result) onShowMessage?(goingOnline ? "已切换到在线状态,正在上报位置" : "已切换到离线状态") } else { onShowMessage?("已切换到离线状态") } timeoutDialogTriggered = false dismissedLocationTimeoutDialog = false } catch let error as LocationReportServiceError { onShowMessage?(error.localizedDescription) } catch is CancellationError { return } catch { onShowMessage?(error.localizedDescription) } } /// 手动上报位置。 func manualReportLocation(api: HomeAPI) async { isLocationReporting = true notifyStateChange() defer { isLocationReporting = locationReportService.isReporting notifyStateChange() } do { let result = try await locationReportService.manualReport(api: api) applyReportSuccess(result) onShowMessage?("位置上报成功") } catch let error as LocationReportServiceError { onShowMessage?(error.localizedDescription) } catch is CancellationError { return } catch { onShowMessage?(error.localizedDescription) } } /// 指定坐标上报(标记上报 type=2)。 func reportLocation( api: HomeAPI, latitude: Double, longitude: Double, address: String?, type: Int ) async { isLocationReporting = true notifyStateChange() defer { isLocationReporting = locationReportService.isReporting notifyStateChange() } do { let result = try await locationReportService.reportWithCoordinates( api: api, latitude: latitude, longitude: longitude, address: address, type: type ) applyReportSuccess(result) onShowMessage?("位置上报成功") } catch let error as LocationReportServiceError { onShowMessage?(error.localizedDescription) } catch is CancellationError { return } catch { onShowMessage?(error.localizedDescription) } } /// 位置超时弹窗确认上报。 func confirmLocationTimeoutReport(api: HomeAPI) async { showLocationTimeoutDialog = false dismissedLocationTimeoutDialog = false await manualReportLocation(api: api) } /// 触发本地倒计时提醒。 func triggerLocationTimeoutIfNeeded() { guard locationStateStore.shouldTriggerTimeoutReminder() else { return } guard !timeoutDialogTriggered else { return } timeoutDialogTriggered = true showLocationTimeoutDialog = true notifyStateChange() } /// 刷新当前位置文案。 func refreshLocationInfo() async { isLocationReporting = true notifyStateChange() defer { isLocationReporting = false notifyStateChange() } do { let snapshot = try await locationProvider.requestSnapshot() locationInfoText = snapshot.address.isEmpty ? String(format: "%.5f, %.5f", snapshot.latitude, snapshot.longitude) : snapshot.address } catch { locationInfoText = "定位失败" } } private func applyReportSuccess(_ result: LocationReportSuccessResult) { locationInfoText = result.locationInfoText if result.shouldShowSuccessDialog { reportTimeText = result.reportTimeText showLocationReportSuccessDialog = true timeoutDialogTriggered = false dismissedLocationTimeoutDialog = false } notifyStateChange() } private func notifyStateChange() { onStateChange?() } }