迁移合作订单完整能力,并统一 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,312 @@
//
// ReferralOrderSelectView.swift
// suixinkan
//
// Created by Codex on 2026/6/29.
//
import SwiftUI
import Combine
/// ViewModel
@MainActor
final class ReferralOrderSelectViewModel: ObservableObject {
enum Step {
case selectAcquirer
case selectReferralOrder
}
@Published var step: Step = .selectAcquirer
@Published var acquirers: [CooperativeSaler] = []
@Published var selectedAcquirer: CooperativeSaler?
@Published var referralOrders: [ReferralOrder] = []
@Published var selectedReferralOrder: ReferralOrder?
@Published var isLoadingMore = false
@Published var canLoadMore = false
@Published private(set) var hasLoadedAcquirersOnce = false
@Published private(set) var hasLoadedReferralOrdersOnce = false
private var orderNumber = ""
private var currentPage = 1
private var totalCount = 0
private var isReloadingReferralOrders = false
private let pageSize = 10
///
func initOrder(orderNumber: String, api: CooperationOrderServing, storeId: Int?) async {
if self.orderNumber == orderNumber && !acquirers.isEmpty { return }
self.orderNumber = orderNumber
await loadAcquirers(api: api, storeId: storeId)
}
private func loadAcquirers(api: CooperationOrderServing, storeId: Int?) async {
do {
let storeParam = storeId.flatMap { $0 > 0 ? $0 : nil }
let data = try await api.cooperativeSalers(storeId: storeParam, page: 1, pageSize: 50)
acquirers = data.list
if data.list.count == 1 {
await selectAcquirer(data.list[0], api: api)
} else {
step = .selectAcquirer
}
} catch {
if !hasLoadedAcquirersOnce {
acquirers = []
}
}
hasLoadedAcquirersOnce = true
}
///
func selectAcquirer(_ acquirer: CooperativeSaler, api: CooperationOrderServing) async {
selectedAcquirer = acquirer
selectedReferralOrder = nil
hasLoadedReferralOrdersOnce = false
step = .selectReferralOrder
await refreshReferralOrders(api: api)
}
///
func backToAcquirerStep() {
if acquirers.count <= 1 { return }
step = .selectAcquirer
selectedReferralOrder = nil
referralOrders = []
}
///
func selectReferralOrder(_ order: ReferralOrder) {
selectedReferralOrder = order
}
///
func refreshReferralOrders(api: CooperationOrderServing) async {
guard let acquirer = selectedAcquirer else { return }
guard !isReloadingReferralOrders else { return }
isReloadingReferralOrders = true
defer { isReloadingReferralOrders = false }
currentPage = 1
isLoadingMore = false
await loadReferralOrders(api: api, saleUserId: acquirer.displayId)
}
///
func loadMoreReferralOrders(api: CooperationOrderServing) async {
guard !isReloadingReferralOrders, !isLoadingMore, canLoadMore,
let acquirer = selectedAcquirer else { return }
isLoadingMore = true
currentPage += 1
await loadReferralOrders(api: api, saleUserId: acquirer.displayId)
}
private func loadReferralOrders(api: CooperationOrderServing, saleUserId: Int) async {
do {
let data = try await api.saleUserReferralOrders(
saleUserId: saleUserId,
page: currentPage,
pageSize: pageSize
)
totalCount = data.total
if currentPage == 1 {
referralOrders = data.list
} else {
referralOrders.append(contentsOf: data.list)
}
canLoadMore = referralOrders.count < totalCount
} catch {
if currentPage == 1, !hasLoadedReferralOrdersOnce {
referralOrders = []
}
if currentPage > 1 {
currentPage = max(currentPage - 1, 1)
}
}
if currentPage == 1 {
hasLoadedReferralOrdersOnce = true
}
isLoadingMore = false
}
///
func confirmBind(api: CooperationOrderServing) async throws {
guard let referralOrder = selectedReferralOrder else {
throw NSError(domain: "ReferralOrder", code: 1, userInfo: [NSLocalizedDescriptionKey: "请选择带客单"])
}
guard !orderNumber.isEmpty else {
throw NSError(domain: "ReferralOrder", code: 2, userInfo: [NSLocalizedDescriptionKey: "订单号无效"])
}
try await api.saleUserBindReferralOrder(
orderNumber: orderNumber,
referralOrderNo: referralOrder.displayReferralOrderNo
)
}
}
///
struct ReferralOrderSelectView: View {
@EnvironmentObject private var accountContext: AccountContext
@EnvironmentObject private var toastCenter: ToastCenter
@Environment(\.cooperationOrderAPI) private var cooperationOrderAPI
@Environment(\.globalLoading) private var globalLoading
@Environment(\.dismiss) private var dismiss
@StateObject private var viewModel = ReferralOrderSelectViewModel()
let orderNumber: String
let onBound: () -> Void
@State private var binding = false
var body: some View {
Group {
if viewModel.step == .selectAcquirer {
acquirerList
} else {
referralOrderList
}
}
.navigationTitle(viewModel.step == .selectAcquirer ? "选择获客员" : "选择带客单")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
if viewModel.step == .selectReferralOrder && viewModel.acquirers.count > 1 {
viewModel.backToAcquirerStep()
} else {
dismiss()
}
} label: {
Image(systemName: "chevron.backward")
}
}
}
.safeAreaInset(edge: .bottom) {
if viewModel.step == .selectReferralOrder {
Button {
Task { await confirmBind() }
} label: {
Text(binding ? "提交中..." : "确认绑定")
.font(.system(size: 16))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 48)
.background(
viewModel.selectedReferralOrder != nil
? OrderSourceColors.primary
: OrderSourceColors.primary.opacity(0.4),
in: RoundedRectangle(cornerRadius: 8)
)
}
.buttonStyle(.plain)
.disabled(viewModel.selectedReferralOrder == nil || binding)
.padding(.horizontal, 16)
.padding(.vertical, 10)
.background(.white)
}
}
.background(OrderSourceColors.pageBg.ignoresSafeArea())
.task {
await globalLoading.withLoading {
await viewModel.initOrder(
orderNumber: orderNumber,
api: cooperationOrderAPI,
storeId: accountContext.currentStore?.id
)
}
if viewModel.acquirers.isEmpty {
toastCenter.show("暂无合作获客员,请先绑定获客员")
dismiss()
}
}
}
private func selectAcquirer(_ acquirer: CooperativeSaler) async {
await globalLoading.withLoading {
await viewModel.selectAcquirer(acquirer, api: cooperationOrderAPI)
}
}
private var acquirerList: some View {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(viewModel.acquirers, id: \.displayId) { acquirer in
AcquirerSelectCard(
name: acquirer.displayName,
phone: acquirer.displayPhone
) {
Task { await selectAcquirer(acquirer) }
}
}
}
.padding(.horizontal, 14)
.padding(.vertical, 12)
}
}
private var referralOrderList: some View {
ScrollView {
LazyVStack(spacing: 12) {
if let acquirer = viewModel.selectedAcquirer, !acquirer.displayName.isEmpty {
Text("获客员:\(acquirer.displayName)")
.font(.system(size: 13))
.foregroundStyle(OrderSourceColors.text666)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, 4)
}
if viewModel.hasLoadedReferralOrdersOnce && viewModel.referralOrders.isEmpty {
Text("暂无可绑定带客单")
.font(.system(size: 14))
.foregroundStyle(OrderSourceColors.text999)
.frame(maxWidth: .infinity)
.padding(.vertical, 32)
} else {
ForEach(Array(viewModel.referralOrders.enumerated()), id: \.element.id) { index, order in
let selected = viewModel.selectedReferralOrder?.id == order.id
ReferralOrderSelectCard(
order: order,
selected: selected
) {
viewModel.selectReferralOrder(order)
}
.onAppear {
guard index >= viewModel.referralOrders.count - 3 else { return }
guard viewModel.canLoadMore, !viewModel.isLoadingMore else { return }
Task { await viewModel.loadMoreReferralOrders(api: cooperationOrderAPI) }
}
}
if viewModel.isLoadingMore {
ProgressView()
.tint(OrderSourceColors.primary)
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
}
}
}
.padding(.horizontal, 14)
.padding(.vertical, 12)
}
.refreshable {
await globalLoading.withOptionalLoading(false) {
await viewModel.refreshReferralOrders(api: cooperationOrderAPI)
}
}
}
private func confirmBind() async {
binding = true
defer { binding = false }
do {
try await viewModel.confirmBind(api: cooperationOrderAPI)
onBound()
dismiss()
} catch {
toastCenter.show(error.localizedDescription)
}
}
}
/// Sheet
struct OrderNumberSheetItem: Identifiable {
let orderNumber: String
var id: String { orderNumber }
}