新增景区权限模块,集成 AMap CocoaPods 并支持模拟器

将景区选择与权限申请流程接入首页路由,集成高德 SDK 用于真机构建,并通过 Podfile post_install 钩子保证 arm64 模拟器可编译。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-23 10:15:26 +08:00
parent 5d4613f317
commit b61bf0d070
416 changed files with 18641 additions and 38 deletions

View File

@ -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
}
}