772 lines
28 KiB
Swift
772 lines
28 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 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 var amountDialog: PaymentSetAmountDialogView?
|
|
private var appliedBrandConfig: PayPageConfig?
|
|
private var appliedBrandingRefreshVersion = -1
|
|
|
|
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.usesNalatiBranding ? nil : "收款详情"
|
|
}
|
|
|
|
override func setupUI() {
|
|
let usesBranding = viewModel.usesNalatiBranding
|
|
view.backgroundColor = usesBranding ? .clear : AppColor.pageBackground
|
|
scrollView.showsVerticalScrollIndicator = !usesBranding
|
|
scrollView.isScrollEnabled = !usesBranding
|
|
scrollView.alwaysBounceVertical = false
|
|
|
|
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: "payment_nalati_background")
|
|
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)
|
|
}
|
|
|
|
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.usesNalatiBranding {
|
|
backgroundImageView.snp.makeConstraints { make in
|
|
make.edges.equalToSuperview()
|
|
}
|
|
}
|
|
scrollView.snp.makeConstraints { make in
|
|
if viewModel.usesNalatiBranding {
|
|
make.leading.trailing.equalToSuperview()
|
|
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(-44)
|
|
make.bottom.equalTo(view.safeAreaLayoutGuide)
|
|
} else {
|
|
make.edges.equalTo(view.safeAreaLayoutGuide)
|
|
}
|
|
}
|
|
contentContainerView.snp.makeConstraints { make in
|
|
make.edges.equalToSuperview()
|
|
make.width.equalTo(scrollView.snp.width)
|
|
if viewModel.usesNalatiBranding {
|
|
make.height.equalTo(scrollView.snp.height)
|
|
}
|
|
}
|
|
contentStack.snp.makeConstraints { make in
|
|
make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.screenHorizontalInset * 2)
|
|
if viewModel.usesNalatiBranding {
|
|
make.centerX.centerY.equalToSuperview()
|
|
make.top.greaterThanOrEqualToSuperview()
|
|
make.bottom.lessThanOrEqualToSuperview()
|
|
} 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)
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
applyViewModel()
|
|
Task { await loadData() }
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
applyBrandNavigationAppearanceIfNeeded()
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
restoreNavigationAppearanceIfNeeded()
|
|
}
|
|
|
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
viewModel.usesNalatiBranding ? .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.usesNalatiBranding
|
|
? .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.usesNalatiBranding
|
|
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
|
|
}
|
|
}
|
|
|
|
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: "payment_nalati_logo")
|
|
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 = 1
|
|
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.usesNalatiBranding 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: "payment_nalati_background",
|
|
forceRefresh: forceRefresh
|
|
)
|
|
setBrandImage(
|
|
brandLogoImageView,
|
|
urlString: config.logo,
|
|
fallbackName: "payment_nalati_logo",
|
|
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.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.usesNalatiBranding, 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.usesNalatiBranding, 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 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)
|
|
}
|
|
}
|
|
}
|