实现权限驱动的首页 Tab 并对齐 Android
This commit is contained in:
418
suixinkan/Features/Home/ViewModels/HomeViewModel.swift
Normal file
418
suixinkan/Features/Home/ViewModels/HomeViewModel.swift
Normal file
@ -0,0 +1,418 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新景区名与角色展示状态。
|
||||
func refreshLocalDisplayState() {
|
||||
currentScenicName = appStore.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
currentAppRole = appStore.currentAppRole
|
||||
isMinimalTopRole = currentAppRole?.usesHomeMinimalTopLayout ?? false
|
||||
}
|
||||
|
||||
/// 重建常用应用列表。
|
||||
func rebuildCommonMenus() {
|
||||
let permissions = appStore.permissionItems()
|
||||
let allMenus = HomeMenuCatalog.visibleMenus(from: permissions)
|
||||
commonMenus = commonMenuStore.commonMenus(
|
||||
from: allMenus,
|
||||
permissions: permissions,
|
||||
accountScope: appStore.accountCachePrefix,
|
||||
roleCode: appStore.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?()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user