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

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

259 lines
10 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.

//
// PermissionApplyViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// Android `PermissionApplyScreen`
@MainActor
final class PermissionApplyViewController: BaseViewController {
private let viewModel: PermissionApplyViewModel
private let homeAPI: HomeAPI
private let pendingData: RoleApplyPendingResponse?
private let bannerView = PermissionApplyBannerView()
private let scrollView = UIScrollView()
private let cardView = UIView()
private let roleSelector = PermissionFormSelectorRow(
title: "角色信息",
tag: "单选",
description: "请选择您需要申请权限的角色",
placeholder: "请选择角色"
)
private let newRoleTags = PermissionSelectedTagsView()
private let scenicSelector = PermissionFormSelectorRow(
title: "选择景区",
tag: "可多选",
description: "请选择您需要申请权限的景区",
placeholder: "请选择景区"
)
private let selectedScenicTags = PermissionSelectedTagsView()
private let existingRoleList = PermissionExistingRoleListView()
private let bottomBar = UIView()
private let submitButton = PermissionSubmitButton()
private weak var roleDropdown: PermissionDropdownViewController?
private weak var scenicDropdown: PermissionDropdownViewController?
///
init(
pendingData: RoleApplyPendingResponse? = nil,
viewModel: PermissionApplyViewModel = PermissionApplyViewModel(),
homeAPI: HomeAPI? = nil
) {
self.pendingData = pendingData
self.viewModel = viewModel
self.homeAPI = homeAPI ?? NetworkServices.shared.homeAPI
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "申请权限"
navigationItem.backButtonDisplayMode = .minimal
}
override func setupUI() {
view.backgroundColor = PermissionApplyUITokens.pageBackground
scrollView.keyboardDismissMode = .onDrag
cardView.backgroundColor = .white
cardView.layer.cornerRadius = 8
bottomBar.backgroundColor = .white
let roleSection = UIStackView(arrangedSubviews: [roleSelector, newRoleTags])
roleSection.axis = .vertical
roleSection.spacing = 12
let scenicSection = UIStackView(arrangedSubviews: [scenicSelector, selectedScenicTags, existingRoleList])
scenicSection.axis = .vertical
scenicSection.spacing = 12
let formStack = UIStackView(arrangedSubviews: [roleSection, scenicSection])
formStack.axis = .vertical
formStack.spacing = 20
view.addSubview(bannerView)
view.addSubview(scrollView)
scrollView.addSubview(cardView)
cardView.addSubview(formStack)
view.addSubview(bottomBar)
bottomBar.addSubview(submitButton)
bannerView.snp.makeConstraints { make in
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
}
bottomBar.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
}
submitButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(16)
make.leading.trailing.equalToSuperview().inset(16)
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-16)
}
scrollView.snp.makeConstraints { make in
make.top.equalTo(bannerView.snp.bottom)
make.leading.trailing.equalToSuperview()
make.bottom.equalTo(bottomBar.snp.top)
}
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(16)
make.width.equalTo(scrollView).offset(-32)
}
formStack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(16) }
newRoleTags.isHidden = true
selectedScenicTags.isHidden = true
existingRoleList.isHidden = true
}
override func bindActions() {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() }
}
viewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
viewModel.onSubmitSuccess = { [weak self] in
Task { @MainActor in self?.handleSubmitSuccess() }
}
bannerView.onApplyNewScenic = { [weak self] in
self?.navigationController?.pushViewController(ScenicApplicationViewController(), animated: true)
}
roleSelector.onTap = { [weak self] in self?.viewModel.showRolePickerSheet() }
scenicSelector.onTap = { [weak self] in
guard let self else { return }
self.viewModel.showScenicPickerSheet()
if self.viewModel.showScenicPicker, self.viewModel.availableScenics.isEmpty {
Task { await self.viewModel.loadAvailableScenics(api: self.homeAPI) }
}
}
existingRoleList.onShowAllScenics = { [weak self] roleName, scenics in
self?.viewModel.showScenicList(roleName: roleName, scenics: scenics)
}
submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)
if let pendingData {
viewModel.initWithPendingData(pendingData)
Task { await viewModel.loadAvailableScenics(api: homeAPI) }
}
applyViewModel()
}
@MainActor
private func applyViewModel() {
if viewModel.selectedRoleName.isEmpty {
roleSelector.setValue("请选择角色", isPlaceholder: true)
} else {
roleSelector.setValue(viewModel.selectedRoleName, isPlaceholder: false)
}
newRoleTags.apply(title: "新增角色", names: viewModel.selectedRoleName.isEmpty ? [] : [viewModel.selectedRoleName]) {
[weak self] _ in self?.viewModel.clearRoleSelection()
}
let scenicValue: String
switch viewModel.selectedScenicNames.count {
case 0: scenicValue = "请选择景区"
case 1: scenicValue = viewModel.selectedScenicNames[0]
default: scenicValue = "已选中\(viewModel.selectedScenicNames.count)个景区"
}
scenicSelector.setValue(scenicValue, isPlaceholder: viewModel.selectedScenicNames.isEmpty)
selectedScenicTags.apply(title: "新增景区", names: viewModel.selectedScenicNames) { [weak self] index in
guard let self,
self.viewModel.selectedScenicIds.indices.contains(index),
self.viewModel.selectedScenicNames.indices.contains(index) else { return }
self.viewModel.removeSelectedScenic(
id: self.viewModel.selectedScenicIds[index],
name: self.viewModel.selectedScenicNames[index]
)
}
existingRoleList.apply(rolePermissions: viewModel.rolePermissionList)
submitButton.applyState(canSubmit: viewModel.canSubmit, submitting: viewModel.isSubmitting)
if viewModel.isSubmitting { showLoading() } else { hideLoading() }
scenicDropdown?.updateScenics(viewModel.filteredScenics, isLoading: viewModel.isLoadingScenics)
presentRolePickerIfNeeded()
presentScenicPickerIfNeeded()
presentDeletionIfNeeded()
presentScenicListIfNeeded()
}
private func presentRolePickerIfNeeded() {
guard viewModel.showRolePicker, roleDropdown == nil, presentedViewController == nil else { return }
let dropdown = PermissionDropdownViewController(
anchor: roleSelector.selectorControl,
mode: .roles(viewModel.availableRoles)
)
dropdown.onRoleSelect = { [weak self] role in self?.viewModel.selectRole(role) }
dropdown.onDismiss = { [weak self, weak dropdown] in
self?.viewModel.hideRolePickerSheet()
if self?.roleDropdown === dropdown { self?.roleDropdown = nil }
}
roleDropdown = dropdown
present(dropdown, animated: true)
}
private func presentScenicPickerIfNeeded() {
guard viewModel.showScenicPicker, scenicDropdown == nil, presentedViewController == nil else { return }
let dropdown = PermissionDropdownViewController(
anchor: scenicSelector.selectorControl,
mode: .scenics(viewModel.filteredScenics, isLoading: viewModel.isLoadingScenics)
)
dropdown.onScenicToggle = { [weak self] scenic in self?.viewModel.toggleScenic(scenic) }
dropdown.onSearchChange = { [weak self] query in self?.viewModel.updateScenicSearchQuery(query) }
dropdown.onDismiss = { [weak self, weak dropdown] in
self?.viewModel.hideScenicPickerSheet()
if self?.scenicDropdown === dropdown { self?.scenicDropdown = nil }
}
scenicDropdown = dropdown
present(dropdown, animated: true)
}
private func presentDeletionIfNeeded() {
guard let target = viewModel.pendingDeletion, presentedViewController == nil else { return }
present(
PermissionDeleteDialogViewController(
target: target,
onCancel: { [weak self] in self?.viewModel.cancelDeletion() },
onConfirm: { [weak self] in self?.viewModel.confirmDeletion() }
),
animated: true
)
}
private func presentScenicListIfNeeded() {
guard viewModel.showScenicListDialog, presentedViewController == nil else { return }
present(
PermissionScenicListDialogViewController(
roleName: viewModel.dialogRoleName,
scenics: viewModel.dialogScenicList,
onDismiss: { [weak self] in self?.viewModel.hideScenicList() }
),
animated: true
)
}
@objc private func submitTapped() {
Task { await viewModel.submit(api: homeAPI) }
}
private func handleSubmitSuccess() {
hideLoading()
guard let navigationController else {
showToast("提交成功")
return
}
navigationController.popViewController(animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak navigationController] in
(navigationController?.topViewController as? BaseViewController)?.showToast("提交成功")
}
}
}