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:
2026-06-23 10:15:26 +08:00
parent e8bc9b7f44
commit c5374666de
416 changed files with 18641 additions and 38 deletions

View File

@ -0,0 +1,212 @@
//
// PermissionApplyViewModel.swift
// suixinkan
//
// Created by Codex on 2026/6/23.
//
import Foundation
import Observation
///
struct PermissionRoleOption: Equatable, Identifiable {
let id: Int
let name: String
let notes: String
}
///
struct PermissionScenicOption: Equatable, Identifiable {
let id: Int
let name: String
var selected: Bool
var disabled: Bool
}
///
struct ExistingRolePermissionInfo: Equatable, Identifiable {
let id: Int
let name: String
let scenics: [ScenicInfo]
}
@MainActor
@Observable
/// ViewModel/
final class PermissionApplyViewModel {
var roleOptions: [PermissionRoleOption] = []
var selectedRoleId: Int?
var scenicOptions: [PermissionScenicOption] = []
var loadingScenics = false
var scenicLoadFailed = false
var scenicLoadFailureReason: String?
var submitting = false
var message: String?
private let initialPending: RoleApplyPendingResponse?
private var rolePermissions: [RolePermissionResponse] = []
private var selectedIds: Set<Int> = []
/// ViewModel
init(initialPending: RoleApplyPendingResponse? = nil) {
self.initialPending = initialPending
}
///
var canSubmit: Bool {
!submitting && selectedRoleId != nil && !selectedIds.isEmpty
}
///
var selectedCount: Int {
selectedIds.count
}
/// ID 使
var selectedScenicIds: Set<Int> {
selectedIds
}
///
var selectedScenicSummary: String {
let names = scenicOptions
.filter { !$0.disabled && selectedIds.contains($0.id) }
.map(\.name)
if names.isEmpty { return "请选择景区" }
if names.count == 1 { return names[0] }
return "已选择 \(names.count) 个景区"
}
///
var selectedRoleNotes: String? {
guard let selectedRoleId else { return nil }
return roleOptions.first(where: { $0.id == selectedRoleId })?.notes
}
///
var selectedRoleName: String? {
guard let selectedRoleId else { return nil }
return roleOptions.first(where: { $0.id == selectedRoleId })?.name
}
///
var existingRoleInfos: [ExistingRolePermissionInfo] {
rolePermissions.map {
ExistingRolePermissionInfo(id: $0.role.id, name: $0.role.name, scenics: $0.scenic)
}
}
///
func bootstrap(rolePermissions: [RolePermissionResponse]) {
self.rolePermissions = rolePermissions
roleOptions = uniqueRoles(from: rolePermissions)
if let pending = initialPending {
selectedRoleId = pending.roleId
selectedIds = Set(pending.scenicList.map(\.id))
}
}
///
func selectRole(id: Int) {
selectedRoleId = id
selectedIds.removeAll()
scenicOptions = []
scenicLoadFailed = false
scenicLoadFailureReason = nil
}
///
func loadScenicListIfNeeded(api: any ScenicPermissionServing, force: Bool = false) async {
guard selectedRoleId != nil else { return }
if !force && !scenicOptions.isEmpty { return }
loadingScenics = true
scenicLoadFailed = false
scenicLoadFailureReason = nil
defer { loadingScenics = false }
do {
let list = try await api.scenicListAll().list
let disabledIds = Set(currentRolePermission()?.scenic.map(\.id) ?? [])
scenicOptions = list.map { scenic in
let selected = disabledIds.contains(scenic.id) || selectedIds.contains(scenic.id)
return PermissionScenicOption(id: scenic.id, name: scenic.name, selected: selected, disabled: disabledIds.contains(scenic.id))
}
selectedIds = Set(scenicOptions.filter { $0.selected && !$0.disabled }.map(\.id))
} catch {
scenicOptions = []
selectedIds.removeAll()
scenicLoadFailed = true
scenicLoadFailureReason = error.localizedDescription
message = "景区列表加载失败,请重试"
}
}
///
func toggleScenic(id: Int) {
guard let index = scenicOptions.firstIndex(where: { $0.id == id }), !scenicOptions[index].disabled else { return }
scenicOptions[index].selected.toggle()
if scenicOptions[index].selected {
selectedIds.insert(id)
} else {
selectedIds.remove(id)
}
}
///
func submit(api: any ScenicPermissionServing) async {
guard let roleId = selectedRoleId, !selectedIds.isEmpty, !submitting else { return }
submitting = true
defer { submitting = false }
do {
try await api.roleApplySubmit(roleId: roleId, scenicIds: Array(selectedIds).sorted())
message = "提交成功,等待审核"
} catch {
message = error.localizedDescription
}
}
///
private func uniqueRoles(from list: [RolePermissionResponse]) -> [PermissionRoleOption] {
var seen = Set<Int>()
return list.compactMap { item in
guard seen.insert(item.role.id).inserted else { return nil }
return PermissionRoleOption(id: item.role.id, name: item.role.name, notes: item.role.notes ?? "")
}
}
///
private func currentRolePermission() -> RolePermissionResponse? {
guard let selectedRoleId else { return nil }
return rolePermissions.first { $0.role.id == selectedRoleId }
}
}
@MainActor
@Observable
/// ViewModel
final class PermissionApplyStatusViewModel {
var loading = false
var pending: RoleApplyPendingResponse?
var loadFailed = false
var loadFailureReason: String?
///
func load(api: any ScenicPermissionServing, applyCode: String?) async {
loading = true
loadFailed = false
loadFailureReason = nil
defer { loading = false }
do {
let all = try await api.roleApplyAll()
let normalizedCode = applyCode?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !normalizedCode.isEmpty {
pending = all.first { $0.code == normalizedCode }
} else {
pending = all.first { $0.status == 1 || $0.status == 3 }
}
} catch {
pending = nil
loadFailed = true
loadFailureReason = error.localizedDescription
}
}
}