Add scenic selection and cooperation order flows aligned with Android.
Upgrade scenic picker with search, location sorting, permission apply/status, and blocking home dialog; add cooperation order module and order source picker improvements with unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -6,16 +6,23 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 景区选择页,使用 role-permission 返回的景区列表。
|
||||
final class ScenicSelectionViewController: BaseViewController {
|
||||
/// 景区选择页,对齐 Android `ScenicSelectionScreen`。
|
||||
final class ScenicSelectionViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate {
|
||||
|
||||
var onSelected: (() -> Void)?
|
||||
var onSelectionComplete: (() -> Void)?
|
||||
|
||||
private let scenicList: [ScenicInfo]
|
||||
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
|
||||
private let viewModel: ScenicSelectionViewModel
|
||||
private let homeAPI: HomeAPI
|
||||
|
||||
init(scenicList: [ScenicInfo]) {
|
||||
self.scenicList = scenicList
|
||||
private let bannerView = ScenicPermissionBannerView()
|
||||
private let searchBar = ScenicSelectionSearchBar()
|
||||
private let locationBar = ScenicCurrentLocationBar()
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
private let emptyLabel = UILabel()
|
||||
|
||||
init(viewModel: ScenicSelectionViewModel = ScenicSelectionViewModel(), homeAPI: HomeAPI = NetworkServices.shared.homeAPI) {
|
||||
self.viewModel = viewModel
|
||||
self.homeAPI = homeAPI
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@ -25,61 +32,122 @@ final class ScenicSelectionViewController: BaseViewController {
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "选择景区"
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
||||
barButtonSystemItem: .close,
|
||||
target: self,
|
||||
action: #selector(closeTapped)
|
||||
)
|
||||
title = "景区选择"
|
||||
navigationController?.setNavigationBarHidden(false, animated: false)
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = UIColor(hex: 0xF7FAFF)
|
||||
view.backgroundColor = .white
|
||||
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.separatorStyle = .none
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
|
||||
view.addSubview(tableView)
|
||||
}
|
||||
tableView.register(ScenicSpotCell.self, forCellReuseIdentifier: ScenicSpotCell.reuseIdentifier)
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
|
||||
override func setupConstraints() {
|
||||
emptyLabel.text = "暂无景区"
|
||||
emptyLabel.font = .systemFont(ofSize: 14)
|
||||
emptyLabel.textColor = AppColor.textTertiary
|
||||
emptyLabel.textAlignment = .center
|
||||
emptyLabel.isHidden = true
|
||||
|
||||
let headerStack = UIStackView(arrangedSubviews: [bannerView, searchBar, locationBar])
|
||||
headerStack.axis = .vertical
|
||||
headerStack.spacing = 16
|
||||
headerStack.isLayoutMarginsRelativeArrangement = true
|
||||
headerStack.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16)
|
||||
|
||||
view.addSubview(headerStack)
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(emptyLabel)
|
||||
|
||||
headerStack.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
}
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
||||
make.top.equalTo(headerStack.snp.bottom)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
emptyLabel.snp.makeConstraints { make in
|
||||
make.center.equalTo(tableView)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func closeTapped() {
|
||||
dismiss(animated: 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.onSelectionComplete = { [weak self] in
|
||||
Task { @MainActor in
|
||||
self?.onSelectionComplete?()
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
viewModel.onNavigateApply = { [weak self] in
|
||||
Task { @MainActor in self?.pushPermissionApply() }
|
||||
}
|
||||
viewModel.onNavigateApplyStatus = { [weak self] applyCode in
|
||||
Task { @MainActor in self?.pushPermissionApplyStatus(applyCode: applyCode) }
|
||||
}
|
||||
|
||||
bannerView.onApplyNow = { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.onApplyNow(api: self.homeAPI) }
|
||||
}
|
||||
searchBar.onSearchTextChange = { [weak self] query in
|
||||
self?.viewModel.onSearchQueryChange(query)
|
||||
}
|
||||
locationBar.onRelocate = { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.startLocation() }
|
||||
}
|
||||
|
||||
viewModel.loadFromLocal()
|
||||
Task { await viewModel.startLocation() }
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
locationBar.apply(locationText: viewModel.currentLocationText)
|
||||
emptyLabel.isHidden = !viewModel.spots.isEmpty
|
||||
tableView.reloadData()
|
||||
if viewModel.isLoading {
|
||||
showLoading()
|
||||
} else {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
private func pushPermissionApply(pendingData: RoleApplyPendingResponse? = nil) {
|
||||
let controller = PermissionApplyViewController(pendingData: pendingData)
|
||||
navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
|
||||
private func pushPermissionApplyStatus(applyCode: String) {
|
||||
let controller = PermissionApplyStatusViewController(applyCode: applyCode)
|
||||
navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
|
||||
// MARK: - UITableView
|
||||
|
||||
extension ScenicSelectionViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
scenicList.count
|
||||
viewModel.spots.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
|
||||
var config = cell.defaultContentConfiguration()
|
||||
config.text = scenicList[indexPath.row].name
|
||||
cell.contentConfiguration = config
|
||||
cell.accessoryType = scenicList[indexPath.row].id == AppStore.shared.currentScenicId ? .checkmark : .none
|
||||
let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: ScenicSpotCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as! ScenicSpotCell
|
||||
let spot = viewModel.spots[indexPath.row]
|
||||
cell.apply(spot)
|
||||
cell.onTap = { [weak self] in
|
||||
self?.viewModel.selectSpot(spot)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
let scenic = scenicList[indexPath.row]
|
||||
AppStore.shared.currentScenicId = scenic.id
|
||||
AppStore.shared.currentScenicName = scenic.name
|
||||
NotificationCenter.default.post(
|
||||
name: NotificationName.scenicDidChange,
|
||||
object: nil,
|
||||
userInfo: [
|
||||
NotificationUserInfoKey.scenicId: scenic.id,
|
||||
NotificationUserInfoKey.scenicName: scenic.name,
|
||||
]
|
||||
)
|
||||
onSelected?()
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user