迁移合作订单完整能力,并统一 AppRoleCode 角色权限与首页菜单展示。

新增 CooperationOrder 模块(双 Tab 列表、获客员绑定、主 Tab 扫码)、订单来源/带客单绑定及配套 API 测试;同步引入 roleCode 权限匹配与相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 13:33:00 +08:00
parent d310d26293
commit 1970572edd
66 changed files with 4247 additions and 201 deletions

View File

@ -0,0 +1,89 @@
//
// CooperationAcquirerView.swift
// suixinkan
//
// Created by Codex on 2026/6/29.
//
import SwiftUI
///
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 showScanner = false
let onBind: (Int) -> 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)
}
}
}
}
.padding(.horizontal, 14)
.padding(.vertical, 12)
}
.navigationTitle("合作获客员")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("扫码绑定") {
showScanner = true
}
.font(.system(size: 14))
.foregroundStyle(CooperationOrderColors.primary)
}
}
.background(CooperationOrderColors.pageBg.ignoresSafeArea())
.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)
}
}
private func reload(showLoading: Bool) async {
await globalLoading.withOptionalLoading(showLoading) {
await viewModel.reload(api: cooperationOrderAPI, storeId: accountContext.currentStore?.id)
}
}
private func dialPhone(_ phone: String) {
let trimmed = phone.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let url = URL(string: "tel:\(trimmed)") else { return }
openURL(url)
}
}