将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
123 lines
3.9 KiB
Swift
123 lines
3.9 KiB
Swift
//
|
|
// ScenicSelectionViewModel.swift
|
|
// suixinkan
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// 权限申请审核中状态,对齐 Android role-apply pending status。
|
|
enum PermissionApplyPendingStatus {
|
|
static let reviewing = 1
|
|
static let rejected = 3
|
|
}
|
|
|
|
/// 景区选择 ViewModel,对齐 Android `ScenicSelectionViewModel`。
|
|
final class ScenicSelectionViewModel {
|
|
|
|
private(set) var spots: [ScenicSpotDisplayItem] = []
|
|
private(set) var searchQuery = ""
|
|
private(set) var currentLocationText = "最近的景区"
|
|
private(set) var isLocating = false
|
|
private(set) var isLoading = false
|
|
|
|
var onStateChange: (() -> Void)?
|
|
var onShowMessage: ((String) -> Void)?
|
|
var onSelectionComplete: (() -> Void)?
|
|
var onNavigateApply: (() -> Void)?
|
|
var onNavigateApplyStatus: ((String) -> Void)?
|
|
|
|
private let appStore: AppStore
|
|
private let locationProvider: any LocationProviding
|
|
private var currentLat: Double?
|
|
private var currentLng: Double?
|
|
|
|
init(appStore: AppStore = .shared, locationProvider: any LocationProviding = LocationProvider.shared) {
|
|
self.appStore = appStore
|
|
self.locationProvider = locationProvider
|
|
}
|
|
|
|
func loadFromLocal() {
|
|
let scenicList = resolvedScenicList()
|
|
spots = ScenicSpotDisplayMapper.buildItems(
|
|
scenicList: scenicList,
|
|
searchQuery: searchQuery,
|
|
currentLat: currentLat,
|
|
currentLng: currentLng,
|
|
selectedScenicId: appStore.session.currentScenicId
|
|
)
|
|
notifyStateChange()
|
|
}
|
|
|
|
func onSearchQueryChange(_ query: String) {
|
|
searchQuery = query
|
|
loadFromLocal()
|
|
}
|
|
|
|
func startLocation() async {
|
|
isLocating = true
|
|
notifyStateChange()
|
|
do {
|
|
let snapshot = try await locationProvider.requestSnapshot()
|
|
currentLat = snapshot.latitude
|
|
currentLng = snapshot.longitude
|
|
let address = snapshot.address.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
currentLocationText = address.isEmpty ? "最近的景区" : address
|
|
loadFromLocal()
|
|
} catch LocationProviderError.permissionDenied {
|
|
onShowMessage?("未授予定位权限,无法获取附近景区")
|
|
} catch {
|
|
onShowMessage?("定位失败: \(error.localizedDescription)")
|
|
}
|
|
isLocating = false
|
|
notifyStateChange()
|
|
}
|
|
|
|
func selectSpot(_ spot: ScenicSpotDisplayItem) {
|
|
appStore.session.currentScenicId = spot.id
|
|
appStore.session.currentScenicName = spot.name
|
|
NotificationCenter.default.post(
|
|
name: NotificationName.scenicDidChange,
|
|
object: nil,
|
|
userInfo: [
|
|
NotificationUserInfoKey.scenicId: spot.id,
|
|
NotificationUserInfoKey.scenicName: spot.name,
|
|
]
|
|
)
|
|
loadFromLocal()
|
|
onSelectionComplete?()
|
|
}
|
|
|
|
func onApplyNow(api: HomeAPI) async {
|
|
isLoading = true
|
|
notifyStateChange()
|
|
defer {
|
|
isLoading = false
|
|
notifyStateChange()
|
|
}
|
|
do {
|
|
let pendingList = try await api.roleApplyPending()
|
|
if let first = pendingList.first,
|
|
first.status == PermissionApplyPendingStatus.reviewing
|
|
|| first.status == PermissionApplyPendingStatus.rejected {
|
|
onNavigateApplyStatus?(first.code)
|
|
} else {
|
|
onNavigateApply?()
|
|
}
|
|
} catch {
|
|
onNavigateApply?()
|
|
}
|
|
}
|
|
|
|
private func resolvedScenicList() -> [ScenicInfo] {
|
|
let cached = appStore.permissions.roleScenicList()
|
|
if !cached.isEmpty { return cached }
|
|
let rolePermissionList = appStore.permissions.rolePermissionList()
|
|
guard !rolePermissionList.isEmpty else { return [] }
|
|
return appStore.permissions.matchRolePermissionItem(in: rolePermissionList)?.scenic ?? []
|
|
}
|
|
|
|
private func notifyStateChange() {
|
|
onStateChange?()
|
|
}
|
|
}
|