Files
suixinkan_uikit/suixinkan/UI/Orders/OrderSourcePickerViewController.swift
汉秋 2bea05b1d9 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>
2026-07-07 10:48:43 +08:00

278 lines
10 KiB
Swift
Raw 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.

//
// OrderSourcePickerViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// Bottom Sheet Android `OrderSourcePicker`
final class OrderSourcePickerViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
private enum MenuLevel {
case person
case lead
}
private let orderNumber: String
private let viewModel: OrderListViewModel
private let onPersonSelected: (OrderSourcePerson) -> Void
private let onLeadBound: (OrderSourceSelection) -> Void
private let onCallPhone: (String) -> Void
private let onPreviewImages: ([String], Int) -> Void
private var level: MenuLevel = .person
private var pendingPerson: OrderSourcePerson?
private let headerStack = UIStackView()
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let leadHeaderRow = UIStackView()
private let backButton = UIButton(type: .system)
private let leadTitleLabel = UILabel()
private let leadPhoneButton = UIButton(type: .system)
private let leadCallButton = UIButton(type: .system)
private let tableView = UITableView(frame: .zero, style: .plain)
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
private let emptyLabel = UILabel()
private var referralOrder: BoundReferralOrderEntity?
init(
orderNumber: String,
viewModel: OrderListViewModel,
referralOrder: BoundReferralOrderEntity?,
onPersonSelected: @escaping (OrderSourcePerson) -> Void,
onLeadBound: @escaping (OrderSourceSelection) -> Void,
onCallPhone: @escaping (String) -> Void,
onPreviewImages: @escaping ([String], Int) -> Void
) {
self.orderNumber = orderNumber
self.viewModel = viewModel
self.onPersonSelected = onPersonSelected
self.onLeadBound = onLeadBound
self.onCallPhone = onCallPhone
self.onPreviewImages = onPreviewImages
self.referralOrder = referralOrder
self.pendingPerson = viewModel.orderSourceSelection(
orderNumber: orderNumber,
referralOrder: referralOrder
)?.person
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupUI()
applyViewModel()
}
func reloadFromViewModel() {
applyViewModel()
}
private func setupUI() {
titleLabel.font = .systemFont(ofSize: 18, weight: .bold)
titleLabel.textColor = AppColor.text333
titleLabel.text = "选择获客员"
subtitleLabel.font = .systemFont(ofSize: 13)
subtitleLabel.textColor = AppColor.text666
subtitleLabel.text = "选择人员后,再选择该人员创建的带客单"
subtitleLabel.numberOfLines = 0
backButton.setImage(UIImage(systemName: "chevron.left"), for: .normal)
backButton.tintColor = AppColor.text333
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
leadTitleLabel.font = .systemFont(ofSize: 18, weight: .bold)
leadTitleLabel.textColor = AppColor.text333
leadTitleLabel.numberOfLines = 2
leadPhoneButton.titleLabel?.font = .systemFont(ofSize: 13)
leadPhoneButton.setTitleColor(AppColor.primary, for: .normal)
leadPhoneButton.contentHorizontalAlignment = .leading
leadPhoneButton.addTarget(self, action: #selector(leadPhoneTapped), for: .touchUpInside)
leadCallButton.backgroundColor = OrderTokens.phoneIconBackground
leadCallButton.layer.cornerRadius = 16
leadCallButton.tintColor = AppColor.primary
leadCallButton.setImage(UIImage(systemName: "phone.fill"), for: .normal)
leadCallButton.addTarget(self, action: #selector(leadPhoneTapped), for: .touchUpInside)
leadHeaderRow.axis = .horizontal
leadHeaderRow.alignment = .top
leadHeaderRow.spacing = 10
leadHeaderRow.isHidden = true
let leadTextStack = UIStackView(arrangedSubviews: [leadTitleLabel, leadPhoneButton])
leadTextStack.axis = .vertical
leadTextStack.spacing = 3
leadHeaderRow.addArrangedSubview(backButton)
leadHeaderRow.addArrangedSubview(leadTextStack)
leadHeaderRow.addArrangedSubview(leadCallButton)
headerStack.axis = .vertical
headerStack.spacing = 5
headerStack.addArrangedSubview(titleLabel)
headerStack.addArrangedSubview(subtitleLabel)
headerStack.addArrangedSubview(leadHeaderRow)
tableView.separatorStyle = .none
tableView.backgroundColor = .white
tableView.dataSource = self
tableView.delegate = self
tableView.register(OrderSourcePersonCell.self, forCellReuseIdentifier: OrderSourcePersonCell.reuseIdentifier)
tableView.register(OrderSourceLeadCell.self, forCellReuseIdentifier: OrderSourceLeadCell.reuseIdentifier)
loadingIndicator.hidesWhenStopped = true
emptyLabel.font = .systemFont(ofSize: 14)
emptyLabel.textColor = AppColor.text666
emptyLabel.textAlignment = .center
emptyLabel.numberOfLines = 0
view.addSubview(headerStack)
view.addSubview(tableView)
view.addSubview(loadingIndicator)
view.addSubview(emptyLabel)
headerStack.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(AppSpacing.md)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
}
backButton.snp.makeConstraints { make in
make.width.height.equalTo(28)
}
leadCallButton.snp.makeConstraints { make in
make.width.height.equalTo(32)
}
tableView.snp.makeConstraints { make in
make.top.equalTo(headerStack.snp.bottom).offset(14)
make.leading.trailing.bottom.equalToSuperview()
}
loadingIndicator.snp.makeConstraints { make in
make.center.equalTo(tableView)
}
emptyLabel.snp.makeConstraints { make in
make.center.equalTo(tableView)
make.leading.trailing.equalTo(tableView).inset(AppSpacing.lg)
}
}
private func applyViewModel() {
let currentSelection = viewModel.orderSourceSelection(
orderNumber: orderNumber,
referralOrder: referralOrder
)
titleLabel.isHidden = level == .lead
subtitleLabel.isHidden = level == .lead
leadHeaderRow.isHidden = level == .person
if level == .lead, let person = pendingPerson {
leadTitleLabel.text = "\(person.name)创建的带客单"
let phone = person.phone.trimmingCharacters(in: .whitespacesAndNewlines)
leadPhoneButton.setTitle(phone, for: .normal)
leadPhoneButton.isHidden = phone.isEmpty
leadCallButton.isHidden = phone.isEmpty
}
let isLoadingPeople = viewModel.isLoadingOrderSourcePeople && level == .person
let isLoadingLeads = viewModel.loadingOrderSourceLeadsFor != nil && level == .lead
if isLoadingPeople || isLoadingLeads {
loadingIndicator.startAnimating()
tableView.isHidden = true
emptyLabel.isHidden = true
} else {
loadingIndicator.stopAnimating()
tableView.isHidden = false
let isEmpty: Bool
switch level {
case .person:
isEmpty = viewModel.orderSourcePeople.isEmpty
case .lead:
isEmpty = leadsForPendingPerson.isEmpty
}
emptyLabel.isHidden = !isEmpty
emptyLabel.text = level == .person ? "暂无合作获客员" : "暂无可绑定带客单"
tableView.reloadData()
}
_ = currentSelection
}
private var leadsForPendingPerson: [OrderSourceLead] {
guard let person = pendingPerson else { return [] }
return viewModel.orderSourceLeads[person.key] ?? []
}
@objc private func backTapped() {
level = .person
applyViewModel()
}
@objc private func leadPhoneTapped() {
guard let phone = pendingPerson?.phone.trimmingCharacters(in: .whitespacesAndNewlines), !phone.isEmpty else { return }
onCallPhone(phone)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch level {
case .person:
return viewModel.orderSourcePeople.count
case .lead:
return leadsForPendingPerson.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let currentSelection = viewModel.orderSourceSelection(
orderNumber: orderNumber,
referralOrder: referralOrder
)
switch level {
case .person:
let cell = tableView.dequeueReusableCell(
withIdentifier: OrderSourcePersonCell.reuseIdentifier,
for: indexPath
) as! OrderSourcePersonCell
let person = viewModel.orderSourcePeople[indexPath.row]
cell.configure(
person: person,
selected: currentSelection?.person.key == person.key
)
cell.onCallPhone = onCallPhone
return cell
case .lead:
let cell = tableView.dequeueReusableCell(
withIdentifier: OrderSourceLeadCell.reuseIdentifier,
for: indexPath
) as! OrderSourceLeadCell
let lead = leadsForPendingPerson[indexPath.row]
let selected = currentSelection?.person.key == pendingPerson?.key
&& currentSelection?.lead.key == lead.key
cell.configure(lead: lead, selected: selected)
cell.onCallPhone = onCallPhone
cell.onImageTap = onPreviewImages
cell.onSelect = { [weak self] in
guard let self, let person = self.pendingPerson else { return }
let selection = OrderSourceSelection(person: person, lead: lead)
self.onLeadBound(selection)
}
return cell
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard level == .person else { return }
let person = viewModel.orderSourcePeople[indexPath.row]
pendingPerson = person
level = .lead
onPersonSelected(person)
applyViewModel()
}
}