将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
239 lines
9.1 KiB
Swift
239 lines
9.1 KiB
Swift
//
|
|
// CooperationOrderListViewController.swift
|
|
// suixinkan
|
|
//
|
|
|
|
import SnapKit
|
|
import UIKit
|
|
|
|
/// 合作订单列表页,对齐 Android `CooperationOrderListScreen`。
|
|
final class CooperationOrderListViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate {
|
|
|
|
private let viewModel = CooperationOrderListViewModel()
|
|
private let orderAPI = NetworkServices.shared.orderAPI
|
|
|
|
private let tabBarView = CooperationOrderTabBar()
|
|
private let searchBar = CooperationOrderSearchBar()
|
|
private let tableView = UITableView(frame: .zero, style: .plain)
|
|
private let refreshControl = UIRefreshControl()
|
|
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
|
private let footerSpinner = UIActivityIndicatorView(style: .medium)
|
|
private let footerLabel = UILabel()
|
|
private let emptyLabel = UILabel()
|
|
private var isApplyingTab = false
|
|
private var isSearchBarVisible = false
|
|
private var searchBarHeightConstraint: Constraint?
|
|
|
|
override func setupNavigationBar() {
|
|
title = "合作订单"
|
|
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
|
title: "合作获客员",
|
|
style: .plain,
|
|
target: self,
|
|
action: #selector(openAcquirer)
|
|
)
|
|
navigationItem.rightBarButtonItem?.tintColor = AppColor.primary
|
|
}
|
|
|
|
override func setupUI() {
|
|
view.backgroundColor = AppColor.pageBackground
|
|
searchBar.isHidden = true
|
|
|
|
tableView.backgroundColor = .clear
|
|
tableView.separatorStyle = .none
|
|
tableView.contentInset.top = AppSpacing.sm
|
|
tableView.verticalScrollIndicatorInsets.top = AppSpacing.sm
|
|
tableView.dataSource = self
|
|
tableView.delegate = self
|
|
tableView.register(ReferralLeadCell.self, forCellReuseIdentifier: ReferralLeadCell.reuseIdentifier)
|
|
tableView.register(AcquisitionOrderCell.self, forCellReuseIdentifier: AcquisitionOrderCell.reuseIdentifier)
|
|
tableView.refreshControl = refreshControl
|
|
|
|
loadingIndicator.hidesWhenStopped = true
|
|
loadingIndicator.color = AppColor.primary
|
|
footerSpinner.hidesWhenStopped = true
|
|
footerSpinner.color = AppColor.primary
|
|
footerLabel.font = .systemFont(ofSize: 14)
|
|
footerLabel.textColor = AppColor.textTertiary
|
|
footerLabel.textAlignment = .center
|
|
footerLabel.text = "没有更多"
|
|
|
|
emptyLabel.text = "暂无相关合作订单"
|
|
emptyLabel.font = .systemFont(ofSize: 14)
|
|
emptyLabel.textColor = AppColor.textTertiary
|
|
emptyLabel.textAlignment = .center
|
|
emptyLabel.isHidden = true
|
|
|
|
view.addSubview(tabBarView)
|
|
view.addSubview(searchBar)
|
|
view.addSubview(tableView)
|
|
view.addSubview(loadingIndicator)
|
|
view.addSubview(emptyLabel)
|
|
}
|
|
|
|
override func setupConstraints() {
|
|
tabBarView.snp.makeConstraints { make in
|
|
make.top.equalTo(view.safeAreaLayoutGuide)
|
|
make.leading.trailing.equalToSuperview()
|
|
}
|
|
searchBar.snp.makeConstraints { make in
|
|
make.top.equalTo(tabBarView.snp.bottom)
|
|
make.leading.trailing.equalToSuperview()
|
|
searchBarHeightConstraint = make.height.equalTo(0).constraint
|
|
}
|
|
tableView.snp.makeConstraints { make in
|
|
make.top.equalTo(searchBar.snp.bottom)
|
|
make.leading.trailing.bottom.equalToSuperview()
|
|
}
|
|
loadingIndicator.snp.makeConstraints { make in
|
|
make.center.equalTo(tableView)
|
|
}
|
|
emptyLabel.snp.makeConstraints { make in
|
|
make.center.equalTo(tableView)
|
|
}
|
|
}
|
|
|
|
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) }
|
|
}
|
|
|
|
tabBarView.onTabSelected = { [weak self] index in
|
|
guard let self else { return }
|
|
self.viewModel.onTabSelected(index)
|
|
Task { await self.viewModel.refreshCurrentTab(api: self.orderAPI) }
|
|
}
|
|
searchBar.onSearchTextChange = { [weak self] text in
|
|
self?.viewModel.onSearchTextChange(text)
|
|
}
|
|
searchBar.onSearch = { [weak self] in
|
|
guard let self else { return }
|
|
Task { await self.viewModel.refreshAcquisitionOrders(api: self.orderAPI) }
|
|
}
|
|
refreshControl.addTarget(self, action: #selector(refreshTriggered), for: .valueChanged)
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
guard CooperationOrderFeature.hasPermission(in: AppStore.shared.permissions.permissionItems()) else {
|
|
showToast("暂无合作订单权限")
|
|
navigationController?.popViewController(animated: false)
|
|
return
|
|
}
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
Task { await viewModel.refreshCurrentTab(api: orderAPI) }
|
|
}
|
|
|
|
@objc private func openAcquirer() {
|
|
navigationController?.pushViewController(CooperationAcquirerViewController(), animated: true)
|
|
}
|
|
|
|
@objc private func refreshTriggered() {
|
|
Task { await viewModel.refreshCurrentTab(api: orderAPI) }
|
|
}
|
|
|
|
private func applyViewModel() {
|
|
if !isApplyingTab {
|
|
isApplyingTab = true
|
|
tabBarView.setSelectedTab(viewModel.selectedTab)
|
|
isApplyingTab = false
|
|
}
|
|
updateSearchBarVisibility(viewModel.selectedTab == 1)
|
|
searchBar.textField.text = viewModel.searchText
|
|
|
|
let isEmpty = viewModel.selectedTab == 0 ? viewModel.leads.isEmpty : viewModel.orders.isEmpty
|
|
let isLoading = viewModel.isRefreshing && isEmpty
|
|
|
|
if viewModel.isRefreshing {
|
|
if !refreshControl.isRefreshing { refreshControl.beginRefreshing() }
|
|
} else {
|
|
refreshControl.endRefreshing()
|
|
}
|
|
|
|
if isLoading {
|
|
loadingIndicator.startAnimating()
|
|
emptyLabel.isHidden = true
|
|
tableView.tableFooterView = nil
|
|
} else {
|
|
loadingIndicator.stopAnimating()
|
|
emptyLabel.isHidden = !isEmpty
|
|
tableView.tableFooterView = tableFooterView(isEmpty: isEmpty)
|
|
}
|
|
tableView.reloadData()
|
|
}
|
|
|
|
private func updateSearchBarVisibility(_ isVisible: Bool) {
|
|
guard isSearchBarVisible != isVisible else { return }
|
|
isSearchBarVisible = isVisible
|
|
searchBarHeightConstraint?.update(offset: isVisible ? 62 : 0)
|
|
if isVisible {
|
|
searchBar.isHidden = false
|
|
} else {
|
|
searchBar.textField.resignFirstResponder()
|
|
}
|
|
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseInOut]) {
|
|
self.view.layoutIfNeeded()
|
|
} completion: { _ in
|
|
if self.isSearchBarVisible == isVisible {
|
|
self.searchBar.isHidden = !isVisible
|
|
}
|
|
}
|
|
}
|
|
|
|
private func tableFooterView(isEmpty: Bool) -> UIView? {
|
|
guard !isEmpty else { return nil }
|
|
if viewModel.isLoadingMore {
|
|
let container = UIView(frame: CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 44))
|
|
footerSpinner.center = CGPoint(x: container.bounds.midX, y: container.bounds.midY)
|
|
footerSpinner.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
|
|
container.addSubview(footerSpinner)
|
|
footerSpinner.startAnimating()
|
|
return container
|
|
}
|
|
footerSpinner.stopAnimating()
|
|
guard !viewModel.canLoadMore else { return nil }
|
|
footerLabel.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 44)
|
|
return footerLabel
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
viewModel.selectedTab == 0 ? viewModel.leads.count : viewModel.orders.count
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
if viewModel.selectedTab == 0 {
|
|
guard let cell = tableView.dequeueReusableCell(
|
|
withIdentifier: ReferralLeadCell.reuseIdentifier,
|
|
for: indexPath
|
|
) as? ReferralLeadCell else {
|
|
return UITableViewCell()
|
|
}
|
|
cell.configure(with: viewModel.leads[indexPath.row])
|
|
cell.onImageTap = { [weak self] index, urls in
|
|
let preview = ImagePreviewViewController(imageURLs: urls, startIndex: index)
|
|
self?.present(preview, animated: true)
|
|
}
|
|
return cell
|
|
}
|
|
guard let cell = tableView.dequeueReusableCell(
|
|
withIdentifier: AcquisitionOrderCell.reuseIdentifier,
|
|
for: indexPath
|
|
) as? AcquisitionOrderCell else {
|
|
return UITableViewCell()
|
|
}
|
|
cell.configure(with: viewModel.orders[indexPath.row])
|
|
return cell
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
|
guard viewModel.shouldLoadMore(at: indexPath.row) else { return }
|
|
Task { await viewModel.loadMore(api: orderAPI) }
|
|
}
|
|
}
|