// // 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)? = nil ) { self.viewModel = viewModel self.walletAPI = walletAPI ?? NetworkServices.shared.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() { let titleLabel = UILabel() titleLabel.text = "提现设置" titleLabel.textColor = WalletTokens.text333 titleLabel.font = UIFont.systemFont(ofSize: 18) navigationItem.titleView = titleLabel } override func setupUI() { view.backgroundColor = WalletTokens.withdrawPageBackground contentStack.axis = .vertical contentStack.spacing = WalletTokens.cardGap contentStack.backgroundColor = .white contentStack.layer.cornerRadius = WalletTokens.cardRadius contentStack.clipsToBounds = true bottomBar.backgroundColor = .white bottomBar.isHidden = true 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(WalletTokens.screenInset) make.leading.trailing.equalToSuperview().inset(WalletTokens.screenInset) make.bottom.equalTo(view.safeAreaLayoutGuide).inset(WalletTokens.screenInset) } 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(WalletTokens.screenInset) 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 { showLoading() await viewModel.load(api: walletAPI) hideLoading() if viewModel.withdrawInfo == nil, let message = viewModel.errorMessage { navigationController?.popViewController(animated: true) (navigationController?.topViewController as? BaseViewController)?.showToast(message) } } } 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 updateBottomBarVisibility(hasContent: viewModel.withdrawInfo != nil && !viewModel.loading) if viewModel.countdown > 0 { startCountdownTimerIfNeeded() } showMessageIfNeeded(viewModel.statusMessage) if viewModel.withdrawInfo != nil { showMessageIfNeeded(viewModel.errorMessage) } } private func updateBottomBarVisibility(hasContent: Bool) { bottomBar.isHidden = !hasContent scrollView.snp.remakeConstraints { make in make.top.leading.trailing.equalToSuperview() if hasContent { make.bottom.equalTo(bottomBar.snp.top) } else { make.bottom.equalToSuperview() } } } 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 = WalletTokens.bodyMediumFont withdrawableLabel.textColor = WalletTokens.primary row.addArrangedSubview(withdrawableTitle) row.addArrangedSubview(withdrawableLabel) let amountContainer = borderedInputContainer() amountField.placeholder = "请输入金额" amountField.keyboardType = .decimalPad amountField.font = WalletTokens.bodyFont let allButton = UIButton(type: .system) allButton.setTitle("全部提现", for: .normal) allButton.setTitleColor(WalletTokens.primary, for: .normal) allButton.titleLabel?.font = WalletTokens.bodyFont 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 = WalletTokens.captionFont limitLabel.textColor = WalletTokens.textTertiary phoneLabel.font = WalletTokens.captionFont phoneLabel.textColor = WalletTokens.textSecondary let codeRow = UIStackView() codeRow.axis = .horizontal codeRow.spacing = 12 let codeContainer = borderedInputContainer() codeField.placeholder = "请输入验证码" codeField.keyboardType = .numberPad codeField.font = WalletTokens.bodyFont 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(WalletTokens.primary, for: .normal) codeButton.setTitleColor(WalletTokens.textDisabled, for: .disabled) codeButton.titleLabel?.font = WalletTokens.bodyMediumFont codeButton.layer.borderWidth = 1 codeButton.layer.borderColor = WalletTokens.border.cgColor codeButton.layer.cornerRadius = WalletTokens.chipRadius codeRow.addArrangedSubview(codeContainer) codeRow.addArrangedSubview(codeButton) codeContainer.snp.makeConstraints { make in make.height.equalTo(WalletTokens.inputHeight) } 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(WalletTokens.inputHeight) } return card } private func makeBankCard() -> UIView { let card = makeCard() let title = makeTitle("银行卡信息", size: 16) let inner = makeInnerPanel() bankCardLabel.font = WalletTokens.bodyMediumFont bankCardLabel.textColor = WalletTokens.text333 bankHolderLabel.font = WalletTokens.bodyFont bankHolderLabel.textColor = WalletTokens.textSecondary let innerStack = UIStackView(arrangedSubviews: [bankCardLabel, bankHolderLabel]) innerStack.axis = .vertical innerStack.spacing = 12 inner.addSubview(innerStack) innerStack.snp.makeConstraints { make in make.top.bottom.equalToSuperview().inset(WalletTokens.compactGap) make.leading.trailing.equalToSuperview().inset(WalletTokens.cardPadding) } 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("预计到账时间", size: 16) let inner = makeInnerPanel() settlementLabel.font = WalletTokens.bodyFont settlementLabel.textColor = WalletTokens.textSecondary settlementLabel.numberOfLines = 0 inner.addSubview(settlementLabel) settlementLabel.snp.makeConstraints { make in make.top.bottom.equalToSuperview().inset(WalletTokens.compactGap) make.leading.trailing.equalToSuperview().inset(WalletTokens.cardPadding) } let stack = UIStackView(arrangedSubviews: [title, inner]) stack.axis = .vertical stack.spacing = WalletTokens.tinyGap 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("提现说明", size: 16) let inner = makeInnerPanel() infoStack.axis = .vertical infoStack.spacing = 4 inner.addSubview(infoStack) infoStack.snp.makeConstraints { make in make.top.bottom.equalToSuperview().inset(WalletTokens.compactGap) make.leading.trailing.equalToSuperview().inset(WalletTokens.cardPadding) } let stack = UIStackView(arrangedSubviews: [title, inner]) stack.axis = .vertical stack.spacing = WalletTokens.rowGap 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 = WalletTokens.cardRadius view.clipsToBounds = true return view } private func makeInnerPanel() -> UIView { let view = UIView() view.backgroundColor = WalletTokens.inputBackground view.layer.cornerRadius = WalletTokens.innerRadius view.layer.borderWidth = 1 view.layer.borderColor = WalletTokens.border.cgColor return view } private func borderedInputContainer() -> UIView { let view = UIView() view.layer.cornerRadius = WalletTokens.chipRadius view.layer.borderWidth = 1 view.layer.borderColor = WalletTokens.border.cgColor return view } private func makeTitle(_ text: String, size: CGFloat = 18) -> UILabel { let label = UILabel() label.text = text label.font = .systemFont(ofSize: size, 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: WalletTokens.textSecondary) 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) } } } }