Implement profile tab with Android-aligned flows and OSS upload.
Add personal info page, account switch, real-name auth, withdrawal settings, session cache extensions, AlibabaCloudOSS SPM, and unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
20
suixinkan/UI/Common/UIImageView+Remote.swift
Normal file
20
suixinkan/UI/Common/UIImageView+Remote.swift
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// UIImageView+Remote.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Kingfisher
|
||||
import UIKit
|
||||
|
||||
extension UIImageView {
|
||||
/// 加载远程头像,空 URL 时显示占位背景。
|
||||
func loadRemoteImage(urlString: String?, placeholderColor: UIColor = UIColor(hex: 0xEFF6FF)) {
|
||||
backgroundColor = placeholderColor
|
||||
let trimmed = urlString?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard let url = URL(string: trimmed), !trimmed.isEmpty else {
|
||||
image = nil
|
||||
return
|
||||
}
|
||||
kf.setImage(with: url, placeholder: nil)
|
||||
}
|
||||
}
|
||||
@ -273,8 +273,8 @@ final class LoginViewController: BaseViewController {
|
||||
do {
|
||||
let resolution = try await viewModel.login(authAPI: authAPI)
|
||||
switch resolution {
|
||||
case let .completed(response):
|
||||
completeLogin(with: response)
|
||||
case let .completed(response, account):
|
||||
completeLogin(with: response, account: account)
|
||||
case .needsAccountSelection:
|
||||
break
|
||||
}
|
||||
@ -311,7 +311,7 @@ final class LoginViewController: BaseViewController {
|
||||
|
||||
do {
|
||||
let response = try await viewModel.selectAccount(account, authAPI: authAPI)
|
||||
completeLogin(with: response)
|
||||
completeLogin(with: response, account: account)
|
||||
accountSelectionController = nil
|
||||
dismiss(animated: true)
|
||||
} catch is CancellationError {
|
||||
@ -322,11 +322,12 @@ final class LoginViewController: BaseViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func completeLogin(with response: V9AuthResponse) {
|
||||
private func completeLogin(with response: V9AuthResponse, account: AccountSwitchAccount) {
|
||||
AuthSessionHelper.completeLogin(
|
||||
with: response,
|
||||
username: viewModel.normalizedUsername,
|
||||
privacyAgreementAccepted: viewModel.isPrivacyChecked
|
||||
privacyAgreementAccepted: viewModel.isPrivacyChecked,
|
||||
selectedAccount: account
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -182,7 +182,7 @@ final class LoginViewModel {
|
||||
guard !finalResponse.token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
throw LoginFlowError.missingToken
|
||||
}
|
||||
return .completed(finalResponse)
|
||||
return .completed(finalResponse, account)
|
||||
}
|
||||
|
||||
let payload = AccountSelectionPayload(tempToken: token, accounts: accounts)
|
||||
|
||||
245
suixinkan/UI/Profile/AccountSwitchViewController.swift
Normal file
245
suixinkan/UI/Profile/AccountSwitchViewController.swift
Normal file
@ -0,0 +1,245 @@
|
||||
//
|
||||
// AccountSwitchViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
private typealias AccountSwitchSection = Int
|
||||
private typealias AccountSwitchItem = String
|
||||
|
||||
/// 账号切换页面,展示当前登录主体下可进入的景区账号和门店账号。
|
||||
final class AccountSwitchViewController: BaseViewController, UITableViewDelegate {
|
||||
|
||||
private let viewModel = AccountSwitchViewModel()
|
||||
private let profileAPI = NetworkServices.shared.profileAPI
|
||||
private let authAPI = NetworkServices.shared.authAPI
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .plain)
|
||||
table.backgroundColor = UIColor(hex: 0xF5F7FB)
|
||||
table.separatorStyle = .none
|
||||
table.delegate = self
|
||||
table.register(AccountSwitchCell.self, forCellReuseIdentifier: AccountSwitchCell.reuseID)
|
||||
return table
|
||||
}()
|
||||
|
||||
private let confirmButton = AppButton(title: "确认切换")
|
||||
|
||||
private var tableDataSource: UITableViewDiffableDataSource<AccountSwitchSection, AccountSwitchItem>!
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "账号切换"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = UIColor(hex: 0xF5F7FB)
|
||||
configureTableDataSource()
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(confirmButton)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
make.bottom.equalTo(confirmButton.snp.top).offset(-12)
|
||||
}
|
||||
confirmButton.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?.applyTableSnapshot(reconfigure: true)
|
||||
self?.updateConfirmButton()
|
||||
}
|
||||
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
||||
Task { await loadAccounts() }
|
||||
}
|
||||
|
||||
private func configureTableDataSource() {
|
||||
tableDataSource = UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, item in
|
||||
guard let self else { return UITableViewCell() }
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: AccountSwitchCell.reuseID, for: indexPath) as! AccountSwitchCell
|
||||
guard let account = self.viewModel.accounts.first(where: { $0.id == item }) else { return cell }
|
||||
cell.configure(
|
||||
account: account,
|
||||
selected: account.id == self.viewModel.selectedAccountId,
|
||||
isCurrent: account.isCurrent || self.isCurrentAccount(account)
|
||||
)
|
||||
return cell
|
||||
}
|
||||
applyTableSnapshot(animated: false)
|
||||
}
|
||||
|
||||
private func buildSnapshot() -> NSDiffableDataSourceSnapshot<AccountSwitchSection, AccountSwitchItem> {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<AccountSwitchSection, AccountSwitchItem>()
|
||||
snapshot.appendSections([0])
|
||||
snapshot.appendItems(viewModel.accounts.map(\.id), toSection: 0)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
private func applyTableSnapshot(animated: Bool = true, reconfigure: Bool = false) {
|
||||
var snapshot = buildSnapshot()
|
||||
if reconfigure, !snapshot.itemIdentifiers.isEmpty {
|
||||
snapshot.reconfigureItems(snapshot.itemIdentifiers)
|
||||
}
|
||||
tableDataSource.apply(snapshot, animatingDifferences: animated)
|
||||
}
|
||||
|
||||
private func updateConfirmButton() {
|
||||
let enabled = viewModel.selectedAccount != nil && !viewModel.loading && !viewModel.switching
|
||||
confirmButton.isEnabled = enabled
|
||||
}
|
||||
|
||||
private var currentAccountId: String? {
|
||||
let store = AppStore.shared
|
||||
if store.currentStoreId > 0 {
|
||||
return "\(V9StoreUser.accountTypeValue)_\(store.currentStoreId)"
|
||||
}
|
||||
if store.currentScenicId > 0 {
|
||||
return "\(V9ScenicUser.accountTypeValue)_\(store.userId)"
|
||||
}
|
||||
return store.userId.isEmpty ? nil : store.userId
|
||||
}
|
||||
|
||||
private func isCurrentAccount(_ account: AccountSwitchAccount) -> Bool {
|
||||
viewModel.isCurrent(account, currentAccountId: currentAccountId)
|
||||
}
|
||||
|
||||
private func loadAccounts(force: Bool = false) async {
|
||||
showLoading()
|
||||
defer { hideLoading() }
|
||||
do {
|
||||
try await viewModel.load(api: profileAPI, force: force, currentAccountId: currentAccountId)
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func confirmTapped() {
|
||||
guard let account = viewModel.selectedAccount else { return }
|
||||
if account.isCurrent || isCurrentAccount(account) {
|
||||
navigationController?.popViewController(animated: true)
|
||||
return
|
||||
}
|
||||
Task { await switchAccount(account) }
|
||||
}
|
||||
|
||||
private func switchAccount(_ account: AccountSwitchAccount) async {
|
||||
showLoading()
|
||||
defer { hideLoading() }
|
||||
do {
|
||||
let response = try await viewModel.switchAccount(account, api: authAPI)
|
||||
AuthSessionHelper.applyAccountSwitch(with: response, account: account)
|
||||
showToast("账号已切换")
|
||||
navigationController?.popViewController(animated: true)
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard let item = tableDataSource.itemIdentifier(for: indexPath),
|
||||
let account = viewModel.accounts.first(where: { $0.id == item }) else { return }
|
||||
viewModel.select(account)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 92 }
|
||||
}
|
||||
|
||||
private final class AccountSwitchCell: UITableViewCell {
|
||||
static let reuseID = "AccountSwitchCell"
|
||||
|
||||
private let card = UIView()
|
||||
private let avatarView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
private let phoneLabel = UILabel()
|
||||
private let tagLabel = UILabel()
|
||||
private let checkView = UIImageView()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
card.backgroundColor = .white
|
||||
card.layer.cornerRadius = 12
|
||||
contentView.addSubview(card)
|
||||
|
||||
avatarView.layer.cornerRadius = 25
|
||||
avatarView.clipsToBounds = true
|
||||
avatarView.contentMode = .scaleAspectFill
|
||||
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
subtitleLabel.font = .systemFont(ofSize: 13)
|
||||
subtitleLabel.textColor = AppColor.text666
|
||||
phoneLabel.font = .systemFont(ofSize: 12)
|
||||
phoneLabel.textColor = UIColor(hex: 0x9AA1AA)
|
||||
tagLabel.font = .systemFont(ofSize: 11, weight: .semibold)
|
||||
tagLabel.textAlignment = .center
|
||||
tagLabel.layer.cornerRadius = 4
|
||||
tagLabel.clipsToBounds = true
|
||||
|
||||
card.addSubview(avatarView)
|
||||
card.addSubview(titleLabel)
|
||||
card.addSubview(subtitleLabel)
|
||||
card.addSubview(phoneLabel)
|
||||
card.addSubview(tagLabel)
|
||||
card.addSubview(checkView)
|
||||
|
||||
card.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 6, left: 16, bottom: 6, right: 16))
|
||||
}
|
||||
avatarView.snp.makeConstraints { make in
|
||||
make.leading.centerY.equalToSuperview().inset(14)
|
||||
make.width.height.equalTo(50)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(avatarView.snp.trailing).offset(13)
|
||||
make.top.equalTo(avatarView).offset(2)
|
||||
make.trailing.lessThanOrEqualTo(tagLabel.snp.leading).offset(-8)
|
||||
}
|
||||
subtitleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(titleLabel)
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(4)
|
||||
}
|
||||
phoneLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(titleLabel)
|
||||
make.top.equalTo(subtitleLabel.snp.bottom).offset(4)
|
||||
}
|
||||
tagLabel.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().inset(14)
|
||||
make.top.equalToSuperview().offset(14)
|
||||
make.height.equalTo(22)
|
||||
make.width.greaterThanOrEqualTo(40)
|
||||
}
|
||||
checkView.snp.makeConstraints { make in
|
||||
make.trailing.bottom.equalToSuperview().inset(14)
|
||||
make.width.height.equalTo(22)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
func configure(account: AccountSwitchAccount, selected: Bool, isCurrent: Bool) {
|
||||
titleLabel.text = account.title
|
||||
if isCurrent {
|
||||
titleLabel.text = (account.title) + " (当前)"
|
||||
}
|
||||
subtitleLabel.text = account.subtitle
|
||||
phoneLabel.text = account.phone
|
||||
tagLabel.text = account.isStoreUser ? "门店" : "景区"
|
||||
tagLabel.textColor = account.isStoreUser ? UIColor(hex: 0x0F9F6E) : UIColor(hex: 0x7C3AED)
|
||||
tagLabel.backgroundColor = account.isStoreUser ? UIColor(hex: 0xE8F8F1) : UIColor(hex: 0xF3ECFF)
|
||||
checkView.image = UIImage(systemName: selected ? "checkmark.circle.fill" : "circle")
|
||||
checkView.tintColor = selected ? AppColor.primary : UIColor(hex: 0xB6BECA)
|
||||
avatarView.loadRemoteImage(urlString: account.avatar)
|
||||
}
|
||||
}
|
||||
135
suixinkan/UI/Profile/ProfileInfoRowView.swift
Normal file
135
suixinkan/UI/Profile/ProfileInfoRowView.swift
Normal file
@ -0,0 +1,135 @@
|
||||
//
|
||||
// ProfileInfoRowView.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 个人信息卡片行,展示左侧标题与右侧内容,可选点击与箭头。
|
||||
final class ProfileInfoRowView: UIControl {
|
||||
|
||||
enum AccessoryStyle {
|
||||
case none
|
||||
case chevron
|
||||
case action(String)
|
||||
}
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let valueLabel = UILabel()
|
||||
private let accessoryLabel = UILabel()
|
||||
private let chevronView = UIImageView()
|
||||
private let divider = UIView()
|
||||
private let badgeContainer = UIView()
|
||||
private var badgeView: ProfileStatusBadgeView?
|
||||
|
||||
var showsDivider = true {
|
||||
didSet { divider.isHidden = !showsDivider }
|
||||
}
|
||||
|
||||
init(title: String) {
|
||||
super.init(frame: .zero)
|
||||
titleLabel.text = title
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func configure(
|
||||
value: String?,
|
||||
valueColor: UIColor = AppColor.text333,
|
||||
accessory: AccessoryStyle = .none,
|
||||
badge: ProfileStatusBadgeView.Style? = nil
|
||||
) {
|
||||
valueLabel.text = value
|
||||
valueLabel.textColor = valueColor
|
||||
valueLabel.isHidden = badge != nil
|
||||
badgeContainer.isHidden = badge == nil
|
||||
|
||||
if let badge {
|
||||
if badgeView == nil {
|
||||
let view = ProfileStatusBadgeView()
|
||||
badgeContainer.addSubview(view)
|
||||
view.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
badgeView = view
|
||||
}
|
||||
badgeView?.apply(style: badge, text: value ?? "")
|
||||
}
|
||||
|
||||
accessoryLabel.isHidden = true
|
||||
chevronView.isHidden = true
|
||||
switch accessory {
|
||||
case .none:
|
||||
break
|
||||
case .chevron:
|
||||
chevronView.isHidden = false
|
||||
case .action(let text):
|
||||
accessoryLabel.text = text
|
||||
accessoryLabel.isHidden = false
|
||||
chevronView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
titleLabel.font = .systemFont(ofSize: 14)
|
||||
titleLabel.textColor = UIColor(hex: 0x4B5563)
|
||||
titleLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||
|
||||
valueLabel.font = .systemFont(ofSize: 14)
|
||||
valueLabel.textAlignment = .right
|
||||
valueLabel.numberOfLines = 1
|
||||
|
||||
accessoryLabel.font = .systemFont(ofSize: 14)
|
||||
accessoryLabel.textColor = AppColor.primary
|
||||
|
||||
chevronView.image = UIImage(systemName: "chevron.right")
|
||||
chevronView.tintColor = AppColor.text666
|
||||
chevronView.contentMode = .scaleAspectFit
|
||||
|
||||
divider.backgroundColor = UIColor(hex: 0xE5E7EB)
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(valueLabel)
|
||||
addSubview(badgeContainer)
|
||||
addSubview(accessoryLabel)
|
||||
addSubview(chevronView)
|
||||
addSubview(divider)
|
||||
|
||||
snp.makeConstraints { make in
|
||||
make.height.equalTo(52)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.equalTo(102)
|
||||
}
|
||||
chevronView.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(16)
|
||||
}
|
||||
accessoryLabel.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(chevronView.snp.leading).offset(-4)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
valueLabel.snp.makeConstraints { make in
|
||||
make.leading.greaterThanOrEqualTo(titleLabel.snp.trailing).offset(8)
|
||||
make.trailing.equalTo(chevronView.snp.leading).offset(-4)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
badgeContainer.snp.makeConstraints { make in
|
||||
make.leading.equalTo(titleLabel.snp.trailing).offset(8)
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.lessThanOrEqualTo(chevronView.snp.leading).offset(-8)
|
||||
}
|
||||
divider.snp.makeConstraints { make in
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
make.height.equalTo(0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
62
suixinkan/UI/Profile/ProfileStatusBadgeView.swift
Normal file
62
suixinkan/UI/Profile/ProfileStatusBadgeView.swift
Normal file
@ -0,0 +1,62 @@
|
||||
//
|
||||
// ProfileStatusBadgeView.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 个人信息页状态标签,用于账号状态、审核状态等展示。
|
||||
final class ProfileStatusBadgeView: UIView {
|
||||
|
||||
enum Style {
|
||||
case accountActive
|
||||
case scenic
|
||||
case auditPending
|
||||
case auditApproved
|
||||
case auditRejected
|
||||
case bankPending
|
||||
case bankApproved
|
||||
case bankRejected
|
||||
}
|
||||
|
||||
private let label = UILabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
label.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
label.textAlignment = .center
|
||||
addSubview(label)
|
||||
label.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 3, left: 8, bottom: 3, right: 8))
|
||||
}
|
||||
layer.cornerRadius = 4
|
||||
clipsToBounds = true
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(style: Style, text: String) {
|
||||
label.text = text
|
||||
switch style {
|
||||
case .accountActive:
|
||||
label.textColor = UIColor(hex: 0x22C55E)
|
||||
backgroundColor = UIColor(hex: 0xF0FDF4)
|
||||
case .scenic:
|
||||
label.textColor = AppColor.primary
|
||||
backgroundColor = UIColor(hex: 0xEFF6FF)
|
||||
case .auditPending, .bankPending:
|
||||
label.textColor = UIColor(hex: 0xFF7B00)
|
||||
backgroundColor = UIColor(hex: 0xFFF0E2)
|
||||
case .auditApproved, .bankApproved:
|
||||
label.textColor = AppColor.primary
|
||||
backgroundColor = UIColor(hex: 0xEFF6FF)
|
||||
case .auditRejected, .bankRejected:
|
||||
label.textColor = UIColor(hex: 0xEF4444)
|
||||
backgroundColor = UIColor(hex: 0xFFE7E7)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,26 +3,430 @@
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import PhotosUI
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 我的 Tab 根页面。
|
||||
/// 我的 Tab 根页面,展示个人信息、账号状态与子流程入口。
|
||||
final class ProfileViewController: BaseViewController {
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let viewModel = ProfileViewModel()
|
||||
private let profileAPI = NetworkServices.shared.profileAPI
|
||||
private let ossUploadService = NetworkServices.shared.ossUploadService
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
private let refreshControl = UIRefreshControl()
|
||||
|
||||
private let avatarImageView = UIImageView()
|
||||
private let nicknameField = UITextField()
|
||||
private let uidLabel = UILabel()
|
||||
private let editButton = AppButton(title: "编辑", style: .primary)
|
||||
|
||||
private let infoCard = UIStackView()
|
||||
private let nameRow = ProfileInfoRowView(title: "姓名")
|
||||
private let accountRow = ProfileInfoRowView(title: "当前账号")
|
||||
private let phoneRow = ProfileInfoRowView(title: "手机号")
|
||||
private let withdrawalRow = ProfileInfoRowView(title: "提现设置")
|
||||
private let passwordRow = ProfileInfoRowView(title: "修改密码")
|
||||
private let realNameRow = ProfileInfoRowView(title: "认证状态")
|
||||
private let statusRow = ProfileInfoRowView(title: "账号状态")
|
||||
private let scenicRow = ProfileInfoRowView(title: "当前景区")
|
||||
private let logoutButton = UIButton(type: .system)
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "个人信息"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
title = "我的"
|
||||
titleLabel.text = "我的"
|
||||
titleLabel.font = .systemFont(ofSize: 24, weight: .medium)
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.textColor = AppColor.text333
|
||||
view.addSubview(titleLabel)
|
||||
view.backgroundColor = UIColor(hex: 0xF7FAFF)
|
||||
|
||||
scrollView.alwaysBounceVertical = true
|
||||
scrollView.refreshControl = refreshControl
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(contentStack)
|
||||
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 24
|
||||
contentStack.layoutMargins = UIEdgeInsets(top: 16, left: 15, bottom: 24, right: 15)
|
||||
contentStack.isLayoutMarginsRelativeArrangement = true
|
||||
|
||||
setupHeader()
|
||||
setupInfoCard()
|
||||
setupLogoutButton()
|
||||
|
||||
contentStack.addArrangedSubview(headerContainer)
|
||||
contentStack.addArrangedSubview(infoCardWrapper)
|
||||
contentStack.addArrangedSubview(logoutButton)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
scrollView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
make.width.equalTo(scrollView.snp.width)
|
||||
}
|
||||
logoutButton.snp.makeConstraints { make in
|
||||
make.height.equalTo(46)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
viewModel.onStateChange = { [weak self] in
|
||||
self?.applyViewModel()
|
||||
}
|
||||
|
||||
refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
|
||||
editButton.addTarget(self, action: #selector(editTapped), for: .touchUpInside)
|
||||
logoutButton.addTarget(self, action: #selector(logoutTapped), for: .touchUpInside)
|
||||
|
||||
accountRow.addTarget(self, action: #selector(accountSwitchTapped), for: .touchUpInside)
|
||||
passwordRow.addTarget(self, action: #selector(changePasswordTapped), for: .touchUpInside)
|
||||
withdrawalRow.addTarget(self, action: #selector(withdrawalTapped), for: .touchUpInside)
|
||||
realNameRow.addTarget(self, action: #selector(realNameTapped), for: .touchUpInside)
|
||||
|
||||
nicknameField.addTarget(self, action: #selector(nicknameChanged), for: .editingChanged)
|
||||
|
||||
let avatarTap = UITapGestureRecognizer(target: self, action: #selector(pickAvatar))
|
||||
avatarImageView.isUserInteractionEnabled = true
|
||||
avatarImageView.addGestureRecognizer(avatarTap)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleAccountDidSwitch),
|
||||
name: NotificationName.accountDidSwitch,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
Task { await reloadProfile(showLoading: false) }
|
||||
}
|
||||
|
||||
private let headerContainer = UIView()
|
||||
private let infoCardWrapper = UIView()
|
||||
|
||||
private func setupHeader() {
|
||||
avatarImageView.layer.cornerRadius = 43
|
||||
avatarImageView.clipsToBounds = true
|
||||
avatarImageView.contentMode = .scaleAspectFill
|
||||
|
||||
nicknameField.font = .systemFont(ofSize: 18, weight: .medium)
|
||||
nicknameField.textColor = AppColor.text333
|
||||
nicknameField.borderStyle = .none
|
||||
nicknameField.isEnabled = false
|
||||
|
||||
uidLabel.font = .systemFont(ofSize: 14, weight: .semibold)
|
||||
uidLabel.textColor = UIColor(hex: 0x9CA3AF)
|
||||
|
||||
editButton.layer.cornerRadius = 18
|
||||
editButton.snp.updateConstraints { make in
|
||||
make.height.equalTo(36)
|
||||
}
|
||||
editButton.titleLabel?.font = .systemFont(ofSize: 12)
|
||||
|
||||
headerContainer.addSubview(avatarImageView)
|
||||
headerContainer.addSubview(nicknameField)
|
||||
headerContainer.addSubview(uidLabel)
|
||||
headerContainer.addSubview(editButton)
|
||||
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.leading.top.bottom.equalToSuperview()
|
||||
make.width.height.equalTo(86)
|
||||
}
|
||||
nicknameField.snp.makeConstraints { make in
|
||||
make.leading.equalTo(avatarImageView.snp.trailing).offset(16)
|
||||
make.trailing.lessThanOrEqualTo(editButton.snp.leading).offset(-8)
|
||||
make.top.equalTo(avatarImageView).offset(12)
|
||||
}
|
||||
uidLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(nicknameField)
|
||||
make.top.equalTo(nicknameField.snp.bottom).offset(8)
|
||||
}
|
||||
editButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview()
|
||||
make.centerY.equalTo(avatarImageView)
|
||||
make.width.equalTo(80)
|
||||
make.height.equalTo(36)
|
||||
}
|
||||
headerContainer.snp.makeConstraints { make in
|
||||
make.height.greaterThanOrEqualTo(86)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupInfoCard() {
|
||||
infoCard.axis = .vertical
|
||||
infoCard.backgroundColor = .white
|
||||
infoCard.layer.cornerRadius = 8
|
||||
infoCard.clipsToBounds = true
|
||||
infoCard.isLayoutMarginsRelativeArrangement = true
|
||||
infoCard.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
|
||||
|
||||
infoCardWrapper.addSubview(infoCard)
|
||||
infoCard.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
[
|
||||
nameRow, accountRow, phoneRow, withdrawalRow,
|
||||
passwordRow, realNameRow, statusRow, scenicRow
|
||||
].forEach { infoCard.addArrangedSubview($0) }
|
||||
|
||||
nameRow.isUserInteractionEnabled = false
|
||||
phoneRow.isUserInteractionEnabled = false
|
||||
statusRow.isUserInteractionEnabled = false
|
||||
scenicRow.isUserInteractionEnabled = false
|
||||
scenicRow.showsDivider = false
|
||||
}
|
||||
|
||||
private func setupLogoutButton() {
|
||||
logoutButton.setTitle("退出登录", for: .normal)
|
||||
logoutButton.setTitleColor(UIColor(hex: 0xEF4444), for: .normal)
|
||||
logoutButton.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
logoutButton.backgroundColor = .white
|
||||
logoutButton.layer.cornerRadius = 8
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
nicknameField.text = viewModel.isEditingProfile ? viewModel.editingNickname : viewModel.displayNickname
|
||||
nicknameField.isEnabled = viewModel.isEditingProfile
|
||||
uidLabel.text = "UID:\(viewModel.displayUID)"
|
||||
|
||||
if let pendingData = viewModel.pendingAvatarData, let image = UIImage(data: pendingData) {
|
||||
avatarImageView.image = image
|
||||
} else {
|
||||
avatarImageView.loadRemoteImage(urlString: viewModel.displayAvatarURL)
|
||||
}
|
||||
|
||||
editButton.setTitle(viewModel.isEditingProfile ? "完成" : "编辑", for: .normal)
|
||||
editButton.isEnabled = !viewModel.isSaving
|
||||
avatarImageView.alpha = viewModel.isEditingProfile ? 1 : 1
|
||||
|
||||
nameRow.configure(value: viewModel.displayRealName)
|
||||
accountRow.configure(
|
||||
value: viewModel.accountDisplayName,
|
||||
accessory: .action("切换账号")
|
||||
)
|
||||
phoneRow.configure(value: viewModel.displayPhone)
|
||||
|
||||
withdrawalRow.isHidden = !viewModel.showPhotographerFields
|
||||
realNameRow.isHidden = !viewModel.showPhotographerFields
|
||||
if viewModel.showPhotographerFields {
|
||||
configureWithdrawalRow()
|
||||
configureRealNameRow()
|
||||
}
|
||||
|
||||
passwordRow.configure(value: "点击修改密码", valueColor: UIColor(hex: 0x9CA3AF), accessory: .chevron)
|
||||
statusRow.configure(
|
||||
value: viewModel.accountStatusText,
|
||||
badge: viewModel.accountStatusText == "--" ? nil : .accountActive
|
||||
)
|
||||
scenicRow.configure(
|
||||
value: viewModel.currentScenicName,
|
||||
badge: .scenic
|
||||
)
|
||||
|
||||
if viewModel.isLoading, !refreshControl.isRefreshing {
|
||||
showLoading()
|
||||
} else {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
private func configureWithdrawalRow() {
|
||||
let info = viewModel.bankCardInfo
|
||||
let text = viewModel.bankCardDisplayText
|
||||
var valueColor = UIColor(hex: 0x9CA3AF)
|
||||
var badge: ProfileStatusBadgeView.Style?
|
||||
if let info {
|
||||
switch info.auditStatus {
|
||||
case 2:
|
||||
valueColor = AppColor.text333
|
||||
case 3:
|
||||
valueColor = UIColor(hex: 0xEF4444)
|
||||
badge = .bankRejected
|
||||
default:
|
||||
valueColor = UIColor(hex: 0xFF7B00)
|
||||
badge = .bankPending
|
||||
}
|
||||
}
|
||||
withdrawalRow.configure(
|
||||
value: text,
|
||||
valueColor: valueColor,
|
||||
accessory: .chevron,
|
||||
badge: badge
|
||||
)
|
||||
_ = info
|
||||
}
|
||||
|
||||
private func configureRealNameRow() {
|
||||
if let info = viewModel.realNameInfo {
|
||||
let style: ProfileStatusBadgeView.Style
|
||||
switch info.auditStatus {
|
||||
case 2: style = .auditApproved
|
||||
case 3: style = .auditRejected
|
||||
default: style = .auditPending
|
||||
}
|
||||
realNameRow.configure(
|
||||
value: viewModel.realNameStatusText,
|
||||
accessory: .chevron,
|
||||
badge: style
|
||||
)
|
||||
} else {
|
||||
realNameRow.configure(
|
||||
value: "点击去实名认证",
|
||||
valueColor: UIColor(hex: 0x9CA3AF),
|
||||
accessory: .chevron
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func refreshPulled() {
|
||||
Task {
|
||||
await reloadProfile(showLoading: false)
|
||||
refreshControl.endRefreshing()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleAccountDidSwitch() {
|
||||
Task { await reloadProfile(showLoading: true) }
|
||||
}
|
||||
|
||||
@objc private func nicknameChanged() {
|
||||
viewModel.updateEditingNickname(nicknameField.text ?? "")
|
||||
}
|
||||
|
||||
@objc private func editTapped() {
|
||||
if viewModel.isEditingProfile {
|
||||
Task { await saveProfile() }
|
||||
} else {
|
||||
viewModel.beginEditing()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func pickAvatar() {
|
||||
guard viewModel.isEditingProfile else { return }
|
||||
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 accountSwitchTapped() {
|
||||
navigationController?.pushViewController(AccountSwitchViewController(), animated: true)
|
||||
}
|
||||
|
||||
@objc private func changePasswordTapped() {
|
||||
let alert = UIAlertController(title: "修改密码", message: "请输入新密码(至少 6 位)", preferredStyle: .alert)
|
||||
alert.addTextField { field in
|
||||
field.isSecureTextEntry = true
|
||||
field.placeholder = "新密码"
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
|
||||
guard let password = alert.textFields?.first?.text else { return }
|
||||
Task { await self?.updatePassword(password) }
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
@objc private func withdrawalTapped() {
|
||||
if let bankCard = viewModel.bankCardInfo, bankCard.auditStatus != 0 {
|
||||
navigationController?.pushViewController(
|
||||
WithdrawalSettingsAuditViewController(bankCard: bankCard),
|
||||
animated: true
|
||||
)
|
||||
return
|
||||
}
|
||||
if viewModel.realNameInfo?.verified != true {
|
||||
showToast("请实名认证成功后再试")
|
||||
return
|
||||
}
|
||||
navigationController?.pushViewController(WithdrawalSettingsViewController(), animated: true)
|
||||
}
|
||||
|
||||
@objc private func realNameTapped() {
|
||||
if viewModel.realNameInfo == nil {
|
||||
navigationController?.pushViewController(RealNameAuthViewController(), animated: true)
|
||||
} else if let info = viewModel.realNameInfo {
|
||||
navigationController?.pushViewController(
|
||||
RealNameAuthAuditViewController(info: info),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func logoutTapped() {
|
||||
let alert = UIAlertController(title: "确定退出登录?", message: "退出后将需要重新登录", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "确定", style: .destructive) { _ in
|
||||
NotificationCenter.default.post(name: NotificationName.userDidLogout, object: nil)
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func reloadProfile(showLoading: Bool) async {
|
||||
if showLoading { self.showLoading() }
|
||||
defer {
|
||||
if showLoading { hideLoading() }
|
||||
}
|
||||
do {
|
||||
try await viewModel.reload(api: profileAPI)
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func saveProfile() async {
|
||||
let scenicId = AppStore.shared.currentScenicId
|
||||
guard scenicId > 0 else {
|
||||
showToast("当前景区信息缺失")
|
||||
return
|
||||
}
|
||||
showLoading()
|
||||
defer { hideLoading() }
|
||||
do {
|
||||
try await viewModel.saveProfile(
|
||||
api: profileAPI,
|
||||
uploader: ossUploadService,
|
||||
scenicId: scenicId
|
||||
)
|
||||
showToast("保存成功")
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePassword(_ password: String) async {
|
||||
showLoading()
|
||||
defer { hideLoading() }
|
||||
do {
|
||||
try await viewModel.updatePassword(password, api: profileAPI)
|
||||
showToast("密码修改成功")
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileViewController: 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.prepareAvatarImage(data: data)
|
||||
} catch {
|
||||
self.showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
113
suixinkan/UI/Profile/RealNameAuthAuditViewController.swift
Normal file
113
suixinkan/UI/Profile/RealNameAuthAuditViewController.swift
Normal file
@ -0,0 +1,113 @@
|
||||
//
|
||||
// RealNameAuthAuditViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 实名认证审核状态页,展示审核结果与证件预览。
|
||||
final class RealNameAuthAuditViewController: BaseViewController {
|
||||
|
||||
private let info: RealNameInfo
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
private let statusBadge = ProfileStatusBadgeView()
|
||||
private let rejectLabel = UILabel()
|
||||
private let frontImageView = UIImageView()
|
||||
private let backImageView = UIImageView()
|
||||
|
||||
init(info: RealNameInfo) {
|
||||
self.info = info
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "认证状态"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = UIColor(hex: 0xF7FAFF)
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 16
|
||||
|
||||
rejectLabel.numberOfLines = 0
|
||||
rejectLabel.font = .systemFont(ofSize: 14)
|
||||
rejectLabel.textColor = UIColor(hex: 0xEF4444)
|
||||
|
||||
[frontImageView, backImageView].forEach {
|
||||
$0.contentMode = .scaleAspectFit
|
||||
$0.backgroundColor = UIColor(hex: 0xF8FAFC)
|
||||
$0.layer.cornerRadius = 8
|
||||
$0.clipsToBounds = true
|
||||
$0.snp.makeConstraints { make in make.height.equalTo(160) }
|
||||
}
|
||||
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(contentStack)
|
||||
|
||||
contentStack.addArrangedSubview(makeTitle("审核状态"))
|
||||
contentStack.addArrangedSubview(statusBadge)
|
||||
if let reason = info.rejectReason, !reason.isEmpty, info.auditStatus == 3 {
|
||||
rejectLabel.text = "驳回原因:\(reason)"
|
||||
contentStack.addArrangedSubview(rejectLabel)
|
||||
}
|
||||
contentStack.addArrangedSubview(makeInfoRow("姓名", info.realName))
|
||||
contentStack.addArrangedSubview(makeInfoRow("身份证号", maskID(info.idCardNo)))
|
||||
contentStack.addArrangedSubview(makeTitle("身份证国徽面"))
|
||||
contentStack.addArrangedSubview(backImageView)
|
||||
contentStack.addArrangedSubview(makeTitle("身份证人像面"))
|
||||
contentStack.addArrangedSubview(frontImageView)
|
||||
|
||||
applyStatus()
|
||||
frontImageView.loadRemoteImage(urlString: info.frontUrl)
|
||||
backImageView.loadRemoteImage(urlString: info.backUrl)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
scrollView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(16)
|
||||
make.width.equalTo(scrollView).offset(-32)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyStatus() {
|
||||
switch info.auditStatus {
|
||||
case 2:
|
||||
statusBadge.apply(style: .auditApproved, text: "已实名认证")
|
||||
case 3:
|
||||
statusBadge.apply(style: .auditRejected, text: "审核不通过")
|
||||
default:
|
||||
statusBadge.apply(style: .auditPending, text: info.auditStatusText ?? "审核中")
|
||||
}
|
||||
}
|
||||
|
||||
private func makeTitle(_ text: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||
return label
|
||||
}
|
||||
|
||||
private func makeInfoRow(_ title: String, _ value: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 14)
|
||||
label.textColor = AppColor.text666
|
||||
label.text = "\(title):\(value)"
|
||||
return label
|
||||
}
|
||||
|
||||
private func maskID(_ value: String) -> String {
|
||||
guard value.count > 8 else { return value }
|
||||
let prefix = value.prefix(4)
|
||||
let suffix = value.suffix(4)
|
||||
return "\(prefix)**********\(suffix)"
|
||||
}
|
||||
}
|
||||
255
suixinkan/UI/Profile/RealNameAuthViewController.swift
Normal file
255
suixinkan/UI/Profile/RealNameAuthViewController.swift
Normal file
@ -0,0 +1,255 @@
|
||||
//
|
||||
// RealNameAuthViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import PhotosUI
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 实名认证页面,展示表单并允许提交基础实名资料。
|
||||
final class RealNameAuthViewController: BaseViewController {
|
||||
|
||||
private let viewModel = RealNameAuthViewModel()
|
||||
private let profileAPI = NetworkServices.shared.profileAPI
|
||||
private let ossUploadService = NetworkServices.shared.ossUploadService
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
private let realNameField = UITextField()
|
||||
private let idCardField = UITextField()
|
||||
private let smsCodeField = UITextField()
|
||||
private let frontImageView = UIImageView()
|
||||
private let backImageView = UIImageView()
|
||||
private let longValidSwitch = UISwitch()
|
||||
private let startDatePicker = UIDatePicker()
|
||||
private let endDatePicker = UIDatePicker()
|
||||
private let submitButton = AppButton(title: "提交审核")
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "实名认证"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = UIColor(hex: 0xF7FAFF)
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 16
|
||||
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(contentStack)
|
||||
view.addSubview(submitButton)
|
||||
|
||||
[realNameField, idCardField, smsCodeField].forEach {
|
||||
$0.borderStyle = .roundedRect
|
||||
$0.font = .systemFont(ofSize: 16)
|
||||
}
|
||||
realNameField.placeholder = "请输入姓名"
|
||||
idCardField.placeholder = "请输入身份证号码"
|
||||
smsCodeField.placeholder = "请输入短信验证码"
|
||||
smsCodeField.keyboardType = .numberPad
|
||||
|
||||
[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(160) }
|
||||
}
|
||||
frontImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(pickFront)))
|
||||
backImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(pickBack)))
|
||||
|
||||
startDatePicker.datePickerMode = .date
|
||||
endDatePicker.datePickerMode = .date
|
||||
if #available(iOS 17.0, *) {
|
||||
startDatePicker.preferredDatePickerStyle = .compact
|
||||
endDatePicker.preferredDatePickerStyle = .compact
|
||||
}
|
||||
|
||||
let sendCodeButton = UIButton(type: .system)
|
||||
sendCodeButton.setTitle("获取验证码", for: .normal)
|
||||
sendCodeButton.addTarget(self, action: #selector(sendCodeTapped), for: .touchUpInside)
|
||||
|
||||
contentStack.addArrangedSubview(makeSectionTitle("身份证国徽面"))
|
||||
contentStack.addArrangedSubview(backImageView)
|
||||
contentStack.addArrangedSubview(makeSectionTitle("身份证人像面"))
|
||||
contentStack.addArrangedSubview(frontImageView)
|
||||
contentStack.addArrangedSubview(labeledField("姓名", realNameField))
|
||||
contentStack.addArrangedSubview(labeledField("身份证号码", idCardField))
|
||||
|
||||
let longValidRow = UIStackView(arrangedSubviews: [UILabel(text: "长期有效"), longValidSwitch])
|
||||
longValidRow.axis = .horizontal
|
||||
contentStack.addArrangedSubview(longValidRow)
|
||||
contentStack.addArrangedSubview(labeledField("起始日期", startDatePicker))
|
||||
contentStack.addArrangedSubview(labeledField("结束日期", endDatePicker))
|
||||
|
||||
let smsRow = UIStackView(arrangedSubviews: [smsCodeField, sendCodeButton])
|
||||
smsRow.axis = .horizontal
|
||||
smsRow.spacing = 8
|
||||
smsCodeField.snp.makeConstraints { make in make.width.equalTo(200) }
|
||||
contentStack.addArrangedSubview(labeledField("短信验证码", smsRow))
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
scrollView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
make.bottom.equalTo(submitButton.snp.top).offset(-12)
|
||||
}
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(16)
|
||||
make.width.equalTo(scrollView).offset(-32)
|
||||
}
|
||||
submitButton.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() }
|
||||
submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)
|
||||
longValidSwitch.addTarget(self, action: #selector(longValidChanged), for: .valueChanged)
|
||||
realNameField.addTarget(self, action: #selector(fieldChanged), for: .editingChanged)
|
||||
idCardField.addTarget(self, action: #selector(fieldChanged), for: .editingChanged)
|
||||
smsCodeField.addTarget(self, action: #selector(fieldChanged), for: .editingChanged)
|
||||
startDatePicker.addTarget(self, action: #selector(dateChanged), for: .valueChanged)
|
||||
endDatePicker.addTarget(self, action: #selector(dateChanged), for: .valueChanged)
|
||||
Task { await loadInfo() }
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
realNameField.text = viewModel.realName
|
||||
idCardField.text = viewModel.idCardNo
|
||||
smsCodeField.text = viewModel.smsCode
|
||||
longValidSwitch.isOn = viewModel.isLongValid
|
||||
startDatePicker.date = viewModel.startDate
|
||||
endDatePicker.date = viewModel.endDate
|
||||
endDatePicker.isEnabled = !viewModel.isLongValid
|
||||
submitButton.isEnabled = !viewModel.submitting
|
||||
|
||||
if let data = viewModel.pendingFrontImageData, let image = UIImage(data: data) {
|
||||
frontImageView.image = image
|
||||
} else {
|
||||
frontImageView.loadRemoteImage(urlString: viewModel.frontUrl)
|
||||
}
|
||||
if let data = viewModel.pendingBackImageData, let image = UIImage(data: data) {
|
||||
backImageView.image = image
|
||||
} else {
|
||||
backImageView.loadRemoteImage(urlString: viewModel.backUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func fieldChanged() {
|
||||
viewModel.updateRealName(realNameField.text ?? "")
|
||||
viewModel.updateIdCardNo(idCardField.text ?? "")
|
||||
viewModel.updateSmsCode(smsCodeField.text ?? "")
|
||||
}
|
||||
|
||||
@objc private func dateChanged() {
|
||||
viewModel.updateStartDate(startDatePicker.date)
|
||||
viewModel.updateEndDate(endDatePicker.date)
|
||||
}
|
||||
|
||||
@objc private func longValidChanged() {
|
||||
viewModel.updateLongValid(longValidSwitch.isOn)
|
||||
}
|
||||
|
||||
@objc private func sendCodeTapped() {
|
||||
Task {
|
||||
showLoading()
|
||||
defer { hideLoading() }
|
||||
do {
|
||||
try await viewModel.sendCode(api: profileAPI)
|
||||
showToast(viewModel.statusMessage ?? "验证码已发送")
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func submitTapped() {
|
||||
Task { await submit() }
|
||||
}
|
||||
|
||||
@objc private func pickFront() { pickImage(side: .front) }
|
||||
@objc private func pickBack() { pickImage(side: .back) }
|
||||
|
||||
private var pickingSide: RealNameImageSide = .front
|
||||
|
||||
private func pickImage(side: RealNameImageSide) {
|
||||
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)
|
||||
}
|
||||
|
||||
private func loadInfo() async {
|
||||
showLoading()
|
||||
defer { hideLoading() }
|
||||
do {
|
||||
try await viewModel.load(api: profileAPI)
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func submit() async {
|
||||
let scenicId = AppStore.shared.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 makeSectionTitle(_ text: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||
return label
|
||||
}
|
||||
|
||||
private func labeledField(_ title: String, _ field: UIView) -> UIStackView {
|
||||
let titleLabel = UILabel(text: title)
|
||||
titleLabel.font = .systemFont(ofSize: 14)
|
||||
let stack = UIStackView(arrangedSubviews: [titleLabel, field])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 8
|
||||
return stack
|
||||
}
|
||||
}
|
||||
|
||||
extension RealNameAuthViewController: 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.prepareIdentityImage(data: data, side: self.pickingSide)
|
||||
} catch {
|
||||
self.showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension UILabel {
|
||||
convenience init(text: String) {
|
||||
self.init()
|
||||
self.text = text
|
||||
}
|
||||
}
|
||||
109
suixinkan/UI/Profile/WithdrawalSettingsAuditViewController.swift
Normal file
109
suixinkan/UI/Profile/WithdrawalSettingsAuditViewController.swift
Normal file
@ -0,0 +1,109 @@
|
||||
//
|
||||
// WithdrawalSettingsAuditViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 提现设置审核状态页,展示银行卡审核结果。
|
||||
final class WithdrawalSettingsAuditViewController: BaseViewController {
|
||||
|
||||
private let bankCard: BankCardInfo
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
private let statusBadge = ProfileStatusBadgeView()
|
||||
private let rejectLabel = UILabel()
|
||||
private let frontImageView = UIImageView()
|
||||
private let backImageView = UIImageView()
|
||||
|
||||
init(bankCard: BankCardInfo) {
|
||||
self.bankCard = bankCard
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "提现设置"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = UIColor(hex: 0xF7FAFF)
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 16
|
||||
|
||||
rejectLabel.numberOfLines = 0
|
||||
rejectLabel.font = .systemFont(ofSize: 14)
|
||||
rejectLabel.textColor = UIColor(hex: 0xEF4444)
|
||||
|
||||
[frontImageView, backImageView].forEach {
|
||||
$0.contentMode = .scaleAspectFit
|
||||
$0.backgroundColor = UIColor(hex: 0xF8FAFC)
|
||||
$0.layer.cornerRadius = 8
|
||||
$0.clipsToBounds = true
|
||||
$0.snp.makeConstraints { make in make.height.equalTo(140) }
|
||||
}
|
||||
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(contentStack)
|
||||
|
||||
contentStack.addArrangedSubview(makeTitle("审核状态"))
|
||||
contentStack.addArrangedSubview(statusBadge)
|
||||
if !bankCard.rejectReason.isEmpty, bankCard.auditStatus == 3 {
|
||||
rejectLabel.text = "驳回原因:\(bankCard.rejectReason)"
|
||||
contentStack.addArrangedSubview(rejectLabel)
|
||||
}
|
||||
contentStack.addArrangedSubview(makeInfoRow("持卡人", bankCard.realName))
|
||||
contentStack.addArrangedSubview(makeInfoRow("开户银行", bankCard.bankName))
|
||||
contentStack.addArrangedSubview(makeInfoRow("支行", bankCard.branchName))
|
||||
contentStack.addArrangedSubview(makeInfoRow("卡号", ProfileViewModel.maskCardNumber(bankCard.cardNumber)))
|
||||
contentStack.addArrangedSubview(makeTitle("银行卡正面"))
|
||||
contentStack.addArrangedSubview(frontImageView)
|
||||
contentStack.addArrangedSubview(makeTitle("银行卡反面"))
|
||||
contentStack.addArrangedSubview(backImageView)
|
||||
|
||||
applyStatus()
|
||||
frontImageView.loadRemoteImage(urlString: bankCard.frontUrl)
|
||||
backImageView.loadRemoteImage(urlString: bankCard.backUrl)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
scrollView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(16)
|
||||
make.width.equalTo(scrollView).offset(-32)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyStatus() {
|
||||
let text = bankCard.auditStatusLabel.isEmpty ? "审核中" : bankCard.auditStatusLabel
|
||||
switch bankCard.auditStatus {
|
||||
case 2:
|
||||
statusBadge.apply(style: .bankApproved, text: text)
|
||||
case 3:
|
||||
statusBadge.apply(style: .bankRejected, text: text)
|
||||
default:
|
||||
statusBadge.apply(style: .bankPending, text: text)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeTitle(_ text: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||
return label
|
||||
}
|
||||
|
||||
private func makeInfoRow(_ title: String, _ value: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 14)
|
||||
label.textColor = AppColor.text666
|
||||
label.text = "\(title):\(value)"
|
||||
return label
|
||||
}
|
||||
}
|
||||
294
suixinkan/UI/Profile/WithdrawalSettingsViewController.swift
Normal file
294
suixinkan/UI/Profile/WithdrawalSettingsViewController.swift
Normal file
@ -0,0 +1,294 @@
|
||||
//
|
||||
// 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.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user