Add scenic selection and cooperation order flows aligned with Android.

Upgrade scenic picker with search, location sorting, permission apply/status, and blocking home dialog; add cooperation order module and order source picker improvements with unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 10:48:43 +08:00
parent 3c4ca7eefb
commit 2bea05b1d9
50 changed files with 5603 additions and 191 deletions

View File

@ -0,0 +1,121 @@
//
// 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 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: HomeLocationProvider
private var currentLat: Double?
private var currentLng: Double?
init(appStore: AppStore = .shared, locationProvider: HomeLocationProvider = HomeLocationProvider()) {
self.appStore = appStore
self.locationProvider = locationProvider
}
func loadFromLocal() {
let scenicList = resolvedScenicList()
spots = ScenicSpotDisplayMapper.buildItems(
scenicList: scenicList,
searchQuery: searchQuery,
currentLat: currentLat,
currentLng: currentLng,
selectedScenicId: appStore.currentScenicId
)
notifyStateChange()
}
func onSearchQueryChange(_ query: String) {
searchQuery = query
loadFromLocal()
}
func startLocation() async {
isLoading = 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 HomeLocationProviderError.permissionDenied {
onShowMessage?("未授予定位权限,无法获取附近景区")
} catch {
onShowMessage?("定位失败: \(error.localizedDescription)")
}
isLoading = false
notifyStateChange()
}
func selectSpot(_ spot: ScenicSpotDisplayItem) {
appStore.currentScenicId = spot.id
appStore.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.roleScenicList()
if !cached.isEmpty { return cached }
let rolePermissionList = appStore.rolePermissionList()
guard !rolePermissionList.isEmpty else { return [] }
return appStore.matchRolePermissionItem(in: rolePermissionList)?.scenic ?? []
}
private func notifyStateChange() {
onStateChange?()
}
}