// // HomeViewModel.swift // suixinkan // import Foundation /// 首页 ViewModel,对齐 Android `HomeViewModel` 的权限、门店与位置逻辑。 final class HomeViewModel { 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 = "定位中..." private(set) var reportTimeText = "" private(set) var isLocationReporting = false private(set) var needsPermissionReload = false var onStateChange: (() -> Void)? var onShowMessage: ((String) -> Void)? let locationStateStore: HomeLocationStateStore private let appStore: AppStore private let commonMenuStore: HomeCommonMenuStore private let locationProvider: HomeLocationProvider private var lastOnlineStatusSwitchTime: Int64 = 0 private var lastLocationReportTime: Int64 = 0 private var timeoutDialogTriggered = false private var dismissedLocationTimeoutDialog = false private let operationIntervalMillis: Int64 = 30_000 init( appStore: AppStore = .shared, locationStateStore: HomeLocationStateStore = HomeLocationStateStore(), commonMenuStore: HomeCommonMenuStore = HomeCommonMenuStore(), locationProvider: HomeLocationProvider = HomeLocationProvider() ) { self.appStore = appStore self.locationStateStore = locationStateStore self.commonMenuStore = commonMenuStore self.locationProvider = locationProvider self.locationStateStore.onStateChange = { [weak self] in self?.notifyStateChange() } } 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 { 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.permissionItems().isEmpty { rebuildCommonMenus() await loadStoreListIfNeeded(api: api) notifyStateChange() return } do { let rolePermissionList = try await api.rolePermissions() if rolePermissionList.isEmpty { appStore.savePermissionItems([]) showPermissionDialog = true rebuildCommonMenus() notifyStateChange() return } appStore.saveRolePermissionList(rolePermissionList) let matched = appStore.matchRolePermissionItem(in: rolePermissionList) ?? rolePermissionList[0] if !matched.role.name.isEmpty { appStore.roleName = matched.role.name } appStore.saveRoleScenicList(matched.scenic) let permissions = matched.role.permission.map(HomePermissionItem.init) appStore.savePermissionItems(permissions) showPermissionDialog = false refreshLocalDisplayState() rebuildCommonMenus() await loadStoreListIfNeeded(api: api) notifyStateChange() } catch is CancellationError { return } catch { appStore.savePermissionItems([]) showPermissionDialog = true rebuildCommonMenus() onShowMessage?(error.localizedDescription) notifyStateChange() } } /// 店铺管理员加载当前景区门店。 func loadStoreListIfNeeded(api: HomeAPI) async { guard currentAppRole == .storeAdmin else { storeItem = nil notifyStateChange() return } guard appStore.currentScenicId > 0 else { storeItem = nil notifyStateChange() return } do { let response = try await api.storeList() let scenicId = appStore.currentScenicId let savedStoreId = appStore.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.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.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.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.currentScenicId let scenicName = appStore.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines) if scenicId > 0, !scenicName.isEmpty { currentScenicName = scenicName } else { currentScenicName = "" appStore.currentScenicId = 0 appStore.currentScenicName = "" } currentAppRole = appStore.currentAppRole isMinimalTopRole = currentAppRole?.usesHomeMinimalTopLayout ?? false } /// 重建常用应用列表。 func rebuildCommonMenus() { let permissions = appStore.permissionItems() let allMenus = HomeMenuCatalog.visibleMenus(from: permissions) let accountScope = appStore.accountCachePrefix let roleCode = appStore.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 { let now = currentTimestampMillis() if now - lastOnlineStatusSwitchTime < operationIntervalMillis { let remaining = Int((operationIntervalMillis - (now - lastOnlineStatusSwitchTime)) / 1000) onShowMessage?("操作过于频繁,请\(remaining)秒后再试") hideOnlineStatusSwitchDialog() return } guard appStore.currentScenicId > 0 else { onShowMessage?("请先选择景区") hideOnlineStatusSwitchDialog() return } let goingOnline = !isOnline if goingOnline { do { _ = try await locationProvider.requestSnapshot() } catch { onShowMessage?(error.localizedDescription) hideOnlineStatusSwitchDialog() return } locationStateStore.updateOnlineStatus(true) timeoutDialogTriggered = false dismissedLocationTimeoutDialog = false lastOnlineStatusSwitchTime = now onShowMessage?("已切换到在线状态,正在上报位置") hideOnlineStatusSwitchDialog() await reportLocation(api: api, type: 1) } else { locationStateStore.updateOnlineStatus(false) lastOnlineStatusSwitchTime = now onShowMessage?("已切换到离线状态") hideOnlineStatusSwitchDialog() await reportLocation(api: api, type: 3) } } /// 手动上报位置。 func manualReportLocation(api: HomeAPI) async { let now = currentTimestampMillis() if now - lastLocationReportTime < operationIntervalMillis { let remaining = Int((operationIntervalMillis - (now - lastLocationReportTime)) / 1000) onShowMessage?("操作过于频繁,请\(remaining)秒后再试") return } await reportLocation(api: api, type: 1) } /// 位置超时弹窗确认上报。 func confirmLocationTimeoutReport(api: HomeAPI) async { showLocationTimeoutDialog = false dismissedLocationTimeoutDialog = false await reportLocation(api: api, type: 1) } /// 触发本地倒计时提醒。 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 reportLocation(api: HomeAPI, type: Int) async { guard appStore.currentScenicId > 0 else { onShowMessage?("请先选择景区") return } let staffId = appStore.userId.trimmingCharacters(in: .whitespacesAndNewlines) guard !staffId.isEmpty else { onShowMessage?("获取用户ID失败") return } 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 _ = try await api.reportLocation( staffId: staffId, latitude: snapshot.latitude, longitude: snapshot.longitude, address: snapshot.address, type: type, scenicId: String(appStore.currentScenicId) ) lastLocationReportTime = currentTimestampMillis() if type != 3 { locationStateStore.startLocationReport() reportTimeText = formattedNow() showLocationReportSuccessDialog = true timeoutDialogTriggered = false dismissedLocationTimeoutDialog = false } notifyStateChange() } catch is CancellationError { return } catch { onShowMessage?(error.localizedDescription) } } private func formattedNow() -> String { let formatter = DateFormatter() formatter.locale = Locale(identifier: "zh_CN") formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" return formatter.string(from: Date()) } private func currentTimestampMillis() -> Int64 { Int64(Date().timeIntervalSince1970 * 1000) } private func notifyStateChange() { onStateChange?() } }