实现我的 Tab、对齐 Android 流程并接入 OSS 上传
This commit is contained in:
@ -0,0 +1,77 @@
|
||||
//
|
||||
// AccountSwitchViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 账号切换 ViewModel,管理可切换账号列表、选择状态和提交状态。
|
||||
final class AccountSwitchViewModel {
|
||||
private(set) var accounts: [AccountSwitchAccount] = []
|
||||
private(set) var selectedAccountId: String?
|
||||
private(set) var loading = false
|
||||
private(set) var switching = false
|
||||
private var didLoad = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
|
||||
var selectedAccount: AccountSwitchAccount? {
|
||||
accounts.first { $0.id == selectedAccountId }
|
||||
}
|
||||
|
||||
func load(api: ProfileAPI, force: Bool = false, currentAccountId: String? = nil) async throws {
|
||||
guard force || !didLoad else { return }
|
||||
didLoad = true
|
||||
loading = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
loading = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
let response = try await api.switchableAccounts()
|
||||
accounts = response.accounts.filter { $0.businessUserId > 0 }
|
||||
selectedAccountId = accounts.first(where: { $0.isCurrent || isCurrent($0, currentAccountId: currentAccountId) })?.id
|
||||
?? accounts.first?.id
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func select(_ account: AccountSwitchAccount) {
|
||||
selectedAccountId = account.id
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func switchAccount(_ account: AccountSwitchAccount, api: AuthAPI) async throws -> V9AuthResponse {
|
||||
guard !switching else { throw AccountSwitchError.submitting }
|
||||
guard account.businessUserId > 0 else { throw AccountSwitchError.invalidAccount }
|
||||
|
||||
switching = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
switching = false
|
||||
notifyStateChange()
|
||||
}
|
||||
return try await api.setUser(account.toSetUserRequest())
|
||||
}
|
||||
|
||||
func isCurrent(_ account: AccountSwitchAccount, currentAccountId: String?) -> Bool {
|
||||
guard let currentAccountId else { return false }
|
||||
return account.id == currentAccountId || String(account.businessUserId) == currentAccountId
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
|
||||
enum AccountSwitchError: LocalizedError, Equatable {
|
||||
case submitting
|
||||
case invalidAccount
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .submitting: "账号正在切换,请稍候"
|
||||
case .invalidAccount: "账号信息异常,请重新选择账号"
|
||||
}
|
||||
}
|
||||
}
|
||||
249
suixinkan/Features/Profile/ViewModels/ProfileViewModel.swift
Normal file
249
suixinkan/Features/Profile/ViewModels/ProfileViewModel.swift
Normal file
@ -0,0 +1,249 @@
|
||||
//
|
||||
// ProfileViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 个人信息页 ViewModel,负责资料加载、编辑状态和提交更新。
|
||||
final class ProfileViewModel {
|
||||
|
||||
private(set) var userInfo: UserInfoResponse?
|
||||
private(set) var realNameInfo: RealNameInfo?
|
||||
private(set) var bankCardInfo: BankCardInfo?
|
||||
private(set) var isLoading = false
|
||||
private(set) var isSaving = false
|
||||
private(set) var isEditingProfile = false
|
||||
private(set) var editingNickname = ""
|
||||
private(set) var pendingAvatarData: Data?
|
||||
private(set) var pendingAvatarFileName: String?
|
||||
private(set) var avatarUploadProgress: Int?
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
|
||||
var displayNickname: String {
|
||||
nonEmpty(userInfo?.nickname) ?? "未设置昵称"
|
||||
}
|
||||
|
||||
var displayRealName: String {
|
||||
nonEmpty(userInfo?.realName) ?? "--"
|
||||
}
|
||||
|
||||
var displayPhone: String {
|
||||
nonEmpty(userInfo?.phone) ?? AppStore.shared.phone.nonEmpty ?? "--"
|
||||
}
|
||||
|
||||
var displayAvatarURL: String {
|
||||
nonEmpty(userInfo?.avatar) ?? AppStore.shared.avatar.nonEmpty ?? ""
|
||||
}
|
||||
|
||||
var displayUID: String {
|
||||
let uid = AppStore.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return uid.isEmpty ? "--" : uid
|
||||
}
|
||||
|
||||
var accountDisplayName: String {
|
||||
let name = AppStore.shared.accountDisplayName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return name.isEmpty ? "--" : name
|
||||
}
|
||||
|
||||
var currentScenicName: String {
|
||||
let name = AppStore.shared.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return name.isEmpty ? "--" : name
|
||||
}
|
||||
|
||||
var accountStatusText: String {
|
||||
nonEmpty(userInfo?.statusName) ?? "--"
|
||||
}
|
||||
|
||||
var showPhotographerFields: Bool {
|
||||
if AppStore.shared.isPhotographerRole {
|
||||
return true
|
||||
}
|
||||
if let roleName = nonEmpty(userInfo?.roleName) {
|
||||
return AppRoleCode.fromRoleName(roleName)?.isPhotographer == true || roleName == "摄影师"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var realNameStatusText: String {
|
||||
guard let realNameInfo else { return "点击去实名认证" }
|
||||
switch realNameInfo.auditStatus {
|
||||
case 2: return "已实名认证"
|
||||
case 3: return "审核不通过"
|
||||
default: return "审核中"
|
||||
}
|
||||
}
|
||||
|
||||
var bankCardDisplayText: String {
|
||||
guard let bankCardInfo else { return "点击绑定银行卡" }
|
||||
if bankCardInfo.auditStatus == 2 {
|
||||
let masked = Self.maskCardNumber(bankCardInfo.cardNumber)
|
||||
return "\(bankCardInfo.bankName) \(masked)"
|
||||
}
|
||||
let label = bankCardInfo.auditStatusLabel.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return label.isEmpty ? "审核中" : label
|
||||
}
|
||||
|
||||
func updateEditingNickname(_ value: String) {
|
||||
editingNickname = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 重新加载用户资料,摄影师额外拉取实名与银行卡信息。
|
||||
func reload(api: ProfileAPI) async throws {
|
||||
guard !isLoading else { return }
|
||||
isLoading = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isLoading = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
let info = try await api.userInfo()
|
||||
userInfo = info
|
||||
AppStore.shared.applyUserInfo(info)
|
||||
if !info.roleName.isEmpty {
|
||||
AppStore.shared.roleName = info.roleName
|
||||
}
|
||||
|
||||
if showPhotographerFields {
|
||||
async let realName = api.realNameInfo()
|
||||
async let bankCard = api.bankCardInfo()
|
||||
let extra = try await (realName, bankCard)
|
||||
realNameInfo = extra.0.realNameInfo
|
||||
bankCardInfo = extra.1.bankCard
|
||||
} else {
|
||||
realNameInfo = nil
|
||||
bankCardInfo = nil
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func beginEditing() {
|
||||
editingNickname = displayNickname == "未设置昵称" ? "" : displayNickname
|
||||
isEditingProfile = true
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func cancelEditing() {
|
||||
editingNickname = ""
|
||||
isEditingProfile = false
|
||||
clearPendingAvatar()
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func prepareAvatarImage(data: Data, timestamp: TimeInterval = Date().timeIntervalSince1970) throws {
|
||||
let processed = try AvatarImageProcessor.process(data: data, timestamp: timestamp)
|
||||
pendingAvatarData = processed.data
|
||||
pendingAvatarFileName = processed.fileName
|
||||
if !isEditingProfile {
|
||||
beginEditing()
|
||||
} else {
|
||||
notifyStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
func saveProfile(api: ProfileAPI, uploader: any OSSUploadServing, scenicId: Int) async throws {
|
||||
let nextNickname = editingNickname.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !nextNickname.isEmpty else {
|
||||
throw ProfileValidationError.emptyNickname
|
||||
}
|
||||
|
||||
let nicknameChanged = nextNickname != displayNickname
|
||||
let avatarChanged = pendingAvatarData != nil
|
||||
guard nicknameChanged || avatarChanged else {
|
||||
cancelEditing()
|
||||
return
|
||||
}
|
||||
|
||||
guard !isSaving else { return }
|
||||
isSaving = true
|
||||
avatarUploadProgress = avatarChanged ? 1 : nil
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isSaving = false
|
||||
avatarUploadProgress = nil
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
let uploadedAvatarURL = try await uploadPendingAvatarIfNeeded(uploader: uploader, scenicId: scenicId)
|
||||
if let uploadedAvatarURL {
|
||||
try await api.updateUserAvatarURL(uploadedAvatarURL)
|
||||
}
|
||||
if nicknameChanged {
|
||||
try await api.updateUserInfo(nickname: nextNickname)
|
||||
}
|
||||
|
||||
let previous = userInfo ?? UserInfoResponse()
|
||||
userInfo = UserInfoResponse(
|
||||
avatar: uploadedAvatarURL ?? previous.avatar,
|
||||
realName: previous.realName,
|
||||
phone: previous.phone,
|
||||
nickname: nextNickname,
|
||||
roleName: previous.roleName,
|
||||
status: previous.status,
|
||||
statusName: previous.statusName
|
||||
)
|
||||
AppStore.shared.userName = nextNickname
|
||||
if let uploadedAvatarURL {
|
||||
AppStore.shared.avatar = uploadedAvatarURL
|
||||
}
|
||||
clearPendingAvatar()
|
||||
cancelEditing()
|
||||
}
|
||||
|
||||
func updatePassword(_ password: String, api: ProfileAPI) async throws {
|
||||
let nextPassword = password.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard nextPassword.count >= 6 else {
|
||||
throw ProfileValidationError.shortPassword
|
||||
}
|
||||
|
||||
guard !isSaving else { return }
|
||||
isSaving = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isSaving = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
try await api.updateUserInfo(password: nextPassword)
|
||||
}
|
||||
|
||||
static func maskCardNumber(_ cardNumber: String) -> String {
|
||||
let digits = cardNumber.filter(\.isNumber)
|
||||
guard digits.count >= 4 else { return cardNumber }
|
||||
return "**** **** **** \(digits.suffix(4))"
|
||||
}
|
||||
|
||||
private func uploadPendingAvatarIfNeeded(uploader: any OSSUploadServing, scenicId: Int) async throws -> String? {
|
||||
guard let pendingAvatarData else { return nil }
|
||||
let fileName = pendingAvatarFileName ?? "avatar_\(Int(Date().timeIntervalSince1970)).jpg"
|
||||
return try await uploader.uploadUserAvatar(data: pendingAvatarData, fileName: fileName, scenicId: scenicId) { [weak self] progress in
|
||||
self?.avatarUploadProgress = progress
|
||||
self?.notifyStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
private func clearPendingAvatar() {
|
||||
pendingAvatarData = nil
|
||||
pendingAvatarFileName = nil
|
||||
avatarUploadProgress = nil
|
||||
}
|
||||
|
||||
private func nonEmpty(_ value: String?) -> String? {
|
||||
let text = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return text.isEmpty ? nil : text
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
var nonEmpty: String? {
|
||||
let text = trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return text.isEmpty ? nil : text
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,237 @@
|
||||
//
|
||||
// RealNameAuthViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 实名认证 ViewModel,管理认证资料表单、审核状态、短信验证码和提交状态。
|
||||
final class RealNameAuthViewModel {
|
||||
private(set) var info: RealNameInfo?
|
||||
private(set) var realName = ""
|
||||
private(set) var idCardNo = ""
|
||||
private(set) var smsCode = ""
|
||||
private(set) var startDate = Date()
|
||||
private(set) var endDate = Calendar.current.date(byAdding: .year, value: 10, to: Date()) ?? Date()
|
||||
private(set) var isLongValid = false
|
||||
private(set) var frontUrl = ""
|
||||
private(set) var backUrl = ""
|
||||
private(set) var pendingFrontImageData: Data?
|
||||
private(set) var pendingBackImageData: Data?
|
||||
private(set) var loading = false
|
||||
private(set) var sendingCode = false
|
||||
private(set) var submitting = false
|
||||
private(set) var statusMessage: String?
|
||||
|
||||
private var pendingFrontFileName: String?
|
||||
private var pendingBackFileName: String?
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
|
||||
func updateRealName(_ value: String) {
|
||||
realName = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateIdCardNo(_ value: String) {
|
||||
idCardNo = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateSmsCode(_ value: String) {
|
||||
smsCode = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateLongValid(_ value: Bool) {
|
||||
isLongValid = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateStartDate(_ value: Date) {
|
||||
startDate = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateEndDate(_ value: Date) {
|
||||
endDate = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func load(api: ProfileAPI) async throws {
|
||||
guard !loading else { return }
|
||||
loading = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
loading = false
|
||||
notifyStateChange()
|
||||
}
|
||||
let response = try await api.realNameInfo()
|
||||
apply(response.realNameInfo)
|
||||
}
|
||||
|
||||
func sendCode(api: ProfileAPI) async throws {
|
||||
guard !sendingCode else { return }
|
||||
sendingCode = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
sendingCode = false
|
||||
notifyStateChange()
|
||||
}
|
||||
try await api.realNameSmsVerifyCode()
|
||||
statusMessage = "验证码已发送"
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func prepareIdentityImage(data: Data, side: RealNameImageSide, timestamp: TimeInterval = Date().timeIntervalSince1970) throws {
|
||||
let processed = try RealNameImageProcessor.process(data: data, side: side, timestamp: timestamp)
|
||||
switch side {
|
||||
case .front:
|
||||
pendingFrontImageData = processed.data
|
||||
pendingFrontFileName = processed.fileName
|
||||
frontUrl = ""
|
||||
case .back:
|
||||
pendingBackImageData = processed.data
|
||||
pendingBackFileName = processed.fileName
|
||||
backUrl = ""
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func submit(api: ProfileAPI, uploader: any OSSUploadServing, scenicId: Int) async throws {
|
||||
if info?.verified == true {
|
||||
statusMessage = "已完成实名认证"
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
if let validationMessage {
|
||||
throw RealNameValidationError.message(validationMessage)
|
||||
}
|
||||
|
||||
submitting = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
submitting = false
|
||||
notifyStateChange()
|
||||
}
|
||||
try await uploadPendingIdentityImages(uploader: uploader, scenicId: scenicId)
|
||||
try await api.realNameSubmit(makeRequest())
|
||||
statusMessage = "已提交审核"
|
||||
notifyStateChange()
|
||||
try await load(api: api)
|
||||
}
|
||||
|
||||
var validationMessage: String? {
|
||||
if realName.trimmed.isEmpty { return "请输入真实姓名" }
|
||||
if idCardNo.trimmed.isEmpty { return "请输入身份证号" }
|
||||
if !Self.isValidMainlandIDCardNumber(idCardNo) { return "请输入有效的身份证号" }
|
||||
if info?.verified != true {
|
||||
if smsCode.trimmed.isEmpty { return "请输入短信验证码" }
|
||||
if frontUrl.trimmed.isEmpty && pendingFrontImageData == nil { return "请选择身份证人像面图片" }
|
||||
if backUrl.trimmed.isEmpty && pendingBackImageData == nil { return "请选择身份证国徽面图片" }
|
||||
}
|
||||
if !isLongValid && endDate < startDate { return "证件结束日期不能早于起始日期" }
|
||||
return nil
|
||||
}
|
||||
|
||||
nonisolated static func normalizedIDCardNumber(_ value: String) -> String {
|
||||
value.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||
}
|
||||
|
||||
nonisolated static func isValidMainlandIDCardNumber(_ value: String) -> Bool {
|
||||
let number = normalizedIDCardNumber(value)
|
||||
guard number.count == 18 else { return false }
|
||||
let chars = Array(number)
|
||||
guard chars.prefix(17).allSatisfy(\.isNumber) else { return false }
|
||||
guard chars[17].isNumber || chars[17] == "X" else { return false }
|
||||
let weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
||||
let checkCodes = Array("10X98765432")
|
||||
let sum = zip(chars.prefix(17), weights).reduce(0) { partial, pair in
|
||||
partial + (pair.0.wholeNumberValue ?? 0) * pair.1
|
||||
}
|
||||
return chars[17] == checkCodes[sum % 11]
|
||||
}
|
||||
|
||||
private func apply(_ info: RealNameInfo?) {
|
||||
self.info = info
|
||||
guard let info else {
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
realName = info.realName
|
||||
idCardNo = info.idCardNo
|
||||
frontUrl = info.frontUrl ?? ""
|
||||
backUrl = info.backUrl ?? ""
|
||||
pendingFrontImageData = nil
|
||||
pendingBackImageData = nil
|
||||
isLongValid = info.isLongValid
|
||||
startDate = Self.date(from: info.startDate) ?? startDate
|
||||
endDate = Self.date(from: info.endDate) ?? endDate
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
private func makeRequest() -> RealNameAuthRequest {
|
||||
RealNameAuthRequest(
|
||||
realName: realName.trimmed,
|
||||
idCardNo: Self.normalizedIDCardNumber(idCardNo),
|
||||
smsVerifyCode: smsCode.trimmed,
|
||||
startDate: Self.dateFormatter.string(from: startDate),
|
||||
endDate: isLongValid ? "" : Self.dateFormatter.string(from: endDate),
|
||||
isLongValid: isLongValid ? 1 : 0,
|
||||
frontUrl: frontUrl.trimmed,
|
||||
backUrl: backUrl.trimmed
|
||||
)
|
||||
}
|
||||
|
||||
private static let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
return formatter
|
||||
}()
|
||||
|
||||
private static func date(from value: String?) -> Date? {
|
||||
guard let value, !value.isEmpty else { return nil }
|
||||
return dateFormatter.date(from: value)
|
||||
}
|
||||
|
||||
private func uploadPendingIdentityImages(uploader: any OSSUploadServing, scenicId: Int) async throws {
|
||||
if let pendingFrontImageData {
|
||||
frontUrl = try await uploader.uploadRealNameImage(
|
||||
data: pendingFrontImageData,
|
||||
fileName: pendingFrontFileName ?? "real_name_front.jpg",
|
||||
scenicId: scenicId
|
||||
) { _ in }
|
||||
self.pendingFrontImageData = nil
|
||||
}
|
||||
if let pendingBackImageData {
|
||||
backUrl = try await uploader.uploadRealNameImage(
|
||||
data: pendingBackImageData,
|
||||
fileName: pendingBackFileName ?? "real_name_back.jpg",
|
||||
scenicId: scenicId
|
||||
) { _ in }
|
||||
self.pendingBackImageData = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
|
||||
enum RealNameValidationError: LocalizedError, Equatable {
|
||||
case message(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .message(let message): message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
var trimmed: String {
|
||||
trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,219 @@
|
||||
//
|
||||
// WithdrawalSettingsViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 提现设置 ViewModel,管理银行卡绑定两步表单与提交状态。
|
||||
final class WithdrawalSettingsViewModel {
|
||||
private(set) var currentStep = 1
|
||||
private(set) var holderName = ""
|
||||
private(set) var bankNo = ""
|
||||
private(set) var bankCardFrontURL = ""
|
||||
private(set) var bankCardBackURL = ""
|
||||
private(set) var bankName = ""
|
||||
private(set) var provinceCode = ""
|
||||
private(set) var provinceName = ""
|
||||
private(set) var cityCode = ""
|
||||
private(set) var cityName = ""
|
||||
private(set) var branchName = ""
|
||||
private(set) var verificationCode = ""
|
||||
private(set) var isAgree = false
|
||||
private(set) var banks: [String] = []
|
||||
private(set) var areas: [AreasResponse] = []
|
||||
private(set) var loading = false
|
||||
private(set) var sendingCode = false
|
||||
private(set) var submitting = false
|
||||
|
||||
private(set) var pendingFrontImageData: Data?
|
||||
private(set) var pendingBackImageData: Data?
|
||||
private var pendingFrontFileName: String?
|
||||
private var pendingBackFileName: String?
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
|
||||
var canGoNextStep: Bool {
|
||||
currentStep == 1
|
||||
&& !holderName.trimmed.isEmpty
|
||||
&& (12 ... 25).contains(bankNo.filter(\.isNumber).count)
|
||||
&& !bankCardFrontURL.isEmpty
|
||||
&& !bankCardBackURL.isEmpty
|
||||
&& isAgree
|
||||
}
|
||||
|
||||
var canSubmit: Bool {
|
||||
currentStep == 2
|
||||
&& !holderName.trimmed.isEmpty
|
||||
&& (12 ... 25).contains(bankNo.filter(\.isNumber).count)
|
||||
&& !bankName.trimmed.isEmpty
|
||||
&& !provinceCode.isEmpty
|
||||
&& !cityCode.isEmpty
|
||||
&& !branchName.trimmed.isEmpty
|
||||
&& !verificationCode.trimmed.isEmpty
|
||||
&& isAgree
|
||||
&& !submitting
|
||||
}
|
||||
|
||||
func updateHolderName(_ value: String) {
|
||||
holderName = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateBankNo(_ value: String) {
|
||||
bankNo = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateBankName(_ value: String) {
|
||||
bankName = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateBranchName(_ value: String) {
|
||||
branchName = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateVerificationCode(_ value: String) {
|
||||
verificationCode = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateAgree(_ value: Bool) {
|
||||
isAgree = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func selectProvince(_ area: AreasResponse) {
|
||||
provinceCode = area.code
|
||||
provinceName = area.name
|
||||
cityCode = ""
|
||||
cityName = ""
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func selectCity(_ area: AreasResponse) {
|
||||
cityCode = area.code
|
||||
cityName = area.name
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func goNextStep() {
|
||||
guard canGoNextStep else { return }
|
||||
currentStep = 2
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func prepareBankCardImage(data: Data, side: BankCardImageSide) throws {
|
||||
let processed = try BankCardImageProcessor.process(data: data, side: side)
|
||||
switch side {
|
||||
case .front:
|
||||
pendingFrontImageData = processed.data
|
||||
pendingFrontFileName = processed.fileName
|
||||
bankCardFrontURL = "pending"
|
||||
case .back:
|
||||
pendingBackImageData = processed.data
|
||||
pendingBackFileName = processed.fileName
|
||||
bankCardBackURL = "pending"
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func load(api: ProfileAPI) async throws {
|
||||
guard !loading else { return }
|
||||
loading = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
loading = false
|
||||
notifyStateChange()
|
||||
}
|
||||
async let bankList = api.bankList()
|
||||
async let areaList = api.areas()
|
||||
let result = try await (bankList, areaList)
|
||||
banks = result.0.banks
|
||||
areas = result.1
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func sendCode(api: ProfileAPI) async throws {
|
||||
guard !sendingCode else { return }
|
||||
sendingCode = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
sendingCode = false
|
||||
notifyStateChange()
|
||||
}
|
||||
try await api.bankCardSmsVerifyCode()
|
||||
}
|
||||
|
||||
func submit(api: ProfileAPI, uploader: any OSSUploadServing, scenicId: Int) async throws {
|
||||
guard canSubmit else {
|
||||
throw WithdrawalValidationError.message("请完善银行卡信息")
|
||||
}
|
||||
submitting = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
submitting = false
|
||||
notifyStateChange()
|
||||
}
|
||||
try await uploadPendingImages(uploader: uploader, scenicId: scenicId)
|
||||
let request = UpdateBankInfoRequest(
|
||||
realName: holderName.trimmed,
|
||||
cardNumber: bankNo.filter(\.isNumber),
|
||||
bankName: bankName.trimmed,
|
||||
branchName: branchName.trimmed,
|
||||
frontUrl: bankCardFrontURL,
|
||||
backUrl: bankCardBackURL,
|
||||
provinceCode: provinceCode,
|
||||
cityCode: cityCode,
|
||||
smsVerifyCode: verificationCode.trimmed
|
||||
)
|
||||
try await api.updateBankCard(request)
|
||||
}
|
||||
|
||||
var provinceOptions: [AreasResponse] { areas }
|
||||
|
||||
var cityOptions: [AreasResponse] {
|
||||
areas.first { $0.code == provinceCode || $0.name == provinceName }?.children ?? []
|
||||
}
|
||||
|
||||
private func uploadPendingImages(uploader: any OSSUploadServing, scenicId: Int) async throws {
|
||||
if let pendingFrontImageData {
|
||||
bankCardFrontURL = try await uploader.uploadBankCardImage(
|
||||
data: pendingFrontImageData,
|
||||
fileName: pendingFrontFileName ?? "bank_card_front.jpg",
|
||||
scenicId: scenicId
|
||||
) { _ in }
|
||||
self.pendingFrontImageData = nil
|
||||
}
|
||||
if let pendingBackImageData {
|
||||
bankCardBackURL = try await uploader.uploadBankCardImage(
|
||||
data: pendingBackImageData,
|
||||
fileName: pendingBackFileName ?? "bank_card_back.jpg",
|
||||
scenicId: scenicId
|
||||
) { _ in }
|
||||
self.pendingBackImageData = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
|
||||
enum WithdrawalValidationError: LocalizedError, Equatable {
|
||||
case message(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .message(let message): message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
var trimmed: String {
|
||||
trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user