将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
295 lines
12 KiB
Swift
295 lines
12 KiB
Swift
//
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|