Add scenic selection and cooperation order flows aligned with Android.
Upgrade scenic picker with search, location sorting, permission apply/status, and blocking home dialog; add cooperation order module and order source picker improvements with unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
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?()
|
||||
}
|
||||
}
|
||||
@ -63,4 +63,29 @@ final class HomeAPI {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 拉取待审核权限申请列表。
|
||||
func roleApplyPending() async throws -> [RoleApplyPendingResponse] {
|
||||
try await client.send(
|
||||
APIRequest(method: .get, path: "/api/yf-handset-app/photog/role-apply/all")
|
||||
)
|
||||
}
|
||||
|
||||
/// 拉取全部可选景区。
|
||||
func scenicListAll() async throws -> ScenicListAllResponse {
|
||||
try await client.send(
|
||||
APIRequest(method: .get, path: "/api/yf-handset-app/photog/scenic/list-all")
|
||||
)
|
||||
}
|
||||
|
||||
/// 提交角色权限申请。
|
||||
func submitRoleApply(scenicIds: [Int], roleId: Int) async throws -> RoleApplySubmitResult {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/role-apply/submit",
|
||||
body: RoleApplySubmitRequest(scenicId: scenicIds, roleId: roleId)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
107
suixinkan/Features/Home/Models/PermissionApplyModels.swift
Normal file
107
suixinkan/Features/Home/Models/PermissionApplyModels.swift
Normal file
@ -0,0 +1,107 @@
|
||||
//
|
||||
// PermissionApplyModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct RoleApplyPendingResponse: Decodable, Equatable {
|
||||
let code: String
|
||||
let roleId: Int
|
||||
let roleName: String
|
||||
let scenicList: [RoleApplyScenicItem]
|
||||
let status: Int
|
||||
let statusLabel: String
|
||||
let createdAt: String
|
||||
let auditNote: String
|
||||
let auditedAt: String?
|
||||
let auditedBy: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case code
|
||||
case roleId = "role_id"
|
||||
case roleName = "role_name"
|
||||
case scenicList = "scenic_list"
|
||||
case status
|
||||
case statusLabel = "status_label"
|
||||
case createdAt = "created_at"
|
||||
case auditNote = "audit_note"
|
||||
case auditedAt = "audited_at"
|
||||
case auditedBy = "audited_by"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
code = try container.decodeIfPresent(String.self, forKey: .code) ?? ""
|
||||
roleId = try container.decodeIfPresent(Int.self, forKey: .roleId) ?? 0
|
||||
roleName = try container.decodeIfPresent(String.self, forKey: .roleName) ?? ""
|
||||
scenicList = try container.decodeIfPresent([RoleApplyScenicItem].self, forKey: .scenicList) ?? []
|
||||
status = try container.decodeIfPresent(Int.self, forKey: .status) ?? 0
|
||||
statusLabel = try container.decodeIfPresent(String.self, forKey: .statusLabel) ?? ""
|
||||
createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? ""
|
||||
auditNote = try container.decodeIfPresent(String.self, forKey: .auditNote) ?? ""
|
||||
auditedAt = try container.decodeIfPresent(String.self, forKey: .auditedAt)
|
||||
auditedBy = try container.decodeIfPresent(String.self, forKey: .auditedBy)
|
||||
}
|
||||
}
|
||||
|
||||
struct RoleApplyScenicItem: Decodable, Equatable {
|
||||
let id: Int
|
||||
let name: String
|
||||
}
|
||||
|
||||
struct ScenicListAllResponse: Decodable, Equatable {
|
||||
let total: Int
|
||||
let list: [ScenicListItem]
|
||||
|
||||
init(total: Int = 0, list: [ScenicListItem] = []) {
|
||||
self.total = total
|
||||
self.list = list
|
||||
}
|
||||
}
|
||||
|
||||
struct ScenicListItem: Decodable, Equatable {
|
||||
let id: Int
|
||||
let name: String
|
||||
}
|
||||
|
||||
struct RoleApplySubmitRequest: Encodable {
|
||||
let scenicId: [Int]
|
||||
let roleId: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case scenicId = "scenic_id"
|
||||
case roleId = "role_id"
|
||||
}
|
||||
}
|
||||
|
||||
struct RoleOption: Equatable, Hashable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let description: String
|
||||
var isSelected: Bool
|
||||
}
|
||||
|
||||
struct ScenicOption: Equatable, Hashable {
|
||||
let id: Int
|
||||
let name: String
|
||||
var isSelected: Bool
|
||||
var isDisabled: Bool
|
||||
}
|
||||
|
||||
struct RoleApplySubmitResult: Decodable, Equatable {
|
||||
let code: String
|
||||
|
||||
init(code: String = "") {
|
||||
self.code = code
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
code = try container.decodeIfPresent(String.self, forKey: .code) ?? ""
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case code
|
||||
}
|
||||
}
|
||||
149
suixinkan/Features/Home/Models/ScenicSelectionModels.swift
Normal file
149
suixinkan/Features/Home/Models/ScenicSelectionModels.swift
Normal file
@ -0,0 +1,149 @@
|
||||
//
|
||||
// ScenicSelectionModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 景区列表展示标签,对齐 Android `openStatus`。
|
||||
enum ScenicSelectionOpenStatus: Equatable {
|
||||
case nearest
|
||||
case closed
|
||||
case none
|
||||
}
|
||||
|
||||
/// 景区选择列表项,对齐 Android `Sceniclistv2Entity` 展示字段。
|
||||
struct ScenicSpotDisplayItem: Equatable, Hashable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let coverImg: String
|
||||
let address: String
|
||||
let distanceText: String
|
||||
let openStatus: ScenicSelectionOpenStatus
|
||||
let isSelected: Bool
|
||||
|
||||
var statusLabel: String? {
|
||||
switch openStatus {
|
||||
case .nearest: "距离最近"
|
||||
case .closed: "暂未营业"
|
||||
case .none: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 景区距离计算,对齐 Android Haversine 逻辑。
|
||||
enum ScenicDistanceCalculator {
|
||||
private static let earthRadiusMeters = 6_371_000.0
|
||||
|
||||
static func distanceKilometers(
|
||||
fromLat: Double,
|
||||
fromLng: Double,
|
||||
toLat: Double,
|
||||
toLng: Double
|
||||
) -> Double {
|
||||
let dLat = (toLat - fromLat) * .pi / 180
|
||||
let dLng = (toLng - fromLng) * .pi / 180
|
||||
let lat1 = fromLat * .pi / 180
|
||||
let lat2 = toLat * .pi / 180
|
||||
let a = sin(dLat / 2) * sin(dLat / 2)
|
||||
+ cos(lat1) * cos(lat2) * sin(dLng / 2) * sin(dLng / 2)
|
||||
let c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
return (earthRadiusMeters * c) / 1000
|
||||
}
|
||||
|
||||
static func formatDistanceKilometers(_ kilometers: Double) -> String {
|
||||
if kilometers <= 0 || !kilometers.isFinite {
|
||||
return "0"
|
||||
}
|
||||
return String(format: "%.2f", kilometers)
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 role-permission 景区映射为选择页展示项。
|
||||
enum ScenicSpotDisplayMapper {
|
||||
static func buildItems(
|
||||
scenicList: [ScenicInfo],
|
||||
searchQuery: String,
|
||||
currentLat: Double?,
|
||||
currentLng: Double?,
|
||||
selectedScenicId: Int
|
||||
) -> [ScenicSpotDisplayItem] {
|
||||
let hasLocation = (currentLat ?? 0) > 0 && (currentLng ?? 0) > 0
|
||||
let searchLower = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
|
||||
struct ScenicWithDistance {
|
||||
let item: ScenicSpotDisplayItem
|
||||
let distance: Double
|
||||
}
|
||||
|
||||
let mapped = scenicList.map { scenic -> ScenicWithDistance in
|
||||
let loc = scenic.location
|
||||
let distance: Double
|
||||
if let loc, hasLocation, let currentLat, let currentLng {
|
||||
distance = ScenicDistanceCalculator.distanceKilometers(
|
||||
fromLat: currentLat,
|
||||
fromLng: currentLng,
|
||||
toLat: loc.lat,
|
||||
toLng: loc.lng
|
||||
)
|
||||
} else if loc == nil, hasLocation {
|
||||
distance = .greatestFiniteMagnitude
|
||||
} else {
|
||||
distance = 0
|
||||
}
|
||||
|
||||
let address = loc?.address.isEmpty == false ? (loc?.address ?? "--") : "--"
|
||||
let distanceText: String
|
||||
if loc == nil {
|
||||
distanceText = "--"
|
||||
} else if distance > 0, distance < .greatestFiniteMagnitude {
|
||||
distanceText = ScenicDistanceCalculator.formatDistanceKilometers(distance)
|
||||
} else {
|
||||
distanceText = "0"
|
||||
}
|
||||
|
||||
let openStatus: ScenicSelectionOpenStatus = scenic.status == 2 ? .closed : .none
|
||||
let item = ScenicSpotDisplayItem(
|
||||
id: scenic.id,
|
||||
name: scenic.name,
|
||||
coverImg: scenic.coverImg ?? "",
|
||||
address: address,
|
||||
distanceText: distanceText,
|
||||
openStatus: openStatus,
|
||||
isSelected: selectedScenicId > 0 && scenic.id == selectedScenicId
|
||||
)
|
||||
return ScenicWithDistance(item: item, distance: distance)
|
||||
}
|
||||
|
||||
let filtered = mapped.filter { entry in
|
||||
guard !searchLower.isEmpty else { return true }
|
||||
return entry.item.name.lowercased().contains(searchLower)
|
||||
|| entry.item.address.lowercased().contains(searchLower)
|
||||
}
|
||||
|
||||
let sorted: [ScenicWithDistance]
|
||||
if hasLocation {
|
||||
sorted = filtered.sorted { $0.distance < $1.distance }
|
||||
} else {
|
||||
sorted = filtered
|
||||
}
|
||||
|
||||
let baseItems = sorted.map(\.item)
|
||||
guard hasLocation, !baseItems.isEmpty else { return baseItems }
|
||||
|
||||
return baseItems.enumerated().map { index, item in
|
||||
if index == 0, item.openStatus != .closed {
|
||||
return ScenicSpotDisplayItem(
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
coverImg: item.coverImg,
|
||||
address: item.address,
|
||||
distanceText: item.distanceText,
|
||||
openStatus: .nearest,
|
||||
isSelected: item.isSelected
|
||||
)
|
||||
}
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -47,7 +47,7 @@ enum HomeRouteHandler {
|
||||
showToast("暂无合作订单权限", from: viewController)
|
||||
return
|
||||
}
|
||||
showDeveloping(from: viewController)
|
||||
viewController.navigationController?.pushViewController(CooperationOrderListViewController(), animated: true)
|
||||
case "task_management", "task_management_editor":
|
||||
viewController.navigationController?.pushViewController(TaskAddViewController(), animated: true)
|
||||
default:
|
||||
|
||||
@ -212,9 +212,17 @@ final class HomeViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新景区名与角色展示状态。
|
||||
/// 刷新景区名与角色展示状态,对齐 Android `initCurrentScenicName`。
|
||||
func refreshLocalDisplayState() {
|
||||
currentScenicName = appStore.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let scenicId = appStore.currentScenicId
|
||||
let scenicName = appStore.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if scenicId > 0, !scenicName.isEmpty {
|
||||
currentScenicName = scenicName
|
||||
} else {
|
||||
currentScenicName = ""
|
||||
appStore.currentScenicId = 0
|
||||
appStore.currentScenicName = ""
|
||||
}
|
||||
currentAppRole = appStore.currentAppRole
|
||||
isMinimalTopRole = currentAppRole?.usesHomeMinimalTopLayout ?? false
|
||||
}
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
//
|
||||
// PermissionApplyStatusViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 权限申请审核状态 ViewModel,对齐 Android `PermissionApplyStatusViewModel`。
|
||||
final class PermissionApplyStatusViewModel {
|
||||
|
||||
private(set) var pendingData: RoleApplyPendingResponse?
|
||||
private(set) var isLoading = false
|
||||
private(set) var rolePermissionList: [RolePermissionResponse] = []
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onNavigateToEdit: ((RoleApplyPendingResponse) -> Void)?
|
||||
|
||||
private let applyCode: String
|
||||
private let appStore: AppStore
|
||||
|
||||
init(applyCode: String, appStore: AppStore = .shared) {
|
||||
self.applyCode = applyCode
|
||||
self.appStore = appStore
|
||||
rolePermissionList = appStore.rolePermissionList()
|
||||
}
|
||||
|
||||
func loadPendingData(api: HomeAPI) async {
|
||||
isLoading = true
|
||||
notifyStateChange()
|
||||
do {
|
||||
let pendingList = try await api.roleApplyPending()
|
||||
pendingData = pendingList.first { $0.code == applyCode }
|
||||
} catch {
|
||||
pendingData = nil
|
||||
}
|
||||
isLoading = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func navigateToEditIfRejected() {
|
||||
guard let pendingData, pendingData.status == PermissionApplyPendingStatus.rejected else { return }
|
||||
onNavigateToEdit?(pendingData)
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,199 @@
|
||||
//
|
||||
// PermissionApplyViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 权限申请 ViewModel,对齐 Android `PermissionApplyViewModel`。
|
||||
final class PermissionApplyViewModel {
|
||||
|
||||
private(set) var availableRoles: [RoleOption] = []
|
||||
private(set) var selectedRoleId: Int?
|
||||
private(set) var selectedRoleName = ""
|
||||
private(set) var availableScenics: [ScenicOption] = []
|
||||
private(set) var selectedScenicIds: [Int] = []
|
||||
private(set) var selectedScenicNames: [String] = []
|
||||
private(set) var isLoadingScenics = false
|
||||
private(set) var isSubmitting = false
|
||||
private(set) var showRolePicker = false
|
||||
private(set) var showScenicPicker = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onSubmitSuccess: ((String) -> Void)?
|
||||
|
||||
private let appStore: AppStore
|
||||
|
||||
init(appStore: AppStore = .shared) {
|
||||
self.appStore = appStore
|
||||
loadRolesFromLocal()
|
||||
}
|
||||
|
||||
/// 从待审核数据预填表单(status=3 去编辑)。
|
||||
func initWithPendingData(_ pending: RoleApplyPendingResponse) {
|
||||
selectedRoleId = pending.roleId
|
||||
selectedRoleName = pending.roleName
|
||||
selectedScenicIds = pending.scenicList.map(\.id)
|
||||
selectedScenicNames = pending.scenicList.map(\.name)
|
||||
availableRoles = availableRoles.map { role in
|
||||
var copy = role
|
||||
copy.isSelected = role.id == pending.roleId
|
||||
return copy
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func loadRolesFromLocal() {
|
||||
let rolePermissionList = appStore.rolePermissionList()
|
||||
var seen = Set<Int>()
|
||||
availableRoles = rolePermissionList.compactMap { item in
|
||||
guard !seen.contains(item.role.id) else { return nil }
|
||||
seen.insert(item.role.id)
|
||||
return RoleOption(
|
||||
id: item.role.id,
|
||||
name: item.role.name,
|
||||
description: item.role.notes ?? "",
|
||||
isSelected: false
|
||||
)
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func showRolePickerSheet() {
|
||||
showRolePicker = true
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func hideRolePickerSheet() {
|
||||
showRolePicker = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func selectRole(_ role: RoleOption) {
|
||||
selectedRoleId = role.id
|
||||
selectedRoleName = role.name
|
||||
availableRoles = availableRoles.map { item in
|
||||
var copy = item
|
||||
copy.isSelected = item.id == role.id
|
||||
return copy
|
||||
}
|
||||
selectedScenicIds = []
|
||||
selectedScenicNames = []
|
||||
availableScenics = []
|
||||
showRolePicker = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func showScenicPickerSheet() {
|
||||
guard selectedRoleId != nil else {
|
||||
onShowMessage?("请先选择角色")
|
||||
return
|
||||
}
|
||||
showScenicPicker = true
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func hideScenicPickerSheet() {
|
||||
showScenicPicker = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func loadAvailableScenics(api: HomeAPI?) async {
|
||||
guard let roleId = selectedRoleId else { return }
|
||||
guard let api else { return }
|
||||
isLoadingScenics = true
|
||||
notifyStateChange()
|
||||
do {
|
||||
let response = try await api.scenicListAll()
|
||||
let existingIds = existingScenicIds(for: roleId)
|
||||
availableScenics = response.list.map { scenic in
|
||||
let isExisting = existingIds.contains(scenic.id)
|
||||
let isPreselected = selectedScenicIds.contains(scenic.id)
|
||||
return ScenicOption(
|
||||
id: scenic.id,
|
||||
name: scenic.name,
|
||||
isSelected: isExisting || isPreselected,
|
||||
isDisabled: isExisting
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
onShowMessage?("获取景区列表失败")
|
||||
}
|
||||
isLoadingScenics = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func toggleScenic(_ scenic: ScenicOption) {
|
||||
guard !scenic.isDisabled else { return }
|
||||
if selectedScenicIds.contains(scenic.id) {
|
||||
selectedScenicIds.removeAll { $0 == scenic.id }
|
||||
selectedScenicNames.removeAll { $0 == scenic.name }
|
||||
} else {
|
||||
selectedScenicIds.append(scenic.id)
|
||||
selectedScenicNames.append(scenic.name)
|
||||
}
|
||||
availableScenics = availableScenics.map { item in
|
||||
var copy = item
|
||||
if !copy.isDisabled {
|
||||
copy.isSelected = selectedScenicIds.contains(copy.id)
|
||||
}
|
||||
return copy
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func removeSelectedScenic(id: Int, name: String) {
|
||||
selectedScenicIds.removeAll { $0 == id }
|
||||
selectedScenicNames.removeAll { $0 == name }
|
||||
availableScenics = availableScenics.map { item in
|
||||
var copy = item
|
||||
if !copy.isDisabled {
|
||||
copy.isSelected = selectedScenicIds.contains(copy.id)
|
||||
}
|
||||
return copy
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 已有角色下已开通的景区列表。
|
||||
func existingScenics(for roleId: Int) -> [ScenicInfo] {
|
||||
appStore.rolePermissionList()
|
||||
.filter { $0.role.id == roleId }
|
||||
.flatMap(\.scenic)
|
||||
}
|
||||
|
||||
func submit(api: HomeAPI) async {
|
||||
guard let roleId = selectedRoleId else {
|
||||
onShowMessage?("请先选择角色")
|
||||
return
|
||||
}
|
||||
let newScenicIds = selectedScenicIds.filter { id in
|
||||
!existingScenicIds(for: roleId).contains(id)
|
||||
}
|
||||
guard !newScenicIds.isEmpty else {
|
||||
onShowMessage?("请至少选择一个景区")
|
||||
return
|
||||
}
|
||||
|
||||
isSubmitting = true
|
||||
notifyStateChange()
|
||||
do {
|
||||
let result = try await api.submitRoleApply(scenicIds: newScenicIds, roleId: roleId)
|
||||
onShowMessage?("提交成功")
|
||||
onSubmitSuccess?(result.code)
|
||||
} catch {
|
||||
onShowMessage?("提交失败,请稍后重试")
|
||||
}
|
||||
isSubmitting = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
private func existingScenicIds(for roleId: Int) -> Set<Int> {
|
||||
Set(existingScenics(for: roleId).map(\.id))
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
//
|
||||
// ScenicSelectionViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 权限申请审核中状态,对齐 Android role-apply pending status。
|
||||
enum PermissionApplyPendingStatus {
|
||||
static let reviewing = 1
|
||||
static let rejected = 3
|
||||
}
|
||||
|
||||
/// 景区选择 ViewModel,对齐 Android `ScenicSelectionViewModel`。
|
||||
final class ScenicSelectionViewModel {
|
||||
|
||||
private(set) var spots: [ScenicSpotDisplayItem] = []
|
||||
private(set) var searchQuery = ""
|
||||
private(set) var currentLocationText = "最近的景区"
|
||||
private(set) var isLoading = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onSelectionComplete: (() -> Void)?
|
||||
var onNavigateApply: (() -> Void)?
|
||||
var onNavigateApplyStatus: ((String) -> Void)?
|
||||
|
||||
private let appStore: AppStore
|
||||
private let locationProvider: HomeLocationProvider
|
||||
private var currentLat: Double?
|
||||
private var currentLng: Double?
|
||||
|
||||
init(appStore: AppStore = .shared, locationProvider: HomeLocationProvider = HomeLocationProvider()) {
|
||||
self.appStore = appStore
|
||||
self.locationProvider = locationProvider
|
||||
}
|
||||
|
||||
func loadFromLocal() {
|
||||
let scenicList = resolvedScenicList()
|
||||
spots = ScenicSpotDisplayMapper.buildItems(
|
||||
scenicList: scenicList,
|
||||
searchQuery: searchQuery,
|
||||
currentLat: currentLat,
|
||||
currentLng: currentLng,
|
||||
selectedScenicId: appStore.currentScenicId
|
||||
)
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func onSearchQueryChange(_ query: String) {
|
||||
searchQuery = query
|
||||
loadFromLocal()
|
||||
}
|
||||
|
||||
func startLocation() async {
|
||||
isLoading = true
|
||||
notifyStateChange()
|
||||
do {
|
||||
let snapshot = try await locationProvider.requestSnapshot()
|
||||
currentLat = snapshot.latitude
|
||||
currentLng = snapshot.longitude
|
||||
let address = snapshot.address.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
currentLocationText = address.isEmpty ? "最近的景区" : address
|
||||
loadFromLocal()
|
||||
} catch HomeLocationProviderError.permissionDenied {
|
||||
onShowMessage?("未授予定位权限,无法获取附近景区")
|
||||
} catch {
|
||||
onShowMessage?("定位失败: \(error.localizedDescription)")
|
||||
}
|
||||
isLoading = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func selectSpot(_ spot: ScenicSpotDisplayItem) {
|
||||
appStore.currentScenicId = spot.id
|
||||
appStore.currentScenicName = spot.name
|
||||
NotificationCenter.default.post(
|
||||
name: NotificationName.scenicDidChange,
|
||||
object: nil,
|
||||
userInfo: [
|
||||
NotificationUserInfoKey.scenicId: spot.id,
|
||||
NotificationUserInfoKey.scenicName: spot.name,
|
||||
]
|
||||
)
|
||||
loadFromLocal()
|
||||
onSelectionComplete?()
|
||||
}
|
||||
|
||||
func onApplyNow(api: HomeAPI) async {
|
||||
isLoading = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isLoading = false
|
||||
notifyStateChange()
|
||||
}
|
||||
do {
|
||||
let pendingList = try await api.roleApplyPending()
|
||||
if let first = pendingList.first,
|
||||
first.status == PermissionApplyPendingStatus.reviewing
|
||||
|| first.status == PermissionApplyPendingStatus.rejected {
|
||||
onNavigateApplyStatus?(first.code)
|
||||
} else {
|
||||
onNavigateApply?()
|
||||
}
|
||||
} catch {
|
||||
onNavigateApply?()
|
||||
}
|
||||
}
|
||||
|
||||
private func resolvedScenicList() -> [ScenicInfo] {
|
||||
let cached = appStore.roleScenicList()
|
||||
if !cached.isEmpty { return cached }
|
||||
let rolePermissionList = appStore.rolePermissionList()
|
||||
guard !rolePermissionList.isEmpty else { return [] }
|
||||
return appStore.matchRolePermissionItem(in: rolePermissionList)?.scenic ?? []
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
@ -206,6 +206,93 @@ final class OrderAPI {
|
||||
)
|
||||
}
|
||||
|
||||
func referralLeads(page: String = "1", pageSize: String = "10") async throws -> OrderListResponse<ReferralLeadEntity> {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/sale-user/referral-leads",
|
||||
queryItems: [
|
||||
URLQueryItem(name: "page", value: page),
|
||||
URLQueryItem(name: "page_size", value: pageSize),
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func acquisitionOrders(keyword: String? = nil, page: String = "1", pageSize: String = "10") async throws -> OrderListResponse<AcquisitionOrderEntity> {
|
||||
var items = [
|
||||
URLQueryItem(name: "page", value: page),
|
||||
URLQueryItem(name: "page_size", value: pageSize),
|
||||
]
|
||||
if let keyword, !keyword.isEmpty {
|
||||
items.append(URLQueryItem(name: "keyword", value: keyword))
|
||||
}
|
||||
return try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/sale-user/acquisition-orders",
|
||||
queryItems: items
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func saleUserInviteInfo(saleUserId: String) async throws -> SaleUserInviteInfoResponse {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/sale-user/invite-info",
|
||||
queryItems: [URLQueryItem(name: "sale_user_id", value: saleUserId)]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func saleUserAcceptBind(saleUserId: Int, commissionRate: Int) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/sale-user/accept-bind",
|
||||
queryItems: [
|
||||
URLQueryItem(name: "sale_user_id", value: String(saleUserId)),
|
||||
URLQueryItem(name: "commission_rate", value: String(commissionRate)),
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func saleUserUpdateRemark(saleUserId: Int, photogRemarkName: String) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/sale-user/update-remark",
|
||||
body: SaleUserUpdateRemarkRequest(saleUserId: saleUserId, photogRemarkName: photogRemarkName)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func saleUserCommissionRateSendSmsCode(phone: String) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/sale-user/commission-rate/send-sms-code",
|
||||
body: SaleUserCommissionRateSmsRequest(phone: phone)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func saleUserUpdateCommissionRate(saleUserId: Int, commissionRate: Int, smsCode: String) async throws -> CooperativeSalerEntity {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/sale-user/commission-rate",
|
||||
body: SaleUserCommissionRateUpdateRequest(
|
||||
saleUserId: saleUserId,
|
||||
commissionRate: commissionRate,
|
||||
smsCode: smsCode
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func shootHistory(orderNumber: String) async throws -> MultiTravelShootHistoryResponse {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
|
||||
@ -789,10 +789,19 @@ struct CooperativeSalerEntity: Decodable, Equatable {
|
||||
let photogRemarkName: String
|
||||
let phone: String
|
||||
let salerPhone: String
|
||||
let commissionRate: Int
|
||||
let commissionRateLabel: String
|
||||
let bindTime: String
|
||||
let boundAt: String
|
||||
|
||||
var displayId: Int { saleUserId }
|
||||
var displayName: String { OrderDisplayName.saleUser(remark: photogRemarkName, primary: name, fallback: salerName) }
|
||||
var displayPhone: String { salerPhone.isEmpty ? phone : salerPhone }
|
||||
var displayBindTime: String { bindTime.isEmpty ? boundAt : bindTime }
|
||||
var displayCommissionRate: String {
|
||||
if !commissionRateLabel.isEmpty { return commissionRateLabel }
|
||||
return "\(commissionRate)%"
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case saleUserId = "sale_user_id"
|
||||
@ -802,6 +811,67 @@ struct CooperativeSalerEntity: Decodable, Equatable {
|
||||
case photogRemarkName = "photog_remark_name"
|
||||
case phone
|
||||
case salerPhone = "saler_phone"
|
||||
case commissionRate = "commission_rate"
|
||||
case commissionRateLabel = "commission_rate_label"
|
||||
case bindTime = "bind_time"
|
||||
case boundAt = "bound_at"
|
||||
}
|
||||
|
||||
init(
|
||||
saleUserId: Int = 0,
|
||||
bindingId: Int = 0,
|
||||
name: String = "",
|
||||
salerName: String = "",
|
||||
photogRemarkName: String = "",
|
||||
phone: String = "",
|
||||
salerPhone: String = "",
|
||||
commissionRate: Int = 0,
|
||||
commissionRateLabel: String = "",
|
||||
bindTime: String = "",
|
||||
boundAt: String = ""
|
||||
) {
|
||||
self.saleUserId = saleUserId
|
||||
self.bindingId = bindingId
|
||||
self.name = name
|
||||
self.salerName = salerName
|
||||
self.photogRemarkName = photogRemarkName
|
||||
self.phone = phone
|
||||
self.salerPhone = salerPhone
|
||||
self.commissionRate = commissionRate
|
||||
self.commissionRateLabel = commissionRateLabel
|
||||
self.bindTime = bindTime
|
||||
self.boundAt = boundAt
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
saleUserId = try container.decodeIfPresent(Int.self, forKey: .saleUserId) ?? 0
|
||||
bindingId = try container.decodeIfPresent(Int.self, forKey: .bindingId) ?? 0
|
||||
name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
|
||||
salerName = try container.decodeIfPresent(String.self, forKey: .salerName) ?? ""
|
||||
photogRemarkName = try container.decodeIfPresent(String.self, forKey: .photogRemarkName) ?? ""
|
||||
phone = try container.decodeIfPresent(String.self, forKey: .phone) ?? ""
|
||||
salerPhone = try container.decodeIfPresent(String.self, forKey: .salerPhone) ?? ""
|
||||
commissionRate = try container.decodeIfPresent(Int.self, forKey: .commissionRate) ?? 0
|
||||
commissionRateLabel = try container.decodeIfPresent(String.self, forKey: .commissionRateLabel) ?? ""
|
||||
bindTime = try container.decodeIfPresent(String.self, forKey: .bindTime) ?? ""
|
||||
boundAt = try container.decodeIfPresent(String.self, forKey: .boundAt) ?? ""
|
||||
}
|
||||
|
||||
func withRemarkName(_ remark: String) -> CooperativeSalerEntity {
|
||||
CooperativeSalerEntity(
|
||||
saleUserId: saleUserId,
|
||||
bindingId: bindingId,
|
||||
name: name,
|
||||
salerName: salerName,
|
||||
photogRemarkName: remark,
|
||||
phone: phone,
|
||||
salerPhone: salerPhone,
|
||||
commissionRate: commissionRate,
|
||||
commissionRateLabel: commissionRateLabel,
|
||||
bindTime: bindTime,
|
||||
boundAt: boundAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -819,6 +889,7 @@ struct ReferralOrderEntity: Decodable, Equatable {
|
||||
let createdAt: String
|
||||
let status: Int
|
||||
let statusLabel: String
|
||||
let images: [String]
|
||||
|
||||
var displayReferralOrderNo: String { referralOrderNo.isEmpty ? orderNo : referralOrderNo }
|
||||
|
||||
@ -831,6 +902,20 @@ struct ReferralOrderEntity: Decodable, Equatable {
|
||||
case createdAt = "created_at"
|
||||
case status
|
||||
case statusLabel = "status_label"
|
||||
case images
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0
|
||||
referralOrderNo = try container.decodeIfPresent(String.self, forKey: .referralOrderNo) ?? ""
|
||||
orderNo = try container.decodeIfPresent(String.self, forKey: .orderNo) ?? ""
|
||||
userMobile = try container.decodeIfPresent(String.self, forKey: .userMobile) ?? ""
|
||||
remark = try container.decodeIfPresent(String.self, forKey: .remark) ?? ""
|
||||
createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? ""
|
||||
status = try container.decodeIfPresent(Int.self, forKey: .status) ?? 0
|
||||
statusLabel = try container.decodeIfPresent(String.self, forKey: .statusLabel) ?? ""
|
||||
images = (try? container.decode([String].self, forKey: .images)) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ final class MainTabScanHandler: NSObject, CLLocationManagerDelegate {
|
||||
var onShowWriteOff: ((WriteOffOrderResponse) -> Void)?
|
||||
var onMultiVerifyPending: ((MultiVerifyParseData) -> Void)?
|
||||
var onShowMultiVerifySheet: (([MultiTravelScenicSpotEntity], MultiTravelScenicSpotEntity?) -> Void)?
|
||||
var onNavigateBindAcquirer: ((Int) -> Void)?
|
||||
|
||||
private let api: OrderAPI
|
||||
private let locationManager = CLLocationManager()
|
||||
@ -64,7 +65,7 @@ final class MainTabScanHandler: NSObject, CLLocationManagerDelegate {
|
||||
await fetchSpotsForMultiVerify(pending: pending)
|
||||
case SaleUserQrParser.methodSaleUserBind:
|
||||
if let id = SaleUserQrParser.parseSaleUserId(scanResult), id > 0 {
|
||||
onShowMessage?("获客员绑定功能待接入")
|
||||
onNavigateBindAcquirer?(id)
|
||||
} else {
|
||||
onShowMessage?("请扫描正确的获客员二维码")
|
||||
}
|
||||
|
||||
@ -20,9 +20,59 @@ struct OrderSourcePerson: Equatable {
|
||||
|
||||
struct OrderSourceLead: Equatable {
|
||||
let key: String
|
||||
let displayText: String
|
||||
let userMobile: String
|
||||
let createdAt: String
|
||||
let userPhone: String
|
||||
let remark: String
|
||||
let images: [String]
|
||||
}
|
||||
|
||||
enum OrderSourceMapping {
|
||||
|
||||
/// 将 API 带客单实体映射为订单来源选择项,对齐 Android `toOrderSourceLead()`。
|
||||
static func lead(from entity: ReferralOrderEntity) -> OrderSourceLead {
|
||||
let orderNo = entity.displayReferralOrderNo
|
||||
let remark: String
|
||||
if !entity.remark.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
remark = entity.remark
|
||||
} else if !entity.statusLabel.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
remark = entity.statusLabel
|
||||
} else {
|
||||
remark = "带客单 \(orderNo)"
|
||||
}
|
||||
return OrderSourceLead(
|
||||
key: orderNo,
|
||||
userPhone: entity.userMobile,
|
||||
remark: remark,
|
||||
images: entity.images
|
||||
)
|
||||
}
|
||||
|
||||
/// 将已绑定带客单实体映射为订单来源选择项,对齐 Android `toOrderSourceSelection()`。
|
||||
static func selection(from referral: BoundReferralOrderEntity) -> OrderSourceSelection? {
|
||||
let referralNo = referral.orderNo.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !referralNo.isEmpty else { return nil }
|
||||
|
||||
let personName = referral.displaySalerName.isEmpty ? "—" : referral.displaySalerName
|
||||
let person = OrderSourcePerson(
|
||||
key: referral.saleUserId > 0 ? String(referral.saleUserId) : "",
|
||||
name: personName,
|
||||
phone: referral.salerPhone
|
||||
)
|
||||
let remark: String
|
||||
if !referral.remark.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
remark = referral.remark
|
||||
} else if !referral.displayText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
remark = referral.displayText
|
||||
} else {
|
||||
remark = "带客单号 \(referralNo)"
|
||||
}
|
||||
let lead = OrderSourceLead(
|
||||
key: referralNo,
|
||||
userPhone: referral.userMobile,
|
||||
remark: remark,
|
||||
images: []
|
||||
)
|
||||
return OrderSourceSelection(person: person, lead: lead)
|
||||
}
|
||||
}
|
||||
|
||||
struct OrderSourceSelection: Equatable {
|
||||
@ -54,13 +104,14 @@ final class OrderListViewModel {
|
||||
private(set) var orderSourceLeads: [String: [OrderSourceLead]] = [:]
|
||||
private(set) var orderSourceSelections: [String: OrderSourceSelection] = [:]
|
||||
private(set) var loadingOrderSourceLeadsFor: String?
|
||||
private(set) var isLoadingOrderSourcePeople = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onNavigateBindAcquirer: ((Int) -> Void)?
|
||||
|
||||
private var currentPage = 1
|
||||
private var totalCount = 0
|
||||
private var loadingOrderSourcePeople = false
|
||||
|
||||
private let apiDateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
@ -188,7 +239,7 @@ final class OrderListViewModel {
|
||||
return
|
||||
case SaleUserQrParser.methodSaleUserBind:
|
||||
if let saleUserId = SaleUserQrParser.parseSaleUserId(scanResult), saleUserId > 0 {
|
||||
onShowMessage?("获客员绑定功能待接入")
|
||||
onNavigateBindAcquirer?(saleUserId)
|
||||
} else {
|
||||
onShowMessage?("请扫描正确的获客员二维码")
|
||||
}
|
||||
@ -287,27 +338,27 @@ final class OrderListViewModel {
|
||||
}
|
||||
|
||||
func orderSourceSelection(for order: OrderEntity) -> OrderSourceSelection? {
|
||||
if let selection = orderSourceSelections[order.orderNumber] { return selection }
|
||||
guard let referral = order.referralOrder else { return nil }
|
||||
let person = OrderSourcePerson(
|
||||
key: String(referral.saleUserId),
|
||||
name: referral.displaySalerName,
|
||||
phone: referral.salerPhone
|
||||
)
|
||||
let lead = OrderSourceLead(
|
||||
key: referral.orderNo,
|
||||
displayText: referral.displayText.isEmpty ? referral.orderNo : referral.displayText,
|
||||
userMobile: referral.userMobile,
|
||||
createdAt: referral.createdAt
|
||||
)
|
||||
return OrderSourceSelection(person: person, lead: lead)
|
||||
orderSourceSelection(orderNumber: order.orderNumber, referralOrder: order.referralOrder)
|
||||
}
|
||||
|
||||
/// 按订单号读取当前绑定状态,供订单来源 Sheet 使用。
|
||||
func orderSourceSelection(orderNumber: String, referralOrder: BoundReferralOrderEntity? = nil) -> OrderSourceSelection? {
|
||||
if let selection = orderSourceSelections[orderNumber] { return selection }
|
||||
if let referral = referralOrder ?? orderList.first(where: { $0.orderNumber == orderNumber })?.referralOrder {
|
||||
return OrderSourceMapping.selection(from: referral)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func onOrderSourceSheetOpen(api: OrderAPI) async {
|
||||
orderSourceLeads = [:]
|
||||
guard !loadingOrderSourcePeople else { return }
|
||||
loadingOrderSourcePeople = true
|
||||
defer { loadingOrderSourcePeople = false }
|
||||
guard !isLoadingOrderSourcePeople else { return }
|
||||
isLoadingOrderSourcePeople = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isLoadingOrderSourcePeople = false
|
||||
notifyStateChange()
|
||||
}
|
||||
let storeId = AppStore.shared.currentStoreId
|
||||
let storeIdParam = storeId > 0 ? String(storeId) : nil
|
||||
do {
|
||||
@ -331,15 +382,7 @@ final class OrderListViewModel {
|
||||
}
|
||||
do {
|
||||
let response = try await api.referralOrders(saleUserId: person.key)
|
||||
let leads = response.list.map {
|
||||
OrderSourceLead(
|
||||
key: $0.displayReferralOrderNo,
|
||||
displayText: $0.displayReferralOrderNo,
|
||||
userMobile: $0.userMobile,
|
||||
createdAt: $0.createdAt
|
||||
)
|
||||
}
|
||||
orderSourceLeads[person.key] = leads
|
||||
orderSourceLeads[person.key] = response.list.map(OrderSourceMapping.lead(from:))
|
||||
notifyStateChange()
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
|
||||
@ -42,6 +42,23 @@ final class ProfileViewModel {
|
||||
return name.isEmpty ? "--" : name
|
||||
}
|
||||
|
||||
/// 当前账号类型文案,如「门店账号」「景区账号」。
|
||||
var accountTypeLabel: String {
|
||||
let type = AppStore.shared.accountType.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if type == V9StoreUser.accountTypeValue {
|
||||
return "门店账号"
|
||||
}
|
||||
if type == V9ScenicUser.accountTypeValue {
|
||||
return "景区账号"
|
||||
}
|
||||
return "--"
|
||||
}
|
||||
|
||||
/// 当前是否为门店账号。
|
||||
var isStoreAccount: Bool {
|
||||
AppStore.shared.accountType == V9StoreUser.accountTypeValue
|
||||
}
|
||||
|
||||
var currentScenicName: String {
|
||||
let name = AppStore.shared.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return name.isEmpty ? "--" : name
|
||||
|
||||
Reference in New Issue
Block a user