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:
2026-07-07 11:09:27 +08:00
parent 2bea05b1d9
commit 77fec21563
22 changed files with 2202 additions and 216 deletions

View File

@ -0,0 +1,189 @@
//
// OrderSourceLeadListViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// push
final class OrderSourceLeadListViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate {
private let person: OrderSourcePerson
private let orderNumber: String
private let viewModel: OrderListViewModel
private let referralOrder: BoundReferralOrderEntity?
private let loadLeads: (OrderSourcePerson) async -> Void
private let onLeadBound: (OrderSourceSelection) -> Void
private let onCallPhone: (String) -> Void
private let onPreviewImages: ([String], Int) -> Void
private let phoneRow = UIStackView()
private let phoneLabel = UILabel()
private let callButton = UIButton(type: .system)
private let tableView = UITableView(frame: .zero, style: .plain)
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
private let emptyLabel = UILabel()
init(
person: OrderSourcePerson,
orderNumber: String,
viewModel: OrderListViewModel,
referralOrder: BoundReferralOrderEntity?,
loadLeads: @escaping (OrderSourcePerson) async -> Void,
onLeadBound: @escaping (OrderSourceSelection) -> Void,
onCallPhone: @escaping (String) -> Void,
onPreviewImages: @escaping ([String], Int) -> Void
) {
self.person = person
self.orderNumber = orderNumber
self.viewModel = viewModel
self.referralOrder = referralOrder
self.loadLeads = loadLeads
self.onLeadBound = onLeadBound
self.onCallPhone = onCallPhone
self.onPreviewImages = onPreviewImages
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "\(person.name)的带客单"
}
override func setupUI() {
view.backgroundColor = .white
phoneLabel.font = .systemFont(ofSize: 14)
phoneLabel.textColor = AppColor.primary
phoneLabel.text = person.phone
callButton.backgroundColor = OrderTokens.phoneIconBackground
callButton.layer.cornerRadius = 16
callButton.tintColor = AppColor.primary
callButton.setImage(UIImage(systemName: "phone.fill"), for: .normal)
callButton.addTarget(self, action: #selector(callTapped), for: .touchUpInside)
phoneRow.axis = .horizontal
phoneRow.alignment = .center
phoneRow.spacing = 10
phoneRow.addArrangedSubview(phoneLabel)
phoneRow.addArrangedSubview(callButton)
let phone = person.phone.trimmingCharacters(in: .whitespacesAndNewlines)
phoneRow.isHidden = phone.isEmpty
tableView.separatorStyle = .none
tableView.backgroundColor = .white
tableView.dataSource = self
tableView.delegate = self
tableView.register(OrderSourceLeadCell.self, forCellReuseIdentifier: OrderSourceLeadCell.reuseIdentifier)
loadingIndicator.hidesWhenStopped = true
loadingIndicator.color = AppColor.primary
emptyLabel.font = .systemFont(ofSize: 14)
emptyLabel.textColor = AppColor.text666
emptyLabel.textAlignment = .center
emptyLabel.numberOfLines = 0
emptyLabel.text = "暂无可绑定带客单"
view.addSubview(phoneRow)
view.addSubview(tableView)
view.addSubview(loadingIndicator)
view.addSubview(emptyLabel)
}
override func setupConstraints() {
phoneRow.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(AppSpacing.sm)
make.leading.equalToSuperview().inset(AppSpacing.md)
}
callButton.snp.makeConstraints { make in
make.width.height.equalTo(32)
}
tableView.snp.makeConstraints { make in
make.top.equalTo(phoneRow.snp.bottom).offset(AppSpacing.sm)
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)
}
}
override func viewDidLoad() {
super.viewDidLoad()
applyViewModel()
Task {
await loadLeads(person)
reloadFromViewModel()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParent {
navigationController?.setNavigationBarHidden(true, animated: false)
}
}
func reloadFromViewModel() {
applyViewModel()
}
private func applyViewModel() {
let isLoading = viewModel.loadingOrderSourceLeadsFor == person.key
let leads = viewModel.orderSourceLeads[person.key] ?? []
if isLoading {
loadingIndicator.startAnimating()
tableView.isHidden = true
emptyLabel.isHidden = true
} else {
loadingIndicator.stopAnimating()
tableView.isHidden = false
emptyLabel.isHidden = !leads.isEmpty
tableView.reloadData()
}
}
@objc private func callTapped() {
onCallPhone(person.phone)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewModel.orderSourceLeads[person.key]?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: OrderSourceLeadCell.reuseIdentifier,
for: indexPath
) as! OrderSourceLeadCell
let leads = viewModel.orderSourceLeads[person.key] ?? []
let lead = leads[indexPath.row]
let currentSelection = viewModel.orderSourceSelection(
orderNumber: orderNumber,
referralOrder: referralOrder
)
let selected = currentSelection?.person.key == person.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 else { return }
let selection = OrderSourceSelection(person: self.person, lead: lead)
self.onLeadBound(selection)
}
return cell
}
}

View File

@ -6,58 +6,39 @@
import SnapKit
import UIKit
/// Bottom Sheet Android `OrderSourcePicker`
/// Bottom Sheet push
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 referralOrder: BoundReferralOrderEntity?
private let loadLeads: (OrderSourcePerson) async -> 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,
loadLeads: @escaping (OrderSourcePerson) async -> Void,
onLeadBound: @escaping (OrderSourceSelection) -> Void,
onCallPhone: @escaping (String) -> Void,
onPreviewImages: @escaping ([String], Int) -> Void
) {
self.orderNumber = orderNumber
self.viewModel = viewModel
self.onPersonSelected = onPersonSelected
self.referralOrder = referralOrder
self.loadLeads = loadLeads
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)
}
@ -75,6 +56,8 @@ final class OrderSourcePickerViewController: UIViewController, UITableViewDataSo
func reloadFromViewModel() {
applyViewModel()
(navigationController?.topViewController as? OrderSourceLeadListViewController)?
.reloadFromViewModel()
}
private func setupUI() {
@ -87,72 +70,37 @@ final class OrderSourcePickerViewController: UIViewController, UITableViewDataSo
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
loadingIndicator.color = AppColor.primary
emptyLabel.font = .systemFont(ofSize: 14)
emptyLabel.textColor = AppColor.text666
emptyLabel.textAlignment = .center
emptyLabel.numberOfLines = 0
emptyLabel.text = "暂无合作获客员"
view.addSubview(headerStack)
view.addSubview(titleLabel)
view.addSubview(subtitleLabel)
view.addSubview(tableView)
view.addSubview(loadingIndicator)
view.addSubview(emptyLabel)
headerStack.snp.makeConstraints { make in
titleLabel.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)
subtitleLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(5)
make.leading.trailing.equalTo(titleLabel)
}
tableView.snp.makeConstraints { make in
make.top.equalTo(headerStack.snp.bottom).offset(14)
make.top.equalTo(subtitleLabel.snp.bottom).offset(14)
make.leading.trailing.bottom.equalToSuperview()
}
loadingIndicator.snp.makeConstraints { make in
@ -165,113 +113,54 @@ final class OrderSourcePickerViewController: UIViewController, UITableViewDataSo
}
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 {
if viewModel.isLoadingOrderSourcePeople {
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
}
let isEmpty = viewModel.orderSourcePeople.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
}
viewModel.orderSourcePeople.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: OrderSourcePersonCell.reuseIdentifier,
for: indexPath
) as! OrderSourcePersonCell
let person = viewModel.orderSourcePeople[indexPath.row]
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
}
cell.configure(
person: person,
selected: currentSelection?.person.key == person.key
)
cell.onCallPhone = onCallPhone
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()
let leadController = OrderSourceLeadListViewController(
person: person,
orderNumber: orderNumber,
viewModel: viewModel,
referralOrder: referralOrder,
loadLeads: loadLeads,
onLeadBound: onLeadBound,
onCallPhone: onCallPhone,
onPreviewImages: onPreviewImages
)
navigationController?.setNavigationBarHidden(false, animated: true)
navigationController?.pushViewController(leadController, animated: true)
}
}

View File

@ -473,11 +473,9 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
orderNumber: order.orderNumber,
viewModel: self.orderListViewModel,
referralOrder: order.referralOrder,
onPersonSelected: { [weak self] person in
loadLeads: { [weak self] person in
guard let self else { return }
Task {
await self.orderListViewModel.loadOrderSourceLeads(person: person, api: self.orderAPI)
}
await self.orderListViewModel.loadOrderSourceLeads(person: person, api: self.orderAPI)
},
onLeadBound: { [weak self] selection in
guard let self, let orderNumber = self.pendingOrderSourceNumber else { return }