// // WithdrawalSettingsViewController.swift // suixinkan // import PhotosUI import SnapKit import UIKit /// 提现设置页面,两步表单绑定银行卡。 final class WithdrawalSettingsViewController: BaseViewController { private let viewModel = WithdrawalSettingsViewModel() private let profileAPI = NetworkServices.shared.profileAPI private let ossUploadService = NetworkServices.shared.ossUploadService private let scrollView = UIScrollView() private let contentStack = UIStackView() private let stepLabel = UILabel() private let holderNameField = UITextField() private let bankNoField = UITextField() private let frontImageView = UIImageView() private let backImageView = UIImageView() private let bankNameField = UITextField() private let provinceButton = UIButton(type: .system) private let cityButton = UIButton(type: .system) private let branchField = UITextField() private let smsCodeField = UITextField() private let agreeSwitch = UISwitch() private let actionButton = AppButton(title: "下一步") private var pickingSide: BankCardImageSide = .front private var stepTwoStack = UIStackView() override func setupNavigationBar() { title = "提现设置" } override func setupUI() { view.backgroundColor = UIColor(hex: 0xF7FAFF) contentStack.axis = .vertical contentStack.spacing = 16 stepLabel.font = .systemFont(ofSize: 14, weight: .medium) stepLabel.textColor = AppColor.text666 [holderNameField, bankNoField, bankNameField, branchField, smsCodeField].forEach { $0.borderStyle = .roundedRect $0.font = .systemFont(ofSize: 16) } holderNameField.placeholder = "持卡人姓名" bankNoField.placeholder = "银行卡号" bankNoField.keyboardType = .numberPad bankNameField.placeholder = "开户银行" branchField.placeholder = "支行名称" smsCodeField.placeholder = "短信验证码" smsCodeField.keyboardType = .numberPad provinceButton.contentHorizontalAlignment = .leading cityButton.contentHorizontalAlignment = .leading provinceButton.setTitle("请选择省份", for: .normal) cityButton.setTitle("请选择城市", for: .normal) [frontImageView, backImageView].forEach { $0.backgroundColor = UIColor(hex: 0xF8FAFC) $0.contentMode = .scaleAspectFit $0.layer.cornerRadius = 8 $0.clipsToBounds = true $0.isUserInteractionEnabled = true $0.snp.makeConstraints { make in make.height.equalTo(140) } } frontImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(pickFront))) backImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(pickBack))) stepTwoStack.axis = .vertical stepTwoStack.spacing = 16 stepTwoStack.isHidden = true stepTwoStack.addArrangedSubview(labeled("开户银行", bankNameField)) stepTwoStack.addArrangedSubview(labeled("所在省份", provinceButton)) stepTwoStack.addArrangedSubview(labeled("所在城市", cityButton)) stepTwoStack.addArrangedSubview(labeled("支行名称", branchField)) let smsRow = UIStackView(arrangedSubviews: [smsCodeField, sendCodeButton()]) smsRow.axis = .horizontal smsRow.spacing = 8 stepTwoStack.addArrangedSubview(labeled("短信验证码", smsRow)) view.addSubview(scrollView) scrollView.addSubview(contentStack) view.addSubview(actionButton) contentStack.addArrangedSubview(stepLabel) contentStack.addArrangedSubview(labeled("持卡人姓名", holderNameField)) contentStack.addArrangedSubview(labeled("银行卡号", bankNoField)) contentStack.addArrangedSubview(labeled("银行卡正面", frontImageView)) contentStack.addArrangedSubview(labeled("银行卡反面", backImageView)) contentStack.addArrangedSubview(agreeRow()) contentStack.addArrangedSubview(stepTwoStack) } override func setupConstraints() { scrollView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.bottom.equalTo(actionButton.snp.top).offset(-12) } contentStack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(16) make.width.equalTo(scrollView).offset(-32) } actionButton.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(16) make.bottom.equalTo(view.safeAreaLayoutGuide).inset(14) } } override func bindActions() { viewModel.onStateChange = { [weak self] in self?.applyViewModel() } actionButton.addTarget(self, action: #selector(actionTapped), for: .touchUpInside) provinceButton.addTarget(self, action: #selector(selectProvince), for: .touchUpInside) cityButton.addTarget(self, action: #selector(selectCity), for: .touchUpInside) agreeSwitch.addTarget(self, action: #selector(agreeChanged), for: .valueChanged) holderNameField.addTarget(self, action: #selector(fieldsChanged), for: .editingChanged) bankNoField.addTarget(self, action: #selector(fieldsChanged), for: .editingChanged) bankNameField.addTarget(self, action: #selector(fieldsChanged), for: .editingChanged) branchField.addTarget(self, action: #selector(fieldsChanged), for: .editingChanged) smsCodeField.addTarget(self, action: #selector(fieldsChanged), for: .editingChanged) Task { await loadData() } } private func applyViewModel() { stepLabel.text = viewModel.currentStep == 1 ? "步骤 1/2:填写银行卡信息" : "步骤 2/2:完善开户信息" holderNameField.text = viewModel.holderName bankNoField.text = viewModel.bankNo bankNameField.text = viewModel.bankName branchField.text = viewModel.branchName smsCodeField.text = viewModel.verificationCode agreeSwitch.isOn = viewModel.isAgree stepTwoStack.isHidden = viewModel.currentStep == 1 actionButton.setTitle(viewModel.currentStep == 1 ? "下一步" : "提交", for: .normal) actionButton.isEnabled = viewModel.currentStep == 1 ? viewModel.canGoNextStep : viewModel.canSubmit provinceButton.setTitle(viewModel.provinceName.isEmpty ? "请选择省份" : viewModel.provinceName, for: .normal) cityButton.setTitle(viewModel.cityName.isEmpty ? "请选择城市" : viewModel.cityName, for: .normal) updateImageView(frontImageView, pending: viewModel.pendingFrontImageData, url: viewModel.bankCardFrontURL) updateImageView(backImageView, pending: viewModel.pendingBackImageData, url: viewModel.bankCardBackURL) } private func updateImageView(_ imageView: UIImageView, pending: Data?, url: String) { if let pending, let image = UIImage(data: pending) { imageView.image = image } else if url != "pending" { imageView.loadRemoteImage(urlString: url) } } @objc private func fieldsChanged() { viewModel.updateHolderName(holderNameField.text ?? "") viewModel.updateBankNo(bankNoField.text ?? "") viewModel.updateBankName(bankNameField.text ?? "") viewModel.updateBranchName(branchField.text ?? "") viewModel.updateVerificationCode(smsCodeField.text ?? "") } @objc private func agreeChanged() { viewModel.updateAgree(agreeSwitch.isOn) } @objc private func actionTapped() { if viewModel.currentStep == 1 { viewModel.goNextStep() } else { Task { await submit() } } } @objc private func pickFront() { pickImage(side: .front) } @objc private func pickBack() { pickImage(side: .back) } private func pickImage(side: BankCardImageSide) { pickingSide = side var configuration = PHPickerConfiguration(photoLibrary: .shared()) configuration.filter = .images configuration.selectionLimit = 1 let picker = PHPickerViewController(configuration: configuration) picker.delegate = self present(picker, animated: true) } @objc private func selectProvince() { presentAreaPicker(title: "选择省份", options: viewModel.provinceOptions) { [weak self] area in self?.viewModel.selectProvince(area) } } @objc private func selectCity() { presentAreaPicker(title: "选择城市", options: viewModel.cityOptions) { [weak self] area in self?.viewModel.selectCity(area) } } private func presentAreaPicker(title: String, options: [AreasResponse], onSelect: @escaping (AreasResponse) -> Void) { let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) options.forEach { area in alert.addAction(UIAlertAction(title: area.name, style: .default) { _ in onSelect(area) }) } alert.addAction(UIAlertAction(title: "取消", style: .cancel)) present(alert, animated: true) } private func loadData() async { showLoading() defer { hideLoading() } do { try await viewModel.load(api: profileAPI) } catch { showToast(error.localizedDescription) } } private func submit() async { let scenicId = AppStore.shared.session.currentScenicId guard scenicId > 0 else { showToast("当前景区信息缺失") return } showLoading() defer { hideLoading() } do { try await viewModel.submit(api: profileAPI, uploader: ossUploadService, scenicId: scenicId) showToast("提交成功") navigationController?.popViewController(animated: true) } catch { showToast(error.localizedDescription) } } private func labeled(_ title: String, _ view: UIView) -> UIStackView { let label = UILabel() label.text = title label.font = .systemFont(ofSize: 14) let stack = UIStackView(arrangedSubviews: [label, view]) stack.axis = .vertical stack.spacing = 8 return stack } private func agreeRow() -> UIStackView { let label = UILabel() label.text = "我已阅读并同意相关协议" label.font = .systemFont(ofSize: 14) let row = UIStackView(arrangedSubviews: [label, agreeSwitch]) row.axis = .horizontal return row } private func sendCodeButton() -> UIButton { let button = UIButton(type: .system) button.setTitle("获取验证码", for: .normal) button.addAction(UIAction { [weak self] _ in Task { guard let self else { return } self.showLoading() defer { self.hideLoading() } do { try await self.viewModel.sendCode(api: self.profileAPI) self.showToast("验证码已发送") } catch { self.showToast(error.localizedDescription) } } }, for: .touchUpInside) return button } } extension WithdrawalSettingsViewController: PHPickerViewControllerDelegate { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) guard let provider = results.first?.itemProvider, provider.canLoadObject(ofClass: UIImage.self) else { return } provider.loadObject(ofClass: UIImage.self) { [weak self] object, _ in guard let self, let image = object as? UIImage, let data = image.jpegData(compressionQuality: 0.9) else { return } Task { @MainActor in do { try self.viewModel.prepareBankCardImage(data: data, side: self.pickingSide) } catch { self.showToast(error.localizedDescription) } } } } }