修复合作获客扫码绑定崩溃与 Toast 层级,并接入分成比例修改。
将扫码页提升到列表层弹出并补齐 AppDelegate.window,避免扫码时 UIKit 取主窗口崩溃;Toast 改用独立 UIWindow 显示在 sheet 之上;合作获客员支持短信验证修改分成比例,同时优化冷启动根视图创建时机并补充相关测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -41,6 +41,15 @@ struct BindAcquirerView: View {
|
||||
|
||||
infoCard(title: "获客员", value: info.salerName)
|
||||
infoCard(title: "手机号", value: info.salerPhone)
|
||||
BindCommissionRateInputRow(
|
||||
value: Binding(
|
||||
get: { viewModel.commissionRateDraft },
|
||||
set: { viewModel.updateCommissionRateDraft($0) }
|
||||
),
|
||||
enabled: !viewModel.isAlreadyBound
|
||||
)
|
||||
.padding(14)
|
||||
.background(.white, in: RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
.padding(16)
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
/// 合作获客员列表页,支持查看已绑定获客员与扫码绑定。
|
||||
struct CooperationAcquirerView: View {
|
||||
@ -15,11 +16,12 @@ struct CooperationAcquirerView: View {
|
||||
@Environment(\.globalLoading) private var globalLoading
|
||||
@Environment(\.openURL) private var openURL
|
||||
@StateObject private var viewModel = CooperationAcquirerViewModel()
|
||||
@State private var showScanner = false
|
||||
@State private var showRemarkAlert = false
|
||||
@State private var remarkEditingAcquirer: CooperativeSaler?
|
||||
@State private var remarkDraft = ""
|
||||
let onBind: (Int) -> Void
|
||||
@State private var sendingCommissionSms = false
|
||||
@State private var submittingCommissionRate = false
|
||||
let onScanBind: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@ -38,6 +40,8 @@ struct CooperationAcquirerView: View {
|
||||
remarkEditingAcquirer = acquirer
|
||||
remarkDraft = acquirer.photogRemarkName
|
||||
showRemarkAlert = true
|
||||
} onEditCommission: {
|
||||
viewModel.startEditCommissionRate(acquirer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -50,7 +54,7 @@ struct CooperationAcquirerView: View {
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("扫码绑定") {
|
||||
showScanner = true
|
||||
onScanBind()
|
||||
}
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(CooperationOrderColors.primary)
|
||||
@ -60,26 +64,38 @@ struct CooperationAcquirerView: View {
|
||||
.refreshable {
|
||||
await reload(showLoading: false)
|
||||
}
|
||||
.fullScreenCover(isPresented: $showScanner) {
|
||||
OrderScannerPage(
|
||||
onClose: { showScanner = false },
|
||||
onSuccess: { raw in
|
||||
showScanner = false
|
||||
if let saleUserId = SaleUserQrParser.parseSaleUserId(raw) {
|
||||
onBind(saleUserId)
|
||||
} else {
|
||||
toastCenter.show("请扫描正确的获客员二维码")
|
||||
}
|
||||
},
|
||||
onFailure: { error in
|
||||
showScanner = false
|
||||
toastCenter.show(error.localizedDescription)
|
||||
}
|
||||
)
|
||||
}
|
||||
.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) {
|
||||
@ -105,6 +121,9 @@ struct CooperationAcquirerView: View {
|
||||
Text("为 \(acquirer.salerRealName.cooperationNonEmptyOrDefault("—")) 设置备注名")
|
||||
}
|
||||
}
|
||||
.onReceive(Timer.publish(every: 1, on: .main, in: .common).autoconnect()) { _ in
|
||||
viewModel.tickCommissionSmsCountdown()
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存当前编辑中的获客员备注名。
|
||||
@ -130,6 +149,32 @@ struct CooperationAcquirerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送分成比例修改验证码。
|
||||
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 }
|
||||
|
||||
@ -160,6 +160,10 @@ struct ReferralLeadCard: View {
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
}
|
||||
CooperationListInfoRow(
|
||||
label: "获客员",
|
||||
value: lead.displaySalerName.isEmpty ? "—" : lead.displaySalerName
|
||||
)
|
||||
|
||||
Text("备注")
|
||||
.font(.system(size: 13))
|
||||
@ -302,6 +306,7 @@ struct CooperativeSalerCard: View {
|
||||
let acquirer: CooperativeSaler
|
||||
let onDial: (String) -> Void
|
||||
let onEditRemark: () -> Void
|
||||
let onEditCommission: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
@ -323,6 +328,10 @@ struct CooperativeSalerCard: View {
|
||||
label: "绑定时间",
|
||||
value: acquirer.displayBindTime.isEmpty ? "—" : acquirer.displayBindTime
|
||||
)
|
||||
AcquirerCommissionInfoRow(
|
||||
commissionRate: acquirer.displayCommissionRate.isEmpty ? "—" : acquirer.displayCommissionRate,
|
||||
onEdit: onEditCommission
|
||||
)
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@ -344,6 +353,179 @@ struct CooperativeSalerCard: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct AcquirerCommissionInfoRow: View {
|
||||
let commissionRate: String
|
||||
let onEdit: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Text("分成比例")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
Text(commissionRate)
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.foregroundStyle(CooperationOrderColors.text666)
|
||||
.padding(.leading, 12)
|
||||
Spacer(minLength: 8)
|
||||
Button("修改比例") {
|
||||
onEdit()
|
||||
}
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.primary)
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
struct BindCommissionRateInputRow: View {
|
||||
@Binding var value: String
|
||||
var enabled: Bool = true
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 7) {
|
||||
Text("分账比例")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text666)
|
||||
HStack(spacing: 8) {
|
||||
TextField("请输入0到100", text: $value)
|
||||
.keyboardType(.numberPad)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(CooperationOrderColors.text333)
|
||||
.disabled(!enabled)
|
||||
Text("%")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(CooperationOrderColors.text666)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 13)
|
||||
.background(Color(hex: 0xF7F8FA), in: RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
struct CommissionRateEditSheet: View {
|
||||
let acquirer: CooperativeSaler
|
||||
@Binding var commissionRate: String
|
||||
@Binding var smsCode: String
|
||||
let smsCountdown: Int
|
||||
let sending: Bool
|
||||
let submitting: Bool
|
||||
let onSendSmsCode: () -> Void
|
||||
let onConfirm: () -> Void
|
||||
let onCancel: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Capsule()
|
||||
.fill(Color.black.opacity(0.16))
|
||||
.frame(width: 36, height: 4)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 8)
|
||||
|
||||
Text("修改分成比例")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.foregroundStyle(CooperationOrderColors.text333)
|
||||
|
||||
Text("修改分成比例需验证手机号")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.primary)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 10)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(CooperationOrderColors.primary.opacity(0.08), in: RoundedRectangle(cornerRadius: 8))
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("分成比例")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text666)
|
||||
HStack(spacing: 8) {
|
||||
TextField("请输入0到100", text: $commissionRate)
|
||||
.keyboardType(.numberPad)
|
||||
.font(.system(size: 15))
|
||||
Text("%")
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.foregroundStyle(CooperationOrderColors.text666)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.frame(height: 46)
|
||||
.background(CooperationOrderColors.searchBg, in: RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("验证码发送手机号")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text666)
|
||||
Text(maskAcquirerPhone(acquirer.displayMaskedPhone))
|
||||
.font(.system(size: 15))
|
||||
.foregroundStyle(CooperationOrderColors.text333)
|
||||
.padding(.horizontal, 12)
|
||||
.frame(maxWidth: .infinity, minHeight: 46, alignment: .leading)
|
||||
.background(CooperationOrderColors.searchBg, in: RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("验证码")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text666)
|
||||
HStack(spacing: 10) {
|
||||
TextField("请输入验证码", text: $smsCode)
|
||||
.keyboardType(.numberPad)
|
||||
.font(.system(size: 15))
|
||||
.padding(.horizontal, 12)
|
||||
.frame(height: 46)
|
||||
.background(CooperationOrderColors.searchBg, in: RoundedRectangle(cornerRadius: 8))
|
||||
Button(smsButtonTitle) {
|
||||
onSendSmsCode()
|
||||
}
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 104, height: 46)
|
||||
.background(smsButtonEnabled ? CooperationOrderColors.primary : CooperationOrderColors.primary.opacity(0.45), in: RoundedRectangle(cornerRadius: 8))
|
||||
.disabled(!smsButtonEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button("取消") {
|
||||
onCancel()
|
||||
}
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.foregroundStyle(CooperationOrderColors.text666)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 44)
|
||||
.background(Color(hex: 0xF1F3F5), in: RoundedRectangle(cornerRadius: 8))
|
||||
|
||||
Button(submitting ? "提交中..." : "确认修改") {
|
||||
onConfirm()
|
||||
}
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.foregroundStyle(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 44)
|
||||
.background(CooperationOrderColors.primary, in: RoundedRectangle(cornerRadius: 8))
|
||||
.disabled(submitting)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
|
||||
private var smsButtonTitle: String {
|
||||
if smsCountdown > 0 { return "\(smsCountdown)秒" }
|
||||
return sending ? "发送中" : "获取验证码"
|
||||
}
|
||||
|
||||
private var smsButtonEnabled: Bool {
|
||||
smsCountdown == 0 && !sending
|
||||
}
|
||||
}
|
||||
|
||||
struct AcquirerInfoRow: View {
|
||||
let label: String
|
||||
let value: String
|
||||
|
||||
@ -9,10 +9,13 @@ import SwiftUI
|
||||
|
||||
/// 合作订单主页,展示获客线索与获客订单双 Tab 列表。
|
||||
struct CooperationOrderListView: View {
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
@Environment(\.cooperationOrderAPI) private var cooperationOrderAPI
|
||||
@Environment(\.globalLoading) private var globalLoading
|
||||
@StateObject private var viewModel = CooperationOrderListViewModel()
|
||||
@State private var route: CooperationOrderRoute?
|
||||
/// 扫码绑定页在列表层弹出,避免在 navigationDestination 子页内嵌 fullScreenCover 触发 SwiftUI 栈溢出。
|
||||
@State private var showBindScanner = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@ -81,11 +84,28 @@ struct CooperationOrderListView: View {
|
||||
}
|
||||
}
|
||||
.background(CooperationOrderColors.pageBg.ignoresSafeArea())
|
||||
.fullScreenCover(isPresented: $showBindScanner) {
|
||||
OrderScannerPage(
|
||||
onClose: { showBindScanner = false },
|
||||
onSuccess: { raw in
|
||||
showBindScanner = false
|
||||
guard let saleUserId = SaleUserQrParser.parseSaleUserId(raw), saleUserId > 0 else {
|
||||
toastCenter.show("请扫描正确的获客员二维码")
|
||||
return
|
||||
}
|
||||
route = .bind(saleUserId: saleUserId)
|
||||
},
|
||||
onFailure: { error in
|
||||
showBindScanner = false
|
||||
toastCenter.show(error.localizedDescription)
|
||||
}
|
||||
)
|
||||
}
|
||||
.appNavigationDestination(item: $route) { destination in
|
||||
switch destination {
|
||||
case .acquirer:
|
||||
CooperationAcquirerView(onBind: { saleUserId in
|
||||
route = .bind(saleUserId: saleUserId)
|
||||
CooperationAcquirerView(onScanBind: {
|
||||
showBindScanner = true
|
||||
})
|
||||
case let .bind(saleUserId):
|
||||
BindAcquirerView(saleUserId: saleUserId, onSuccess: {
|
||||
|
||||
Reference in New Issue
Block a user