Add scenic application flow aligned with Android.
Implement scenic apply models, API, upload, UI, and tests; wire entry from permission apply pages and include related permission/scenic selection UI fixes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -63,7 +63,7 @@ final class PermissionApplyStatusViewController: BaseViewController {
|
||||
Task { @MainActor in self?.navigateToEdit(pending: pending) }
|
||||
}
|
||||
bannerView.onApplyNewScenic = { [weak self] in
|
||||
self?.showToast("功能开发中")
|
||||
self?.navigationController?.pushViewController(ScenicApplicationViewController(), animated: true)
|
||||
}
|
||||
Task { await viewModel.loadPendingData(api: homeAPI) }
|
||||
}
|
||||
|
||||
@ -16,9 +16,19 @@ final class PermissionApplyViewController: BaseViewController {
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
private let bannerView = PermissionApplyBannerView()
|
||||
private let roleSelector = PermissionFormSelectorRow(title: "角色信息", subtitle: "单选 · 请选择您需要申请权限的角色", placeholder: "请选择角色")
|
||||
private let existingScenicTags = PermissionSelectedTagsView()
|
||||
private let scenicSelector = PermissionFormSelectorRow(title: "新增景区", subtitle: "多选 · 请选择需要新增权限的景区", placeholder: "请选择景区")
|
||||
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 submitButton = PermissionSubmitButton()
|
||||
|
||||
@ -49,7 +59,7 @@ final class PermissionApplyViewController: BaseViewController {
|
||||
let card = UIView()
|
||||
card.backgroundColor = .white
|
||||
card.layer.cornerRadius = 8
|
||||
let cardStack = UIStackView(arrangedSubviews: [roleSelector, existingScenicTags, scenicSelector, selectedScenicTags])
|
||||
let cardStack = UIStackView(arrangedSubviews: [roleSelector, newRoleTags, scenicSelector, selectedScenicTags])
|
||||
cardStack.axis = .vertical
|
||||
cardStack.spacing = 20
|
||||
card.addSubview(cardStack)
|
||||
@ -89,7 +99,7 @@ final class PermissionApplyViewController: BaseViewController {
|
||||
}
|
||||
|
||||
bannerView.onApplyNewScenic = { [weak self] in
|
||||
self?.showToast("功能开发中")
|
||||
self?.navigationController?.pushViewController(ScenicApplicationViewController(), animated: true)
|
||||
}
|
||||
roleSelector.onTap = { [weak self] in self?.presentRolePicker() }
|
||||
scenicSelector.onTap = { [weak self] in
|
||||
@ -112,12 +122,11 @@ final class PermissionApplyViewController: BaseViewController {
|
||||
private func applyViewModel() {
|
||||
if viewModel.selectedRoleName.isEmpty {
|
||||
roleSelector.setValue("请选择角色", isPlaceholder: true)
|
||||
existingScenicTags.isHidden = true
|
||||
newRoleTags.isHidden = true
|
||||
} else {
|
||||
roleSelector.setValue(viewModel.selectedRoleName, isPlaceholder: false)
|
||||
if let roleId = viewModel.selectedRoleId {
|
||||
let names = viewModel.existingScenics(for: roleId).map(\.name)
|
||||
existingScenicTags.apply(title: "已开通景区", names: names, onRemove: nil)
|
||||
newRoleTags.apply(title: "新增角色", names: [viewModel.selectedRoleName]) { [weak self] _ in
|
||||
self?.viewModel.clearRoleSelection()
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,9 +134,15 @@ final class PermissionApplyViewController: BaseViewController {
|
||||
scenicSelector.setValue("请选择景区", isPlaceholder: true)
|
||||
selectedScenicTags.isHidden = true
|
||||
} else {
|
||||
scenicSelector.setValue("已选 \(viewModel.selectedScenicNames.count) 个景区", isPlaceholder: false)
|
||||
let displayText: String
|
||||
if viewModel.selectedScenicNames.count == 1 {
|
||||
displayText = viewModel.selectedScenicNames[0]
|
||||
} else {
|
||||
displayText = "已选中\(viewModel.selectedScenicNames.count)个景区"
|
||||
}
|
||||
scenicSelector.setValue(displayText, isPlaceholder: false)
|
||||
selectedScenicTags.apply(
|
||||
title: "待申请景区",
|
||||
title: "新增景区",
|
||||
names: viewModel.selectedScenicNames
|
||||
) { [weak self] index in
|
||||
guard let self else { return }
|
||||
@ -186,7 +201,7 @@ final class PermissionApplyViewController: BaseViewController {
|
||||
self?.viewModel.toggleScenic(scenic)
|
||||
})
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "完成", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
|
||||
@ -57,6 +57,8 @@ final class ScenicSelectionViewController: BaseViewController, UITableViewDataSo
|
||||
headerStack.spacing = 16
|
||||
headerStack.isLayoutMarginsRelativeArrangement = true
|
||||
headerStack.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16)
|
||||
headerStack.setContentHuggingPriority(.required, for: .vertical)
|
||||
headerStack.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
|
||||
view.addSubview(headerStack)
|
||||
view.addSubview(tableView)
|
||||
@ -112,7 +114,7 @@ final class ScenicSelectionViewController: BaseViewController, UITableViewDataSo
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
locationBar.apply(locationText: viewModel.currentLocationText)
|
||||
locationBar.apply(locationText: viewModel.currentLocationText, isLocating: viewModel.isLocating)
|
||||
emptyLabel.isHidden = !viewModel.spots.isEmpty
|
||||
tableView.reloadData()
|
||||
if viewModel.isLoading {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 权限申请页橙色横幅(新景区申请入口暂 toast)。
|
||||
/// 权限申请页橙色横幅(跳转景区申请)。
|
||||
final class PermissionApplyBannerView: UIView {
|
||||
|
||||
var onApplyNewScenic: (() -> Void)?
|
||||
@ -18,11 +18,11 @@ final class PermissionApplyBannerView: UIView {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = AppColor.warningBackground
|
||||
|
||||
titleLabel.text = "希望开通全新景区?"
|
||||
titleLabel.text = "没有找到心仪的景区"
|
||||
titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
titleLabel.textColor = AppColor.warning
|
||||
|
||||
applyButton.setTitle("新景区申请 ›", for: .normal)
|
||||
applyButton.setTitle("申请平台开通新景区", for: .normal)
|
||||
applyButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
applyButton.setTitleColor(AppColor.warning, for: .normal)
|
||||
applyButton.addTarget(self, action: #selector(applyTapped), for: .touchUpInside)
|
||||
@ -52,50 +52,63 @@ final class PermissionApplyBannerView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 表单字段选择行。
|
||||
/// 表单字段选择行,对齐 Android `FormField` + Selector。
|
||||
final class PermissionFormSelectorRow: UIView {
|
||||
|
||||
var onTap: (() -> Void)?
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
private let tagLabel = UILabel()
|
||||
private let descriptionLabel = UILabel()
|
||||
private let valueLabel = UILabel()
|
||||
private let chevron = UIImageView(image: UIImage(systemName: "chevron.down"))
|
||||
|
||||
init(title: String, subtitle: String, placeholder: String) {
|
||||
init(title: String, tag: String, description: String, placeholder: String) {
|
||||
super.init(frame: .zero)
|
||||
titleLabel.text = title
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
|
||||
subtitleLabel.text = subtitle
|
||||
subtitleLabel.font = .systemFont(ofSize: 12)
|
||||
subtitleLabel.textColor = AppColor.textTertiary
|
||||
tagLabel.text = tag
|
||||
tagLabel.font = .systemFont(ofSize: 12)
|
||||
tagLabel.textColor = AppColor.textTertiary
|
||||
tagLabel.textAlignment = .right
|
||||
|
||||
descriptionLabel.text = description
|
||||
descriptionLabel.font = .systemFont(ofSize: 12)
|
||||
descriptionLabel.textColor = UIColor(hex: 0xB3B8C2)
|
||||
descriptionLabel.numberOfLines = 0
|
||||
|
||||
valueLabel.text = placeholder
|
||||
valueLabel.font = .systemFont(ofSize: 14)
|
||||
valueLabel.textColor = AppColor.textTertiary
|
||||
valueLabel.textColor = UIColor(hex: 0xB3B8C2)
|
||||
|
||||
chevron.tintColor = AppColor.textTertiary
|
||||
chevron.tintColor = UIColor(hex: 0xB3B8C2)
|
||||
chevron.contentMode = .scaleAspectFit
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(rowTapped))
|
||||
addGestureRecognizer(tap)
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(subtitleLabel)
|
||||
addSubview(tagLabel)
|
||||
addSubview(descriptionLabel)
|
||||
addSubview(valueLabel)
|
||||
addSubview(chevron)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview()
|
||||
}
|
||||
subtitleLabel.snp.makeConstraints { make in
|
||||
tagLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(titleLabel)
|
||||
make.trailing.equalToSuperview()
|
||||
make.leading.greaterThanOrEqualTo(titleLabel.snp.trailing).offset(8)
|
||||
}
|
||||
descriptionLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(4)
|
||||
make.leading.equalToSuperview()
|
||||
make.leading.trailing.equalToSuperview()
|
||||
}
|
||||
valueLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(subtitleLabel.snp.bottom).offset(12)
|
||||
make.top.equalTo(descriptionLabel.snp.bottom).offset(8)
|
||||
make.leading.bottom.equalToSuperview()
|
||||
make.trailing.lessThanOrEqualTo(chevron.snp.leading).offset(-8)
|
||||
}
|
||||
@ -113,7 +126,7 @@ final class PermissionFormSelectorRow: UIView {
|
||||
|
||||
func setValue(_ text: String, isPlaceholder: Bool) {
|
||||
valueLabel.text = text
|
||||
valueLabel.textColor = isPlaceholder ? AppColor.textTertiary : AppColor.textPrimary
|
||||
valueLabel.textColor = isPlaceholder ? UIColor(hex: 0xB3B8C2) : AppColor.textPrimary
|
||||
}
|
||||
|
||||
@objc private func rowTapped() {
|
||||
@ -129,8 +142,8 @@ final class PermissionSelectedTagsView: UIView {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
titleLabel.textColor = UIColor(hex: 0x111827)
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 8
|
||||
addSubview(titleLabel)
|
||||
@ -229,7 +242,7 @@ final class PermissionSubmitButton: UIButton {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setTitle("提交申请", for: .normal)
|
||||
setTitle("提交审核", for: .normal)
|
||||
setTitleColor(.white, for: .normal)
|
||||
titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
backgroundColor = AppColor.primary
|
||||
@ -247,6 +260,6 @@ final class PermissionSubmitButton: UIButton {
|
||||
func setSubmitting(_ submitting: Bool) {
|
||||
isEnabled = !submitting
|
||||
alpha = submitting ? 0.6 : 1
|
||||
setTitle(submitting ? "提交中..." : "提交申请", for: .normal)
|
||||
setTitle(submitting ? "提交中..." : "提交审核", for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,8 +41,10 @@ final class ScenicPermissionBannerView: UIView {
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
snp.makeConstraints { make in
|
||||
make.height.greaterThanOrEqualTo(44)
|
||||
make.height.equalTo(44)
|
||||
}
|
||||
setContentHuggingPriority(.required, for: .vertical)
|
||||
setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@ -146,18 +148,20 @@ final class ScenicCurrentLocationBar: UIView {
|
||||
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
make.centerY.equalTo(locationLabel)
|
||||
make.size.equalTo(16)
|
||||
}
|
||||
locationLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(iconView.snp.trailing).offset(4)
|
||||
make.centerY.equalToSuperview()
|
||||
make.top.bottom.equalToSuperview().inset(8)
|
||||
make.trailing.lessThanOrEqualTo(relocateButton.snp.leading).offset(-12)
|
||||
}
|
||||
relocateButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
make.centerY.equalTo(locationLabel)
|
||||
}
|
||||
setContentHuggingPriority(.required, for: .vertical)
|
||||
setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@ -165,8 +169,10 @@ final class ScenicCurrentLocationBar: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(locationText: String) {
|
||||
locationLabel.text = locationText
|
||||
func apply(locationText: String, isLocating: Bool = false) {
|
||||
locationLabel.text = isLocating ? "定位中..." : locationText
|
||||
relocateButton.isEnabled = !isLocating
|
||||
relocateButton.alpha = isLocating ? 0.5 : 1
|
||||
}
|
||||
|
||||
@objc private func relocateTapped() {
|
||||
|
||||
Reference in New Issue
Block a user