// // 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! 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( 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() 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: ",") } }