完善首页权限申请流程与队列播报体验。

对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 13:46:45 +08:00
parent c083f1d4b3
commit efb3925257
50 changed files with 2585 additions and 455 deletions

View File

@ -78,6 +78,21 @@ final class HomeViewModel {
needsPermissionReload = true
}
///
func resolvePermissionApplyDestination(api: HomeAPI) async -> PermissionApplyDestination {
do {
let pendingList = try await api.roleApplyPending()
guard let first = pendingList.first,
first.status == PermissionApplyPendingStatus.reviewing
|| first.status == PermissionApplyPendingStatus.rejected else {
return .apply
}
return .status(applyCode: first.code)
} catch {
return .apply
}
}
///
func reloadIfNeeded(api: HomeAPI) async {
locationStateStore.restoreStateIfNeeded()

View File

@ -11,6 +11,9 @@ final class PermissionApplyStatusViewModel {
private(set) var pendingData: RoleApplyPendingResponse?
private(set) var isLoading = false
private(set) var rolePermissionList: [RolePermissionResponse] = []
private(set) var showScenicListDialog = false
private(set) var dialogRoleName = ""
private(set) var dialogScenicList: [ScenicInfo] = []
var onStateChange: (() -> Void)?
var onNavigateToEdit: ((RoleApplyPendingResponse) -> Void)?
@ -29,7 +32,11 @@ final class PermissionApplyStatusViewModel {
notifyStateChange()
do {
let pendingList = try await api.roleApplyPending()
pendingData = pendingList.first { $0.code == applyCode }
pendingData = pendingList.first {
$0.code == applyCode
&& ($0.status == PermissionApplyPendingStatus.reviewing
|| $0.status == PermissionApplyPendingStatus.rejected)
}
} catch {
pendingData = nil
}
@ -42,6 +49,22 @@ final class PermissionApplyStatusViewModel {
onNavigateToEdit?(pendingData)
}
///
func showScenicList(roleName: String, scenics: [ScenicInfo]) {
dialogRoleName = roleName
dialogScenicList = scenics
showScenicListDialog = true
notifyStateChange()
}
///
func hideScenicList() {
dialogRoleName = ""
dialogScenicList = []
showScenicListDialog = false
notifyStateChange()
}
private func notifyStateChange() {
onStateChange?()
}

View File

@ -18,10 +18,16 @@ final class PermissionApplyViewModel {
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: ((String) -> Void)?
var onSubmitSuccess: (() -> Void)?
private let appStore: AppStore
@ -30,6 +36,18 @@ final class PermissionApplyViewModel {
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
@ -45,7 +63,7 @@ final class PermissionApplyViewModel {
}
func loadRolesFromLocal() {
let rolePermissionList = appStore.permissions.rolePermissionList()
rolePermissionList = appStore.permissions.rolePermissionList()
var seen = Set<Int>()
availableRoles = rolePermissionList.compactMap { item in
guard !seen.contains(item.role.id) else { return nil }
@ -102,6 +120,7 @@ final class PermissionApplyViewModel {
func loadAvailableScenics(api: HomeAPI?) async {
guard let roleId = selectedRoleId else { return }
guard let api else { return }
guard !isLoadingScenics else { return }
isLoadingScenics = true
notifyStateChange()
do {
@ -143,7 +162,37 @@ final class PermissionApplyViewModel {
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
@ -153,7 +202,6 @@ final class PermissionApplyViewModel {
}
return copy
}
notifyStateChange()
}
///
@ -164,6 +212,12 @@ final class PermissionApplyViewModel {
}
func clearRoleSelection() {
guard !selectedRoleName.isEmpty else { return }
pendingDeletion = .role(name: selectedRoleName)
notifyStateChange()
}
private func clearRoleSelectionImmediately() {
selectedRoleId = nil
selectedRoleName = ""
selectedScenicIds = []
@ -174,33 +228,42 @@ final class PermissionApplyViewModel {
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 let roleId = selectedRoleId else {
onShowMessage?("请选择角色")
return
}
let newScenicIds = selectedScenicIds.filter { id in
!existingScenicIds(for: roleId).contains(id)
}
guard !newScenicIds.isEmpty else {
onShowMessage?("请至少选择一个景区")
guard canSubmit, let roleId = selectedRoleId else {
onShowMessage?("请选择景区和角色")
return
}
isSubmitting = true
notifyStateChange()
do {
let result = try await api.submitRoleApply(scenicIds: newScenicIds, roleId: roleId)
onShowMessage?("提交成功")
onSubmitSuccess?(result.code)
try await api.submitRoleApply(scenicIds: selectedScenicIds, roleId: roleId)
isSubmitting = false
notifyStateChange()
onSubmitSuccess?()
} catch {
onShowMessage?("提交失败,请稍后重试")
isSubmitting = false
notifyStateChange()
}
isSubmitting = false
notifyStateChange()
}
private func existingScenicIds(for roleId: Int) -> Set<Int> {