添加景区选择与合作订单流程并对齐 Android
This commit is contained in:
@ -0,0 +1,248 @@
|
||||
//
|
||||
// CooperationOrderModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 合作订单功能常量与校验,对齐 Android `CooperationOrderFeature`。
|
||||
enum CooperationOrderFeature {
|
||||
static let uri = "cooperation_order"
|
||||
static let remarkNameMaxLength = 10
|
||||
|
||||
static func remarkNameValidationMessage(_ remark: String) -> String? {
|
||||
let trimmed = remark.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.count > remarkNameMaxLength {
|
||||
return "备注名不能超过\(remarkNameMaxLength)个字符"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func hasPermission(in permissions: [HomePermissionItem]) -> Bool {
|
||||
permissions.contains { $0.uri == uri }
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /photog/sale-user/referral-leads 获客线索。
|
||||
struct ReferralLeadEntity: Decodable, Equatable {
|
||||
let id: Int
|
||||
let leadId: Int
|
||||
let displayName: String
|
||||
let photogRemarkName: String
|
||||
let name: String
|
||||
let salerName: String
|
||||
let userPhone: String
|
||||
let userMobileMasked: String
|
||||
let phone: String
|
||||
let remark: String
|
||||
let createdAt: String
|
||||
let imageUrls: [String]
|
||||
let images: [String]
|
||||
|
||||
var displayId: String { String(leadId > 0 ? leadId : id) }
|
||||
var displayPhone: String {
|
||||
if !userMobileMasked.isEmpty { return userMobileMasked }
|
||||
if !userPhone.isEmpty { return userPhone }
|
||||
return phone
|
||||
}
|
||||
|
||||
var displayImages: [String] { imageUrls.isEmpty ? images : imageUrls }
|
||||
|
||||
var displaySalerName: String {
|
||||
if !displayName.isEmpty { return displayName }
|
||||
return OrderDisplayName.saleUser(remark: photogRemarkName, primary: name, fallback: salerName)
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case leadId = "lead_id"
|
||||
case displayName = "display_name"
|
||||
case photogRemarkName = "photog_remark_name"
|
||||
case name
|
||||
case salerName = "saler_name"
|
||||
case userPhone = "user_phone"
|
||||
case userMobileMasked = "user_mobile_masked"
|
||||
case phone
|
||||
case remark
|
||||
case createdAt = "created_at"
|
||||
case imageUrls = "image_urls"
|
||||
case images
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0
|
||||
leadId = try container.decodeIfPresent(Int.self, forKey: .leadId) ?? 0
|
||||
displayName = try container.decodeIfPresent(String.self, forKey: .displayName) ?? ""
|
||||
photogRemarkName = try container.decodeIfPresent(String.self, forKey: .photogRemarkName) ?? ""
|
||||
name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
|
||||
salerName = try container.decodeIfPresent(String.self, forKey: .salerName) ?? ""
|
||||
userPhone = try container.decodeIfPresent(String.self, forKey: .userPhone) ?? ""
|
||||
userMobileMasked = try container.decodeIfPresent(String.self, forKey: .userMobileMasked) ?? ""
|
||||
phone = try container.decodeIfPresent(String.self, forKey: .phone) ?? ""
|
||||
remark = try container.decodeIfPresent(String.self, forKey: .remark) ?? ""
|
||||
createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? ""
|
||||
imageUrls = try container.decodeIfPresent([String].self, forKey: .imageUrls) ?? []
|
||||
images = try container.decodeIfPresent([String].self, forKey: .images) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /photog/sale-user/acquisition-orders 获客订单。
|
||||
struct AcquisitionOrderEntity: Decodable, Equatable {
|
||||
let id: Int
|
||||
let orderNumber: String
|
||||
let projectName: String
|
||||
let orderType: Int
|
||||
let orderTypeLabel: String
|
||||
let orderAmount: String
|
||||
let amount: String
|
||||
let actualPayAmount: String
|
||||
let createdAt: String
|
||||
let customerPhone: String
|
||||
let userMobileMasked: String
|
||||
let phone: String
|
||||
let saleUserName: String
|
||||
let photogRemarkName: String
|
||||
let partnerName: String
|
||||
|
||||
var displayAmount: String {
|
||||
if !orderAmount.isEmpty { return orderAmount }
|
||||
if !amount.isEmpty { return amount }
|
||||
return actualPayAmount
|
||||
}
|
||||
|
||||
var displayCustomerPhone: String {
|
||||
if !userMobileMasked.isEmpty { return userMobileMasked }
|
||||
if !customerPhone.isEmpty { return customerPhone }
|
||||
return phone
|
||||
}
|
||||
|
||||
var displayPartner: String {
|
||||
let salerDisplay = OrderDisplayName.saleUser(remark: photogRemarkName, primary: saleUserName)
|
||||
if !salerDisplay.isEmpty { return salerDisplay }
|
||||
return partnerName
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case orderNumber = "order_number"
|
||||
case projectName = "project_name"
|
||||
case orderType = "order_type"
|
||||
case orderTypeLabel = "order_type_label"
|
||||
case orderAmount = "order_amount"
|
||||
case amount
|
||||
case actualPayAmount = "actual_pay_amount"
|
||||
case createdAt = "created_at"
|
||||
case customerPhone = "customer_phone"
|
||||
case userMobileMasked = "user_mobile_masked"
|
||||
case phone
|
||||
case saleUserName = "sale_user_name"
|
||||
case photogRemarkName = "photog_remark_name"
|
||||
case partnerName = "partner_name"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0
|
||||
orderNumber = try container.decodeIfPresent(String.self, forKey: .orderNumber) ?? ""
|
||||
projectName = try container.decodeIfPresent(String.self, forKey: .projectName) ?? ""
|
||||
orderType = try container.decodeIfPresent(Int.self, forKey: .orderType) ?? 0
|
||||
orderTypeLabel = try container.decodeIfPresent(String.self, forKey: .orderTypeLabel) ?? ""
|
||||
orderAmount = try container.decodeIfPresent(String.self, forKey: .orderAmount) ?? ""
|
||||
amount = try container.decodeIfPresent(String.self, forKey: .amount) ?? ""
|
||||
actualPayAmount = try container.decodeIfPresent(String.self, forKey: .actualPayAmount) ?? ""
|
||||
createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? ""
|
||||
customerPhone = try container.decodeIfPresent(String.self, forKey: .customerPhone) ?? ""
|
||||
userMobileMasked = try container.decodeIfPresent(String.self, forKey: .userMobileMasked) ?? ""
|
||||
phone = try container.decodeIfPresent(String.self, forKey: .phone) ?? ""
|
||||
saleUserName = try container.decodeIfPresent(String.self, forKey: .saleUserName) ?? ""
|
||||
photogRemarkName = try container.decodeIfPresent(String.self, forKey: .photogRemarkName) ?? ""
|
||||
partnerName = try container.decodeIfPresent(String.self, forKey: .partnerName) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /photog/sale-user/invite-info 扫码邀请信息。
|
||||
struct SaleUserInviteInfoResponse: Decodable, Equatable {
|
||||
let saleUserId: Int
|
||||
let isBound: Bool
|
||||
let photographerName: String
|
||||
let photographerPhone: String
|
||||
let salerName: String
|
||||
let salerPhone: String
|
||||
let shareCode: String
|
||||
let subtitle: String
|
||||
let title: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case saleUserId = "sale_user_id"
|
||||
case isBound = "is_bound"
|
||||
case photographerName = "photographer_name"
|
||||
case photographerPhone = "photographer_phone"
|
||||
case salerName = "saler_name"
|
||||
case salerPhone = "saler_phone"
|
||||
case shareCode = "share_code"
|
||||
case subtitle
|
||||
case title
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
saleUserId = try container.decodeIfPresent(Int.self, forKey: .saleUserId) ?? 0
|
||||
isBound = try container.decodeIfPresent(Bool.self, forKey: .isBound) ?? false
|
||||
photographerName = try container.decodeIfPresent(String.self, forKey: .photographerName) ?? ""
|
||||
photographerPhone = try container.decodeIfPresent(String.self, forKey: .photographerPhone) ?? ""
|
||||
salerName = try container.decodeIfPresent(String.self, forKey: .salerName) ?? ""
|
||||
salerPhone = try container.decodeIfPresent(String.self, forKey: .salerPhone) ?? ""
|
||||
shareCode = try container.decodeIfPresent(String.self, forKey: .shareCode) ?? ""
|
||||
subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle) ?? ""
|
||||
title = try container.decodeIfPresent(String.self, forKey: .title) ?? ""
|
||||
}
|
||||
|
||||
init(
|
||||
saleUserId: Int = 0,
|
||||
isBound: Bool = false,
|
||||
photographerName: String = "",
|
||||
photographerPhone: String = "",
|
||||
salerName: String = "",
|
||||
salerPhone: String = "",
|
||||
shareCode: String = "",
|
||||
subtitle: String = "",
|
||||
title: String = ""
|
||||
) {
|
||||
self.saleUserId = saleUserId
|
||||
self.isBound = isBound
|
||||
self.photographerName = photographerName
|
||||
self.photographerPhone = photographerPhone
|
||||
self.salerName = salerName
|
||||
self.salerPhone = salerPhone
|
||||
self.shareCode = shareCode
|
||||
self.subtitle = subtitle
|
||||
self.title = title
|
||||
}
|
||||
}
|
||||
|
||||
struct SaleUserUpdateRemarkRequest: Encodable {
|
||||
let saleUserId: Int
|
||||
let photogRemarkName: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case saleUserId = "sale_user_id"
|
||||
case photogRemarkName = "photog_remark_name"
|
||||
}
|
||||
}
|
||||
|
||||
struct SaleUserCommissionRateSmsRequest: Encodable {
|
||||
let phone: String
|
||||
}
|
||||
|
||||
struct SaleUserCommissionRateUpdateRequest: Encodable {
|
||||
let saleUserId: Int
|
||||
let commissionRate: Int
|
||||
let smsCode: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case saleUserId = "sale_user_id"
|
||||
case commissionRate = "commission_rate"
|
||||
case smsCode = "sms_code"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
//
|
||||
// BindAcquirerViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 接受邀请 / 绑定获客员 ViewModel,对齐 Android `BindAcquirerViewModel`。
|
||||
final class BindAcquirerViewModel {
|
||||
|
||||
private(set) var inviteInfo: SaleUserInviteInfoResponse?
|
||||
private(set) var isLoading = false
|
||||
private(set) var isAlreadyBound = false
|
||||
private(set) var commissionRateDraft = ""
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onBindSuccess: (() -> Void)?
|
||||
var onShouldPop: (() -> Void)?
|
||||
|
||||
private var saleUserId = 0
|
||||
|
||||
func initSaleUserId(_ id: Int) {
|
||||
guard saleUserId != id || (inviteInfo == nil && !isAlreadyBound) else { return }
|
||||
saleUserId = id
|
||||
isAlreadyBound = false
|
||||
commissionRateDraft = ""
|
||||
inviteInfo = nil
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateCommissionRateDraft(_ text: String) {
|
||||
commissionRateDraft = String(text.filter(\.isNumber).prefix(3))
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func loadInviteInfo(api: OrderAPI) async {
|
||||
guard saleUserId > 0 else {
|
||||
onShowMessage?("获客员信息无效")
|
||||
onShouldPop?()
|
||||
return
|
||||
}
|
||||
isLoading = true
|
||||
notifyStateChange()
|
||||
do {
|
||||
let data = try await api.saleUserInviteInfo(saleUserId: String(saleUserId))
|
||||
let resolvedSaleUserId = data.saleUserId > 0 ? data.saleUserId : saleUserId
|
||||
inviteInfo = SaleUserInviteInfoResponse(
|
||||
saleUserId: resolvedSaleUserId,
|
||||
isBound: data.isBound,
|
||||
photographerName: data.photographerName,
|
||||
photographerPhone: data.photographerPhone,
|
||||
salerName: data.salerName,
|
||||
salerPhone: data.salerPhone,
|
||||
shareCode: data.shareCode,
|
||||
subtitle: data.subtitle,
|
||||
title: data.title
|
||||
)
|
||||
if data.isBound {
|
||||
isAlreadyBound = true
|
||||
}
|
||||
} catch {
|
||||
handleBindError(error.localizedDescription.isEmpty ? "获取邀请信息失败" : error.localizedDescription)
|
||||
}
|
||||
isLoading = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func acceptBind(api: OrderAPI) async {
|
||||
guard saleUserId > 0, !isAlreadyBound else { return }
|
||||
guard let commissionRate = Int(commissionRateDraft.trimmingCharacters(in: .whitespacesAndNewlines)) else {
|
||||
onShowMessage?("请输入分账比例")
|
||||
return
|
||||
}
|
||||
guard (0...100).contains(commissionRate) else {
|
||||
onShowMessage?("分账比例需在0到100之间")
|
||||
return
|
||||
}
|
||||
do {
|
||||
try await api.saleUserAcceptBind(saleUserId: saleUserId, commissionRate: commissionRate)
|
||||
onShowMessage?("绑定成功")
|
||||
onBindSuccess?()
|
||||
} catch {
|
||||
handleBindError(error.localizedDescription.isEmpty ? "绑定失败" : error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleBindError(_ message: String) {
|
||||
onShowMessage?(message)
|
||||
if message.contains("已绑定") {
|
||||
isAlreadyBound = true
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
onShouldPop?()
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,213 @@
|
||||
//
|
||||
// CooperationAcquirerViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 合作获客员 ViewModel,对齐 Android `CooperationAcquirerViewModel`。
|
||||
final class CooperationAcquirerViewModel {
|
||||
|
||||
private(set) var acquirers: [CooperativeSalerEntity] = []
|
||||
private(set) var isRefreshing = false
|
||||
private(set) var remarkEditingAcquirer: CooperativeSalerEntity?
|
||||
private(set) var remarkDraft = ""
|
||||
private(set) var commissionEditingAcquirer: CooperativeSalerEntity?
|
||||
private(set) var commissionRateDraft = ""
|
||||
private(set) var commissionSmsCodeDraft = ""
|
||||
private(set) var commissionSmsCountdown = 0
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onNavigateBindAcquirer: ((Int) -> Void)?
|
||||
|
||||
private let appStore: AppStore
|
||||
private var commissionSmsTimerTask: Task<Void, Never>?
|
||||
|
||||
init(appStore: AppStore = .shared) {
|
||||
self.appStore = appStore
|
||||
}
|
||||
|
||||
deinit {
|
||||
commissionSmsTimerTask?.cancel()
|
||||
}
|
||||
|
||||
func loadAcquirers(api: OrderAPI, initial: Bool = false) async {
|
||||
if initial { isRefreshing = false }
|
||||
let storeId = appStore.currentStoreId
|
||||
let storeIdParam = storeId > 0 ? String(storeId) : nil
|
||||
do {
|
||||
let response = try await api.cooperativeSalers(storeId: storeIdParam, pageSize: "20")
|
||||
acquirers = response.list
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
isRefreshing = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func refresh(api: OrderAPI) async {
|
||||
isRefreshing = true
|
||||
notifyStateChange()
|
||||
await loadAcquirers(api: api, initial: false)
|
||||
}
|
||||
|
||||
func startEditRemark(_ acquirer: CooperativeSalerEntity) {
|
||||
remarkEditingAcquirer = acquirer
|
||||
remarkDraft = acquirer.photogRemarkName
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateRemarkDraft(_ text: String) {
|
||||
remarkDraft = String(text.prefix(CooperationOrderFeature.remarkNameMaxLength))
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func dismissRemarkDialog() {
|
||||
remarkEditingAcquirer = nil
|
||||
remarkDraft = ""
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func saveRemark(api: OrderAPI) async {
|
||||
guard let acquirer = remarkEditingAcquirer else { return }
|
||||
let saleUserId = acquirer.saleUserId
|
||||
guard saleUserId > 0 else { return }
|
||||
let trimmedRemark = remarkDraft.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let message = CooperationOrderFeature.remarkNameValidationMessage(remarkDraft) {
|
||||
onShowMessage?(message)
|
||||
return
|
||||
}
|
||||
dismissRemarkDialog()
|
||||
do {
|
||||
try await api.saleUserUpdateRemark(saleUserId: saleUserId, photogRemarkName: trimmedRemark)
|
||||
acquirers = acquirers.map { item in
|
||||
item.saleUserId == saleUserId ? item.withRemarkName(trimmedRemark) : item
|
||||
}
|
||||
onShowMessage?("备注已保存")
|
||||
notifyStateChange()
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func startEditCommissionRate(_ acquirer: CooperativeSalerEntity) {
|
||||
stopCommissionSmsTimer()
|
||||
commissionEditingAcquirer = acquirer
|
||||
commissionRateDraft = String(acquirer.commissionRate)
|
||||
commissionSmsCodeDraft = ""
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateCommissionRateDraft(_ text: String) {
|
||||
commissionRateDraft = String(text.filter(\.isNumber).prefix(3))
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateCommissionSmsCodeDraft(_ text: String) {
|
||||
commissionSmsCodeDraft = String(text.filter(\.isNumber).prefix(6))
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func dismissCommissionRateDialog() {
|
||||
commissionEditingAcquirer = nil
|
||||
commissionRateDraft = ""
|
||||
commissionSmsCodeDraft = ""
|
||||
stopCommissionSmsTimer()
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func requestCommissionSmsCode(api: OrderAPI) async {
|
||||
guard let acquirer = commissionEditingAcquirer else { return }
|
||||
if commissionSmsCountdown > 0 {
|
||||
onShowMessage?("请在倒计时结束后重新获取")
|
||||
return
|
||||
}
|
||||
let phone = acquirer.salerPhone.isEmpty ? acquirer.displayPhone : acquirer.salerPhone
|
||||
let trimmedPhone = phone.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmedPhone.isEmpty else {
|
||||
onShowMessage?("获客员手机号为空")
|
||||
return
|
||||
}
|
||||
do {
|
||||
try await api.saleUserCommissionRateSendSmsCode(phone: trimmedPhone)
|
||||
startCommissionSmsTimer()
|
||||
onShowMessage?("验证码已发送")
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func saveCommissionRate(api: OrderAPI) async {
|
||||
guard let acquirer = commissionEditingAcquirer else { return }
|
||||
let saleUserId = acquirer.saleUserId
|
||||
guard saleUserId > 0 else { return }
|
||||
guard let commissionRate = Int(commissionRateDraft.trimmingCharacters(in: .whitespacesAndNewlines)) else {
|
||||
onShowMessage?("请输入分成比例")
|
||||
return
|
||||
}
|
||||
guard (0...100).contains(commissionRate) else {
|
||||
onShowMessage?("分成比例需在0到100之间")
|
||||
return
|
||||
}
|
||||
let smsCode = commissionSmsCodeDraft.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !smsCode.isEmpty else {
|
||||
onShowMessage?("请输入验证码")
|
||||
return
|
||||
}
|
||||
do {
|
||||
let updated = try await api.saleUserUpdateCommissionRate(
|
||||
saleUserId: saleUserId,
|
||||
commissionRate: commissionRate,
|
||||
smsCode: smsCode
|
||||
)
|
||||
dismissCommissionRateDialog()
|
||||
if updated.saleUserId > 0 {
|
||||
acquirers = acquirers.map { item in
|
||||
item.saleUserId == updated.saleUserId ? updated : item
|
||||
}
|
||||
} else {
|
||||
await loadAcquirers(api: api, initial: false)
|
||||
}
|
||||
onShowMessage?("分成比例已修改")
|
||||
notifyStateChange()
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func processScanResult(_ scanResult: String) {
|
||||
guard let saleUserId = SaleUserQrParser.parseSaleUserId(scanResult), saleUserId > 0 else {
|
||||
onShowMessage?("请扫描正确的获客员二维码")
|
||||
return
|
||||
}
|
||||
onNavigateBindAcquirer?(saleUserId)
|
||||
}
|
||||
|
||||
private func startCommissionSmsTimer() {
|
||||
commissionSmsTimerTask?.cancel()
|
||||
commissionSmsTimerTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
for second in stride(from: 60, through: 1, by: -1) {
|
||||
if Task.isCancelled { return }
|
||||
commissionSmsCountdown = second
|
||||
notifyStateChange()
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
}
|
||||
if !Task.isCancelled {
|
||||
commissionSmsCountdown = 0
|
||||
notifyStateChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopCommissionSmsTimer() {
|
||||
commissionSmsTimerTask?.cancel()
|
||||
commissionSmsTimerTask = nil
|
||||
commissionSmsCountdown = 0
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
//
|
||||
// CooperationOrderListViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 合作订单列表 ViewModel,对齐 Android `CooperationOrderListViewModel`。
|
||||
final class CooperationOrderListViewModel {
|
||||
|
||||
private(set) var selectedTab = 0
|
||||
private(set) var searchText = ""
|
||||
private(set) var leads: [ReferralLeadEntity] = []
|
||||
private(set) var orders: [AcquisitionOrderEntity] = []
|
||||
private(set) var isRefreshing = false
|
||||
private(set) var isLoadingMore = false
|
||||
private(set) var canLoadMore = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
|
||||
private let pageSize = 10
|
||||
private var leadsPage = 1
|
||||
private var ordersPage = 1
|
||||
private var leadsTotal = 0
|
||||
private var ordersTotal = 0
|
||||
|
||||
func onTabSelected(_ index: Int) {
|
||||
guard selectedTab != index else { return }
|
||||
selectedTab = index
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func onSearchTextChange(_ text: String) {
|
||||
searchText = text
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func refreshCurrentTab(api: OrderAPI) async {
|
||||
if selectedTab == 0 {
|
||||
await refreshLeads(api: api)
|
||||
} else {
|
||||
await refreshAcquisitionOrders(api: api)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshLeads(api: OrderAPI) async {
|
||||
leadsPage = 1
|
||||
isRefreshing = true
|
||||
isLoadingMore = false
|
||||
notifyStateChange()
|
||||
await loadLeads(api: api)
|
||||
}
|
||||
|
||||
func refreshAcquisitionOrders(api: OrderAPI) async {
|
||||
ordersPage = 1
|
||||
isRefreshing = true
|
||||
isLoadingMore = false
|
||||
notifyStateChange()
|
||||
await loadAcquisitionOrders(api: api)
|
||||
}
|
||||
|
||||
func loadMore(api: OrderAPI) async {
|
||||
guard !isRefreshing, !isLoadingMore, canLoadMore else { return }
|
||||
isLoadingMore = true
|
||||
notifyStateChange()
|
||||
if selectedTab == 0 {
|
||||
leadsPage += 1
|
||||
await loadLeads(api: api)
|
||||
} else {
|
||||
ordersPage += 1
|
||||
await loadAcquisitionOrders(api: api)
|
||||
}
|
||||
}
|
||||
|
||||
func shouldLoadMore(at index: Int) -> Bool {
|
||||
guard canLoadMore, !isRefreshing, !isLoadingMore else { return false }
|
||||
let count = selectedTab == 0 ? leads.count : orders.count
|
||||
return index >= max(0, count - 3)
|
||||
}
|
||||
|
||||
private func loadLeads(api: OrderAPI) async {
|
||||
do {
|
||||
let response = try await api.referralLeads(
|
||||
page: String(leadsPage),
|
||||
pageSize: String(pageSize)
|
||||
)
|
||||
leadsTotal = response.total
|
||||
if leadsPage == 1 {
|
||||
leads = response.list
|
||||
} else {
|
||||
leads.append(contentsOf: response.list)
|
||||
}
|
||||
canLoadMore = leads.count < leadsTotal
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
isRefreshing = false
|
||||
isLoadingMore = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
private func loadAcquisitionOrders(api: OrderAPI) async {
|
||||
let keyword = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
do {
|
||||
let response = try await api.acquisitionOrders(
|
||||
keyword: keyword.isEmpty ? nil : keyword,
|
||||
page: String(ordersPage),
|
||||
pageSize: String(pageSize)
|
||||
)
|
||||
ordersTotal = response.total
|
||||
if ordersPage == 1 {
|
||||
orders = response.list
|
||||
} else {
|
||||
orders.append(contentsOf: response.list)
|
||||
}
|
||||
canLoadMore = orders.count < ordersTotal
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
isRefreshing = false
|
||||
isLoadingMore = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user