Files
suixinkan_uikit/suixinkan/Features/Home/ViewModels/PermissionApplyViewModel.swift
汉秋 efb3925257 完善首页权限申请流程与队列播报体验。
对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 13:46:45 +08:00

277 lines
8.6 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// PermissionApplyViewModel.swift
// suixinkan
//
import Foundation
/// ViewModel Android `PermissionApplyViewModel`
final class PermissionApplyViewModel {
private(set) var availableRoles: [RoleOption] = []
private(set) var selectedRoleId: Int?
private(set) var selectedRoleName = ""
private(set) var availableScenics: [ScenicOption] = []
private(set) var selectedScenicIds: [Int] = []
private(set) var selectedScenicNames: [String] = []
private(set) var isLoadingScenics = false
private(set) var isSubmitting = false
private(set) var showRolePicker = false
private(set) var showScenicPicker = false
private(set) var rolePermissionList: [RolePermissionResponse] = []
private(set) var scenicSearchQuery = ""
private(set) var pendingDeletion: PermissionDeletionTarget?
private(set) var showScenicListDialog = false
private(set) var dialogRoleName = ""
private(set) var dialogScenicList: [ScenicInfo] = []
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
var onSubmitSuccess: (() -> Void)?
private let appStore: AppStore
init(appStore: AppStore = .shared) {
self.appStore = appStore
loadRolesFromLocal()
}
/// Android
var canSubmit: Bool {
selectedRoleId != nil && !selectedScenicIds.isEmpty && !isSubmitting
}
///
var filteredScenics: [ScenicOption] {
let query = scenicSearchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
guard !query.isEmpty else { return availableScenics }
return availableScenics.filter { $0.name.localizedCaseInsensitiveContains(query) }
}
/// status=3
func initWithPendingData(_ pending: RoleApplyPendingResponse) {
selectedRoleId = pending.roleId
selectedRoleName = pending.roleName
selectedScenicIds = pending.scenicList.map(\.id)
selectedScenicNames = pending.scenicList.map(\.name)
availableRoles = availableRoles.map { role in
var copy = role
copy.isSelected = role.id == pending.roleId
return copy
}
notifyStateChange()
}
func loadRolesFromLocal() {
rolePermissionList = appStore.permissions.rolePermissionList()
var seen = Set<Int>()
availableRoles = rolePermissionList.compactMap { item in
guard !seen.contains(item.role.id) else { return nil }
seen.insert(item.role.id)
return RoleOption(
id: item.role.id,
name: item.role.name,
description: item.role.notes ?? "",
isSelected: false
)
}
notifyStateChange()
}
func showRolePickerSheet() {
showRolePicker = true
notifyStateChange()
}
func hideRolePickerSheet() {
showRolePicker = false
notifyStateChange()
}
func selectRole(_ role: RoleOption) {
selectedRoleId = role.id
selectedRoleName = role.name
availableRoles = availableRoles.map { item in
var copy = item
copy.isSelected = item.id == role.id
return copy
}
selectedScenicIds = []
selectedScenicNames = []
availableScenics = []
showRolePicker = false
notifyStateChange()
}
func showScenicPickerSheet() {
guard selectedRoleId != nil else {
onShowMessage?("请先选择角色")
return
}
showScenicPicker = true
notifyStateChange()
}
func hideScenicPickerSheet() {
showScenicPicker = false
notifyStateChange()
}
func loadAvailableScenics(api: HomeAPI?) async {
guard let roleId = selectedRoleId else { return }
guard let api else { return }
guard !isLoadingScenics else { return }
isLoadingScenics = true
notifyStateChange()
do {
let response = try await api.scenicListAll()
let existingIds = existingScenicIds(for: roleId)
availableScenics = response.list.map { scenic in
let isExisting = existingIds.contains(scenic.id)
let isPreselected = selectedScenicIds.contains(scenic.id)
return ScenicOption(
id: scenic.id,
name: scenic.name,
isSelected: isExisting || isPreselected,
isDisabled: isExisting
)
}
} catch {
onShowMessage?("获取景区列表失败")
}
isLoadingScenics = false
notifyStateChange()
}
func toggleScenic(_ scenic: ScenicOption) {
guard !scenic.isDisabled else { return }
if selectedScenicIds.contains(scenic.id) {
selectedScenicIds.removeAll { $0 == scenic.id }
selectedScenicNames.removeAll { $0 == scenic.name }
} else {
selectedScenicIds.append(scenic.id)
selectedScenicNames.append(scenic.name)
}
availableScenics = availableScenics.map { item in
var copy = item
if !copy.isDisabled {
copy.isSelected = selectedScenicIds.contains(copy.id)
}
return copy
}
notifyStateChange()
}
///
func updateScenicSearchQuery(_ query: String) {
scenicSearchQuery = query
notifyStateChange()
}
func removeSelectedScenic(id: Int, name: String) {
pendingDeletion = .scenic(id: id, name: name)
notifyStateChange()
}
///
func confirmDeletion() {
guard let pendingDeletion else { return }
switch pendingDeletion {
case .role:
clearRoleSelectionImmediately()
case .scenic(let id, let name):
removeSelectedScenicImmediately(id: id, name: name)
}
self.pendingDeletion = nil
notifyStateChange()
}
///
func cancelDeletion() {
pendingDeletion = nil
notifyStateChange()
}
private func removeSelectedScenicImmediately(id: Int, name: String) {
selectedScenicIds.removeAll { $0 == id }
selectedScenicNames.removeAll { $0 == name }
availableScenics = availableScenics.map { item in
var copy = item
if !copy.isDisabled {
copy.isSelected = selectedScenicIds.contains(copy.id)
}
return copy
}
}
///
func existingScenics(for roleId: Int) -> [ScenicInfo] {
appStore.permissions.rolePermissionList()
.filter { $0.role.id == roleId }
.flatMap(\.scenic)
}
func clearRoleSelection() {
guard !selectedRoleName.isEmpty else { return }
pendingDeletion = .role(name: selectedRoleName)
notifyStateChange()
}
private func clearRoleSelectionImmediately() {
selectedRoleId = nil
selectedRoleName = ""
selectedScenicIds = []
selectedScenicNames = []
availableScenics = []
availableRoles = availableRoles.map { role in
var copy = role
copy.isSelected = false
return copy
}
}
///
func showScenicList(roleName: String, scenics: [ScenicInfo]) {
dialogRoleName = roleName
dialogScenicList = scenics
showScenicListDialog = true
notifyStateChange()
}
///
func hideScenicList() {
dialogRoleName = ""
dialogScenicList = []
showScenicListDialog = false
notifyStateChange()
}
func submit(api: HomeAPI) async {
guard canSubmit, let roleId = selectedRoleId else {
onShowMessage?("请选择景区和角色")
return
}
isSubmitting = true
notifyStateChange()
do {
try await api.submitRoleApply(scenicIds: selectedScenicIds, roleId: roleId)
isSubmitting = false
notifyStateChange()
onSubmitSuccess?()
} catch {
onShowMessage?("提交失败,请稍后重试")
isSubmitting = false
notifyStateChange()
}
}
private func existingScenicIds(for roleId: Int) -> Set<Int> {
Set(existingScenics(for: roleId).map(\.id))
}
private func notifyStateChange() {
onStateChange?()
}
}