feat: 完成9.7线下收款登记与日清
This commit is contained in:
@@ -0,0 +1,606 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 日清列表条目。
|
||||
private enum OfflineDailyItem: Hashable {
|
||||
case record(OfflineCollectionRecord)
|
||||
case empty(String)
|
||||
}
|
||||
|
||||
/// 日清记录在白色列表卡中的圆角位置。
|
||||
private enum OfflineRecordPosition {
|
||||
case single
|
||||
case first
|
||||
case middle
|
||||
case last
|
||||
}
|
||||
|
||||
/// 线下收款日清页,按 9.7 视觉稿展示日历、2×2 汇总、明细和补缴操作。
|
||||
@MainActor
|
||||
final class OfflineCollectionDailyViewController: BaseViewController {
|
||||
private let viewModel: OfflineCollectionDailyViewModel
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
private var dataSource: UITableViewDiffableDataSource<Int, OfflineDailyItem>!
|
||||
private let headerContainer = UIView()
|
||||
private let headerStack = UIStackView()
|
||||
private let calendarView = OfflineCollectionCalendarView()
|
||||
private let summaryCard = UIView()
|
||||
private let summaryDateLabel = UILabel()
|
||||
private let totalValue = UILabel()
|
||||
private let countValue = UILabel()
|
||||
private let settledValue = UILabel()
|
||||
private let pendingValue = UILabel()
|
||||
private let pendingStatusLabel = UILabel()
|
||||
private let statusButton = UIButton(type: .system)
|
||||
private let bottomContainer = UIView()
|
||||
private let settlementButton = OfflineCollectionGradientButton(
|
||||
startColor: UIColor(hex: 0xFF8B00),
|
||||
endColor: UIColor(hex: 0xFF7200)
|
||||
)
|
||||
private var feedbackPresented = false
|
||||
private var isShowingGlobalLoading = false
|
||||
|
||||
init(
|
||||
businessDate: String,
|
||||
context: OfflineCollectionContext = .current(),
|
||||
api: any OfflineCollectionServing = NetworkServices.shared.offlineCollectionAPI
|
||||
) {
|
||||
viewModel = OfflineCollectionDailyViewModel(businessDate: businessDate, context: context, api: api)
|
||||
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: 0xF7FAFF)
|
||||
configureTableView()
|
||||
configureHeader()
|
||||
configureBottomBar()
|
||||
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(bottomContainer)
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||||
make.bottom.equalTo(bottomContainer.snp.top)
|
||||
}
|
||||
bottomContainer.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview() }
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
calendarView.onDateSelected = { [weak self] date in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.selectBusinessDate(date) }
|
||||
}
|
||||
calendarView.onModeChanged = { [weak self] in self?.resizeHeader() }
|
||||
viewModel.onStateChange = { [weak self] in Task { @MainActor in self?.applyState() } }
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
Task { await viewModel.refresh() }
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
setGlobalLoadingVisible(false)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
resizeHeader()
|
||||
}
|
||||
|
||||
private func configureTableView() {
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.separatorStyle = .none
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 82
|
||||
tableView.showsVerticalScrollIndicator = false
|
||||
tableView.contentInset.bottom = 12
|
||||
tableView.register(OfflineCollectionRecordCell.self, forCellReuseIdentifier: OfflineCollectionRecordCell.reuseIdentifier)
|
||||
tableView.register(OfflineCollectionEmptyCell.self, forCellReuseIdentifier: OfflineCollectionEmptyCell.reuseIdentifier)
|
||||
dataSource = makeDataSource()
|
||||
}
|
||||
|
||||
private func configureHeader() {
|
||||
headerStack.axis = .vertical
|
||||
headerStack.spacing = 16
|
||||
headerContainer.addSubview(headerStack)
|
||||
headerStack.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(12)
|
||||
make.bottom.equalToSuperview().inset(10)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
}
|
||||
headerStack.addArrangedSubview(calendarView)
|
||||
configureSummaryCard()
|
||||
headerStack.addArrangedSubview(summaryCard)
|
||||
|
||||
let listTitle = UILabel()
|
||||
listTitle.text = "收款明细"
|
||||
listTitle.font = .systemFont(ofSize: 18, weight: .bold)
|
||||
listTitle.textColor = UIColor(hex: 0x081739)
|
||||
let listTitleContainer = UIView()
|
||||
listTitleContainer.addSubview(listTitle)
|
||||
listTitle.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(8)
|
||||
make.trailing.centerY.equalToSuperview()
|
||||
}
|
||||
listTitleContainer.snp.makeConstraints { make in make.height.equalTo(42) }
|
||||
headerStack.setCustomSpacing(18, after: summaryCard)
|
||||
headerStack.addArrangedSubview(listTitleContainer)
|
||||
|
||||
statusButton.configuration = .plain()
|
||||
statusButton.configuration?.baseForegroundColor = AppColor.primary
|
||||
statusButton.addTarget(self, action: #selector(retryTapped), for: .touchUpInside)
|
||||
statusButton.snp.makeConstraints { make in make.height.greaterThanOrEqualTo(44) }
|
||||
statusButton.isHidden = true
|
||||
headerStack.addArrangedSubview(statusButton)
|
||||
tableView.tableHeaderView = headerContainer
|
||||
}
|
||||
|
||||
private func configureSummaryCard() {
|
||||
summaryCard.backgroundColor = .white
|
||||
summaryCard.layer.cornerRadius = 12
|
||||
summaryCard.layer.shadowColor = UIColor(hex: 0x8FA0B8).cgColor
|
||||
summaryCard.layer.shadowOpacity = 0.12
|
||||
summaryCard.layer.shadowRadius = 10
|
||||
summaryCard.layer.shadowOffset = CGSize(width: 0, height: 4)
|
||||
|
||||
summaryDateLabel.font = .systemFont(ofSize: 18, weight: .bold)
|
||||
summaryDateLabel.textColor = UIColor(hex: 0x081739)
|
||||
|
||||
let topLeft = metric(title: "线下收款总额", value: totalValue)
|
||||
let topRight = metric(title: "登记笔数", value: countValue)
|
||||
let bottomLeft = metric(title: "已补缴", value: settledValue)
|
||||
let bottomRight = metric(title: "待补缴", value: pendingValue, badge: pendingStatusLabel)
|
||||
let grid = UIView()
|
||||
[topLeft, topRight, bottomLeft, bottomRight].forEach(grid.addSubview)
|
||||
topLeft.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview()
|
||||
make.width.equalToSuperview().multipliedBy(0.5)
|
||||
make.height.equalToSuperview().multipliedBy(0.5)
|
||||
}
|
||||
topRight.snp.makeConstraints { make in
|
||||
make.top.trailing.equalToSuperview()
|
||||
make.width.height.equalTo(topLeft)
|
||||
}
|
||||
bottomLeft.snp.makeConstraints { make in
|
||||
make.bottom.leading.equalToSuperview()
|
||||
make.width.height.equalTo(topLeft)
|
||||
}
|
||||
bottomRight.snp.makeConstraints { make in
|
||||
make.bottom.trailing.equalToSuperview()
|
||||
make.width.height.equalTo(topLeft)
|
||||
}
|
||||
|
||||
let horizontalDivider = UIView()
|
||||
let verticalDivider = UIView()
|
||||
[horizontalDivider, verticalDivider].forEach {
|
||||
$0.backgroundColor = UIColor(hex: 0xE7EBF1)
|
||||
grid.addSubview($0)
|
||||
}
|
||||
horizontalDivider.snp.makeConstraints { make in
|
||||
make.leading.trailing.centerY.equalToSuperview()
|
||||
make.height.equalTo(1)
|
||||
}
|
||||
verticalDivider.snp.makeConstraints { make in
|
||||
make.top.bottom.centerX.equalToSuperview()
|
||||
make.width.equalTo(1)
|
||||
}
|
||||
|
||||
summaryCard.addSubview(summaryDateLabel)
|
||||
summaryCard.addSubview(grid)
|
||||
summaryDateLabel.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview().inset(16)
|
||||
make.height.equalTo(24)
|
||||
}
|
||||
grid.snp.makeConstraints { make in
|
||||
make.top.equalTo(summaryDateLabel.snp.bottom).offset(8)
|
||||
make.leading.trailing.equalToSuperview().inset(10)
|
||||
make.bottom.equalToSuperview().inset(10)
|
||||
}
|
||||
summaryCard.snp.makeConstraints { make in make.height.equalTo(238) }
|
||||
}
|
||||
|
||||
private func metric(title: String, value: UILabel, badge: UILabel? = nil) -> UIView {
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = title
|
||||
titleLabel.font = .systemFont(ofSize: 13)
|
||||
titleLabel.textColor = UIColor(hex: 0x7B8494)
|
||||
titleLabel.textAlignment = .center
|
||||
|
||||
value.font = .systemFont(ofSize: 22, weight: .bold)
|
||||
value.textColor = UIColor(hex: 0x081739)
|
||||
value.textAlignment = .center
|
||||
value.adjustsFontSizeToFitWidth = true
|
||||
value.minimumScaleFactor = 0.72
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [titleLabel, value])
|
||||
stack.axis = .vertical
|
||||
stack.alignment = .fill
|
||||
stack.spacing = 8
|
||||
if let badge {
|
||||
badge.text = "未结清"
|
||||
badge.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
badge.textColor = UIColor(hex: 0xFF7600)
|
||||
badge.textAlignment = .center
|
||||
badge.backgroundColor = UIColor(hex: 0xFFF0E2)
|
||||
badge.layer.cornerRadius = 7
|
||||
badge.clipsToBounds = true
|
||||
badge.snp.makeConstraints { make in make.width.equalTo(72); make.height.equalTo(26) }
|
||||
let badgeContainer = UIView()
|
||||
badgeContainer.addSubview(badge)
|
||||
badge.snp.makeConstraints { make in make.center.equalToSuperview() }
|
||||
stack.addArrangedSubview(badgeContainer)
|
||||
badgeContainer.snp.makeConstraints { make in make.height.equalTo(26) }
|
||||
stack.spacing = 5
|
||||
}
|
||||
|
||||
let container = UIView()
|
||||
container.addSubview(stack)
|
||||
stack.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.leading.trailing.equalToSuperview().inset(12)
|
||||
}
|
||||
return container
|
||||
}
|
||||
|
||||
private func configureBottomBar() {
|
||||
bottomContainer.backgroundColor = .white
|
||||
bottomContainer.layer.shadowColor = UIColor(hex: 0x8090A8).cgColor
|
||||
bottomContainer.layer.shadowOpacity = 0.12
|
||||
bottomContainer.layer.shadowRadius = 12
|
||||
bottomContainer.layer.shadowOffset = CGSize(width: 0, height: -3)
|
||||
settlementButton.setTitle("当日暂无待补缴", for: .normal)
|
||||
settlementButton.setTitleColor(.white, for: .normal)
|
||||
settlementButton.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
settlementButton.layer.cornerRadius = 10
|
||||
settlementButton.clipsToBounds = true
|
||||
settlementButton.accessibilityIdentifier = "offlineCollection.settle"
|
||||
settlementButton.addTarget(self, action: #selector(settlementTapped), for: .touchUpInside)
|
||||
bottomContainer.addSubview(settlementButton)
|
||||
settlementButton.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(14)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
make.height.equalTo(54)
|
||||
make.bottom.equalTo(bottomContainer.safeAreaLayoutGuide).inset(14)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor private func applyState() {
|
||||
let processing = viewModel.settlementState == .processing
|
||||
setGlobalLoadingVisible(viewModel.isLoading || processing)
|
||||
calendarView.apply(
|
||||
selectedDate: viewModel.businessDate,
|
||||
maximumDate: viewModel.serverToday,
|
||||
pendingDates: viewModel.pendingDates
|
||||
)
|
||||
summaryDateLabel.text = formattedSummaryDate()
|
||||
totalValue.text = OfflineCollectionMoney.display(viewModel.summary.totalAmountFen)
|
||||
countValue.text = "\(viewModel.summary.totalCount)笔"
|
||||
settledValue.text = OfflineCollectionMoney.display(viewModel.summary.settledAmountFen)
|
||||
pendingValue.text = OfflineCollectionMoney.display(viewModel.summary.pendingAmountFen)
|
||||
pendingValue.textColor = viewModel.summary.pendingCount > 0 ? UIColor(hex: 0xFF7600) : UIColor(hex: 0x081739)
|
||||
pendingStatusLabel.isHidden = viewModel.summary.pendingCount == 0
|
||||
|
||||
if let error = viewModel.errorMessage, !viewModel.isLoading {
|
||||
statusButton.isHidden = false
|
||||
statusButton.isUserInteractionEnabled = true
|
||||
statusButton.configuration?.title = "\(error) 点击重试"
|
||||
UIAccessibility.post(notification: .announcement, argument: error)
|
||||
} else {
|
||||
statusButton.isHidden = true
|
||||
}
|
||||
|
||||
settlementButton.isEnabled = viewModel.canSettle && !processing
|
||||
if processing {
|
||||
settlementButton.setTitle("补缴中", for: .normal)
|
||||
} else if viewModel.summary.totalCount > 0 && viewModel.summary.pendingCount == 0 {
|
||||
settlementButton.setTitle("本日已全部补缴", for: .normal)
|
||||
} else if viewModel.summary.pendingCount == 0 {
|
||||
settlementButton.setTitle("当日暂无待补缴", for: .normal)
|
||||
} else {
|
||||
settlementButton.setTitle(
|
||||
"补缴本日全部 \(OfflineCollectionMoney.display(viewModel.summary.pendingAmountFen))",
|
||||
for: .normal
|
||||
)
|
||||
}
|
||||
settlementButton.alpha = viewModel.canSettle || processing ? 1 : 0.45
|
||||
applySnapshot()
|
||||
resizeHeader()
|
||||
presentFeedbackIfNeeded()
|
||||
}
|
||||
|
||||
private func formattedSummaryDate() -> String {
|
||||
guard let date = OfflineCollectionDate.date(from: viewModel.businessDate) else { return viewModel.businessDate }
|
||||
let calendar = OfflineCollectionDate.calendar
|
||||
let text = "\(calendar.component(.month, from: date))月\(calendar.component(.day, from: date))日"
|
||||
return viewModel.businessDate == viewModel.serverToday ? "\(text) · 今日" : text
|
||||
}
|
||||
|
||||
private func setGlobalLoadingVisible(_ visible: Bool) {
|
||||
guard visible != isShowingGlobalLoading else { return }
|
||||
isShowingGlobalLoading = visible
|
||||
visible ? showLoading() : hideLoading()
|
||||
}
|
||||
|
||||
@MainActor private func applySnapshot() {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, OfflineDailyItem>()
|
||||
snapshot.appendSections([0])
|
||||
if viewModel.records.isEmpty {
|
||||
let text = viewModel.errorMessage == nil && !viewModel.isLoading ? "本日暂无线下收款记录" : ""
|
||||
snapshot.appendItems([.empty(text)])
|
||||
} else {
|
||||
snapshot.appendItems(viewModel.records.map(OfflineDailyItem.record))
|
||||
}
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
private func makeDataSource() -> UITableViewDiffableDataSource<Int, OfflineDailyItem> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, item in
|
||||
switch item {
|
||||
case let .record(record):
|
||||
let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: OfflineCollectionRecordCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as! OfflineCollectionRecordCell
|
||||
let count = self?.viewModel.records.count ?? 1
|
||||
let position: OfflineRecordPosition
|
||||
if count == 1 { position = .single }
|
||||
else if indexPath.row == 0 { position = .first }
|
||||
else if indexPath.row == count - 1 { position = .last }
|
||||
else { position = .middle }
|
||||
cell.apply(record, position: position)
|
||||
return cell
|
||||
case let .empty(text):
|
||||
let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: OfflineCollectionEmptyCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as! OfflineCollectionEmptyCell
|
||||
cell.apply(text)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func resizeHeader() {
|
||||
guard tableView.bounds.width > 0 else { return }
|
||||
headerContainer.frame.size.width = tableView.bounds.width
|
||||
headerContainer.setNeedsLayout()
|
||||
headerContainer.layoutIfNeeded()
|
||||
let height = headerContainer.systemLayoutSizeFitting(
|
||||
CGSize(width: tableView.bounds.width, height: UIView.layoutFittingCompressedSize.height),
|
||||
withHorizontalFittingPriority: .required,
|
||||
verticalFittingPriority: .fittingSizeLevel
|
||||
).height
|
||||
guard abs(headerContainer.frame.height - height) > 0.5 else { return }
|
||||
headerContainer.frame.size.height = height
|
||||
tableView.tableHeaderView = headerContainer
|
||||
}
|
||||
|
||||
@MainActor private func presentFeedbackIfNeeded() {
|
||||
guard !feedbackPresented else { return }
|
||||
switch viewModel.settlementState {
|
||||
case .idle, .processing:
|
||||
return
|
||||
case let .success(result):
|
||||
feedbackPresented = true
|
||||
let alert = UIAlertController(
|
||||
title: "补缴成功",
|
||||
message: "营业日:\(result.date)\n共结清 \(result.updatedCount) 笔",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "完成", style: .default) { [weak self] _ in self?.finishFeedback() })
|
||||
present(alert, animated: true)
|
||||
case let .failed(message, canRetry):
|
||||
feedbackPresented = true
|
||||
let alert = UIAlertController(title: "补缴失败", message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "暂不处理", style: .cancel) { [weak self] _ in
|
||||
self?.viewModel.cancelPreparedSettlement()
|
||||
self?.finishFeedback()
|
||||
})
|
||||
if canRetry {
|
||||
alert.addAction(UIAlertAction(title: "重新补缴", style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.feedbackPresented = false
|
||||
Task { await self.viewModel.confirmSettlement() }
|
||||
})
|
||||
}
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func finishFeedback() {
|
||||
feedbackPresented = false
|
||||
viewModel.clearSettlementFeedback()
|
||||
}
|
||||
|
||||
@objc private func retryTapped() {
|
||||
Task { await viewModel.refresh() }
|
||||
}
|
||||
|
||||
@objc private func settlementTapped() {
|
||||
do {
|
||||
let request = try viewModel.prepareSettlement()
|
||||
let message = "营业日:\(request.businessDate)\n待补缴记录:\(request.pendingCount) 笔\n本次补缴金额:\(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: "确认补缴", style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.confirmSettlement() }
|
||||
})
|
||||
present(alert, animated: true)
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 日清页的一笔收款记录行,纯展示支付方式、编号、金额、状态与时间。
|
||||
private final class OfflineCollectionRecordCell: UITableViewCell {
|
||||
static let reuseIdentifier = "OfflineCollectionRecordCell"
|
||||
|
||||
private let card = UIView()
|
||||
private let iconView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
private let idLabel = UILabel()
|
||||
private let amountLabel = UILabel()
|
||||
private let statusLabel = UILabel()
|
||||
private let timeLabel = UILabel()
|
||||
private let separator = UIView()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
contentView.backgroundColor = .clear
|
||||
|
||||
card.backgroundColor = .white
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
titleLabel.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||
titleLabel.textColor = UIColor(hex: 0x081739)
|
||||
|
||||
idLabel.font = .systemFont(ofSize: 9.5, weight: .medium)
|
||||
idLabel.textColor = UIColor(hex: 0x6F7B8F)
|
||||
idLabel.backgroundColor = UIColor(hex: 0xF0F2F6)
|
||||
idLabel.layer.cornerRadius = 5
|
||||
idLabel.clipsToBounds = true
|
||||
idLabel.textAlignment = .center
|
||||
idLabel.adjustsFontSizeToFitWidth = true
|
||||
idLabel.minimumScaleFactor = 0.7
|
||||
|
||||
amountLabel.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
amountLabel.textColor = UIColor(hex: 0x081739)
|
||||
amountLabel.textAlignment = .right
|
||||
amountLabel.adjustsFontSizeToFitWidth = true
|
||||
amountLabel.minimumScaleFactor = 0.72
|
||||
|
||||
statusLabel.font = .systemFont(ofSize: 11, weight: .medium)
|
||||
statusLabel.textAlignment = .center
|
||||
statusLabel.layer.cornerRadius = 7
|
||||
statusLabel.clipsToBounds = true
|
||||
|
||||
timeLabel.font = .systemFont(ofSize: 12)
|
||||
timeLabel.textColor = UIColor(hex: 0x7B8494)
|
||||
timeLabel.textAlignment = .right
|
||||
separator.backgroundColor = UIColor(hex: 0xE7EBF1)
|
||||
|
||||
let idContainer = UIView()
|
||||
idContainer.addSubview(idLabel)
|
||||
idLabel.snp.makeConstraints { make in
|
||||
make.leading.top.bottom.equalToSuperview()
|
||||
make.trailing.lessThanOrEqualToSuperview()
|
||||
}
|
||||
idContainer.snp.makeConstraints { make in make.height.equalTo(20) }
|
||||
let leftStack = UIStackView(arrangedSubviews: [titleLabel, idContainer])
|
||||
leftStack.axis = .vertical
|
||||
leftStack.spacing = 4
|
||||
|
||||
card.addSubview(iconView)
|
||||
card.addSubview(leftStack)
|
||||
card.addSubview(amountLabel)
|
||||
card.addSubview(statusLabel)
|
||||
card.addSubview(timeLabel)
|
||||
card.addSubview(separator)
|
||||
contentView.addSubview(card)
|
||||
|
||||
card.snp.makeConstraints { make in
|
||||
make.top.bottom.equalToSuperview()
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
make.height.greaterThanOrEqualTo(78)
|
||||
}
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(12)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(38)
|
||||
}
|
||||
leftStack.snp.makeConstraints { make in
|
||||
make.leading.equalTo(iconView.snp.trailing).offset(10)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.equalTo(106)
|
||||
}
|
||||
amountLabel.snp.makeConstraints { make in
|
||||
make.leading.greaterThanOrEqualTo(leftStack.snp.trailing).offset(4)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.equalTo(68)
|
||||
}
|
||||
statusLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(amountLabel.snp.trailing).offset(4)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.equalTo(50)
|
||||
make.height.equalTo(27)
|
||||
}
|
||||
timeLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(statusLabel.snp.trailing).offset(2)
|
||||
make.trailing.equalToSuperview().inset(10)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.greaterThanOrEqualTo(34)
|
||||
}
|
||||
separator.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(12)
|
||||
make.trailing.equalToSuperview().inset(8)
|
||||
make.bottom.equalToSuperview()
|
||||
make.height.equalTo(1 / UIScreen.main.scale)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
func apply(_ record: OfflineCollectionRecord, position: OfflineRecordPosition) {
|
||||
iconView.image = UIImage(named: record.paymentMethod.assetName)
|
||||
titleLabel.text = record.paymentMethod == .wechat ? "微信收款" : record.paymentMethod.displayName
|
||||
idLabel.text = "编号 \(record.collectNo)"
|
||||
amountLabel.text = OfflineCollectionMoney.display(record.amountFen)
|
||||
let settled = record.status == .settled
|
||||
statusLabel.text = settled ? "已补缴" : "未补缴"
|
||||
statusLabel.textColor = settled ? UIColor(hex: 0x16A34A) : UIColor(hex: 0xFF7600)
|
||||
statusLabel.backgroundColor = settled ? UIColor(hex: 0xEAF8EF) : UIColor(hex: 0xFFF0E2)
|
||||
timeLabel.text = record.timeText
|
||||
separator.isHidden = position == .single || position == .last
|
||||
|
||||
card.layer.cornerRadius = 12
|
||||
switch position {
|
||||
case .single:
|
||||
card.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
case .first:
|
||||
card.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
case .middle:
|
||||
card.layer.maskedCorners = []
|
||||
case .last:
|
||||
card.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
}
|
||||
accessibilityLabel = "\(titleLabel.text ?? ""),编号 \(record.collectNo),\(amountLabel.text ?? ""),\(statusLabel.text ?? ""),\(record.timeText)"
|
||||
}
|
||||
}
|
||||
|
||||
/// 日清页空数据占位。
|
||||
private final class OfflineCollectionEmptyCell: UITableViewCell {
|
||||
static let reuseIdentifier = "OfflineCollectionEmptyCell"
|
||||
private let label = UILabel()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
label.font = .systemFont(ofSize: 14)
|
||||
label.textColor = UIColor(hex: 0x7B8494)
|
||||
label.textAlignment = .center
|
||||
contentView.addSubview(label)
|
||||
label.snp.makeConstraints { make in make.edges.equalToSuperview().inset(36) }
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
func apply(_ text: String) {
|
||||
label.text = text
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 线下收款登记页,按 9.7 视觉稿展示金额、支付方式和登记说明。
|
||||
@MainActor
|
||||
final class OfflineCollectionRegistrationViewController: BaseViewController {
|
||||
private let viewModel: OfflineCollectionRegistrationViewModel
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
private let amountCard = UIView()
|
||||
private let amountField = UITextField()
|
||||
private let methodCard = UIView()
|
||||
private let methodStack = UIStackView()
|
||||
private var methodButtons: [OfflineCollectionPaymentMethod: OfflinePaymentMethodButton] = [:]
|
||||
private let explanationCard = UIView()
|
||||
private let contextLabel = UILabel()
|
||||
private let bottomContainer = UIView()
|
||||
private let submitButton = OfflineCollectionGradientButton(
|
||||
startColor: UIColor(hex: 0x087BFF),
|
||||
endColor: UIColor(hex: 0x0067F4)
|
||||
)
|
||||
private var hasFocusedAmountField = false
|
||||
private var isShowingGlobalLoading = false
|
||||
|
||||
init(
|
||||
context: OfflineCollectionContext = .current(),
|
||||
api: any OfflineCollectionServing = NetworkServices.shared.offlineCollectionAPI
|
||||
) {
|
||||
viewModel = OfflineCollectionRegistrationViewModel(context: context, api: api)
|
||||
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: 0xF7FAFF)
|
||||
configureScrollContent()
|
||||
configureAmountCard()
|
||||
configureMethodCard()
|
||||
configureExplanationCard()
|
||||
configureContextLabel()
|
||||
configureBottomBar()
|
||||
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(contentStack)
|
||||
[amountCard, methodCard, explanationCard, contextLabel].forEach(contentStack.addArrangedSubview)
|
||||
view.addSubview(bottomContainer)
|
||||
bottomContainer.addSubview(submitButton)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
bottomContainer.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.bottom.equalTo(view.keyboardLayoutGuide.snp.top)
|
||||
}
|
||||
submitButton.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(16)
|
||||
make.leading.trailing.equalToSuperview().inset(18)
|
||||
make.height.equalTo(56)
|
||||
make.bottom.equalToSuperview().inset(16)
|
||||
}
|
||||
scrollView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||||
make.bottom.equalTo(bottomContainer.snp.top)
|
||||
}
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalTo(scrollView.contentLayoutGuide).inset(UIEdgeInsets(top: 16, left: 16, bottom: 24, right: 16))
|
||||
make.width.equalTo(scrollView.frameLayoutGuide).offset(-32)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
amountField.addTarget(self, action: #selector(amountChanged), for: .editingChanged)
|
||||
submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)
|
||||
viewModel.onStateChange = { [weak self] in Task { @MainActor in self?.applyState() } }
|
||||
viewModel.onShowMessage = { [weak self] message in Task { @MainActor in self?.showToast(message) } }
|
||||
viewModel.onRegistrationSuccess = { [weak self] receipt in Task { @MainActor in self?.showSuccess(receipt) } }
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
applyState()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
guard !hasFocusedAmountField else { return }
|
||||
hasFocusedAmountField = true
|
||||
amountField.becomeFirstResponder()
|
||||
moveAmountCursorToEnd()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
setGlobalLoadingVisible(false)
|
||||
}
|
||||
|
||||
private func configureScrollContent() {
|
||||
scrollView.keyboardDismissMode = .interactive
|
||||
scrollView.alwaysBounceVertical = true
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 16
|
||||
}
|
||||
|
||||
private func configureAmountCard() {
|
||||
configureCard(amountCard)
|
||||
|
||||
let titleLabel = makeTitleLabel("收款金额")
|
||||
let helperLabel = UILabel()
|
||||
helperLabel.text = "单笔金额 0.01~99,999.99 元"
|
||||
helperLabel.font = .systemFont(ofSize: 13)
|
||||
helperLabel.textColor = UIColor(hex: 0x7B8494)
|
||||
helperLabel.adjustsFontSizeToFitWidth = true
|
||||
helperLabel.minimumScaleFactor = 0.82
|
||||
|
||||
let currencyLabel = UILabel()
|
||||
currencyLabel.text = "¥"
|
||||
currencyLabel.font = .systemFont(ofSize: 34, weight: .semibold)
|
||||
currencyLabel.textColor = UIColor(hex: 0x081739)
|
||||
|
||||
amountField.placeholder = "0.00"
|
||||
amountField.font = .systemFont(ofSize: 42, weight: .bold)
|
||||
amountField.textColor = UIColor(hex: 0x081739)
|
||||
amountField.tintColor = AppColor.primary
|
||||
amountField.textAlignment = .right
|
||||
amountField.keyboardType = .decimalPad
|
||||
amountField.adjustsFontSizeToFitWidth = true
|
||||
amountField.minimumFontSize = 30
|
||||
amountField.delegate = self
|
||||
amountField.accessibilityLabel = "收款金额"
|
||||
amountField.accessibilityIdentifier = "offlineCollection.amount"
|
||||
|
||||
let amountInputStack = UIStackView(arrangedSubviews: [currencyLabel, amountField])
|
||||
amountInputStack.axis = .horizontal
|
||||
amountInputStack.alignment = .center
|
||||
amountInputStack.spacing = 6
|
||||
|
||||
let amountRow = UIView()
|
||||
amountRow.addSubview(helperLabel)
|
||||
amountRow.addSubview(amountInputStack)
|
||||
helperLabel.snp.makeConstraints { make in
|
||||
make.leading.centerY.equalToSuperview()
|
||||
make.trailing.lessThanOrEqualTo(amountInputStack.snp.leading).offset(-8)
|
||||
}
|
||||
amountInputStack.snp.makeConstraints { make in
|
||||
make.trailing.centerY.equalToSuperview()
|
||||
make.width.lessThanOrEqualTo(205)
|
||||
}
|
||||
amountField.snp.makeConstraints { make in
|
||||
make.height.equalTo(56)
|
||||
make.width.greaterThanOrEqualTo(98)
|
||||
}
|
||||
|
||||
let underline = UIView()
|
||||
underline.backgroundColor = UIColor(hex: 0x1684FC)
|
||||
underline.snp.makeConstraints { make in make.height.equalTo(1) }
|
||||
|
||||
let quickAmounts: [(String, Int)] = [("¥50", 5_000), ("¥100", 10_000), ("¥200", 20_000), ("¥500", 50_000)]
|
||||
let quickStack = UIStackView()
|
||||
quickStack.axis = .horizontal
|
||||
quickStack.distribution = .fillEqually
|
||||
quickStack.spacing = 12
|
||||
quickAmounts.forEach { title, fen in
|
||||
let button = UIButton(type: .system)
|
||||
button.tag = fen
|
||||
button.setTitle(title, for: .normal)
|
||||
button.setTitleColor(UIColor(hex: 0x1677FF), for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
|
||||
button.backgroundColor = UIColor(hex: 0xF5F8FD)
|
||||
button.layer.cornerRadius = 8
|
||||
button.layer.borderWidth = 1
|
||||
button.layer.borderColor = UIColor(hex: 0xE3E8F0).cgColor
|
||||
button.addTarget(self, action: #selector(quickAmountTapped(_:)), for: .touchUpInside)
|
||||
button.snp.makeConstraints { make in make.height.equalTo(36) }
|
||||
quickStack.addArrangedSubview(button)
|
||||
}
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [titleLabel, amountRow, underline, quickStack])
|
||||
stack.axis = .vertical
|
||||
stack.setCustomSpacing(8, after: titleLabel)
|
||||
stack.setCustomSpacing(2, after: amountRow)
|
||||
stack.setCustomSpacing(22, after: underline)
|
||||
amountCard.addSubview(stack)
|
||||
stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(20) }
|
||||
amountRow.snp.makeConstraints { make in make.height.equalTo(56) }
|
||||
}
|
||||
|
||||
private func configureMethodCard() {
|
||||
configureCard(methodCard)
|
||||
let titleLabel = makeTitleLabel("收款方式")
|
||||
methodStack.axis = .horizontal
|
||||
methodStack.distribution = .fillEqually
|
||||
methodStack.spacing = 12
|
||||
for method in OfflineCollectionPaymentMethod.allCases {
|
||||
let button = OfflinePaymentMethodButton(method: method)
|
||||
button.addTarget(self, action: #selector(methodTapped(_:)), for: .touchUpInside)
|
||||
methodButtons[method] = button
|
||||
methodStack.addArrangedSubview(button)
|
||||
}
|
||||
let stack = UIStackView(arrangedSubviews: [titleLabel, methodStack])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 20
|
||||
methodCard.addSubview(stack)
|
||||
stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(20) }
|
||||
methodStack.snp.makeConstraints { make in make.height.equalTo(120) }
|
||||
}
|
||||
|
||||
private func configureExplanationCard() {
|
||||
configureCard(explanationCard)
|
||||
let iconView = UIImageView(image: UIImage(named: "offline_security_shield"))
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
iconView.accessibilityLabel = "安全说明"
|
||||
|
||||
let label = UILabel()
|
||||
label.text = "登记后计入今日待补缴,不生成订单"
|
||||
label.font = .systemFont(ofSize: 15)
|
||||
label.textColor = UIColor(hex: 0x22304D)
|
||||
label.numberOfLines = 0
|
||||
|
||||
explanationCard.addSubview(iconView)
|
||||
explanationCard.addSubview(label)
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(20)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(44)
|
||||
}
|
||||
label.snp.makeConstraints { make in
|
||||
make.leading.equalTo(iconView.snp.trailing).offset(14)
|
||||
make.trailing.equalToSuperview().inset(18)
|
||||
make.top.bottom.equalToSuperview().inset(20)
|
||||
}
|
||||
explanationCard.snp.makeConstraints { make in make.height.greaterThanOrEqualTo(72) }
|
||||
}
|
||||
|
||||
private func configureContextLabel() {
|
||||
contextLabel.text = "当前:\(viewModel.context.collectorName) · \(viewModel.context.storeName) · \(viewModel.context.scenicName)"
|
||||
contextLabel.font = .systemFont(ofSize: 13)
|
||||
contextLabel.textColor = UIColor(hex: 0x7B8494)
|
||||
contextLabel.numberOfLines = 0
|
||||
contextLabel.textAlignment = .center
|
||||
contextLabel.snp.makeConstraints { make in make.height.greaterThanOrEqualTo(44) }
|
||||
}
|
||||
|
||||
private func configureBottomBar() {
|
||||
bottomContainer.backgroundColor = .white
|
||||
bottomContainer.layer.shadowColor = UIColor(hex: 0x8090A8).cgColor
|
||||
bottomContainer.layer.shadowOpacity = 0.12
|
||||
bottomContainer.layer.shadowRadius = 12
|
||||
bottomContainer.layer.shadowOffset = CGSize(width: 0, height: -3)
|
||||
submitButton.setTitle("确认登记", for: .normal)
|
||||
submitButton.setTitleColor(.white, for: .normal)
|
||||
submitButton.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
|
||||
submitButton.layer.cornerRadius = 10
|
||||
submitButton.clipsToBounds = true
|
||||
submitButton.accessibilityIdentifier = "offlineCollection.submit"
|
||||
}
|
||||
|
||||
@MainActor private func applyState() {
|
||||
setGlobalLoadingVisible(viewModel.isSubmitting)
|
||||
if amountField.text != viewModel.amountText {
|
||||
amountField.text = viewModel.amountText
|
||||
moveAmountCursorToEnd()
|
||||
}
|
||||
methodButtons.forEach { method, button in button.setSelected(method == viewModel.paymentMethod) }
|
||||
submitButton.isEnabled = viewModel.canSubmit && !viewModel.isSubmitting
|
||||
submitButton.alpha = viewModel.canSubmit || viewModel.isSubmitting ? 1 : 0.45
|
||||
}
|
||||
|
||||
@MainActor private func showSuccess(_ receipt: OfflineCollectionRegistrationReceipt) {
|
||||
setGlobalLoadingVisible(false)
|
||||
amountField.resignFirstResponder()
|
||||
let totalText = receipt.summary.map { OfflineCollectionMoney.display($0.totalAmountFen) } ?? "—"
|
||||
let pendingText = receipt.summary.map { OfflineCollectionMoney.display($0.pendingAmountFen) } ?? "—"
|
||||
let message = """
|
||||
本次登记 \(OfflineCollectionMoney.display(receipt.record.amountFen))
|
||||
今日累计线下收款 \(totalText)
|
||||
今日待补缴 \(pendingText)
|
||||
"""
|
||||
let alert = UIAlertController(title: "登记成功", message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "继续登记", style: .default) { [weak self] _ in
|
||||
self?.viewModel.startAnotherRegistration()
|
||||
self?.amountField.becomeFirstResponder()
|
||||
self?.moveAmountCursorToEnd()
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "查看今日明细", style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.navigationController?.pushViewController(
|
||||
OfflineCollectionDailyViewController(
|
||||
businessDate: receipt.businessDate,
|
||||
context: self.viewModel.context,
|
||||
api: self.viewModel.api
|
||||
),
|
||||
animated: true
|
||||
)
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func setGlobalLoadingVisible(_ visible: Bool) {
|
||||
guard visible != isShowingGlobalLoading else { return }
|
||||
isShowingGlobalLoading = visible
|
||||
visible ? showLoading() : hideLoading()
|
||||
}
|
||||
|
||||
private func makeTitleLabel(_ text: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
label.textColor = UIColor(hex: 0x081739)
|
||||
return label
|
||||
}
|
||||
|
||||
private func configureCard(_ card: UIView) {
|
||||
card.backgroundColor = .white
|
||||
card.layer.cornerRadius = 12
|
||||
card.layer.shadowColor = UIColor(hex: 0x8FA0B8).cgColor
|
||||
card.layer.shadowOpacity = 0.12
|
||||
card.layer.shadowRadius = 10
|
||||
card.layer.shadowOffset = CGSize(width: 0, height: 4)
|
||||
}
|
||||
|
||||
private func moveAmountCursorToEnd() {
|
||||
guard amountField.isFirstResponder else { return }
|
||||
let end = amountField.endOfDocument
|
||||
amountField.selectedTextRange = amountField.textRange(from: end, to: end)
|
||||
}
|
||||
|
||||
@objc private func amountChanged() {
|
||||
viewModel.updateAmount(amountField.text ?? "")
|
||||
moveAmountCursorToEnd()
|
||||
}
|
||||
|
||||
@objc private func quickAmountTapped(_ sender: UIButton) {
|
||||
viewModel.updateAmount(OfflineCollectionMoney.apiAmount(sender.tag))
|
||||
amountField.becomeFirstResponder()
|
||||
moveAmountCursorToEnd()
|
||||
}
|
||||
|
||||
@objc private func methodTapped(_ sender: OfflinePaymentMethodButton) {
|
||||
viewModel.selectPaymentMethod(sender.method)
|
||||
}
|
||||
|
||||
@objc private func submitTapped() {
|
||||
Task { await viewModel.submit() }
|
||||
}
|
||||
}
|
||||
|
||||
extension OfflineCollectionRegistrationViewController: UITextFieldDelegate {
|
||||
func textField(
|
||||
_ textField: UITextField,
|
||||
shouldChangeCharactersIn range: NSRange,
|
||||
replacementString string: String
|
||||
) -> Bool {
|
||||
guard let current = textField.text, let swiftRange = Range(range, in: current) else { return false }
|
||||
return OfflineCollectionMoney.acceptsEditingText(current.replacingCharacters(in: swiftRange, with: string))
|
||||
}
|
||||
}
|
||||
|
||||
/// 登记页的收款方式单选卡,展示生成的品牌图标和右上角选中标记。
|
||||
@MainActor
|
||||
final class OfflinePaymentMethodButton: UIControl {
|
||||
let method: OfflineCollectionPaymentMethod
|
||||
|
||||
private let iconView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
private let checkmarkView = UIImageView(image: UIImage(systemName: "checkmark"))
|
||||
|
||||
init(method: OfflineCollectionPaymentMethod) {
|
||||
self.method = method
|
||||
super.init(frame: .zero)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
func setSelected(_ selected: Bool) {
|
||||
isSelected = selected
|
||||
backgroundColor = selected ? UIColor(hex: 0xF1F7FF) : .white
|
||||
layer.borderColor = (selected ? UIColor(hex: 0x1684FC) : UIColor(hex: 0xE2E7EF)).cgColor
|
||||
layer.borderWidth = selected ? 1.5 : 1
|
||||
checkmarkView.isHidden = !selected
|
||||
titleLabel.textColor = UIColor(hex: 0x081739)
|
||||
accessibilityTraits = selected ? [.button, .selected] : .button
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
layer.cornerRadius = 10
|
||||
clipsToBounds = false
|
||||
accessibilityLabel = method.displayName
|
||||
iconView.image = UIImage(named: method.assetName)?.withRenderingMode(.alwaysOriginal)
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
titleLabel.text = method.displayName
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
titleLabel.textAlignment = .center
|
||||
checkmarkView.tintColor = .white
|
||||
checkmarkView.backgroundColor = UIColor(hex: 0x1684FC)
|
||||
checkmarkView.layer.cornerRadius = 11
|
||||
checkmarkView.contentMode = .center
|
||||
checkmarkView.isHidden = true
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [iconView, titleLabel])
|
||||
stack.axis = .vertical
|
||||
stack.alignment = .center
|
||||
stack.spacing = 12
|
||||
stack.isUserInteractionEnabled = false
|
||||
addSubview(stack)
|
||||
addSubview(checkmarkView)
|
||||
stack.snp.makeConstraints { make in make.center.equalToSuperview() }
|
||||
iconView.snp.makeConstraints { make in make.width.height.equalTo(46) }
|
||||
checkmarkView.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(22)
|
||||
make.top.trailing.equalToSuperview().inset(-2)
|
||||
}
|
||||
setSelected(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// 线下收款页面共用的双端色渐变按钮。
|
||||
@MainActor
|
||||
final class OfflineCollectionGradientButton: UIButton {
|
||||
private let gradientLayer = CAGradientLayer()
|
||||
|
||||
init(startColor: UIColor, endColor: UIColor) {
|
||||
super.init(frame: .zero)
|
||||
gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
|
||||
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
|
||||
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
||||
layer.insertSublayer(gradientLayer, at: 0)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
gradientLayer.frame = bounds
|
||||
gradientLayer.cornerRadius = layer.cornerRadius
|
||||
}
|
||||
|
||||
override var isEnabled: Bool {
|
||||
didSet { gradientLayer.opacity = isEnabled ? 1 : 0.45 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 日清页顶部可横滑的周/月日历组件。
|
||||
@MainActor
|
||||
final class OfflineCollectionCalendarView: UIView {
|
||||
var onDateSelected: ((String) -> Void)?
|
||||
var onModeChanged: (() -> Void)?
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let toggleButton = UIButton(type: .system)
|
||||
private let nextButton = UIButton(type: .system)
|
||||
private let rowsStack = UIStackView()
|
||||
private var rowStacks: [UIStackView] = []
|
||||
private var dayButtons: [OfflineCollectionDayButton] = []
|
||||
private var pendingDates: Set<String> = []
|
||||
private var state = OfflineCollectionCalendarState(selectedDate: Date(), maximumDate: Date())
|
||||
private let calendar = OfflineCollectionDate.calendar
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
CGSize(width: UIView.noIntrinsicMetric, height: state.mode == .week ? 160 : 378)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
/// 使用选中日、服务端今日和待补缴日期刷新组件。
|
||||
func apply(selectedDate: String, maximumDate: String, pendingDates: Set<String>) {
|
||||
guard let selected = OfflineCollectionDate.date(from: selectedDate),
|
||||
let maximum = OfflineCollectionDate.date(from: maximumDate) else { return }
|
||||
self.pendingDates = pendingDates
|
||||
state = OfflineCollectionCalendarState(
|
||||
selectedDate: selected,
|
||||
maximumDate: maximum,
|
||||
mode: state.mode,
|
||||
calendar: calendar
|
||||
)
|
||||
reloadDates()
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
backgroundColor = .white
|
||||
layer.cornerRadius = AppRadius.lg
|
||||
layer.shadowColor = UIColor(hex: 0x8FA0B8).cgColor
|
||||
layer.shadowOpacity = 0.12
|
||||
layer.shadowRadius = 10
|
||||
layer.shadowOffset = CGSize(width: 0, height: 4)
|
||||
|
||||
titleLabel.font = .systemFont(ofSize: 20, weight: .bold)
|
||||
titleLabel.textColor = UIColor(hex: 0x081739)
|
||||
toggleButton.configuration = .plain()
|
||||
toggleButton.configuration?.baseForegroundColor = UIColor(hex: 0x081739)
|
||||
toggleButton.configuration?.imagePlacement = .trailing
|
||||
toggleButton.configuration?.imagePadding = 8
|
||||
toggleButton.configuration?.contentInsets = NSDirectionalEdgeInsets(top: 7, leading: 14, bottom: 7, trailing: 12)
|
||||
toggleButton.layer.cornerRadius = 9
|
||||
toggleButton.layer.borderWidth = 1
|
||||
toggleButton.layer.borderColor = UIColor(hex: 0xE2E7EF).cgColor
|
||||
toggleButton.addTarget(self, action: #selector(toggleMode), for: .touchUpInside)
|
||||
toggleButton.accessibilityIdentifier = "offlineCollection.calendar.toggle"
|
||||
|
||||
nextButton.setImage(UIImage(systemName: "chevron.right"), for: .normal)
|
||||
nextButton.tintColor = UIColor(hex: 0x081739)
|
||||
nextButton.accessibilityLabel = "下一页"
|
||||
nextButton.addTarget(self, action: #selector(nextPage), for: .touchUpInside)
|
||||
nextButton.snp.makeConstraints { make in make.width.height.equalTo(44) }
|
||||
|
||||
let spacer = UIView()
|
||||
let header = UIStackView(arrangedSubviews: [titleLabel, spacer, toggleButton, nextButton])
|
||||
header.axis = .horizontal
|
||||
header.alignment = .center
|
||||
header.spacing = 8
|
||||
|
||||
let weekdayStack = UIStackView()
|
||||
weekdayStack.axis = .horizontal
|
||||
weekdayStack.distribution = .fillEqually
|
||||
["一", "二", "三", "四", "五", "六", "日"].forEach { value in
|
||||
let label = UILabel()
|
||||
label.text = value
|
||||
label.font = .systemFont(ofSize: 13, weight: .medium)
|
||||
label.textColor = UIColor(hex: 0x657084)
|
||||
label.textAlignment = .center
|
||||
weekdayStack.addArrangedSubview(label)
|
||||
}
|
||||
|
||||
rowsStack.axis = .vertical
|
||||
rowsStack.distribution = .fillEqually
|
||||
rowsStack.spacing = 1
|
||||
for _ in 0 ..< 6 {
|
||||
let row = UIStackView()
|
||||
row.axis = .horizontal
|
||||
row.distribution = .fillEqually
|
||||
for _ in 0 ..< 7 {
|
||||
let button = OfflineCollectionDayButton()
|
||||
button.addTarget(self, action: #selector(dayTapped(_:)), for: .touchUpInside)
|
||||
dayButtons.append(button)
|
||||
row.addArrangedSubview(button)
|
||||
}
|
||||
rowStacks.append(row)
|
||||
rowsStack.addArrangedSubview(row)
|
||||
}
|
||||
|
||||
addSubview(header)
|
||||
addSubview(weekdayStack)
|
||||
addSubview(rowsStack)
|
||||
header.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(14)
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.trailing.equalToSuperview().inset(10)
|
||||
make.height.equalTo(44)
|
||||
}
|
||||
weekdayStack.snp.makeConstraints { make in
|
||||
make.top.equalTo(header.snp.bottom).offset(8)
|
||||
make.leading.trailing.equalToSuperview().inset(10)
|
||||
make.height.equalTo(24)
|
||||
}
|
||||
rowsStack.snp.makeConstraints { make in
|
||||
make.top.equalTo(weekdayStack.snp.bottom).offset(4)
|
||||
make.leading.trailing.equalToSuperview().inset(10)
|
||||
make.bottom.equalToSuperview().inset(12)
|
||||
}
|
||||
|
||||
let left = UISwipeGestureRecognizer(target: self, action: #selector(swiped(_:)))
|
||||
left.direction = .left
|
||||
let right = UISwipeGestureRecognizer(target: self, action: #selector(swiped(_:)))
|
||||
right.direction = .right
|
||||
addGestureRecognizer(left)
|
||||
addGestureRecognizer(right)
|
||||
reloadDates()
|
||||
}
|
||||
|
||||
private func reloadDates() {
|
||||
titleLabel.text = state.monthTitle
|
||||
let isMonth = state.mode == .month
|
||||
toggleButton.configuration?.title = isMonth ? "周" : "月"
|
||||
toggleButton.configuration?.image = UIImage(systemName: isMonth ? "chevron.up" : "chevron.down")
|
||||
rowStacks.enumerated().forEach { $0.element.isHidden = !isMonth && $0.offset > 0 }
|
||||
|
||||
let dates = isMonth ? state.monthDates : state.weekDates
|
||||
let selectedMonth = calendar.component(.month, from: state.selectedDate)
|
||||
for (index, button) in dayButtons.enumerated() {
|
||||
guard index < dates.count else {
|
||||
button.isHidden = true
|
||||
continue
|
||||
}
|
||||
let date = dates[index]
|
||||
let key = OfflineCollectionDate.businessDate(for: date, calendar: calendar)
|
||||
button.isHidden = false
|
||||
button.apply(
|
||||
date: date,
|
||||
key: key,
|
||||
selected: calendar.isDate(date, inSameDayAs: state.selectedDate),
|
||||
pending: pendingDates.contains(key),
|
||||
today: calendar.isDate(date, inSameDayAs: state.maximumDate),
|
||||
enabled: date <= state.maximumDate,
|
||||
inCurrentMonth: !isMonth || calendar.component(.month, from: date) == selectedMonth,
|
||||
calendar: calendar
|
||||
)
|
||||
}
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
@objc private func toggleMode() {
|
||||
state.toggleMode()
|
||||
let animations = { [weak self] in
|
||||
self?.reloadDates()
|
||||
self?.superview?.layoutIfNeeded()
|
||||
}
|
||||
if UIAccessibility.isReduceMotionEnabled { animations() } else {
|
||||
UIView.animate(withDuration: 0.2, animations: animations)
|
||||
}
|
||||
onModeChanged?()
|
||||
}
|
||||
|
||||
@objc private func dayTapped(_ sender: OfflineCollectionDayButton) {
|
||||
guard let date = sender.date, state.select(date) else { return }
|
||||
reloadDates()
|
||||
onDateSelected?(OfflineCollectionDate.businessDate(for: date, calendar: calendar))
|
||||
}
|
||||
|
||||
@objc private func swiped(_ gesture: UISwipeGestureRecognizer) {
|
||||
let oldDate = state.selectedDate
|
||||
let offset = gesture.direction == .left ? 1 : -1
|
||||
let date = state.movePage(offset)
|
||||
guard !calendar.isDate(oldDate, inSameDayAs: date) else { return }
|
||||
let transition: UIView.AnimationOptions = gesture.direction == .left ? .transitionCrossDissolve : .transitionCrossDissolve
|
||||
if UIAccessibility.isReduceMotionEnabled { reloadDates() } else {
|
||||
UIView.transition(with: rowsStack, duration: 0.18, options: transition) { [weak self] in self?.reloadDates() }
|
||||
}
|
||||
onDateSelected?(OfflineCollectionDate.businessDate(for: date, calendar: calendar))
|
||||
}
|
||||
|
||||
@objc private func nextPage() {
|
||||
let oldDate = state.selectedDate
|
||||
let date = state.movePage(1)
|
||||
guard !calendar.isDate(oldDate, inSameDayAs: date) else { return }
|
||||
reloadDates()
|
||||
onDateSelected?(OfflineCollectionDate.businessDate(for: date, calendar: calendar))
|
||||
}
|
||||
}
|
||||
|
||||
/// 日历中的单个日期按钮,同时表达选中、今日和待补缴状态。
|
||||
private final class OfflineCollectionDayButton: UIControl {
|
||||
private let dayLabel = UILabel()
|
||||
private let pendingDot = UIView()
|
||||
private(set) var date: Date?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
layer.cornerRadius = 10
|
||||
dayLabel.font = .systemFont(ofSize: 17, weight: .medium)
|
||||
dayLabel.textAlignment = .center
|
||||
pendingDot.layer.cornerRadius = 2.5
|
||||
addSubview(dayLabel)
|
||||
addSubview(pendingDot)
|
||||
dayLabel.snp.makeConstraints { make in make.centerX.equalToSuperview(); make.centerY.equalToSuperview().offset(-2) }
|
||||
pendingDot.snp.makeConstraints { make in make.top.equalTo(dayLabel.snp.bottom).offset(2); make.centerX.equalToSuperview(); make.width.height.equalTo(5) }
|
||||
snp.makeConstraints { make in make.height.greaterThanOrEqualTo(44) }
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
func apply(
|
||||
date: Date,
|
||||
key: String,
|
||||
selected: Bool,
|
||||
pending: Bool,
|
||||
today: Bool,
|
||||
enabled: Bool,
|
||||
inCurrentMonth: Bool,
|
||||
calendar: Calendar
|
||||
) {
|
||||
self.date = date
|
||||
isEnabled = enabled
|
||||
dayLabel.text = String(calendar.component(.day, from: date))
|
||||
pendingDot.isHidden = !pending
|
||||
pendingDot.backgroundColor = selected ? .white : AppColor.danger
|
||||
backgroundColor = selected ? AppColor.primary : (pending ? UIColor(hex: 0xFFF1F0) : .clear)
|
||||
layer.borderWidth = pending && selected ? 2 : (today && !selected ? 1 : 0)
|
||||
layer.borderColor = (pending && selected ? AppColor.danger : AppColor.primary).cgColor
|
||||
dayLabel.textColor = selected ? .white : (enabled ? (inCurrentMonth ? AppColor.textPrimary : AppColor.textTertiary) : AppColor.textTertiary)
|
||||
alpha = enabled ? 1 : 0.35
|
||||
accessibilityLabel = "\(key)\(today ? ",今天" : "")\(pending ? ",待补缴" : "")\(selected ? ",已选中" : "")"
|
||||
accessibilityTraits = selected ? [.button, .selected] : .button
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 收款页中的线下收款入口、今日汇总和逾期提醒区域。
|
||||
@MainActor
|
||||
final class OfflineCollectionHomeView: UIView {
|
||||
var onRegister: (() -> Void)?
|
||||
var onOpenToday: (() -> Void)?
|
||||
var onOpenOverdue: (() -> Void)?
|
||||
var onRetry: (() -> Void)?
|
||||
|
||||
private let stack = UIStackView()
|
||||
private let statusButton = UIButton(type: .system)
|
||||
private let overdueCard = OfflineCollectionHomeCard()
|
||||
private let registerCard = OfflineCollectionHomeCard()
|
||||
private let todayCard = OfflineCollectionHomeCard()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
stack.axis = .vertical
|
||||
stack.spacing = AppSpacing.md
|
||||
let title = UILabel()
|
||||
title.text = "线下收款"
|
||||
title.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
title.textColor = AppColor.textPrimary
|
||||
stack.addArrangedSubview(title)
|
||||
stack.addArrangedSubview(statusButton)
|
||||
stack.addArrangedSubview(overdueCard)
|
||||
stack.addArrangedSubview(registerCard)
|
||||
stack.addArrangedSubview(todayCard)
|
||||
addSubview(stack)
|
||||
stack.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
title.snp.makeConstraints { make in make.height.equalTo(24) }
|
||||
|
||||
statusButton.configuration = .plain()
|
||||
statusButton.configuration?.baseForegroundColor = AppColor.primary
|
||||
statusButton.addTarget(self, action: #selector(retryTapped), for: .touchUpInside)
|
||||
statusButton.isHidden = true
|
||||
|
||||
registerCard.apply(
|
||||
title: "线下收款登记",
|
||||
detail: "线下收款后,请及时登记并在当日完成补缴",
|
||||
action: "去登记",
|
||||
image: "plus.circle.fill",
|
||||
tint: AppColor.primary,
|
||||
background: .white
|
||||
)
|
||||
registerCard.addTarget(self, action: #selector(registerTapped), for: .touchUpInside)
|
||||
todayCard.addTarget(self, action: #selector(todayTapped), for: .touchUpInside)
|
||||
overdueCard.addTarget(self, action: #selector(overdueTapped), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
/// 根据真实统计接口状态刷新区域。
|
||||
func apply(
|
||||
today: OfflineDailySummary,
|
||||
overdueDayCount: Int,
|
||||
overdueRecordCount: Int,
|
||||
overdueAmountFen: Int,
|
||||
errorMessage: String?
|
||||
) {
|
||||
if let errorMessage {
|
||||
statusButton.configuration?.title = "\(errorMessage) 点击重试"
|
||||
statusButton.configuration?.showsActivityIndicator = false
|
||||
statusButton.isUserInteractionEnabled = true
|
||||
statusButton.isHidden = false
|
||||
statusButton.accessibilityTraits.insert(.button)
|
||||
} else {
|
||||
statusButton.isHidden = true
|
||||
}
|
||||
|
||||
let hasOverdue = overdueDayCount > 0
|
||||
overdueCard.isHidden = !hasOverdue
|
||||
if hasOverdue {
|
||||
overdueCard.apply(
|
||||
title: "存在逾期未补缴",
|
||||
detail: "\(overdueDayCount) 个营业日,共 \(overdueRecordCount) 笔,待补缴 \(OfflineCollectionMoney.display(overdueAmountFen))",
|
||||
action: "立即处理",
|
||||
image: "exclamationmark.circle.fill",
|
||||
tint: AppColor.danger,
|
||||
background: UIColor(hex: 0xFFF1F0)
|
||||
)
|
||||
}
|
||||
|
||||
let settled = today.totalCount > 0 && today.pendingCount == 0
|
||||
todayCard.apply(
|
||||
title: "今日待补缴 \(OfflineCollectionMoney.display(today.pendingAmountFen))",
|
||||
detail: "\(today.pendingCount) 笔 · 今日已登记 \(OfflineCollectionMoney.display(today.totalAmountFen))\(settled ? " · 已结清" : "")",
|
||||
action: today.pendingCount > 0 ? "去补缴" : "查看明细",
|
||||
image: "calendar",
|
||||
tint: today.pendingCount > 0 ? UIColor(hex: 0xD97706) : AppColor.primary,
|
||||
background: .white
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func retryTapped() { onRetry?() }
|
||||
@objc private func registerTapped() { onRegister?() }
|
||||
@objc private func todayTapped() { onOpenToday?() }
|
||||
@objc private func overdueTapped() { onOpenOverdue?() }
|
||||
}
|
||||
|
||||
/// 线下收款首页区域的统一可点击卡片。
|
||||
private final class OfflineCollectionHomeCard: UIControl {
|
||||
private let iconView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
private let detailLabel = UILabel()
|
||||
private let actionLabel = UILabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
layer.cornerRadius = AppRadius.lg
|
||||
clipsToBounds = true
|
||||
isAccessibilityElement = true
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
titleLabel.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
detailLabel.font = .systemFont(ofSize: 13)
|
||||
detailLabel.textColor = AppColor.textSecondary
|
||||
detailLabel.numberOfLines = 0
|
||||
actionLabel.font = .systemFont(ofSize: 13, weight: .semibold)
|
||||
actionLabel.textColor = AppColor.primary
|
||||
actionLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
let textStack = UIStackView(arrangedSubviews: [titleLabel, detailLabel])
|
||||
textStack.axis = .vertical
|
||||
textStack.spacing = 5
|
||||
[iconView, titleLabel, detailLabel, actionLabel, textStack].forEach {
|
||||
$0.isUserInteractionEnabled = false
|
||||
}
|
||||
addSubview(iconView)
|
||||
addSubview(textStack)
|
||||
addSubview(actionLabel)
|
||||
iconView.snp.makeConstraints { make in make.leading.equalToSuperview().inset(AppSpacing.md); make.centerY.equalToSuperview(); make.width.height.equalTo(30) }
|
||||
textStack.snp.makeConstraints { make in make.leading.equalTo(iconView.snp.trailing).offset(AppSpacing.sm); make.top.bottom.equalToSuperview().inset(AppSpacing.md); make.trailing.lessThanOrEqualTo(actionLabel.snp.leading).offset(-AppSpacing.sm) }
|
||||
actionLabel.snp.makeConstraints { make in make.trailing.equalToSuperview().inset(AppSpacing.md); make.centerY.equalToSuperview() }
|
||||
snp.makeConstraints { make in make.height.greaterThanOrEqualTo(86) }
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
guard isEnabled,
|
||||
isUserInteractionEnabled,
|
||||
!isHidden,
|
||||
alpha > 0.01,
|
||||
self.point(inside: point, with: event) else { return nil }
|
||||
return self
|
||||
}
|
||||
|
||||
override var isHighlighted: Bool {
|
||||
didSet {
|
||||
UIView.animate(withDuration: 0.12) {
|
||||
self.alpha = self.isHighlighted ? 0.78 : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func apply(title: String, detail: String, action: String, image: String, tint: UIColor, background: UIColor) {
|
||||
titleLabel.text = title
|
||||
detailLabel.text = detail
|
||||
actionLabel.text = "\(action) ›"
|
||||
iconView.image = UIImage(systemName: image)
|
||||
iconView.tintColor = tint
|
||||
backgroundColor = background
|
||||
accessibilityLabel = "\(title),\(detail),\(action)"
|
||||
accessibilityTraits = .button
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user