Implement收款详情与收款记录 pages with pay-code API, QR generation, amount setting, and record grouping so merchants can collect payments from the home quick action. Co-authored-by: Cursor <cursoragent@cursor.com>
383 lines
13 KiB
Swift
383 lines
13 KiB
Swift
//
|
||
// PaymentCollectionDetailsViewController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
/// 收款详情页,展示收款二维码、设金额、保存二维码与语音开关。
|
||
final class PaymentCollectionDetailsViewController: BaseViewController {
|
||
|
||
private let viewModel = PaymentCollectionDetailsViewModel()
|
||
private let paymentAPI = NetworkServices.shared.paymentAPI
|
||
|
||
private let scrollView = UIScrollView()
|
||
private let contentStack = UIStackView()
|
||
|
||
private let qrCardView = UIView()
|
||
private let scenicNameLabel = UILabel()
|
||
private let qrImageView = UIImageView()
|
||
private let qrPlaceholderView = UIView()
|
||
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 recordRow = PaymentNavigationRowView(title: "收款记录")
|
||
|
||
private let voiceCardView = UIView()
|
||
private let voiceTitleLabel = UILabel()
|
||
private let voiceSwitch = UISwitch()
|
||
|
||
private var amountDialog: PaymentSetAmountDialogView?
|
||
|
||
override func setupNavigationBar() {
|
||
title = "收款详情"
|
||
}
|
||
|
||
override func setupUI() {
|
||
view.backgroundColor = AppColor.pageBackground
|
||
|
||
contentStack.axis = .vertical
|
||
contentStack.spacing = AppSpacing.md
|
||
|
||
qrCardView.backgroundColor = .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
|
||
|
||
qrPlaceholderView.backgroundColor = AppColor.inputBackground
|
||
qrPlaceholderView.layer.cornerRadius = 24
|
||
qrPlaceholderView.isHidden = true
|
||
|
||
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 = AppSpacing.xl
|
||
|
||
configureLinkButton(setAmountButton, title: "设置金额")
|
||
configureLinkButton(saveQRButton, title: "保存二维码")
|
||
|
||
infoCardView.backgroundColor = .white
|
||
infoCardView.layer.cornerRadius = AppRadius.lg
|
||
|
||
recordRow.backgroundColor = .white
|
||
recordRow.layer.cornerRadius = AppRadius.lg
|
||
|
||
voiceCardView.backgroundColor = .white
|
||
voiceCardView.layer.cornerRadius = AppRadius.lg
|
||
|
||
voiceTitleLabel.text = "收款到账语音提醒"
|
||
voiceTitleLabel.font = .systemFont(ofSize: 14)
|
||
voiceTitleLabel.textColor = UIColor(hex: 0x4B5563)
|
||
|
||
voiceSwitch.onTintColor = AppColor.primary
|
||
|
||
view.addSubview(scrollView)
|
||
scrollView.addSubview(contentStack)
|
||
|
||
contentStack.addArrangedSubview(qrCardView)
|
||
contentStack.addArrangedSubview(infoCardView)
|
||
contentStack.addArrangedSubview(recordRow)
|
||
contentStack.addArrangedSubview(voiceCardView)
|
||
|
||
let qrContentStack = UIStackView(arrangedSubviews: [
|
||
scenicNameLabel,
|
||
qrImageView,
|
||
qrPlaceholderView,
|
||
actionRow,
|
||
])
|
||
qrContentStack.axis = .vertical
|
||
qrContentStack.alignment = .center
|
||
qrContentStack.spacing = AppSpacing.lg
|
||
qrCardView.addSubview(qrContentStack)
|
||
|
||
qrPlaceholderView.addSubview(qrErrorLabel)
|
||
qrPlaceholderView.addSubview(refreshButton)
|
||
|
||
actionRow.addArrangedSubview(setAmountButton)
|
||
actionRow.addArrangedSubview(saveQRButton)
|
||
|
||
let infoStack = UIStackView(arrangedSubviews: [scenicRow, merchantRow, staffRow])
|
||
infoStack.axis = .vertical
|
||
infoCardView.addSubview(infoStack)
|
||
|
||
voiceCardView.addSubview(voiceTitleLabel)
|
||
voiceCardView.addSubview(voiceSwitch)
|
||
|
||
qrContentStack.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 32, left: 16, bottom: 32, right: 16))
|
||
}
|
||
scenicNameLabel.snp.makeConstraints { make in
|
||
make.leading.trailing.equalToSuperview()
|
||
}
|
||
qrImageView.snp.makeConstraints { make in
|
||
make.width.height.equalTo(182)
|
||
}
|
||
qrPlaceholderView.snp.makeConstraints { make in
|
||
make.width.height.equalTo(182)
|
||
}
|
||
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() {
|
||
scrollView.snp.makeConstraints { make in
|
||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
||
}
|
||
contentStack.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
||
make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.screenHorizontalInset * 2)
|
||
}
|
||
}
|
||
|
||
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()
|
||
Task { await loadData() }
|
||
}
|
||
|
||
private func loadData() async {
|
||
showLoading()
|
||
await viewModel.loadPayCode(api: paymentAPI)
|
||
hideLoading()
|
||
}
|
||
|
||
@MainActor
|
||
private func applyViewModel() {
|
||
scenicNameLabel.text = viewModel.displayScenicName
|
||
scenicRow.setValue(viewModel.displayScenicName)
|
||
merchantRow.setValue(viewModel.merchantInfo.company)
|
||
staffRow.setValue(viewModel.staffId.isEmpty ? "-" : viewModel.staffId)
|
||
voiceSwitch.isOn = viewModel.isVoiceBroadcastOpen
|
||
|
||
if let image = viewModel.qrImage {
|
||
qrImageView.image = image
|
||
qrImageView.isHidden = false
|
||
qrPlaceholderView.isHidden = true
|
||
actionRow.isHidden = false
|
||
} else {
|
||
qrImageView.isHidden = true
|
||
qrPlaceholderView.isHidden = false
|
||
actionRow.isHidden = true
|
||
}
|
||
|
||
if viewModel.showAmountDialog {
|
||
presentAmountDialogIfNeeded()
|
||
} else {
|
||
amountDialog?.dismiss()
|
||
amountDialog = nil
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
@objc private func setAmountTapped() {
|
||
viewModel.showSetAmountDialog()
|
||
}
|
||
|
||
@objc private func saveQRTapped() {
|
||
Task {
|
||
await viewModel.saveQRCode()
|
||
}
|
||
}
|
||
|
||
@objc private func refreshTapped() {
|
||
Task {
|
||
showLoading()
|
||
await viewModel.refresh(api: paymentAPI)
|
||
hideLoading()
|
||
}
|
||
}
|
||
|
||
@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 = 2
|
||
|
||
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)
|
||
}
|
||
}
|
||
}
|