Files
suixinkan_uikit/suixinkan/UI/Profile/WithdrawalSettingsViewController.swift
汉秋 005349f8e6 模块化 AppStore 并完善素材管理与个人空间设置。
将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 11:01:08 +08:00

295 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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