// // 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 = [] /// 初始化权限申请 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 { 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() 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 } } }