feat: 添加获客员分成记录并重构列表

This commit is contained in:
2026-07-23 17:43:13 +08:00
parent 7ccbc5568d
commit 0e0415eec4
7 changed files with 945 additions and 95 deletions

View File

@ -0,0 +1,284 @@
//
// CommissionRateLogViewController.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class CommissionRateLogViewController: BaseViewController {
private let viewModel: CommissionRateLogViewModel
private let orderAPI: OrderAPI
private let navigationTitle: String
private let tableView = UITableView(frame: .zero, style: .plain)
private let emptyLabel = UILabel()
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
private let footerLabel = UILabel()
private var dataSource: UITableViewDiffableDataSource<Int, CommissionRateLogEntity>!
init(saleUserId: Int, acquirerName: String, orderAPI: OrderAPI? = nil) {
viewModel = CommissionRateLogViewModel(saleUserId: saleUserId)
let trimmedName = acquirerName.trimmingCharacters(in: .whitespacesAndNewlines)
navigationTitle = trimmedName.isEmpty ? "修改记录" : "\(trimmedName)的分成记录"
self.orderAPI = orderAPI ?? NetworkServices.shared.orderAPI
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
Task { await viewModel.refresh(api: orderAPI) }
}
override func setupNavigationBar() {
title = navigationTitle
}
override func setupUI() {
view.backgroundColor = AppColor.pageBackground
tableView.backgroundColor = AppColor.pageBackground
tableView.separatorStyle = .none
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 132
tableView.delegate = self
tableView.contentInset.top = AppSpacing.sm
tableView.register(
CommissionRateLogTimelineCell.self,
forCellReuseIdentifier: CommissionRateLogTimelineCell.reuseIdentifier
)
tableView.refreshControl = UIRefreshControl()
tableView.refreshControl?.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
dataSource = UITableViewDiffableDataSource<Int, CommissionRateLogEntity>(
tableView: tableView
) { tableView, indexPath, item in
guard let cell = tableView.dequeueReusableCell(
withIdentifier: CommissionRateLogTimelineCell.reuseIdentifier,
for: indexPath
) as? CommissionRateLogTimelineCell else {
return UITableViewCell()
}
cell.configure(with: item, isLatest: indexPath.item == 0)
return cell
}
emptyLabel.text = "暂无修改记录"
emptyLabel.font = .systemFont(ofSize: 14)
emptyLabel.textColor = AppColor.textTertiary
emptyLabel.textAlignment = .center
emptyLabel.isHidden = true
loadingIndicator.color = AppColor.primary
footerLabel.font = .systemFont(ofSize: 13)
footerLabel.textColor = AppColor.textTertiary
footerLabel.textAlignment = .center
view.addSubview(tableView)
view.addSubview(emptyLabel)
view.addSubview(loadingIndicator)
}
override func setupConstraints() {
tableView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
emptyLabel.snp.makeConstraints { make in
make.center.equalTo(tableView)
}
loadingIndicator.snp.makeConstraints { make in
make.center.equalTo(tableView)
}
}
override func bindActions() {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyState() }
}
viewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
}
private func applyState() {
if !viewModel.isRefreshing {
tableView.refreshControl?.endRefreshing()
}
if viewModel.initialLoading && viewModel.items.isEmpty {
loadingIndicator.startAnimating()
} else {
loadingIndicator.stopAnimating()
}
emptyLabel.isHidden = viewModel.initialLoading || viewModel.isRefreshing || !viewModel.items.isEmpty
var snapshot = NSDiffableDataSourceSnapshot<Int, CommissionRateLogEntity>()
snapshot.appendSections([0])
snapshot.appendItems(viewModel.items)
dataSource.apply(snapshot, animatingDifferences: true)
guard !viewModel.items.isEmpty else {
tableView.tableFooterView = UIView()
return
}
footerLabel.text = viewModel.isLoadingMore ? "加载中…" : (viewModel.canLoadMore ? "" : "没有更多")
footerLabel.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 52)
tableView.tableFooterView = footerLabel
}
@objc private func refreshPulled() {
Task { await viewModel.refresh(api: orderAPI) }
}
}
extension CommissionRateLogViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard !viewModel.items.isEmpty,
scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.height - 180 else {
return
}
Task { await viewModel.loadMore(api: orderAPI) }
}
}
/// Cell
final class CommissionRateLogTimelineCell: UITableViewCell {
static let reuseIdentifier = "CommissionRateLogTimelineCell"
private let railView = UIView()
private let dotView = UIView()
private let cardView = UIView()
private let metadataLabel = UILabel()
private let latestBadgeLabel = UILabel()
private let beforeRateLabel = UILabel()
private let arrowImageView = UIImageView(image: UIImage(systemName: "arrow.right"))
private let afterRateLabel = UILabel()
private let remarkLabel = UILabel()
private let contentStack = UIStackView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = AppColor.pageBackground
contentView.backgroundColor = AppColor.pageBackground
railView.backgroundColor = AppColor.primary.withAlphaComponent(0.16)
dotView.backgroundColor = AppColor.primary
dotView.layer.cornerRadius = 4
cardView.backgroundColor = .white
cardView.layer.cornerRadius = AppRadius.md
cardView.layer.shadowColor = UIColor.black.cgColor
cardView.layer.shadowOpacity = 0.04
cardView.layer.shadowOffset = CGSize(width: 0, height: 1)
cardView.layer.shadowRadius = 2
metadataLabel.font = .systemFont(ofSize: 12)
metadataLabel.textColor = AppColor.textSecondary
metadataLabel.numberOfLines = 2
latestBadgeLabel.text = "最新"
latestBadgeLabel.font = .systemFont(ofSize: 11, weight: .semibold)
latestBadgeLabel.textColor = AppColor.primary
latestBadgeLabel.textAlignment = .center
latestBadgeLabel.backgroundColor = AppColor.primary.withAlphaComponent(0.10)
latestBadgeLabel.layer.cornerRadius = 9
latestBadgeLabel.clipsToBounds = true
latestBadgeLabel.isHidden = true
latestBadgeLabel.setContentHuggingPriority(.required, for: .horizontal)
latestBadgeLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
latestBadgeLabel.snp.makeConstraints { make in
make.width.equalTo(38)
make.height.equalTo(18)
}
let headerStack = UIStackView(arrangedSubviews: [metadataLabel, latestBadgeLabel])
headerStack.axis = .horizontal
headerStack.alignment = .top
headerStack.spacing = AppSpacing.sm
[beforeRateLabel, afterRateLabel].forEach { label in
label.font = .systemFont(ofSize: 17, weight: .semibold)
label.textColor = AppColor.textPrimary
label.setContentCompressionResistancePriority(.required, for: .horizontal)
}
afterRateLabel.textColor = AppColor.primary
arrowImageView.tintColor = AppColor.textTertiary
arrowImageView.contentMode = .scaleAspectFit
let rateStack = UIStackView(arrangedSubviews: [beforeRateLabel, arrowImageView, afterRateLabel, UIView()])
rateStack.axis = .horizontal
rateStack.alignment = .center
rateStack.spacing = AppSpacing.sm
arrowImageView.snp.makeConstraints { make in
make.size.equalTo(16)
}
remarkLabel.font = .systemFont(ofSize: 13)
remarkLabel.textColor = AppColor.textSecondary
remarkLabel.numberOfLines = 0
contentStack.axis = .vertical
contentStack.spacing = AppSpacing.sm
contentStack.addArrangedSubview(headerStack)
contentStack.addArrangedSubview(rateStack)
contentStack.addArrangedSubview(remarkLabel)
contentView.addSubview(railView)
contentView.addSubview(dotView)
contentView.addSubview(cardView)
cardView.addSubview(contentStack)
railView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(24)
make.top.bottom.equalToSuperview()
make.width.equalTo(1.5)
}
dotView.snp.makeConstraints { make in
make.centerX.equalTo(railView)
make.top.equalToSuperview().offset(20)
make.size.equalTo(8)
}
cardView.snp.makeConstraints { make in
make.leading.equalTo(railView.snp.trailing).offset(16)
make.trailing.equalToSuperview().inset(14)
make.top.equalToSuperview().offset(4)
make.bottom.equalToSuperview().inset(8)
}
contentStack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(15)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func configure(with item: CommissionRateLogEntity, isLatest: Bool) {
metadataLabel.text = item.metadataLine
latestBadgeLabel.isHidden = !isLatest
beforeRateLabel.text = item.displayBeforeRate
afterRateLabel.text = item.displayAfterRate
if let remark = item.displayRemark {
remarkLabel.text = "备注:\(remark)"
remarkLabel.isHidden = false
} else {
remarkLabel.text = nil
remarkLabel.isHidden = true
}
accessibilityLabel = [
isLatest ? "最新记录" : nil,
item.metadataLine,
"\(item.displayBeforeRate)修改为\(item.displayAfterRate)",
item.displayRemark.map { "备注\($0)" },
].compactMap { $0 }.joined(separator: "")
}
}