Files
suixinkan_uikit/suixinkan/UI/Payment/PaymentCollectionDetailsViewController.swift

831 lines
31 KiB
Swift

//
// PaymentCollectionDetailsViewController.swift
// suixinkan
//
import Kingfisher
import SnapKit
import UIKit
/// 收款详情页,展示收款二维码、设金额、保存二维码与语音开关。
final class PaymentCollectionDetailsViewController: BaseViewController {
private let viewModel = PaymentCollectionDetailsViewModel()
private let paymentAPI = NetworkServices.shared.paymentAPI
private let offlineCollectionAPI = NetworkServices.shared.offlineCollectionAPI
private lazy var offlineCollectionViewModel = OfflineCollectionHomeViewModel(
context: .current(),
api: offlineCollectionAPI
)
private let scrollView = UIScrollView()
private let contentContainerView = UIView()
private let contentStack = UIStackView()
private let backgroundImageView = UIImageView()
private let brandHeaderStack = UIStackView()
private let brandLogoImageView = UIImageView()
private let brandTitleLabel = UILabel()
private let brandSubtitleLabel = UILabel()
private let qrCardView = UIView()
private let scenicNameLabel = UILabel()
private let qrContainerView = UIView()
private let qrImageView = UIImageView()
private let qrPlaceholderView = UIView()
private let qrLoadingIndicator = UIActivityIndicatorView(style: .medium)
private let qrErrorLabel = UILabel()
private let refreshButton = UIButton(type: .system)
private let setAmountButton = UIButton(type: .system)
private let saveQRButton = UIButton(type: .system)
private let actionRow = UIStackView()
private let infoCardView = UIView()
private let scenicRow = PaymentInfoRowView(title: "景区名称")
private let merchantRow = PaymentInfoRowView(title: "收款方")
private let staffRow = PaymentInfoRowView(title: "员工 ID")
private let photographerRow = PaymentInfoRowView(title: "摄影师名称")
private let storeRow = PaymentInfoRowView(title: "店铺名称")
private let recordRow = PaymentNavigationRowView(title: "收款记录")
private let settingsCardView = UIView()
private let voiceCardView = UIView()
private let voiceTitleLabel = UILabel()
private let voiceSwitch = UISwitch()
private let offlineCollectionView = OfflineCollectionHomeView()
private var amountDialog: PaymentSetAmountDialogView?
private var appliedBrandConfig: PayPageConfig?
private var appliedBrandingRefreshVersion = -1
private var isShowingOfflineCollectionLoading = false
private var previousStandardAppearance: UINavigationBarAppearance?
private var previousScrollEdgeAppearance: UINavigationBarAppearance?
private var previousCompactAppearance: UINavigationBarAppearance?
private var previousNavigationTintColor: UIColor?
private var previousNavigationTranslucency = false
private var brandScreenHeight: CGFloat {
UIScreen.main.bounds.height
}
private var brandLogoHeight: CGFloat {
if brandScreenHeight <= 700 { return 82 }
if brandScreenHeight <= 820 { return 96 }
return 112
}
private var brandQRContainerSize: CGFloat {
if brandScreenHeight <= 700 { return 170 }
if brandScreenHeight <= 820 { return 190 }
return 210
}
private var brandQRImageSize: CGFloat {
if brandScreenHeight <= 700 { return 146 }
if brandScreenHeight <= 820 { return 166 }
return 182
}
private var brandSectionSpacing: CGFloat {
if brandScreenHeight <= 700 { return 8 }
if brandScreenHeight <= 820 { return 10 }
return 12
}
private var brandHeaderToQRSpacing: CGFloat {
if brandScreenHeight <= 700 { return 16 }
if brandScreenHeight <= 820 { return 18 }
return 20
}
override func setupNavigationBar() {
title = viewModel.usesBranding ? nil : "收款详情"
}
override func setupUI() {
let usesBranding = viewModel.usesBranding
view.backgroundColor = usesBranding ? .clear : AppColor.pageBackground
scrollView.showsVerticalScrollIndicator = !usesBranding
scrollView.isScrollEnabled = true
scrollView.alwaysBounceVertical = true
scrollView.contentInsetAdjustmentBehavior = .always
contentStack.axis = .vertical
contentStack.spacing = usesBranding ? brandSectionSpacing : AppSpacing.md
qrCardView.backgroundColor = usesBranding ? .clear : .white
qrCardView.layer.cornerRadius = AppRadius.lg
scenicNameLabel.font = .systemFont(ofSize: 16, weight: .medium)
scenicNameLabel.textColor = AppColor.textPrimary
scenicNameLabel.textAlignment = .center
qrImageView.contentMode = .scaleAspectFit
qrImageView.isHidden = true
if usesBranding {
qrContainerView.backgroundColor = .white
qrContainerView.layer.cornerRadius = 10
qrContainerView.clipsToBounds = true
qrContainerView.isHidden = true
qrContainerView.addSubview(qrImageView)
}
qrPlaceholderView.backgroundColor = AppColor.inputBackground
qrPlaceholderView.layer.cornerRadius = usesBranding ? 10 : 24
qrPlaceholderView.isHidden = true
qrLoadingIndicator.color = AppColor.primary
qrLoadingIndicator.hidesWhenStopped = true
qrLoadingIndicator.accessibilityLabel = "收款码加载中"
qrErrorLabel.text = "未选景区或网络问题"
qrErrorLabel.font = .systemFont(ofSize: 14)
qrErrorLabel.textColor = AppColor.danger
qrErrorLabel.textAlignment = .center
refreshButton.setTitle("点击刷新", for: .normal)
refreshButton.setTitleColor(AppColor.primary, for: .normal)
refreshButton.titleLabel?.font = .systemFont(ofSize: 14)
actionRow.axis = .horizontal
actionRow.alignment = .center
actionRow.spacing = usesBranding ? AppSpacing.lg : AppSpacing.xl
if usesBranding {
actionRow.backgroundColor = UIColor.white.withAlphaComponent(0.88)
actionRow.layer.cornerRadius = 24
actionRow.isLayoutMarginsRelativeArrangement = true
actionRow.directionalLayoutMargins = NSDirectionalEdgeInsets(
top: 10,
leading: 24,
bottom: 10,
trailing: 24
)
}
actionRow.alpha = 0
actionRow.isUserInteractionEnabled = false
actionRow.accessibilityElementsHidden = true
configureLinkButton(setAmountButton, title: "设置金额")
configureLinkButton(saveQRButton, title: "保存二维码")
configureCard(infoCardView, branded: usesBranding)
recordRow.backgroundColor = usesBranding ? .clear : .white
recordRow.layer.cornerRadius = usesBranding ? 0 : AppRadius.lg
voiceCardView.backgroundColor = usesBranding ? .clear : .white
voiceCardView.layer.cornerRadius = usesBranding ? 0 : AppRadius.lg
voiceTitleLabel.text = "收款到账语音提醒"
voiceTitleLabel.font = .systemFont(ofSize: 14)
voiceTitleLabel.textColor = UIColor(hex: 0x4B5563)
voiceSwitch.onTintColor = AppColor.primary
if usesBranding {
configureBrandHeader()
backgroundImageView.contentMode = .scaleAspectFill
backgroundImageView.clipsToBounds = true
backgroundImageView.image = UIImage(named: viewModel.brandBackgroundAssetName)
view.addSubview(backgroundImageView)
}
view.addSubview(scrollView)
scrollView.addSubview(contentContainerView)
contentContainerView.addSubview(contentStack)
if usesBranding {
contentStack.addArrangedSubview(brandHeaderStack)
contentStack.setCustomSpacing(brandHeaderToQRSpacing, after: brandHeaderStack)
}
contentStack.addArrangedSubview(qrCardView)
contentStack.addArrangedSubview(infoCardView)
if usesBranding {
configureCard(settingsCardView, branded: true)
contentStack.addArrangedSubview(settingsCardView)
} else {
contentStack.addArrangedSubview(recordRow)
contentStack.addArrangedSubview(voiceCardView)
}
contentStack.addArrangedSubview(offlineCollectionView)
let qrDisplayView = usesBranding ? qrContainerView : qrImageView
let qrContentStack = UIStackView(arrangedSubviews: [
scenicNameLabel,
qrDisplayView,
qrPlaceholderView,
actionRow,
])
qrContentStack.axis = .vertical
qrContentStack.alignment = .center
qrContentStack.spacing = usesBranding ? brandSectionSpacing : AppSpacing.lg
qrCardView.addSubview(qrContentStack)
qrPlaceholderView.addSubview(qrLoadingIndicator)
qrPlaceholderView.addSubview(qrErrorLabel)
qrPlaceholderView.addSubview(refreshButton)
actionRow.addArrangedSubview(setAmountButton)
if usesBranding {
let divider = UIView()
divider.backgroundColor = UIColor(hex: 0xD6E1EE)
actionRow.addArrangedSubview(divider)
divider.snp.makeConstraints { make in
make.width.equalTo(1)
make.height.equalTo(24)
}
}
actionRow.addArrangedSubview(saveQRButton)
let infoRows = usesBranding
? [photographerRow, storeRow]
: [scenicRow, merchantRow, staffRow]
let infoStack = UIStackView(arrangedSubviews: infoRows)
infoStack.axis = .vertical
infoCardView.addSubview(infoStack)
voiceCardView.addSubview(voiceTitleLabel)
voiceCardView.addSubview(voiceSwitch)
if usesBranding {
let settingsStack = UIStackView(arrangedSubviews: [recordRow, voiceCardView])
settingsStack.axis = .vertical
settingsCardView.addSubview(settingsStack)
settingsStack.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
let divider = UIView()
divider.backgroundColor = UIColor(hex: 0xD9E2EA).withAlphaComponent(0.75)
settingsCardView.addSubview(divider)
divider.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.centerY.equalToSuperview()
make.height.equalTo(0.5)
}
}
qrContentStack.snp.makeConstraints { make in
let verticalInset: CGFloat = usesBranding ? 0 : 32
make.edges.equalToSuperview().inset(
UIEdgeInsets(top: verticalInset, left: 16, bottom: verticalInset, right: 16)
)
}
scenicNameLabel.isHidden = usesBranding
scenicNameLabel.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
}
if usesBranding {
qrContainerView.snp.makeConstraints { make in
make.width.height.equalTo(brandQRContainerSize)
}
qrImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(brandQRImageSize)
}
} else {
qrImageView.snp.makeConstraints { make in
make.width.height.equalTo(182)
}
}
qrPlaceholderView.snp.makeConstraints { make in
make.width.height.equalTo(usesBranding ? brandQRContainerSize : 182)
}
qrLoadingIndicator.snp.makeConstraints { make in
make.center.equalToSuperview()
}
qrErrorLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(56)
make.leading.trailing.equalToSuperview().inset(AppSpacing.sm)
}
refreshButton.snp.makeConstraints { make in
make.top.equalTo(qrErrorLabel.snp.bottom).offset(AppSpacing.xs)
make.centerX.equalToSuperview()
}
infoStack.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
voiceTitleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().inset(AppSpacing.md)
make.centerY.equalToSuperview()
}
voiceSwitch.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(AppSpacing.md)
make.centerY.equalToSuperview()
}
voiceCardView.snp.makeConstraints { make in
make.height.equalTo(AppSpacing.formRowHeight)
}
}
override func setupConstraints() {
if viewModel.usesBranding {
backgroundImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
scrollView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
contentContainerView.snp.makeConstraints { make in
make.edges.equalTo(scrollView.contentLayoutGuide)
make.width.equalTo(scrollView.frameLayoutGuide)
if viewModel.usesBranding {
make.height.greaterThanOrEqualTo(scrollView.snp.height)
}
}
contentStack.snp.makeConstraints { make in
make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.screenHorizontalInset * 2)
if viewModel.usesBranding {
make.centerX.equalToSuperview()
make.top.bottom.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
} else {
make.edges.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
}
}
}
override func bindActions() {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() }
}
viewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
setAmountButton.addTarget(self, action: #selector(setAmountTapped), for: .touchUpInside)
saveQRButton.addTarget(self, action: #selector(saveQRTapped), for: .touchUpInside)
refreshButton.addTarget(self, action: #selector(refreshTapped), for: .touchUpInside)
recordRow.addTarget(self, action: #selector(recordTapped), for: .touchUpInside)
voiceSwitch.addTarget(self, action: #selector(voiceSwitchChanged), for: .valueChanged)
offlineCollectionViewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyOfflineCollection() }
}
offlineCollectionView.onRetry = { [weak self] in
Task { await self?.offlineCollectionViewModel.load() }
}
offlineCollectionView.onRegister = { [weak self] in self?.openOfflineRegistration() }
offlineCollectionView.onOpenToday = { [weak self] in
guard let self, let date = self.offlineCollectionViewModel.todayBusinessDate else { return }
self.openOfflineDaily(date: date)
}
offlineCollectionView.onOpenOverdue = { [weak self] in
guard let self, let date = self.offlineCollectionViewModel.earliestOverdueBusinessDate else { return }
self.openOfflineDaily(date: date)
}
}
override func viewDidLoad() {
super.viewDidLoad()
applyViewModel()
Task { await loadData() }
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
applyBrandNavigationAppearanceIfNeeded()
applyOfflineCollection()
Task { await offlineCollectionViewModel.load() }
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
setOfflineCollectionLoadingVisible(false)
restoreNavigationAppearanceIfNeeded()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
viewModel.usesBranding ? .darkContent : .default
}
private func loadData() async {
async let pageConfig: Void = viewModel.loadPayPageConfig(api: paymentAPI)
await viewModel.loadPayCode(api: paymentAPI)
await pageConfig
}
@MainActor
private func applyViewModel() {
scenicNameLabel.text = viewModel.displayScenicName
scenicRow.setValue(viewModel.displayScenicName)
merchantRow.setValue(viewModel.merchantInfo.company)
staffRow.setValue(viewModel.staffId.isEmpty ? "-" : viewModel.staffId)
photographerRow.setValue(viewModel.displayPhotographerName)
storeRow.setValue(viewModel.displayStoreNames)
voiceSwitch.isOn = viewModel.isVoiceBroadcastOpen
applyBrandConfigIfNeeded()
if viewModel.isPayCodeLoading {
qrImageView.isHidden = true
qrContainerView.isHidden = true
qrPlaceholderView.backgroundColor = viewModel.usesBranding
? .white
: AppColor.inputBackground
qrPlaceholderView.isHidden = false
qrErrorLabel.isHidden = true
refreshButton.isHidden = true
qrLoadingIndicator.startAnimating()
setActionRowAvailable(false)
} else if let image = viewModel.qrImage {
qrImageView.image = image
qrImageView.isHidden = false
qrContainerView.isHidden = !viewModel.usesBranding
qrPlaceholderView.isHidden = true
qrLoadingIndicator.stopAnimating()
setActionRowAvailable(true)
} else {
qrImageView.isHidden = true
qrContainerView.isHidden = true
qrPlaceholderView.backgroundColor = AppColor.inputBackground
qrPlaceholderView.isHidden = false
qrErrorLabel.isHidden = false
refreshButton.isHidden = false
qrLoadingIndicator.stopAnimating()
setActionRowAvailable(false)
}
if viewModel.showAmountDialog {
presentAmountDialogIfNeeded()
} else {
amountDialog?.dismiss()
amountDialog = nil
}
applyOfflineCollection()
}
@MainActor
private func applyOfflineCollection() {
setOfflineCollectionLoadingVisible(offlineCollectionViewModel.isLoading)
offlineCollectionView.apply(
today: offlineCollectionViewModel.todaySummary,
overdueDayCount: offlineCollectionViewModel.overdueDates.count,
overdueRecordCount: offlineCollectionViewModel.overdueRecordCount,
overdueAmountFen: offlineCollectionViewModel.overdueAmountFen,
errorMessage: offlineCollectionViewModel.errorMessage
)
}
@MainActor
private func setOfflineCollectionLoadingVisible(_ visible: Bool) {
guard visible != isShowingOfflineCollectionLoading else { return }
isShowingOfflineCollectionLoading = visible
visible ? showLoading() : hideLoading()
}
private func setActionRowAvailable(_ isAvailable: Bool) {
actionRow.alpha = isAvailable ? 1 : 0
actionRow.isUserInteractionEnabled = isAvailable
actionRow.accessibilityElementsHidden = !isAvailable
}
private func presentAmountDialogIfNeeded() {
guard amountDialog == nil else {
amountDialog?.apply(amount: viewModel.amount, remark: viewModel.remark)
return
}
let dialog = PaymentSetAmountDialogView()
dialog.onAmountChange = { [weak self] value in
self?.viewModel.updateAmount(value)
}
dialog.onRemarkChange = { [weak self] value in
self?.viewModel.updateRemark(value)
}
dialog.onConfirm = { [weak self] in
self?.viewModel.confirmAmount()
}
dialog.onCancel = { [weak self] in
self?.viewModel.hideAmountDialog()
}
dialog.apply(amount: viewModel.amount, remark: viewModel.remark)
dialog.show(in: view)
amountDialog = dialog
}
private func configureLinkButton(_ button: UIButton, title: String) {
button.setTitle(title, for: .normal)
button.setTitleColor(AppColor.primary, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14)
}
private func configureCard(_ view: UIView, branded: Bool) {
view.backgroundColor = branded ? UIColor.white.withAlphaComponent(0.9) : .white
view.layer.cornerRadius = branded ? 18 : AppRadius.lg
view.clipsToBounds = true
}
private func configureBrandHeader() {
brandHeaderStack.axis = .vertical
brandHeaderStack.alignment = .center
brandHeaderStack.spacing = brandScreenHeight <= 700 ? 6 : 8
brandLogoImageView.image = UIImage(named: viewModel.brandLogoAssetName)
brandLogoImageView.contentMode = .scaleAspectFit
brandLogoImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
brandTitleLabel.font = .systemFont(
ofSize: brandScreenHeight <= 700 ? 21 : 23,
weight: .bold
)
brandTitleLabel.textColor = UIColor(hex: 0x102D54)
brandTitleLabel.textAlignment = .center
brandTitleLabel.numberOfLines = viewModel.brandTitleNumberOfLines
brandTitleLabel.adjustsFontSizeToFitWidth = true
brandTitleLabel.minimumScaleFactor = 0.8
brandSubtitleLabel.font = .systemFont(
ofSize: brandScreenHeight <= 700 ? 15 : 16,
weight: .medium
)
brandSubtitleLabel.textColor = UIColor(hex: 0x183A61)
brandSubtitleLabel.textAlignment = .center
let subtitleRow = UIStackView()
subtitleRow.axis = .horizontal
subtitleRow.alignment = .center
subtitleRow.spacing = 9
let leftLine = makeBrandLine()
let leftDiamond = makeBrandDiamond()
let rightDiamond = makeBrandDiamond()
let rightLine = makeBrandLine()
[leftLine, leftDiamond, brandSubtitleLabel, rightDiamond, rightLine].forEach(
subtitleRow.addArrangedSubview
)
brandHeaderStack.addArrangedSubview(brandLogoImageView)
brandHeaderStack.addArrangedSubview(brandTitleLabel)
brandHeaderStack.addArrangedSubview(subtitleRow)
brandHeaderStack.setCustomSpacing(
brandScreenHeight <= 700 ? 8 : 10,
after: brandLogoImageView
)
brandLogoImageView.snp.makeConstraints { make in
make.width.equalToSuperview().inset(8)
make.height.equalTo(brandLogoHeight)
}
brandTitleLabel.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(8)
}
[leftLine, rightLine].forEach { line in
line.snp.makeConstraints { make in
make.width.equalTo(42)
make.height.equalTo(1)
}
}
[leftDiamond, rightDiamond].forEach { diamond in
diamond.snp.makeConstraints { make in
make.width.height.equalTo(6)
}
}
}
private func makeBrandLine() -> UIView {
let view = UIView()
view.backgroundColor = UIColor(hex: 0x27496D).withAlphaComponent(0.75)
return view
}
private func makeBrandDiamond() -> UIView {
let view = UIView()
view.backgroundColor = UIColor(hex: 0x27496D)
view.transform = CGAffineTransform(rotationAngle: .pi / 4)
return view
}
@MainActor
private func applyBrandConfigIfNeeded() {
guard viewModel.usesBranding else { return }
let config = viewModel.payPageConfig
brandTitleLabel.text = config.title
brandSubtitleLabel.text = config.subtitle
let versionChanged = appliedBrandingRefreshVersion != viewModel.brandingRefreshVersion
guard appliedBrandConfig != config || versionChanged else { return }
let forceRefresh = appliedBrandingRefreshVersion >= 0 && versionChanged
setBrandImage(
backgroundImageView,
urlString: config.background,
fallbackName: viewModel.brandBackgroundAssetName,
forceRefresh: forceRefresh
)
setBrandImage(
brandLogoImageView,
urlString: config.logo,
fallbackName: viewModel.brandLogoAssetName,
forceRefresh: forceRefresh
)
appliedBrandConfig = config
appliedBrandingRefreshVersion = viewModel.brandingRefreshVersion
}
private func setBrandImage(
_ imageView: UIImageView,
urlString: String,
fallbackName: String,
forceRefresh: Bool
) {
let fallback = UIImage(named: fallbackName)
let trimmed = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
guard let url = URL(string: trimmed),
url.scheme?.lowercased() == "https",
url.host != nil else {
imageView.kf.cancelDownloadTask()
imageView.image = fallback
return
}
var options: KingfisherOptionsInfo = [
.keepCurrentImageWhileLoading,
.cacheOriginalImage,
.transition(.fade(0.2)),
]
if forceRefresh {
options.append(.forceRefresh)
}
imageView.kf.setImage(with: url, placeholder: fallback, options: options)
}
private func applyBrandNavigationAppearanceIfNeeded() {
guard viewModel.usesBranding, let navigationBar = navigationController?.navigationBar else { return }
previousStandardAppearance = navigationBar.standardAppearance
previousScrollEdgeAppearance = navigationBar.scrollEdgeAppearance
previousCompactAppearance = navigationBar.compactAppearance
previousNavigationTintColor = navigationBar.tintColor
previousNavigationTranslucency = navigationBar.isTranslucent
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground()
appearance.shadowColor = .clear
appearance.titleTextAttributes = [
.foregroundColor: UIColor(hex: 0x111827),
.font: UIFont.app(.title),
]
navigationBar.standardAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
navigationBar.compactAppearance = appearance
navigationBar.tintColor = UIColor(hex: 0x111827)
navigationBar.isTranslucent = true
}
private func restoreNavigationAppearanceIfNeeded() {
guard viewModel.usesBranding, let navigationBar = navigationController?.navigationBar else { return }
if let previousStandardAppearance {
navigationBar.standardAppearance = previousStandardAppearance
}
navigationBar.scrollEdgeAppearance = previousScrollEdgeAppearance
navigationBar.compactAppearance = previousCompactAppearance
navigationBar.tintColor = previousNavigationTintColor
navigationBar.isTranslucent = previousNavigationTranslucency
previousStandardAppearance = nil
previousScrollEdgeAppearance = nil
previousCompactAppearance = nil
previousNavigationTintColor = nil
}
@objc private func setAmountTapped() {
viewModel.showSetAmountDialog()
}
@objc private func saveQRTapped() {
Task {
await viewModel.saveQRCode()
}
}
@objc private func refreshTapped() {
Task {
await viewModel.refresh(api: paymentAPI)
}
}
@objc private func recordTapped() {
navigationController?.pushViewController(PaymentCollectionRecordViewController(), animated: true)
}
@objc private func voiceSwitchChanged() {
viewModel.toggleReceiveVoice()
}
private func openOfflineRegistration() {
let controller = OfflineCollectionRegistrationViewController(
context: offlineCollectionViewModel.context,
api: offlineCollectionAPI
)
navigationController?.pushViewController(controller, animated: true)
}
private func openOfflineDaily(date: String) {
let controller = OfflineCollectionDailyViewController(
businessDate: date,
context: offlineCollectionViewModel.context,
api: offlineCollectionAPI
)
navigationController?.pushViewController(controller, animated: true)
}
}
/// 收款详情信息行。
private final class PaymentInfoRowView: UIView {
private let titleLabel = UILabel()
private let valueLabel = UILabel()
private let divider = UIView()
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) {
valueLabel.text = value
}
private func setupUI() {
titleLabel.font = .systemFont(ofSize: 14)
titleLabel.textColor = UIColor(hex: 0x4B5563)
valueLabel.font = .systemFont(ofSize: 14)
valueLabel.textColor = AppColor.textPrimary
valueLabel.textAlignment = .right
valueLabel.numberOfLines = 0
divider.backgroundColor = UIColor(hex: 0xE5E7EB)
addSubview(titleLabel)
addSubview(valueLabel)
addSubview(divider)
snp.makeConstraints { make in
make.height.greaterThanOrEqualTo(AppSpacing.formRowHeight)
}
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().inset(AppSpacing.md)
make.centerY.equalToSuperview()
make.width.equalTo(102)
}
valueLabel.snp.makeConstraints { make in
make.leading.equalTo(titleLabel.snp.trailing).offset(AppSpacing.xs)
make.trailing.equalToSuperview().inset(AppSpacing.md)
make.top.bottom.equalToSuperview().inset(AppSpacing.sm)
}
divider.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
make.height.equalTo(0.5)
}
}
}
/// 可点击导航行。
private final class PaymentNavigationRowView: UIControl {
private let titleLabel = UILabel()
private let chevronView = UIImageView()
init(title: String) {
super.init(frame: .zero)
titleLabel.text = title
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
titleLabel.font = .systemFont(ofSize: 14)
titleLabel.textColor = UIColor(hex: 0x4B5563)
chevronView.image = UIImage(systemName: "chevron.right")
chevronView.tintColor = AppColor.textSecondary
chevronView.contentMode = .scaleAspectFit
addSubview(titleLabel)
addSubview(chevronView)
snp.makeConstraints { make in
make.height.equalTo(AppSpacing.formRowHeight)
}
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().inset(AppSpacing.md)
make.centerY.equalToSuperview()
}
chevronView.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(AppSpacing.md)
make.centerY.equalToSuperview()
make.width.height.equalTo(16)
}
}
}