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:
2026-07-07 10:48:43 +08:00
parent 3c4ca7eefb
commit 2bea05b1d9
50 changed files with 5603 additions and 191 deletions

View File

@ -20,6 +20,7 @@ struct OrderCardActions {
var onRefinedChanged: ((Int) -> Void)?
var onHistoricalPhoto: (() -> Void)?
var onOrderSourceTap: (() -> Void)?
var onOrderSourceImageTap: (([String], Int) -> Void)?
}
/// Android `OrderView`
@ -34,8 +35,7 @@ final class OrderCardCell: UITableViewCell {
private let statusBadge = OrderStatusBadgeView()
private let createdAtLabel = UILabel()
private let typeChip = OrderTypeChipView()
private let orderSourceContainer = UIView()
private let orderSourceButton = UIButton(type: .system)
private let orderSourceEntryView = OrderSourceEntryView()
private let actionRow = UIStackView()
private let refundButton = OrderActionButton(title: "退款", style: .destructiveLight)
private let verifyButton = OrderActionButton(title: "核销订单", style: .primaryFilled)
@ -79,7 +79,7 @@ final class OrderCardCell: UITableViewCell {
hideWriteActions: Bool,
actions: OrderCardActions,
showOrderSource: Bool,
sourceText: String?
sourceSelection: OrderSourceSelection?
) {
self.actions = actions
orderNumberLabel.text = "订单号:\(order.orderNumber)"
@ -98,8 +98,10 @@ final class OrderCardCell: UITableViewCell {
uploadTaskButton.isHidden = hideWriteActions || order.orderType != 19
orderSourceContainer.isHidden = !showOrderSource
orderSourceButton.setTitle(sourceText ?? "选择订单来源", for: .normal)
orderSourceEntryView.isHidden = !showOrderSource
if showOrderSource {
orderSourceEntryView.apply(selection: sourceSelection)
}
let showEdit = (order.orderStatus == 18 || order.orderStatus == 30) && order.orderType != 19
editOptionView.isHidden = !showEdit
@ -149,17 +151,9 @@ final class OrderCardCell: UITableViewCell {
headerRow.addArrangedSubview(orderNumberLabel)
headerRow.addArrangedSubview(statusBadge)
orderSourceContainer.backgroundColor = OrderTokens.sourceBackground
orderSourceContainer.layer.cornerRadius = AppRadius.sm
orderSourceContainer.layer.borderWidth = 1
orderSourceContainer.layer.borderColor = OrderTokens.sourceBorder.cgColor
orderSourceButton.setTitleColor(AppColor.primary, for: .normal)
orderSourceButton.titleLabel?.font = .app(.body)
orderSourceButton.contentHorizontalAlignment = .leading
orderSourceButton.addTarget(self, action: #selector(orderSourceTapped), for: .touchUpInside)
orderSourceContainer.addSubview(orderSourceButton)
orderSourceButton.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(AppSpacing.sm)
orderSourceEntryView.addTarget(self, action: #selector(orderSourceTapped), for: .touchUpInside)
orderSourceEntryView.onImageTap = { [weak self] images, index in
self?.actions.onOrderSourceImageTap?(images, index)
}
actionRow.axis = .horizontal
@ -211,7 +205,7 @@ final class OrderCardCell: UITableViewCell {
headerRow,
createdAtLabel,
typeChip,
orderSourceContainer,
orderSourceEntryView,
actionRow,
uploadTaskButton,
editOptionView,

View File

@ -6,23 +6,58 @@
import SnapKit
import UIKit
///
/// Bottom Sheet Android `OrderSourcePicker`
final class OrderSourcePickerViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var onPersonSelected: ((OrderSourcePerson) -> Void)?
var onLeadSelected: ((OrderSourceSelection) -> Void)?
private enum MenuLevel {
case person
case lead
}
private let people: [OrderSourcePerson]
private let leads: [OrderSourceLead]
private let selectedPerson: OrderSourcePerson?
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
private var showingLeads = false
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
init(people: [OrderSourcePerson], leads: [OrderSourceLead], selectedPerson: OrderSourcePerson?) {
self.people = people
self.leads = leads
self.selectedPerson = selectedPerson
self.showingLeads = !leads.isEmpty
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)
}
@ -33,44 +68,210 @@ final class OrderSourcePickerViewController: UIViewController, UITableViewDataSo
override func viewDidLoad() {
super.viewDidLoad()
title = showingLeads ? "选择带客单" : "选择获客员"
view.backgroundColor = UIColor(hex: 0xF5F5F5)
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "关闭", style: .plain, target: self, action: #selector(closeTapped))
tableView.dataSource = self
tableView.delegate = self
view.addSubview(tableView)
tableView.snp.makeConstraints { $0.edges.equalToSuperview() }
view.backgroundColor = .white
setupUI()
applyViewModel()
}
@objc private func closeTapped() {
dismiss(animated: true)
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 {
showingLeads ? leads.count : people.count
switch level {
case .person:
return viewModel.orderSourcePeople.count
case .lead:
return leadsForPendingPerson.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
if showingLeads {
let lead = leads[indexPath.row]
cell.textLabel?.text = lead.displayText
cell.detailTextLabel?.text = lead.userMobile
} else {
let person = people[indexPath.row]
cell.textLabel?.text = person.name
cell.detailTextLabel?.text = person.phone
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
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if showingLeads, let person = selectedPerson {
let lead = leads[indexPath.row]
onLeadSelected?(OrderSourceSelection(person: person, lead: lead))
dismiss(animated: true)
} else {
onPersonSelected?(people[indexPath.row])
}
guard level == .person else { return }
let person = viewModel.orderSourcePeople[indexPath.row]
pendingPerson = person
level = .lead
onPersonSelected(person)
applyViewModel()
}
}

View File

@ -20,7 +20,7 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
private var hasInitialized = false
private var isStoreMode: Bool { AppStore.shared.currentAppRole == .storeAdmin }
private var pendingOrderSourceNumber: String?
private var pendingOrderSourcePerson: OrderSourcePerson?
private weak var orderSourcePicker: OrderSourcePickerViewController?
private var pendingGiftOrderNumber: String?
private var isFetchingMultiVerifySpots = false
private let locationProvider = OrderLocationProvider()
@ -51,11 +51,17 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
override func bindActions() {
orderListViewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() }
Task { @MainActor in
self?.applyViewModel()
self?.orderSourcePicker?.reloadFromViewModel()
}
}
orderListViewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
orderListViewModel.onNavigateBindAcquirer = { [weak self] saleUserId in
Task { @MainActor in self?.presentBindAcquirer(saleUserId: saleUserId) }
}
depositViewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() }
}
@ -259,6 +265,13 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
present(nav, animated: true)
}
private func presentBindAcquirer(saleUserId: Int) {
dismiss(animated: true) { [weak self] in
let controller = BindAcquirerViewController(saleUserId: saleUserId)
self?.navigationController?.pushViewController(controller, animated: true)
}
}
private func fetchMultiVerifySpotsIfNeeded() async {
isFetchingMultiVerifySpots = true
defer { isFetchingMultiVerifySpots = false }
@ -322,7 +335,7 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
hideWriteActions: orderListViewModel.isScenicAdminReadOnly,
actions: makeOrderActions(for: order),
showOrderSource: orderListViewModel.shouldShowOrderSource(for: order),
sourceText: source.map { "\($0.person.name) · \($0.lead.displayText)" }
sourceSelection: source
)
listView.notifyLoadMoreIfNeeded(at: indexPath.row, shouldLoad: orderListViewModel.shouldLoadMore(at: indexPath.row))
return cell
@ -363,6 +376,9 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
actions.onOrderSourceTap = { [weak self] in
self?.presentOrderSourcePicker(for: order)
}
actions.onOrderSourceImageTap = { [weak self] images, index in
self?.presentImagePreview(images: images, startIndex: index)
}
if let travel = order.photoTravel, travel.canGiftRetouch {
actions.onSendGift = { [weak self] in
guard let self else { return }
@ -451,23 +467,23 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
pendingOrderSourceNumber = order.orderNumber
Task {
await orderListViewModel.onOrderSourceSheetOpen(api: orderAPI)
let controller = OrderSourcePickerViewController(
people: orderListViewModel.orderSourcePeople,
leads: [],
selectedPerson: nil
)
controller.onPersonSelected = { [weak self] person in
await MainActor.run { [weak self] in
guard let self else { return }
self.pendingOrderSourcePerson = person
Task {
await self.orderListViewModel.loadOrderSourceLeads(person: person, api: self.orderAPI)
let leadsController = OrderSourcePickerViewController(
people: self.orderListViewModel.orderSourcePeople,
leads: self.orderListViewModel.orderSourceLeads[person.key] ?? [],
selectedPerson: person
)
leadsController.onLeadSelected = { [weak self] selection in
let controller = OrderSourcePickerViewController(
orderNumber: order.orderNumber,
viewModel: self.orderListViewModel,
referralOrder: order.referralOrder,
onPersonSelected: { [weak self] person in
guard let self else { return }
Task {
await self.orderListViewModel.loadOrderSourceLeads(person: person, api: self.orderAPI)
}
},
onLeadBound: { [weak self] selection in
guard let self, let orderNumber = self.pendingOrderSourceNumber else { return }
self.dismiss(animated: true) {
self.orderSourcePicker = nil
}
Task {
await self.orderListViewModel.bindOrderSource(
orderNumber: orderNumber,
@ -475,11 +491,44 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
api: self.orderAPI
)
}
},
onCallPhone: { [weak self] phone in
self?.callPhone(phone)
},
onPreviewImages: { [weak self] urls, index in
self?.presentImagePreview(images: urls, startIndex: index)
}
)
self.orderSourcePicker = controller
let nav = UINavigationController(rootViewController: controller)
nav.setNavigationBarHidden(true, animated: false)
if let sheet = nav.sheetPresentationController {
sheet.prefersGrabberVisible = true
if #available(iOS 16.0, *) {
sheet.detents = [
.custom(identifier: .init("orderSource")) { context in
context.maximumDetentValue * 0.82
},
.large(),
]
sheet.selectedDetentIdentifier = .init("orderSource")
} else {
sheet.detents = [.large()]
}
self.present(leadsController, animated: true)
}
self.present(nav, animated: true)
}
present(controller, animated: true)
}
}
private func callPhone(_ phone: String) {
let trimmed = phone.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let url = URL(string: "tel://\(trimmed)") else { return }
UIApplication.shared.open(url)
}
private func presentImagePreview(images: [String], startIndex: Int) {
guard !images.isEmpty else { return }
present(ImagePreviewViewController(imageURLs: images, startIndex: startIndex), animated: true)
}
}

View File

@ -0,0 +1,112 @@
//
// OrderSourceEntryView.swift
// suixinkan
//
import SnapKit
import UIKit
/// Android `OrderSourcePicker`
final class OrderSourceEntryView: UIControl {
var onImageTap: (([String], Int) -> Void)?
private let titleLabel = UILabel()
private let chevronView = UIImageView()
private let hintLabel = UILabel()
private let personRow = UIStackView()
private let personNameLabel = UILabel()
private let personPhoneLabel = UILabel()
private let divider = UIView()
private let leadContentView = OrderSourceLeadContentView()
private var currentImages: [String] = []
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func apply(selection: OrderSourceSelection?) {
if let selection {
hintLabel.isHidden = true
personRow.isHidden = false
divider.isHidden = false
leadContentView.isHidden = false
personNameLabel.text = selection.person.name
let phone = selection.person.phone.trimmingCharacters(in: .whitespacesAndNewlines)
personPhoneLabel.text = phone
personPhoneLabel.isHidden = phone.isEmpty
currentImages = selection.lead.images
leadContentView.apply(lead: selection.lead, compact: true, enablePhoneCall: false)
leadContentView.onImageTap = { [weak self] _, index in
guard let self else { return }
self.onImageTap?(self.currentImages, index)
}
} else {
hintLabel.isHidden = false
personRow.isHidden = true
divider.isHidden = true
leadContentView.isHidden = true
currentImages = []
}
}
private func setupUI() {
backgroundColor = OrderTokens.sourceBackground
layer.cornerRadius = AppRadius.sm
layer.borderWidth = 1
layer.borderColor = OrderTokens.sourceBorder.cgColor
titleLabel.text = "订单来源"
titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
titleLabel.textColor = OrderTokens.label563
chevronView.image = UIImage(systemName: "chevron.right")
chevronView.tintColor = AppColor.primary
chevronView.contentMode = .scaleAspectFit
hintLabel.text = "选择获客员及其创建的带客单"
hintLabel.font = .systemFont(ofSize: 13)
hintLabel.textColor = AppColor.text666
hintLabel.numberOfLines = 0
personRow.axis = .horizontal
personRow.alignment = .center
personRow.spacing = 10
personNameLabel.font = .systemFont(ofSize: 13, weight: .medium)
personNameLabel.textColor = AppColor.text333
personPhoneLabel.font = .systemFont(ofSize: 13)
personPhoneLabel.textColor = AppColor.text666
personRow.addArrangedSubview(personNameLabel)
personRow.addArrangedSubview(personPhoneLabel)
divider.backgroundColor = OrderTokens.sourceDivider
let headerRow = UIStackView(arrangedSubviews: [titleLabel, UIView(), chevronView])
headerRow.axis = .horizontal
headerRow.alignment = .center
let stack = UIStackView(arrangedSubviews: [headerRow, hintLabel, personRow, divider, leadContentView])
stack.axis = .vertical
stack.spacing = 10
stack.isUserInteractionEnabled = false
addSubview(stack)
stack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(AppSpacing.sm)
}
chevronView.snp.makeConstraints { make in
make.width.height.equalTo(20)
}
divider.snp.makeConstraints { make in
make.height.equalTo(OrderTokens.dividerHeight)
}
}
}

View File

@ -0,0 +1,106 @@
//
// OrderSourceLeadCell.swift
// suixinkan
//
import SnapKit
import UIKit
/// cell Android `SourceLeadItem`
final class OrderSourceLeadCell: UITableViewCell {
static let reuseIdentifier = "OrderSourceLeadCell"
var onCallPhone: ((String) -> Void)?
var onImageTap: (([String], Int) -> Void)?
var onSelect: (() -> Void)?
private let cardView = UIView()
private let tagLabel = UILabel()
private let checkView = UIImageView()
private let contentViewPanel = OrderSourceLeadContentView()
private let actionButton = UIButton(type: .system)
private var currentImages: [String] = []
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.backgroundColor = .clear
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(lead: OrderSourceLead, selected: Bool) {
currentImages = lead.images
contentViewPanel.apply(lead: lead, compact: false, enablePhoneCall: true)
contentViewPanel.onCallPhone = { [weak self] phone in self?.onCallPhone?(phone) }
contentViewPanel.onImageTap = { [weak self] images, index in
self?.onImageTap?(images, index)
}
checkView.isHidden = !selected
cardView.layer.borderColor = (selected ? AppColor.primary : OrderTokens.sourceBorder).cgColor
cardView.backgroundColor = selected ? AppColor.primaryLight.withAlphaComponent(0.4) : .white
actionButton.setTitle(selected ? "当前已选择" : "选择此带客单", for: .normal)
actionButton.setTitleColor(selected ? AppColor.primary : .white, for: .normal)
actionButton.backgroundColor = selected ? AppColor.primaryLight : AppColor.primary
}
private func setupUI() {
cardView.layer.cornerRadius = AppRadius.sm
cardView.layer.borderWidth = 1
contentView.addSubview(cardView)
tagLabel.text = "带客单"
tagLabel.font = .systemFont(ofSize: 13, weight: .medium)
tagLabel.textColor = AppColor.primary
tagLabel.backgroundColor = AppColor.primaryLight.withAlphaComponent(0.8)
tagLabel.layer.cornerRadius = 4
tagLabel.clipsToBounds = true
tagLabel.textAlignment = .center
checkView.image = UIImage(systemName: "checkmark")
checkView.tintColor = AppColor.primary
checkView.contentMode = .scaleAspectFit
actionButton.titleLabel?.font = .systemFont(ofSize: 14)
actionButton.layer.cornerRadius = 6
actionButton.addTarget(self, action: #selector(selectTapped), for: .touchUpInside)
cardView.addSubview(tagLabel)
cardView.addSubview(checkView)
cardView.addSubview(contentViewPanel)
cardView.addSubview(actionButton)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 0, bottom: 12, right: 0))
}
tagLabel.snp.makeConstraints { make in
make.leading.top.equalToSuperview().inset(12)
make.height.equalTo(24)
make.width.greaterThanOrEqualTo(56)
}
checkView.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(12)
make.centerY.equalTo(tagLabel)
make.width.height.equalTo(20)
}
contentViewPanel.snp.makeConstraints { make in
make.top.equalTo(tagLabel.snp.bottom).offset(10)
make.leading.trailing.equalToSuperview().inset(12)
}
actionButton.snp.makeConstraints { make in
make.top.equalTo(contentViewPanel.snp.bottom).offset(10)
make.leading.trailing.bottom.equalToSuperview().inset(12)
make.height.equalTo(40)
}
}
@objc private func selectTapped() {
onSelect?()
}
}

View File

@ -0,0 +1,138 @@
//
// OrderSourceLeadContentView.swift
// suixinkan
//
import SnapKit
import UIKit
/// Android `SourceLeadContent`
final class OrderSourceLeadContentView: UIView {
var onCallPhone: ((String) -> Void)?
var onImageTap: (([String], Int) -> Void)?
private let phoneRow = UIStackView()
private let phoneTitleLabel = UILabel()
private let phoneValueLabel = UILabel()
private let remarkLabel = UILabel()
private let imageRow = UIStackView()
private let emptyImageLabel = UILabel()
private var currentImages: [String] = []
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func apply(lead: OrderSourceLead, compact: Bool, enablePhoneCall: Bool = true) {
let phone = lead.userPhone.trimmingCharacters(in: .whitespacesAndNewlines)
phoneRow.isHidden = phone.isEmpty
phoneValueLabel.text = phone
phoneValueLabel.textColor = enablePhoneCall ? AppColor.primary : AppColor.text333
phoneValueLabel.isUserInteractionEnabled = enablePhoneCall && !phone.isEmpty
let remark = lead.remark.trimmingCharacters(in: .whitespacesAndNewlines)
remarkLabel.isHidden = remark.isEmpty
remarkLabel.text = remark
remarkLabel.font = compact ? .systemFont(ofSize: 12) : .systemFont(ofSize: 13)
remarkLabel.numberOfLines = compact ? 2 : 4
imageRow.arrangedSubviews.forEach { $0.removeFromSuperview() }
emptyImageLabel.isHidden = true
if lead.images.isEmpty {
if !compact {
emptyImageLabel.isHidden = false
}
} else {
currentImages = lead.images
lead.images.prefix(3).enumerated().forEach { index, url in
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 6
imageView.backgroundColor = OrderTokens.sourceLeadPlaceholder
imageView.loadRemoteImage(urlString: url, placeholderColor: OrderTokens.sourceLeadPlaceholder)
imageView.isUserInteractionEnabled = true
imageView.tag = index
imageView.addGestureRecognizer(
UITapGestureRecognizer(target: self, action: #selector(imageTapped(_:)))
)
imageRow.addArrangedSubview(imageView)
imageView.snp.makeConstraints { make in
make.height.equalTo(imageView.snp.width)
}
}
let placeholders = max(0, 3 - min(lead.images.count, 3))
if placeholders > 0 {
(0..<placeholders).forEach { _ in
let spacer = UIView()
imageRow.addArrangedSubview(spacer)
spacer.snp.makeConstraints { make in
make.height.equalTo(spacer.snp.width)
}
}
}
}
imageRow.isHidden = compact && lead.images.isEmpty
}
private func setupUI() {
phoneRow.axis = .horizontal
phoneRow.alignment = .center
phoneRow.spacing = 10
phoneTitleLabel.text = "客户手机号"
phoneTitleLabel.font = .systemFont(ofSize: 12)
phoneTitleLabel.textColor = AppColor.text666
phoneValueLabel.font = .systemFont(ofSize: 13, weight: .medium)
phoneValueLabel.addGestureRecognizer(
UITapGestureRecognizer(target: self, action: #selector(phoneTapped))
)
phoneRow.addArrangedSubview(phoneTitleLabel)
phoneRow.addArrangedSubview(phoneValueLabel)
remarkLabel.textColor = AppColor.text333
remarkLabel.lineBreakMode = .byTruncatingTail
imageRow.axis = .horizontal
imageRow.spacing = 8
imageRow.distribution = .fillEqually
emptyImageLabel.text = "暂无图片"
emptyImageLabel.font = .systemFont(ofSize: 12)
emptyImageLabel.textColor = AppColor.text666
emptyImageLabel.textAlignment = .center
emptyImageLabel.backgroundColor = OrderTokens.sourceLeadPlaceholder
emptyImageLabel.layer.cornerRadius = 6
emptyImageLabel.clipsToBounds = true
let stack = UIStackView(arrangedSubviews: [phoneRow, remarkLabel, imageRow, emptyImageLabel])
stack.axis = .vertical
stack.spacing = 10
addSubview(stack)
stack.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
emptyImageLabel.snp.makeConstraints { make in
make.height.equalTo(72)
}
}
@objc private func phoneTapped() {
let phone = phoneValueLabel.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !phone.isEmpty else { return }
onCallPhone?(phone)
}
@objc private func imageTapped(_ gesture: UITapGestureRecognizer) {
guard let imageView = gesture.view as? UIImageView else { return }
onImageTap?(currentImages, imageView.tag)
}
}

View File

@ -0,0 +1,95 @@
//
// OrderSourcePersonCell.swift
// suixinkan
//
import SnapKit
import UIKit
/// cell Android `SourcePersonItem`
final class OrderSourcePersonCell: UITableViewCell {
static let reuseIdentifier = "OrderSourcePersonCell"
var onCallPhone: ((String) -> Void)?
private let cardView = UIView()
private let nameLabel = UILabel()
private let phoneLabel = UILabel()
private let callButton = UIButton(type: .system)
private let chevronView = UIImageView()
private var personPhone = ""
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.backgroundColor = .clear
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(person: OrderSourcePerson, selected: Bool) {
nameLabel.text = person.name
phoneLabel.text = person.phone
personPhone = person.phone
cardView.layer.borderColor = (selected ? AppColor.primary : OrderTokens.sourceBorder).cgColor
cardView.backgroundColor = selected ? AppColor.primaryLight.withAlphaComponent(0.5) : .white
}
private func setupUI() {
cardView.layer.cornerRadius = AppRadius.sm
cardView.layer.borderWidth = 1
contentView.addSubview(cardView)
nameLabel.font = .systemFont(ofSize: 16, weight: .medium)
nameLabel.textColor = AppColor.text333
phoneLabel.font = .systemFont(ofSize: 14)
phoneLabel.textColor = AppColor.text666
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)
chevronView.image = UIImage(systemName: "chevron.right")
chevronView.tintColor = AppColor.text666
chevronView.contentMode = .scaleAspectFit
cardView.addSubview(nameLabel)
cardView.addSubview(phoneLabel)
cardView.addSubview(callButton)
cardView.addSubview(chevronView)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0))
}
nameLabel.snp.makeConstraints { make in
make.leading.top.equalToSuperview().inset(14)
make.trailing.lessThanOrEqualTo(callButton.snp.leading).offset(-8)
}
phoneLabel.snp.makeConstraints { make in
make.leading.equalTo(nameLabel)
make.top.equalTo(nameLabel.snp.bottom).offset(6)
make.bottom.equalToSuperview().inset(14)
}
chevronView.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(14)
make.centerY.equalToSuperview()
make.width.height.equalTo(20)
}
callButton.snp.makeConstraints { make in
make.trailing.equalTo(chevronView.snp.leading).offset(-8)
make.centerY.equalToSuperview()
make.width.height.equalTo(32)
}
}
@objc private func callTapped() {
onCallPhone?(personPhone)
}
}