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