将扫码页提升到列表层弹出并补齐 AppDelegate.window,避免扫码时 UIKit 取主窗口崩溃;Toast 改用独立 UIWindow 显示在 sheet 之上;合作获客员支持短信验证修改分成比例,同时优化冷启动根视图创建时机并补充相关测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
184 lines
7.0 KiB
Swift
184 lines
7.0 KiB
Swift
//
|
||
// CooperationAcquirerView.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/29.
|
||
//
|
||
|
||
import SwiftUI
|
||
import Combine
|
||
|
||
/// 合作获客员列表页,支持查看已绑定获客员与扫码绑定。
|
||
struct CooperationAcquirerView: View {
|
||
@EnvironmentObject private var accountContext: AccountContext
|
||
@EnvironmentObject private var toastCenter: ToastCenter
|
||
@Environment(\.cooperationOrderAPI) private var cooperationOrderAPI
|
||
@Environment(\.globalLoading) private var globalLoading
|
||
@Environment(\.openURL) private var openURL
|
||
@StateObject private var viewModel = CooperationAcquirerViewModel()
|
||
@State private var showRemarkAlert = false
|
||
@State private var remarkEditingAcquirer: CooperativeSaler?
|
||
@State private var remarkDraft = ""
|
||
@State private var sendingCommissionSms = false
|
||
@State private var submittingCommissionRate = false
|
||
let onScanBind: () -> Void
|
||
|
||
var body: some View {
|
||
ScrollView {
|
||
LazyVStack(spacing: 12) {
|
||
if viewModel.hasLoadedOnce && viewModel.acquirers.isEmpty {
|
||
Text("暂无合作获客员")
|
||
.font(.system(size: 14))
|
||
.foregroundStyle(CooperationOrderColors.text999)
|
||
.frame(maxWidth: .infinity)
|
||
.padding(.vertical, 48)
|
||
} else {
|
||
ForEach(viewModel.acquirers, id: \.displayId) { acquirer in
|
||
CooperativeSalerCard(acquirer: acquirer) { phone in
|
||
dialPhone(phone)
|
||
} onEditRemark: {
|
||
remarkEditingAcquirer = acquirer
|
||
remarkDraft = acquirer.photogRemarkName
|
||
showRemarkAlert = true
|
||
} onEditCommission: {
|
||
viewModel.startEditCommissionRate(acquirer)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.padding(.horizontal, 14)
|
||
.padding(.vertical, 12)
|
||
}
|
||
.navigationTitle("合作获客员")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .topBarTrailing) {
|
||
Button("扫码绑定") {
|
||
onScanBind()
|
||
}
|
||
.font(.system(size: 14))
|
||
.foregroundStyle(CooperationOrderColors.primary)
|
||
}
|
||
}
|
||
.background(CooperationOrderColors.pageBg.ignoresSafeArea())
|
||
.refreshable {
|
||
await reload(showLoading: false)
|
||
}
|
||
.task {
|
||
await reload(showLoading: true)
|
||
}
|
||
.sheet(item: $viewModel.commissionEditingAcquirer, onDismiss: {
|
||
viewModel.dismissCommissionRateDialog()
|
||
}) { acquirer in
|
||
CommissionRateEditSheet(
|
||
acquirer: acquirer,
|
||
commissionRate: Binding(
|
||
get: { viewModel.commissionRateDraft },
|
||
set: { viewModel.updateCommissionRateDraft($0) }
|
||
),
|
||
smsCode: Binding(
|
||
get: { viewModel.commissionSmsCodeDraft },
|
||
set: { viewModel.updateCommissionSmsCodeDraft($0) }
|
||
),
|
||
smsCountdown: viewModel.commissionSmsCountdown,
|
||
sending: sendingCommissionSms,
|
||
submitting: submittingCommissionRate,
|
||
onSendSmsCode: {
|
||
Task { await sendCommissionSms() }
|
||
},
|
||
onConfirm: {
|
||
Task { await saveCommissionRate() }
|
||
},
|
||
onCancel: {
|
||
viewModel.dismissCommissionRateDialog()
|
||
}
|
||
)
|
||
.presentationDetents([.height(430)])
|
||
.presentationDragIndicator(.hidden)
|
||
}
|
||
.alert("修改备注", isPresented: $showRemarkAlert) {
|
||
TextField("备注名", text: $remarkDraft)
|
||
Button("取消", role: .cancel) {
|
||
remarkEditingAcquirer = nil
|
||
}
|
||
Button("保存") {
|
||
// 必须在弹窗关闭前同步捕获,避免 alert dismiss 清空 state 导致接口未调用。
|
||
guard let acquirer = remarkEditingAcquirer else { return }
|
||
if let message = CooperationOrderRemarkName.validationMessage(for: remarkDraft) {
|
||
toastCenter.show(message)
|
||
return
|
||
}
|
||
let saleUserId = acquirer.saleUserId
|
||
let draft = remarkDraft
|
||
showRemarkAlert = false
|
||
remarkEditingAcquirer = nil
|
||
Task {
|
||
await saveRemark(saleUserId: saleUserId, draft: draft)
|
||
}
|
||
}
|
||
} message: {
|
||
if let acquirer = remarkEditingAcquirer {
|
||
Text("为 \(acquirer.salerRealName.cooperationNonEmptyOrDefault("—")) 设置备注名")
|
||
}
|
||
}
|
||
.onReceive(Timer.publish(every: 1, on: .main, in: .common).autoconnect()) { _ in
|
||
viewModel.tickCommissionSmsCountdown()
|
||
}
|
||
}
|
||
|
||
/// 保存当前编辑中的获客员备注名。
|
||
private func saveRemark(saleUserId: Int, draft: String) async {
|
||
guard saleUserId > 0 else { return }
|
||
await globalLoading.withOptionalLoading(true) {
|
||
do {
|
||
try await viewModel.updateRemark(
|
||
saleUserId: saleUserId,
|
||
remark: draft,
|
||
api: cooperationOrderAPI
|
||
)
|
||
toastCenter.show("备注已保存")
|
||
} catch {
|
||
toastCenter.show(error.localizedDescription)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func reload(showLoading: Bool) async {
|
||
await globalLoading.withOptionalLoading(showLoading) {
|
||
await viewModel.reload(api: cooperationOrderAPI, storeId: accountContext.currentStore?.id)
|
||
}
|
||
}
|
||
|
||
/// 发送分成比例修改验证码。
|
||
private func sendCommissionSms() async {
|
||
guard !sendingCommissionSms else { return }
|
||
sendingCommissionSms = true
|
||
defer { sendingCommissionSms = false }
|
||
do {
|
||
try await viewModel.sendCommissionRateSms(api: cooperationOrderAPI)
|
||
toastCenter.show("验证码已发送")
|
||
} catch {
|
||
toastCenter.show(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
/// 保存分成比例修改。
|
||
private func saveCommissionRate() async {
|
||
guard !submittingCommissionRate else { return }
|
||
submittingCommissionRate = true
|
||
defer { submittingCommissionRate = false }
|
||
do {
|
||
try await viewModel.saveCommissionRate(api: cooperationOrderAPI)
|
||
toastCenter.show("分成比例已修改")
|
||
} catch {
|
||
toastCenter.show(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
private func dialPhone(_ phone: String) {
|
||
let trimmed = phone.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !trimmed.isEmpty, let url = URL(string: "tel:\(trimmed)") else { return }
|
||
openURL(url)
|
||
}
|
||
}
|