Files
suixinkan_uikit/suixinkan/UI/Invite/InviteRecordViewController.swift
汉秋 6336feff27 完善景区排队设置页与全局按钮配置迁移。
重构排队设置与变更日志交互,补充 Overlay 与资源,并将多页面主操作按钮统一到 UIButton Configuration,同步更新相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 16:21:53 +08:00

543 lines
20 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// InviteRecordViewController.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class InviteRecordViewController: BaseViewController {
private enum Section: Hashable {
case main
}
private enum Item: Hashable {
case invite(InviteDisplayRow)
case reward(WalletTransactionItem)
case footer(String)
}
private let viewModel = InviteRecordViewModel()
private let inviteAPI: InviteServing
private let walletAPI: WalletServing
private let headerView = UIView()
private let totalAmountLabel = UILabel()
private let withdrawableAmountLabel = UILabel()
private let contentCard = UIView()
private let tabStack = UIStackView()
private let inviteTabButton = InviteRecordTabButton(title: "邀请记录")
private let rewardTabButton = InviteRecordTabButton(title: "奖励记录")
private let tableView = UITableView(frame: .zero, style: .plain)
private let emptyLabel = UILabel()
private let bottomBar = UIView()
private let walletButton = UIButton(type: .system)
private var dataSource: UITableViewDiffableDataSource<Section, Item>!
///
init(
inviteAPI: InviteServing? = nil,
walletAPI: WalletServing? = nil
) {
self.inviteAPI = inviteAPI ?? NetworkServices.shared.inviteAPI
self.walletAPI = walletAPI ?? NetworkServices.shared.walletAPI
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "邀请记录"
}
override func setupUI() {
view.backgroundColor = UIColor(hex: 0xF5F6F8)
setupHeader()
setupContentCard()
setupBottomBar()
dataSource = makeDataSource()
}
override func setupConstraints() {
headerView.snp.makeConstraints { make in
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
make.height.equalTo(104)
}
bottomBar.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
}
contentCard.snp.makeConstraints { make in
make.top.equalTo(headerView.snp.bottom).offset(16)
make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(16)
make.bottom.equalTo(bottomBar.snp.top).offset(-16)
}
}
override func bindActions() {
inviteTabButton.addTarget(self, action: #selector(inviteTabTapped), for: .touchUpInside)
rewardTabButton.addTarget(self, action: #selector(rewardTabTapped), for: .touchUpInside)
walletButton.addTarget(self, action: #selector(walletTapped), for: .touchUpInside)
tableView.refreshControl?.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
viewModel.onStateChange = { [weak self] in
Task { @MainActor in
self?.applyViewModel(animated: true)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
Task { await viewModel.loadInitial(inviteAPI: inviteAPI, walletAPI: walletAPI) }
}
private func setupHeader() {
headerView.backgroundColor = AppColor.primary
view.addSubview(headerView)
let divider = UIView()
divider.backgroundColor = UIColor.white.withAlphaComponent(0.8)
let totalMetric = makeMetricView(title: "累计奖励金额", valueLabel: totalAmountLabel)
let withdrawMetric = makeMetricView(title: "可提现金额", valueLabel: withdrawableAmountLabel)
headerView.addSubview(totalMetric)
headerView.addSubview(divider)
headerView.addSubview(withdrawMetric)
totalMetric.snp.makeConstraints { make in
make.leading.top.bottom.equalToSuperview()
make.trailing.equalTo(divider.snp.leading)
make.width.equalTo(withdrawMetric)
}
divider.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalToSuperview()
make.width.equalTo(1)
make.height.equalTo(56)
}
withdrawMetric.snp.makeConstraints { make in
make.leading.equalTo(divider.snp.trailing)
make.trailing.top.bottom.equalToSuperview()
}
}
private func setupContentCard() {
contentCard.backgroundColor = .white
contentCard.layer.cornerRadius = 12
contentCard.clipsToBounds = true
view.addSubview(contentCard)
tabStack.axis = .horizontal
tabStack.distribution = .fillEqually
tabStack.addArrangedSubview(inviteTabButton)
tabStack.addArrangedSubview(rewardTabButton)
contentCard.addSubview(tabStack)
tableView.backgroundColor = .white
tableView.separatorStyle = .none
tableView.delegate = self
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 72
tableView.refreshControl = UIRefreshControl()
tableView.register(InviteUserCell.self, forCellReuseIdentifier: InviteUserCell.reuseIdentifier)
tableView.register(InviteRewardCell.self, forCellReuseIdentifier: InviteRewardCell.reuseIdentifier)
tableView.register(InviteFooterCell.self, forCellReuseIdentifier: InviteFooterCell.reuseIdentifier)
contentCard.addSubview(tableView)
emptyLabel.textColor = AppColor.textSecondary
emptyLabel.font = .app(.body)
emptyLabel.textAlignment = .center
emptyLabel.isHidden = true
contentCard.addSubview(emptyLabel)
tabStack.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(44)
}
tableView.snp.makeConstraints { make in
make.top.equalTo(tabStack.snp.bottom).offset(8)
make.leading.trailing.bottom.equalToSuperview().inset(16)
}
emptyLabel.snp.makeConstraints { make in
make.center.equalTo(tableView)
}
}
private func setupBottomBar() {
bottomBar.backgroundColor = .white
view.addSubview(bottomBar)
walletButton.backgroundColor = AppColor.primary
walletButton.layer.cornerRadius = 12
walletButton.setTitle("去钱包提现", for: .normal)
walletButton.setTitleColor(.white, for: .normal)
walletButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
bottomBar.addSubview(walletButton)
walletButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(16)
make.leading.trailing.equalTo(bottomBar.safeAreaLayoutGuide).inset(16)
make.height.equalTo(48)
make.bottom.equalTo(bottomBar.safeAreaLayoutGuide).inset(22)
}
}
private func makeMetricView(title: String, valueLabel: UILabel) -> UIView {
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .app(.body)
titleLabel.textColor = UIColor.white.withAlphaComponent(0.8)
titleLabel.textAlignment = .center
valueLabel.font = .systemFont(ofSize: 24, weight: .medium)
valueLabel.textColor = .white
valueLabel.textAlignment = .center
valueLabel.adjustsFontSizeToFitWidth = true
valueLabel.minimumScaleFactor = 0.7
let stack = UIStackView(arrangedSubviews: [titleLabel, valueLabel])
stack.axis = .vertical
stack.alignment = .fill
stack.spacing = 4
let container = UIView()
container.addSubview(stack)
stack.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(12)
}
return container
}
private func makeDataSource() -> UITableViewDiffableDataSource<Section, Item> {
UITableViewDiffableDataSource<Section, Item>(tableView: tableView) { tableView, indexPath, item in
switch item {
case .invite(let row):
let cell = tableView.dequeueReusableCell(withIdentifier: InviteUserCell.reuseIdentifier, for: indexPath) as! InviteUserCell
cell.apply(row)
return cell
case .reward(let row):
let cell = tableView.dequeueReusableCell(withIdentifier: InviteRewardCell.reuseIdentifier, for: indexPath) as! InviteRewardCell
cell.apply(row)
return cell
case .footer(let text):
let cell = tableView.dequeueReusableCell(withIdentifier: InviteFooterCell.reuseIdentifier, for: indexPath) as! InviteFooterCell
cell.apply(text: text)
return cell
}
}
}
@MainActor
private func applyViewModel(animated: Bool) {
totalAmountLabel.text = "¥ \(viewModel.totalRewardText)"
withdrawableAmountLabel.text = "¥ \(viewModel.withdrawableText)"
inviteTabButton.isSelected = viewModel.selectedTab == .invite
rewardTabButton.isSelected = viewModel.selectedTab == .reward
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
switch viewModel.selectedTab {
case .invite:
snapshot.appendItems([.footer("已邀请摄影师")], toSection: .main)
snapshot.appendItems(viewModel.inviteRows.map(Item.invite), toSection: .main)
if !viewModel.inviteRows.isEmpty {
snapshot.appendItems([.footer(viewModel.canLoadMoreInvite ? "加载中..." : "没有更多")], toSection: .main)
}
emptyLabel.text = "暂无邀请记录"
emptyLabel.isHidden = !viewModel.inviteRows.isEmpty || viewModel.isInviteRefreshing
case .reward:
snapshot.appendItems([.footer("奖励记录")], toSection: .main)
snapshot.appendItems(viewModel.rewardRows.map(Item.reward), toSection: .main)
if !viewModel.rewardRows.isEmpty {
snapshot.appendItems([.footer(viewModel.canLoadMoreReward ? "加载中..." : "没有更多")], toSection: .main)
}
emptyLabel.text = "暂无数据"
emptyLabel.isHidden = !viewModel.rewardRows.isEmpty || viewModel.isRewardRefreshing
}
dataSource.apply(snapshot, animatingDifferences: animated)
tableView.refreshControl?.endRefreshing()
if let message = viewModel.errorMessage {
showToast(message)
}
}
@objc private func inviteTabTapped() {
Task { await viewModel.selectTab(.invite, inviteAPI: inviteAPI, walletAPI: walletAPI) }
}
@objc private func rewardTabTapped() {
Task { await viewModel.selectTab(.reward, inviteAPI: inviteAPI, walletAPI: walletAPI) }
}
@objc private func refreshPulled() {
Task {
switch viewModel.selectedTab {
case .invite:
await viewModel.refreshInvite(inviteAPI: inviteAPI, walletAPI: walletAPI)
case .reward:
await viewModel.refreshReward(walletAPI: walletAPI)
}
}
}
@objc private func walletTapped() {
HomeRouteHandler.open(uri: "wallet", from: self, storeItem: nil)
}
private func loadMoreIfNeeded() {
Task {
switch viewModel.selectedTab {
case .invite:
await viewModel.loadMoreInvite(inviteAPI: inviteAPI)
case .reward:
await viewModel.loadMoreReward(walletAPI: walletAPI)
}
}
}
}
extension InviteRecordViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let distanceToBottom = scrollView.contentSize.height - scrollView.bounds.height - scrollView.contentOffset.y
guard distanceToBottom < 160, scrollView.contentSize.height > scrollView.bounds.height else { return }
loadMoreIfNeeded()
}
}
/// Tab
private final class InviteRecordTabButton: UIButton {
override var isSelected: Bool {
didSet {
setTitleColor(isSelected ? AppColor.primary : UIColor(hex: 0xB6BECA), for: .normal)
titleLabel?.font = .systemFont(ofSize: 16, weight: isSelected ? .medium : .regular)
}
}
/// Tab
init(title: String) {
super.init(frame: .zero)
setTitle(title, for: .normal)
setTitleColor(UIColor(hex: 0xB6BECA), for: .normal)
titleLabel?.font = .systemFont(ofSize: 16)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
private final class InviteUserCell: UITableViewCell {
static let reuseIdentifier = "InviteUserCell"
private let avatarImageView = UIImageView()
private let avatarLabel = UILabel()
private let nameLabel = UILabel()
private let levelLabel = UILabel()
private let phoneLabel = UILabel()
private let timeLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func apply(_ row: InviteDisplayRow) {
avatarLabel.text = row.avatarPlaceholder
avatarImageView.loadRemoteImage(urlString: row.avatar, placeholderColor: UIColor(hex: 0xE0F2FF))
avatarLabel.isHidden = !row.avatar.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
nameLabel.text = row.title
phoneLabel.text = row.phone
timeLabel.text = row.time
levelLabel.text = row.levelText
levelLabel.textColor = row.isPrimaryLevel ? UIColor(hex: 0x22C55E) : AppColor.warning
levelLabel.backgroundColor = row.isPrimaryLevel ? AppColor.successBackground : AppColor.warningBackground
}
private func setupUI() {
let divider = UIView()
divider.backgroundColor = UIColor(hex: 0xF3F4F6)
contentView.addSubview(divider)
avatarImageView.layer.cornerRadius = 24
avatarImageView.clipsToBounds = true
avatarImageView.contentMode = .scaleAspectFill
contentView.addSubview(avatarImageView)
avatarLabel.font = .systemFont(ofSize: 18, weight: .medium)
avatarLabel.textColor = AppColor.primary
avatarLabel.textAlignment = .center
avatarImageView.addSubview(avatarLabel)
nameLabel.font = .systemFont(ofSize: 16, weight: .medium)
nameLabel.textColor = .black
levelLabel.font = .app(.caption)
levelLabel.textAlignment = .center
levelLabel.layer.cornerRadius = 4
levelLabel.clipsToBounds = true
let nameRow = UIStackView(arrangedSubviews: [nameLabel, levelLabel])
nameRow.axis = .horizontal
nameRow.alignment = .center
nameRow.spacing = 4
phoneLabel.font = .app(.body)
phoneLabel.textColor = UIColor(hex: 0x7B8EAA)
timeLabel.font = .app(.body)
timeLabel.textColor = UIColor(hex: 0xB6BECA)
timeLabel.textAlignment = .right
let infoRow = UIStackView(arrangedSubviews: [phoneLabel, timeLabel])
infoRow.axis = .horizontal
infoRow.spacing = 8
infoRow.distribution = .fillEqually
let textStack = UIStackView(arrangedSubviews: [nameRow, infoRow])
textStack.axis = .vertical
textStack.spacing = 4
contentView.addSubview(textStack)
divider.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(1)
}
avatarImageView.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.top.equalToSuperview().offset(13)
make.size.equalTo(48)
make.bottom.lessThanOrEqualToSuperview().inset(13)
}
avatarLabel.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
levelLabel.snp.makeConstraints { make in
make.width.greaterThanOrEqualTo(40)
make.height.equalTo(20)
}
textStack.snp.makeConstraints { make in
make.leading.equalTo(avatarImageView.snp.trailing).offset(12)
make.trailing.equalToSuperview()
make.centerY.equalTo(avatarImageView)
make.top.greaterThanOrEqualToSuperview().offset(12)
make.bottom.lessThanOrEqualToSuperview().inset(12)
}
}
}
///
private final class InviteRewardCell: UITableViewCell {
static let reuseIdentifier = "InviteRewardCell"
private let amountLabel = UILabel()
private let timeLabel = UILabel()
private let statusLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func apply(_ item: WalletTransactionItem) {
amountLabel.text = "¥ \(item.amount)"
timeLabel.text = item.createdAt
let label = item.withdrawLabel.isEmpty ? "--" : item.withdrawLabel
statusLabel.text = label
let isWithdrawable = label == "可提现"
statusLabel.textColor = isWithdrawable ? UIColor(hex: 0x22C55E) : UIColor(hex: 0x7B8EAA)
statusLabel.backgroundColor = isWithdrawable ? AppColor.successBackground : UIColor(hex: 0xF4F4F4)
}
private func setupUI() {
let divider = UIView()
divider.backgroundColor = UIColor(hex: 0xF3F4F6)
contentView.addSubview(divider)
amountLabel.font = .systemFont(ofSize: 16, weight: .medium)
amountLabel.textColor = .black
timeLabel.font = .app(.body)
timeLabel.textColor = UIColor(hex: 0x7B8EAA)
let textStack = UIStackView(arrangedSubviews: [amountLabel, timeLabel])
textStack.axis = .vertical
textStack.spacing = 4
contentView.addSubview(textStack)
statusLabel.font = .app(.caption)
statusLabel.textAlignment = .center
statusLabel.layer.cornerRadius = 4
statusLabel.clipsToBounds = true
contentView.addSubview(statusLabel)
divider.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(1)
}
textStack.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.top.bottom.equalToSuperview().inset(12)
make.trailing.lessThanOrEqualTo(statusLabel.snp.leading).offset(-12)
}
statusLabel.snp.makeConstraints { make in
make.trailing.equalToSuperview()
make.centerY.equalToSuperview()
make.height.equalTo(22)
make.width.greaterThanOrEqualTo(48)
}
}
}
///
private final class InviteFooterCell: UITableViewCell {
static let reuseIdentifier = "InviteFooterCell"
private let label = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
label.textAlignment = .center
contentView.addSubview(label)
label.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0))
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func apply(text: String) {
label.text = text
if text == "已邀请摄影师" || text == "奖励记录" {
label.textAlignment = .left
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = .black
} else {
label.textAlignment = .center
label.font = .app(.body)
label.textColor = AppColor.textSecondary
}
}
}