固定消息中心为首页入口,并在推送到达、已读后刷新桌面角标。 Co-authored-by: Cursor <cursoragent@cursor.com>
474 lines
16 KiB
Swift
474 lines
16 KiB
Swift
//
|
|
// 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
|
|
private(set) var unreadMessageCount = 0
|
|
|
|
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 }
|
|
var hasUnreadMessages: Bool { unreadMessageCount > 0 }
|
|
|
|
/// 首次进入首页时加载权限并恢复在线状态。
|
|
func initialize(api: HomeAPI) async {
|
|
locationStateStore.restoreStateIfNeeded()
|
|
refreshLocalDisplayState()
|
|
await loadPermissions(api: api, force: true)
|
|
}
|
|
|
|
/// 账号切换后标记需要重新拉取权限。
|
|
func markNeedsPermissionReload() {
|
|
needsPermissionReload = true
|
|
unreadMessageCount = 0
|
|
}
|
|
|
|
/// 查询未读消息总数,成功时更新首页状态并返回 `true`。
|
|
func refreshUnreadMessageStatus(api: any MessageCenterServing) async -> Bool {
|
|
do {
|
|
let response = try await api.unreadCount()
|
|
unreadMessageCount = response.unreadCount
|
|
notifyStateChange()
|
|
return true
|
|
} catch is CancellationError {
|
|
return false
|
|
} catch {
|
|
// 查询失败时保留上一次状态,避免网络抖动导致红点错误消失。
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// 使用已知的最新未读消息数量立即刷新首页状态。
|
|
func updateUnreadMessageCount(_ count: Int) {
|
|
unreadMessageCount = max(count, 0)
|
|
notifyStateChange()
|
|
}
|
|
|
|
/// 查询待审核记录并决定进入申请页或审核状态页。
|
|
func resolvePermissionApplyDestination(api: HomeAPI) async -> PermissionApplyDestination {
|
|
do {
|
|
let pendingList = try await api.roleApplyPending()
|
|
guard let first = pendingList.first,
|
|
first.status == PermissionApplyPendingStatus.reviewing
|
|
|| first.status == PermissionApplyPendingStatus.rejected else {
|
|
return .apply
|
|
}
|
|
return .status(applyCode: first.code)
|
|
} catch {
|
|
return .apply
|
|
}
|
|
}
|
|
|
|
/// 按需重新拉取权限。
|
|
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.roleCode.isEmpty {
|
|
appStore.session.roleCode = matched.role.roleCode
|
|
}
|
|
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?()
|
|
}
|
|
}
|