// // 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) } } }