Files
suixinkan_uikit/suixinkan/UI/Wallet/WithdrawViewController.swift

353 lines
13 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.

//
// WithdrawViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// Android `WithdrawScreen`
final class WithdrawViewController: BaseViewController {
var onSubmitSuccess: (() -> Void)?
private let viewModel: WithdrawViewModel
private let walletAPI: any WalletPageServing
private let scrollView = UIScrollView()
private let contentStack = UIStackView()
private let bottomBar = UIView()
private let submitButton = AppButton(title: "立即提现")
private let amountField = UITextField()
private let codeField = UITextField()
private let codeButton = UIButton(type: .system)
private let withdrawableLabel = UILabel()
private let limitLabel = UILabel()
private let phoneLabel = UILabel()
private let bankCardLabel = UILabel()
private let bankHolderLabel = UILabel()
private let settlementLabel = UILabel()
private let infoStack = UIStackView()
private var countdownTimer: Timer?
private var lastShownMessage: String?
///
init(
viewModel: WithdrawViewModel = WithdrawViewModel(),
walletAPI: any WalletPageServing = NetworkServices.shared.walletAPI
) {
self.viewModel = viewModel
self.walletAPI = walletAPI
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
countdownTimer?.invalidate()
}
override func setupNavigationBar() {
title = "提现设置"
}
override func setupUI() {
view.backgroundColor = UIColor(hex: 0xF5F6F8)
contentStack.axis = .vertical
contentStack.spacing = 16
bottomBar.backgroundColor = .white
view.addSubview(scrollView)
scrollView.addSubview(contentStack)
view.addSubview(bottomBar)
bottomBar.addSubview(submitButton)
contentStack.addArrangedSubview(makeAmountCard())
contentStack.addArrangedSubview(makeBankCard())
contentStack.addArrangedSubview(makeSettlementCard())
contentStack.addArrangedSubview(makeInfoCard())
}
override func setupConstraints() {
bottomBar.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
}
submitButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(16)
make.leading.trailing.equalToSuperview().inset(16)
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
}
scrollView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.bottom.equalTo(bottomBar.snp.top)
}
contentStack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(16)
make.width.equalTo(scrollView).offset(-32)
}
}
override func bindActions() {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyState() }
}
amountField.addTarget(self, action: #selector(amountChanged), for: .editingChanged)
codeField.addTarget(self, action: #selector(codeChanged), for: .editingChanged)
codeButton.addTarget(self, action: #selector(sendCodeTapped), for: .touchUpInside)
submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)
Task { await viewModel.load(api: walletAPI) }
}
private func applyState() {
amountField.text = viewModel.amount
codeField.text = viewModel.verificationCode
if let info = viewModel.withdrawInfo {
withdrawableLabel.text = WalletViewModel.formatAmount(info.amountWithdrawable)
limitLabel.text = "单笔最高提现 \(WalletViewModel.formatAmount(info.maxSingleWithdrawAmount)), 单日最高提现 \(WalletViewModel.formatAmount(info.maxDailyWithdrawAmount))"
phoneLabel.text = "验证码将发送至: \(maskPhone(info.userPhone))"
bankCardLabel.text = "\(info.bankCard.bankName) (尾号\(lastFour(info.bankCard.cardNumber)))"
bankHolderLabel.text = info.bankCard.realName
settlementLabel.text = info.settlement
applyInfoLines(info.withdrawInfo)
}
codeButton.setTitle(viewModel.countdown > 0 ? "\(viewModel.countdown)s" : "获取验证码", for: .normal)
codeButton.isEnabled = viewModel.countdown == 0
submitButton.isEnabled = viewModel.canSubmit
if viewModel.countdown > 0 { startCountdownTimerIfNeeded() }
showMessageIfNeeded(viewModel.statusMessage)
showMessageIfNeeded(viewModel.errorMessage)
}
private func makeAmountCard() -> UIView {
let card = makeCard()
let title = makeTitle("提现金额")
let row = UIStackView()
row.axis = .horizontal
row.alignment = .center
row.spacing = 16
let withdrawableTitle = makeBodyLabel("可提现金额", color: .black, weight: .medium)
withdrawableLabel.font = .systemFont(ofSize: 14, weight: .medium)
withdrawableLabel.textColor = UIColor(hex: 0x0073FF)
row.addArrangedSubview(withdrawableTitle)
row.addArrangedSubview(withdrawableLabel)
let amountContainer = borderedInputContainer()
amountField.placeholder = "请输入金额"
amountField.keyboardType = .decimalPad
amountField.font = .systemFont(ofSize: 14)
let allButton = UIButton(type: .system)
allButton.setTitle("全部提现", for: .normal)
allButton.setTitleColor(UIColor(hex: 0x0073FF), for: .normal)
allButton.titleLabel?.font = .systemFont(ofSize: 14)
allButton.addTarget(self, action: #selector(withdrawAllTapped), for: .touchUpInside)
amountContainer.addSubview(amountField)
amountContainer.addSubview(allButton)
amountField.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(12)
make.top.bottom.equalToSuperview()
make.trailing.equalTo(allButton.snp.leading).offset(-8)
}
allButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(12)
make.centerY.equalToSuperview()
}
limitLabel.font = .systemFont(ofSize: 12)
limitLabel.textColor = UIColor(hex: 0x9CA3AF)
phoneLabel.font = .systemFont(ofSize: 12)
phoneLabel.textColor = UIColor(hex: 0x4B5563)
let codeRow = UIStackView()
codeRow.axis = .horizontal
codeRow.spacing = 12
let codeContainer = borderedInputContainer()
codeField.placeholder = "请输入验证码"
codeField.keyboardType = .numberPad
codeField.font = .systemFont(ofSize: 14)
codeContainer.addSubview(codeField)
codeField.snp.makeConstraints { make in make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)) }
codeButton.setTitle("获取验证码", for: .normal)
codeButton.setTitleColor(UIColor(hex: 0x0073FF), for: .normal)
codeButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
codeButton.layer.borderWidth = 1
codeButton.layer.borderColor = UIColor(hex: 0xEEEEEE).cgColor
codeButton.layer.cornerRadius = 4
codeRow.addArrangedSubview(codeContainer)
codeRow.addArrangedSubview(codeButton)
codeContainer.snp.makeConstraints { make in make.height.equalTo(46) }
codeButton.snp.makeConstraints { make in make.width.equalTo(100) }
let stack = UIStackView(arrangedSubviews: [title, row, amountContainer, limitLabel, phoneLabel, codeRow])
stack.axis = .vertical
stack.spacing = 12
card.addSubview(stack)
stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(16) }
amountContainer.snp.makeConstraints { make in make.height.equalTo(46) }
return card
}
private func makeBankCard() -> UIView {
let card = makeCard()
let title = makeTitle("银行卡信息")
let inner = makeInnerPanel()
bankCardLabel.font = .systemFont(ofSize: 14, weight: .medium)
bankCardLabel.textColor = UIColor(hex: 0x333333)
bankHolderLabel.font = .systemFont(ofSize: 14)
bankHolderLabel.textColor = UIColor(hex: 0x4B5563)
let innerStack = UIStackView(arrangedSubviews: [bankCardLabel, bankHolderLabel])
innerStack.axis = .vertical
innerStack.spacing = 12
inner.addSubview(innerStack)
innerStack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(16) }
let stack = UIStackView(arrangedSubviews: [title, inner])
stack.axis = .vertical
stack.spacing = 8
card.addSubview(stack)
stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(16) }
return card
}
private func makeSettlementCard() -> UIView {
let card = makeCard()
let title = makeTitle("预计到账时间")
let inner = makeInnerPanel()
settlementLabel.font = .systemFont(ofSize: 14)
settlementLabel.textColor = UIColor(hex: 0x4B5563)
settlementLabel.numberOfLines = 0
inner.addSubview(settlementLabel)
settlementLabel.snp.makeConstraints { make in make.edges.equalToSuperview().inset(16) }
let stack = UIStackView(arrangedSubviews: [title, inner])
stack.axis = .vertical
stack.spacing = 8
card.addSubview(stack)
stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(16) }
return card
}
private func makeInfoCard() -> UIView {
let card = makeCard()
let title = makeTitle("提现说明")
let inner = makeInnerPanel()
infoStack.axis = .vertical
infoStack.spacing = 4
inner.addSubview(infoStack)
infoStack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(16) }
let stack = UIStackView(arrangedSubviews: [title, inner])
stack.axis = .vertical
stack.spacing = 8
card.addSubview(stack)
stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(16) }
return card
}
private func makeCard() -> UIView {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 12
view.clipsToBounds = true
return view
}
private func makeInnerPanel() -> UIView {
let view = UIView()
view.backgroundColor = UIColor(hex: 0xF4F4F4)
view.layer.cornerRadius = 8
view.layer.borderWidth = 1
view.layer.borderColor = UIColor(hex: 0xEEEEEE).cgColor
return view
}
private func borderedInputContainer() -> UIView {
let view = UIView()
view.layer.cornerRadius = 4
view.layer.borderWidth = 1
view.layer.borderColor = UIColor(hex: 0xEEEEEE).cgColor
return view
}
private func makeTitle(_ text: String) -> UILabel {
let label = UILabel()
label.text = text
label.font = .systemFont(ofSize: 18, weight: .medium)
label.textColor = .black
return label
}
private func makeBodyLabel(_ text: String, color: UIColor, weight: UIFont.Weight = .regular) -> UILabel {
let label = UILabel()
label.text = text
label.font = .systemFont(ofSize: 14, weight: weight)
label.textColor = color
return label
}
private func applyInfoLines(_ lines: [String]) {
infoStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
lines.forEach { text in
let label = makeBodyLabel(text, color: UIColor(hex: 0x4B5563))
label.numberOfLines = 0
infoStack.addArrangedSubview(label)
}
}
private func showMessageIfNeeded(_ message: String?) {
guard let message, !message.isEmpty, message != lastShownMessage else { return }
lastShownMessage = message
showToast(message)
}
private func startCountdownTimerIfNeeded() {
guard countdownTimer == nil else { return }
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self else { return }
self.viewModel.decrementCountdown()
if self.viewModel.countdown == 0 {
timer.invalidate()
self.countdownTimer = nil
}
}
}
private func maskPhone(_ phone: String) -> String {
guard phone.count >= 7 else { return phone }
let prefix = phone.prefix(3)
let suffix = phone.suffix(4)
return "\(prefix)****\(suffix)"
}
private func lastFour(_ cardNumber: String) -> String {
String(cardNumber.suffix(4))
}
@objc private func amountChanged() {
viewModel.updateAmount(amountField.text ?? "")
}
@objc private func codeChanged() {
viewModel.updateVerificationCode(codeField.text ?? "")
}
@objc private func withdrawAllTapped() {
viewModel.withdrawAll()
}
@objc private func sendCodeTapped() {
Task { await viewModel.requestVerificationCode(api: walletAPI) }
}
@objc private func submitTapped() {
Task {
showLoading()
let success = await viewModel.submit(api: walletAPI)
hideLoading()
if success {
onSubmitSuccess?()
navigationController?.popViewController(animated: true)
}
}
}
}