新增线下收款记录和 ai 修图优化
This commit is contained in:
@@ -0,0 +1,683 @@
|
||||
//
|
||||
// OfflineCollectionDailyViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 日清列表的分区。
|
||||
enum OfflineDailyListSection: Hashable {
|
||||
case records
|
||||
case batches
|
||||
case empty
|
||||
}
|
||||
|
||||
/// 日清列表的条目数据。
|
||||
enum OfflineDailyListItem: Hashable {
|
||||
case record(OfflineCollectionRecord)
|
||||
case batch(OfflineSettlementBatch)
|
||||
case empty(isToday: Bool)
|
||||
}
|
||||
|
||||
/// 线下收款日清页,展示单营业日汇总、明细与独立补缴流水。
|
||||
final class OfflineCollectionDailyViewController: BaseViewController {
|
||||
private let viewModel: OfflineCollectionDailyViewModel
|
||||
|
||||
private let headerStack = UIStackView()
|
||||
private let dateControl = UIButton(type: .system)
|
||||
private let summaryCard = UIView()
|
||||
private let summaryStatusLabel = UILabel()
|
||||
private let totalMetric = OfflineDailyMetricView(title: "线下收款总额")
|
||||
private let countMetric = OfflineDailyMetricView(title: "登记笔数")
|
||||
private let settledMetric = OfflineDailyMetricView(title: "已补缴")
|
||||
private let pendingMetric = OfflineDailyMetricView(title: "待补缴")
|
||||
private let overdueBanner = UILabel()
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
private var dataSource: UITableViewDiffableDataSource<OfflineDailyListSection, OfflineDailyListItem>!
|
||||
private var currentSections: [OfflineDailyListSection] = []
|
||||
private let bottomContainer = UIView()
|
||||
private let settlementButton = UIButton(type: .system)
|
||||
private var isFeedbackPresented = false
|
||||
|
||||
/// 使用指定营业日、收款上下文与 Mock 服务创建日清页。
|
||||
init(
|
||||
businessDate: String,
|
||||
context: OfflineCollectionContext = .current(),
|
||||
service: OfflineCollectionMockService = .shared
|
||||
) {
|
||||
viewModel = OfflineCollectionDailyViewModel(
|
||||
businessDate: businessDate,
|
||||
context: context,
|
||||
service: service
|
||||
)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "线下收款日清"
|
||||
#if DEBUG
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
title: "模拟失败",
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(simulateFailureTapped)
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = AppColor.pageBackground
|
||||
headerStack.axis = .vertical
|
||||
headerStack.spacing = AppSpacing.sm
|
||||
|
||||
dateControl.contentHorizontalAlignment = .leading
|
||||
dateControl.showsMenuAsPrimaryAction = true
|
||||
var dateConfiguration = UIButton.Configuration.plain()
|
||||
dateConfiguration.image = UIImage(systemName: "calendar")
|
||||
dateConfiguration.imagePadding = AppSpacing.sm
|
||||
dateConfiguration.baseForegroundColor = AppColor.textPrimary
|
||||
dateConfiguration.background.backgroundColor = .white
|
||||
dateConfiguration.background.cornerRadius = AppRadius.lg
|
||||
dateConfiguration.contentInsets = NSDirectionalEdgeInsets(
|
||||
top: 12,
|
||||
leading: AppSpacing.md,
|
||||
bottom: 12,
|
||||
trailing: AppSpacing.md
|
||||
)
|
||||
dateControl.configuration = dateConfiguration
|
||||
dateControl.accessibilityIdentifier = "offlineCollection.businessDate"
|
||||
|
||||
summaryCard.backgroundColor = .white
|
||||
summaryCard.layer.cornerRadius = AppRadius.lg
|
||||
summaryStatusLabel.font = .systemFont(ofSize: 13, weight: .semibold)
|
||||
summaryStatusLabel.textAlignment = .center
|
||||
summaryStatusLabel.textColor = AppColor.success
|
||||
summaryStatusLabel.backgroundColor = UIColor(hex: 0xECFDF3)
|
||||
summaryStatusLabel.layer.cornerRadius = 12
|
||||
summaryStatusLabel.clipsToBounds = true
|
||||
|
||||
let firstMetricRow = UIStackView(arrangedSubviews: [totalMetric, countMetric])
|
||||
firstMetricRow.axis = .horizontal
|
||||
firstMetricRow.distribution = .fillEqually
|
||||
let secondMetricRow = UIStackView(arrangedSubviews: [settledMetric, pendingMetric])
|
||||
secondMetricRow.axis = .horizontal
|
||||
secondMetricRow.distribution = .fillEqually
|
||||
let metricStack = UIStackView(arrangedSubviews: [firstMetricRow, secondMetricRow])
|
||||
metricStack.axis = .vertical
|
||||
metricStack.spacing = AppSpacing.md
|
||||
summaryCard.addSubview(metricStack)
|
||||
summaryCard.addSubview(summaryStatusLabel)
|
||||
metricStack.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.bottom.equalToSuperview().inset(AppSpacing.md)
|
||||
}
|
||||
summaryStatusLabel.snp.makeConstraints { make in
|
||||
make.top.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.height.equalTo(24)
|
||||
}
|
||||
|
||||
overdueBanner.text = "该营业日存在逾期未补缴,请尽快完成补缴"
|
||||
overdueBanner.font = .systemFont(ofSize: 13, weight: .medium)
|
||||
overdueBanner.textColor = AppColor.danger
|
||||
overdueBanner.backgroundColor = UIColor(hex: 0xFFF1F0)
|
||||
overdueBanner.layer.cornerRadius = AppRadius.md
|
||||
overdueBanner.clipsToBounds = true
|
||||
overdueBanner.textAlignment = .center
|
||||
overdueBanner.numberOfLines = 0
|
||||
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.separatorStyle = .none
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 92
|
||||
tableView.delegate = self
|
||||
tableView.register(OfflineCollectionRecordCell.self, forCellReuseIdentifier: OfflineCollectionRecordCell.reuseIdentifier)
|
||||
tableView.register(OfflineSettlementBatchCell.self, forCellReuseIdentifier: OfflineSettlementBatchCell.reuseIdentifier)
|
||||
tableView.register(OfflineCollectionEmptyCell.self, forCellReuseIdentifier: OfflineCollectionEmptyCell.reuseIdentifier)
|
||||
dataSource = makeDataSource()
|
||||
|
||||
bottomContainer.backgroundColor = .white
|
||||
bottomContainer.layer.shadowColor = UIColor.black.cgColor
|
||||
bottomContainer.layer.shadowOpacity = 0.06
|
||||
bottomContainer.layer.shadowRadius = 8
|
||||
bottomContainer.layer.shadowOffset = CGSize(width: 0, height: -2)
|
||||
|
||||
var settlementConfiguration = UIButton.Configuration.filled()
|
||||
settlementConfiguration.title = "补缴今日全部"
|
||||
settlementConfiguration.baseBackgroundColor = AppColor.primary
|
||||
settlementConfiguration.baseForegroundColor = .white
|
||||
settlementConfiguration.cornerStyle = .medium
|
||||
settlementConfiguration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
|
||||
var outgoing = incoming
|
||||
outgoing.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
return outgoing
|
||||
}
|
||||
settlementButton.configuration = settlementConfiguration
|
||||
settlementButton.accessibilityIdentifier = "offlineCollection.settle"
|
||||
|
||||
view.addSubview(headerStack)
|
||||
[dateControl, summaryCard, overdueBanner].forEach(headerStack.addArrangedSubview)
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(bottomContainer)
|
||||
bottomContainer.addSubview(settlementButton)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
headerStack.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(AppSpacing.sm)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
||||
}
|
||||
dateControl.snp.makeConstraints { make in
|
||||
make.height.equalTo(50)
|
||||
}
|
||||
summaryCard.snp.makeConstraints { make in
|
||||
make.height.equalTo(172)
|
||||
}
|
||||
overdueBanner.snp.makeConstraints { make in
|
||||
make.height.greaterThanOrEqualTo(48)
|
||||
}
|
||||
bottomContainer.snp.makeConstraints { make in
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
settlementButton.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().inset(AppSpacing.md)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
||||
make.height.equalTo(50)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(AppSpacing.md)
|
||||
}
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.equalTo(headerStack.snp.bottom).offset(AppSpacing.sm)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.bottom.equalTo(bottomContainer.snp.top)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
settlementButton.addTarget(self, action: #selector(settlementTapped), for: .touchUpInside)
|
||||
viewModel.onStateChange = { [weak self] in
|
||||
Task { @MainActor in self?.applyViewModel() }
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
applyViewModel()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
viewModel.load()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func applyViewModel() {
|
||||
let datePrefix = viewModel.isToday ? "今日 · " : "营业日 · "
|
||||
dateControl.configuration?.title = datePrefix + viewModel.businessDate
|
||||
dateControl.menu = makeDateMenu()
|
||||
|
||||
totalMetric.setValue(OfflineCollectionMoney.display(viewModel.summary.totalAmountFen))
|
||||
countMetric.setValue("\(viewModel.summary.totalCount) 笔")
|
||||
settledMetric.setValue(OfflineCollectionMoney.display(viewModel.summary.settledAmountFen))
|
||||
pendingMetric.setValue(OfflineCollectionMoney.display(viewModel.summary.pendingAmountFen), highlighted: viewModel.summary.pendingAmountFen > 0)
|
||||
|
||||
let isSettled = viewModel.summary.totalCount > 0 && viewModel.summary.pendingAmountFen == 0
|
||||
summaryStatusLabel.text = " 本日已结清 "
|
||||
summaryStatusLabel.isHidden = !isSettled
|
||||
overdueBanner.isHidden = !viewModel.summary.hasOverdue
|
||||
|
||||
let isProcessing = viewModel.settlementState == .processing
|
||||
settlementButton.isEnabled = viewModel.canSettle
|
||||
var configuration = settlementButton.configuration
|
||||
configuration?.showsActivityIndicator = isProcessing
|
||||
if isProcessing {
|
||||
configuration?.title = "补缴中"
|
||||
} else if isSettled {
|
||||
configuration?.title = "本日已全部补缴"
|
||||
} else if viewModel.summary.pendingAmountFen == 0 {
|
||||
configuration?.title = "当日暂无待补缴"
|
||||
} else {
|
||||
let prefix = viewModel.isToday ? "补缴今日全部" : "补缴本日全部"
|
||||
configuration?.title = "\(prefix) \(OfflineCollectionMoney.display(viewModel.summary.pendingAmountFen))"
|
||||
}
|
||||
settlementButton.configuration = configuration
|
||||
settlementButton.alpha = settlementButton.isEnabled || isProcessing ? 1 : 0.5
|
||||
|
||||
applySnapshot()
|
||||
presentSettlementFeedbackIfNeeded()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func applySnapshot() {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<OfflineDailyListSection, OfflineDailyListItem>()
|
||||
currentSections = []
|
||||
if viewModel.records.isEmpty {
|
||||
currentSections.append(.empty)
|
||||
snapshot.appendSections([.empty])
|
||||
snapshot.appendItems([.empty(isToday: viewModel.isToday)], toSection: .empty)
|
||||
} else {
|
||||
currentSections.append(.records)
|
||||
snapshot.appendSections([.records])
|
||||
snapshot.appendItems(viewModel.records.map(OfflineDailyListItem.record), toSection: .records)
|
||||
}
|
||||
if !viewModel.batches.isEmpty {
|
||||
currentSections.append(.batches)
|
||||
snapshot.appendSections([.batches])
|
||||
snapshot.appendItems(viewModel.batches.map(OfflineDailyListItem.batch), toSection: .batches)
|
||||
}
|
||||
dataSource.apply(snapshot, animatingDifferences: view.window != nil)
|
||||
}
|
||||
|
||||
private func makeDataSource() -> UITableViewDiffableDataSource<OfflineDailyListSection, OfflineDailyListItem> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, item in
|
||||
switch item {
|
||||
case let .record(record):
|
||||
guard let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: OfflineCollectionRecordCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as? OfflineCollectionRecordCell else { return UITableViewCell() }
|
||||
cell.apply(record: record)
|
||||
return cell
|
||||
case let .batch(batch):
|
||||
guard let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: OfflineSettlementBatchCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as? OfflineSettlementBatchCell else { return UITableViewCell() }
|
||||
cell.apply(batch: batch)
|
||||
return cell
|
||||
case let .empty(isToday):
|
||||
guard let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: OfflineCollectionEmptyCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as? OfflineCollectionEmptyCell else { return UITableViewCell() }
|
||||
cell.apply(isToday: isToday)
|
||||
cell.onRegister = { [weak self] in self?.openRegistration() }
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeDateMenu() -> UIMenu {
|
||||
let actions = viewModel.availableBusinessDates.map { [weak self] date in
|
||||
let isCurrent = date == self?.viewModel.businessDate
|
||||
let title = date == self?.viewModel.service.todayBusinessDate ? "今日 \(date)" : date
|
||||
return UIAction(title: title, state: isCurrent ? .on : .off) { [weak self] _ in
|
||||
self?.viewModel.selectBusinessDate(date)
|
||||
}
|
||||
}
|
||||
return UIMenu(title: "选择营业日", children: actions)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func presentSettlementFeedbackIfNeeded() {
|
||||
guard !isFeedbackPresented else { return }
|
||||
switch viewModel.settlementState {
|
||||
case .idle, .processing:
|
||||
return
|
||||
case let .success(batch):
|
||||
isFeedbackPresented = true
|
||||
let message = """
|
||||
已补缴 \(OfflineCollectionMoney.display(batch.amountFen))
|
||||
共结清 \(batch.recordCount) 笔线下收款
|
||||
补缴流水号:\(batch.id)
|
||||
"""
|
||||
let alert = UIAlertController(title: "补缴成功", message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "完成", style: .default) { [weak self] _ in
|
||||
self?.isFeedbackPresented = false
|
||||
self?.viewModel.clearSettlementFeedback()
|
||||
})
|
||||
present(alert, animated: true)
|
||||
case let .failed(message):
|
||||
isFeedbackPresented = true
|
||||
let alert = UIAlertController(title: "补缴失败", message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "暂不处理", style: .cancel) { [weak self] _ in
|
||||
self?.isFeedbackPresented = false
|
||||
self?.viewModel.cancelPreparedSettlement()
|
||||
self?.viewModel.clearSettlementFeedback()
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "重新补缴", style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.isFeedbackPresented = false
|
||||
self.viewModel.clearSettlementFeedback()
|
||||
Task { await self.viewModel.confirmSettlement() }
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func openRegistration() {
|
||||
guard viewModel.isToday else { return }
|
||||
navigationController?.pushViewController(
|
||||
OfflineCollectionRegistrationViewController(
|
||||
context: viewModel.context,
|
||||
service: viewModel.service
|
||||
),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func settlementTapped() {
|
||||
do {
|
||||
let request = try viewModel.prepareSettlement()
|
||||
let message = """
|
||||
营业日:\(request.businessDate)
|
||||
待补缴记录:\(request.recordIds.count) 笔
|
||||
本次补缴金额:\(OfflineCollectionMoney.display(request.amountFen))
|
||||
|
||||
补缴成功后,本批次记录将统一变为“已补缴”。
|
||||
"""
|
||||
let alert = UIAlertController(title: "确认补缴", message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel) { [weak self] _ in
|
||||
self?.viewModel.cancelPreparedSettlement()
|
||||
})
|
||||
alert.addAction(UIAlertAction(
|
||||
title: "确认补缴 \(OfflineCollectionMoney.display(request.amountFen))",
|
||||
style: .default
|
||||
) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.confirmSettlement() }
|
||||
})
|
||||
present(alert, animated: true)
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
viewModel.load()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func simulateFailureTapped() {
|
||||
viewModel.simulateNextSettlementFailure()
|
||||
showToast("下一次补缴将模拟失败,可验收失败后重试")
|
||||
}
|
||||
}
|
||||
|
||||
extension OfflineCollectionDailyViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
currentSections[safe: section] == .empty ? 0 : 40
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
guard let listSection = currentSections[safe: section], listSection != .empty else { return nil }
|
||||
let container = UIView()
|
||||
container.backgroundColor = AppColor.pageBackground
|
||||
let label = UILabel()
|
||||
label.text = listSection == .records ? "线下收款明细" : "线下收款补缴流水"
|
||||
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||
label.textColor = AppColor.textPrimary
|
||||
container.addSubview(label)
|
||||
label.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
||||
make.bottom.equalToSuperview().inset(AppSpacing.xs)
|
||||
}
|
||||
return container
|
||||
}
|
||||
}
|
||||
|
||||
/// 日清汇总卡片中的单项指标视图。
|
||||
final class OfflineDailyMetricView: UIView {
|
||||
private let titleLabel = UILabel()
|
||||
private let valueLabel = UILabel()
|
||||
|
||||
/// 使用指标名称创建汇总视图。
|
||||
init(title: String) {
|
||||
super.init(frame: .zero)
|
||||
titleLabel.text = title
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 更新指标值及是否使用警示色强调。
|
||||
func setValue(_ value: String, highlighted: Bool = false) {
|
||||
valueLabel.text = value
|
||||
valueLabel.textColor = highlighted ? UIColor(hex: 0xD97706) : AppColor.textPrimary
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
titleLabel.font = .systemFont(ofSize: 12)
|
||||
titleLabel.textColor = AppColor.textTertiary
|
||||
valueLabel.font = .systemFont(ofSize: 19, weight: .bold)
|
||||
valueLabel.textColor = AppColor.textPrimary
|
||||
valueLabel.adjustsFontSizeToFitWidth = true
|
||||
valueLabel.minimumScaleFactor = 0.75
|
||||
let stack = UIStackView(arrangedSubviews: [titleLabel, valueLabel])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 4
|
||||
addSubview(stack)
|
||||
stack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 2, left: 4, bottom: 2, right: 4))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 线下收款明细单元格。
|
||||
final class OfflineCollectionRecordCell: UITableViewCell {
|
||||
static let reuseIdentifier = "OfflineCollectionRecordCell"
|
||||
|
||||
private let cardView = UIView()
|
||||
private let timeLabel = UILabel()
|
||||
private let methodLabel = UILabel()
|
||||
private let identifierLabel = UILabel()
|
||||
private let amountLabel = UILabel()
|
||||
private let statusLabel = UILabel()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 用线下收款记录刷新单元格。
|
||||
func apply(record: OfflineCollectionRecord) {
|
||||
timeLabel.text = OfflineCollectionDate.timeText(record.registeredAt)
|
||||
methodLabel.text = record.paymentMethod.displayName
|
||||
identifierLabel.text = record.id
|
||||
amountLabel.text = OfflineCollectionMoney.display(record.amountFen)
|
||||
statusLabel.text = " \(record.status.displayName) "
|
||||
|
||||
let color: UIColor
|
||||
let background: UIColor
|
||||
switch record.status {
|
||||
case .pending:
|
||||
color = UIColor(hex: 0xD97706)
|
||||
background = UIColor(hex: 0xFFF7E6)
|
||||
case .settled:
|
||||
color = AppColor.success
|
||||
background = UIColor(hex: 0xECFDF3)
|
||||
case .overdue:
|
||||
color = AppColor.danger
|
||||
background = UIColor(hex: 0xFFF1F0)
|
||||
}
|
||||
statusLabel.textColor = color
|
||||
statusLabel.backgroundColor = background
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
contentView.backgroundColor = .clear
|
||||
cardView.backgroundColor = .white
|
||||
cardView.layer.cornerRadius = AppRadius.lg
|
||||
|
||||
timeLabel.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||
timeLabel.textColor = AppColor.textPrimary
|
||||
methodLabel.font = .systemFont(ofSize: 14)
|
||||
methodLabel.textColor = AppColor.textSecondary
|
||||
identifierLabel.font = .systemFont(ofSize: 11)
|
||||
identifierLabel.textColor = AppColor.textTertiary
|
||||
amountLabel.font = .systemFont(ofSize: 19, weight: .bold)
|
||||
amountLabel.textColor = AppColor.textPrimary
|
||||
amountLabel.textAlignment = .right
|
||||
statusLabel.font = .systemFont(ofSize: 12, weight: .semibold)
|
||||
statusLabel.textAlignment = .center
|
||||
statusLabel.layer.cornerRadius = 11
|
||||
statusLabel.clipsToBounds = true
|
||||
|
||||
let leftStack = UIStackView(arrangedSubviews: [timeLabel, methodLabel, identifierLabel])
|
||||
leftStack.axis = .vertical
|
||||
leftStack.spacing = 4
|
||||
let rightStack = UIStackView(arrangedSubviews: [amountLabel, statusLabel])
|
||||
rightStack.axis = .vertical
|
||||
rightStack.alignment = .trailing
|
||||
rightStack.spacing = 8
|
||||
cardView.addSubview(leftStack)
|
||||
cardView.addSubview(rightStack)
|
||||
contentView.addSubview(cardView)
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.top.bottom.equalToSuperview().inset(5)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
||||
}
|
||||
leftStack.snp.makeConstraints { make in
|
||||
make.leading.top.bottom.equalToSuperview().inset(AppSpacing.md)
|
||||
make.trailing.lessThanOrEqualTo(rightStack.snp.leading).offset(-AppSpacing.sm)
|
||||
}
|
||||
rightStack.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
statusLabel.snp.makeConstraints { make in
|
||||
make.height.equalTo(22)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 线下收款补缴流水单元格。
|
||||
final class OfflineSettlementBatchCell: UITableViewCell {
|
||||
static let reuseIdentifier = "OfflineSettlementBatchCell"
|
||||
|
||||
private let cardView = UIView()
|
||||
private let identifierLabel = UILabel()
|
||||
private let amountLabel = UILabel()
|
||||
private let detailLabel = UILabel()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 用成功补缴流水刷新单元格。
|
||||
func apply(batch: OfflineSettlementBatch) {
|
||||
identifierLabel.text = batch.id
|
||||
amountLabel.text = OfflineCollectionMoney.display(batch.amountFen)
|
||||
detailLabel.text = "\(OfflineCollectionDate.dateTimeText(batch.paidAt)) · \(batch.payerName) · 关联 \(batch.recordCount) 笔"
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
contentView.backgroundColor = .clear
|
||||
cardView.backgroundColor = UIColor(hex: 0xF4FBF7)
|
||||
cardView.layer.cornerRadius = AppRadius.lg
|
||||
cardView.layer.borderColor = UIColor(hex: 0xCDEED8).cgColor
|
||||
cardView.layer.borderWidth = 1
|
||||
|
||||
identifierLabel.font = .systemFont(ofSize: 13, weight: .semibold)
|
||||
identifierLabel.textColor = AppColor.success
|
||||
amountLabel.font = .systemFont(ofSize: 18, weight: .bold)
|
||||
amountLabel.textColor = AppColor.textPrimary
|
||||
amountLabel.textAlignment = .right
|
||||
detailLabel.font = .systemFont(ofSize: 12)
|
||||
detailLabel.textColor = AppColor.textSecondary
|
||||
detailLabel.numberOfLines = 0
|
||||
|
||||
let topRow = UIStackView(arrangedSubviews: [identifierLabel, amountLabel])
|
||||
topRow.axis = .horizontal
|
||||
topRow.distribution = .fillEqually
|
||||
let stack = UIStackView(arrangedSubviews: [topRow, detailLabel])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = AppSpacing.sm
|
||||
cardView.addSubview(stack)
|
||||
contentView.addSubview(cardView)
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.top.bottom.equalToSuperview().inset(5)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
||||
}
|
||||
stack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(AppSpacing.md)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 日清页在指定营业日无记录时展示的空状态单元格。
|
||||
final class OfflineCollectionEmptyCell: UITableViewCell {
|
||||
static let reuseIdentifier = "OfflineCollectionEmptyCell"
|
||||
var onRegister: (() -> Void)?
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let detailLabel = UILabel()
|
||||
private let registerButton = UIButton(type: .system)
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 更新空状态,仅今日允许前往登记。
|
||||
func apply(isToday: Bool) {
|
||||
registerButton.isHidden = !isToday
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
onRegister = nil
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
contentView.backgroundColor = .clear
|
||||
titleLabel.text = "当日暂无线下收款记录"
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
titleLabel.textAlignment = .center
|
||||
detailLabel.text = "完成线下收款后,请及时登记"
|
||||
detailLabel.font = .systemFont(ofSize: 13)
|
||||
detailLabel.textColor = AppColor.textSecondary
|
||||
detailLabel.textAlignment = .center
|
||||
registerButton.setTitle("去登记", for: .normal)
|
||||
registerButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .semibold)
|
||||
registerButton.addTarget(self, action: #selector(registerTapped), for: .touchUpInside)
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [titleLabel, detailLabel, registerButton])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = AppSpacing.sm
|
||||
stack.alignment = .center
|
||||
contentView.addSubview(stack)
|
||||
stack.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().inset(48)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
||||
make.bottom.equalToSuperview().inset(32)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func registerTapped() {
|
||||
onRegister?()
|
||||
}
|
||||
}
|
||||
|
||||
private extension Collection {
|
||||
/// 安全按下标读取元素,超出范围时返回 nil。
|
||||
subscript(safe index: Index) -> Element? {
|
||||
indices.contains(index) ? self[index] : nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user