对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
277 lines
8.6 KiB
Swift
277 lines
8.6 KiB
Swift
//
|
|
// 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?()
|
|
}
|
|
}
|