Add ScenicPermission module and AMap CocoaPods with simulator support.
Integrate scenic selection and permission flows into home routing, add AMap SDK for device builds while keeping arm64 simulators compilable via Podfile post_install hooks. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,150 @@
|
||||
//
|
||||
// ScenicSelectionViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/23.
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
/// 景区选择展示实体,表示列表中的一个可切换景区。
|
||||
struct ScenicSelectionItem: Equatable, Identifiable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let status: Int?
|
||||
let address: String
|
||||
let latitude: Double?
|
||||
let longitude: Double?
|
||||
let coverURLString: String?
|
||||
var isClosest: Bool
|
||||
var distanceMeters: CLLocationDistance?
|
||||
|
||||
/// 从业务作用域创建景区选择展示实体。
|
||||
init(scope: BusinessScope) {
|
||||
id = scope.id
|
||||
name = scope.name
|
||||
status = scope.status
|
||||
let trimmedAddress = scope.address?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
address = trimmedAddress.isEmpty ? "--" : trimmedAddress
|
||||
latitude = scope.latitude
|
||||
longitude = scope.longitude
|
||||
coverURLString = scope.coverURLString
|
||||
isClosest = false
|
||||
distanceMeters = nil
|
||||
}
|
||||
|
||||
/// 返回景区距离文案,无定位结果时显示占位。
|
||||
var distanceText: String {
|
||||
guard let distanceMeters else { return "--" }
|
||||
if distanceMeters < 1_000 {
|
||||
return "\(Int(distanceMeters.rounded()))m"
|
||||
}
|
||||
return String(format: "%.2fkm", distanceMeters / 1_000)
|
||||
}
|
||||
|
||||
/// 返回景区是否处于旧工程定义的暂未营业状态。
|
||||
var isClosed: Bool {
|
||||
status == 2
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 景区选择 ViewModel,负责搜索、定位距离计算和切换景区持久化。
|
||||
final class ScenicSelectionViewModel {
|
||||
var searchQuery = ""
|
||||
var currentLocationText = "定位后获取最近景区"
|
||||
var locationWarning: String?
|
||||
private(set) var items: [ScenicSelectionItem] = []
|
||||
|
||||
/// 返回按搜索关键词过滤后的景区列表。
|
||||
var filteredItems: [ScenicSelectionItem] {
|
||||
let keyword = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !keyword.isEmpty else { return items }
|
||||
return items.filter { item in
|
||||
item.name.localizedCaseInsensitiveContains(keyword)
|
||||
|| item.address.localizedCaseInsensitiveContains(keyword)
|
||||
}
|
||||
}
|
||||
|
||||
/// 从账号上下文重新加载可选景区,并优先把当前景区放在第一位。
|
||||
func reload(from accountContext: AccountContext) {
|
||||
let currentScenicId = accountContext.currentScenic?.id
|
||||
items = Self.makeItems(scopes: accountContext.scenicScopes, currentScenicId: currentScenicId)
|
||||
currentLocationText = items.first?.address ?? "定位后获取最近景区"
|
||||
locationWarning = nil
|
||||
}
|
||||
|
||||
/// 切换景区并把当前选择保存到账号快照。
|
||||
func select(
|
||||
scenicId: Int,
|
||||
accountContext: AccountContext,
|
||||
permissionContext: PermissionContext,
|
||||
snapshotStore: AccountSnapshotStore
|
||||
) {
|
||||
accountContext.selectScenic(id: scenicId)
|
||||
snapshotStore.saveCurrentSelection(
|
||||
accountContext: accountContext,
|
||||
currentRoleId: permissionContext.currentRole?.id
|
||||
)
|
||||
}
|
||||
|
||||
/// 定位成功后计算距离,并标记距离最近景区。
|
||||
func applyCurrentLocation(latitude: Double, longitude: Double) {
|
||||
locationWarning = nil
|
||||
let current = CLLocation(latitude: latitude, longitude: longitude)
|
||||
for index in items.indices {
|
||||
guard let scenicLatitude = items[index].latitude,
|
||||
let scenicLongitude = items[index].longitude,
|
||||
scenicLatitude != 0 || scenicLongitude != 0
|
||||
else {
|
||||
items[index].distanceMeters = nil
|
||||
items[index].isClosest = false
|
||||
continue
|
||||
}
|
||||
let scenicLocation = CLLocation(latitude: scenicLatitude, longitude: scenicLongitude)
|
||||
items[index].distanceMeters = scenicLocation.distance(from: current)
|
||||
items[index].isClosest = false
|
||||
}
|
||||
if let closestIndex = items.indices
|
||||
.filter({ items[$0].distanceMeters != nil })
|
||||
.min(by: { (items[$0].distanceMeters ?? .greatestFiniteMagnitude) < (items[$1].distanceMeters ?? .greatestFiniteMagnitude) }) {
|
||||
items[closestIndex].isClosest = true
|
||||
currentLocationText = items[closestIndex].address
|
||||
} else if let first = items.first {
|
||||
items[0].isClosest = true
|
||||
currentLocationText = first.address
|
||||
}
|
||||
}
|
||||
|
||||
/// 定位失败时保留原列表并暴露提示文案。
|
||||
func applyLocationFailure(_ message: String) {
|
||||
locationWarning = message
|
||||
currentLocationText = items.first?.address ?? "定位后获取最近景区"
|
||||
if !items.isEmpty, !items.contains(where: \.isClosest) {
|
||||
items[0].isClosest = true
|
||||
}
|
||||
}
|
||||
|
||||
/// 从景区作用域生成展示列表,并把当前景区移动到首位。
|
||||
static func makeItems(scopes: [BusinessScope], currentScenicId: Int?) -> [ScenicSelectionItem] {
|
||||
var seen = Set<Int>()
|
||||
let uniqueItems = scopes.compactMap { scope -> ScenicSelectionItem? in
|
||||
guard scope.kind == .scenic, seen.insert(scope.id).inserted else { return nil }
|
||||
return ScenicSelectionItem(scope: scope)
|
||||
}
|
||||
var result: [ScenicSelectionItem]
|
||||
if let currentScenicId,
|
||||
let current = uniqueItems.first(where: { $0.id == currentScenicId }) {
|
||||
result = [current] + uniqueItems.filter { $0.id != currentScenicId }
|
||||
} else {
|
||||
result = uniqueItems
|
||||
}
|
||||
if !result.isEmpty {
|
||||
result[0].isClosest = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user