迁移合作订单完整能力,并统一 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,227 @@
import SwiftUI
struct OrderSourcePicker: View {
let selection: OrderSourceSelection?
let people: [OrderSourcePerson]
let leadsForPerson: (OrderSourcePerson) -> [OrderSourceLead]
let leadsLoading: Bool
let onSheetOpen: () -> Void
let onPersonClick: (OrderSourcePerson) -> Void
let onSelectionChange: (OrderSourceSelection) -> Void
let callPhone: (String) -> Void
@State private var sheetVisible = false
@State private var navigationPath = NavigationPath()
@State private var previewImages: [String] = []
@State private var previewIndex: Int?
var body: some View {
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 4) {
Text("订单来源")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(OrderSourceColors.text563)
if let selection {
HStack(spacing: 10) {
Text(selection.person.name)
.font(.system(size: 13, weight: .medium))
.foregroundStyle(OrderSourceColors.text333)
if !selection.person.phone.isEmpty {
Text(selection.person.phone)
.font(.system(size: 13))
.foregroundStyle(OrderSourceColors.text666)
}
}
} else {
Text("选择获客员及其创建的带客单")
.font(.system(size: 13))
.foregroundStyle(OrderSourceColors.text666)
}
}
Spacer(minLength: 0)
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(OrderSourceColors.primary)
.frame(width: 20, height: 20)
}
if let selection {
Divider().overlay(OrderSourceColors.divider)
SourceLeadContent(
lead: selection.lead,
compact: true,
enablePhoneCall: false,
onCall: callPhone,
onImageTap: { images, index in
previewImages = images
previewIndex = index
}
)
}
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(OrderSourceColors.entryBg, in: RoundedRectangle(cornerRadius: 8))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(OrderSourceColors.border, lineWidth: 1)
)
.contentShape(RoundedRectangle(cornerRadius: 8))
.onTapGesture {
openSheet()
}
.sheet(isPresented: $sheetVisible) {
sheetContainer
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
.sheet(item: previewBinding) { item in
SimpleImagePreviewSheet(
imageUrls: item.urls,
startIndex: item.index,
onDismiss: { previewIndex = nil }
)
}
}
private func openSheet() {
onSheetOpen()
navigationPath = NavigationPath()
sheetVisible = true
}
private var previewBinding: Binding<ImagePreviewItem?> {
Binding(
get: {
guard let previewIndex else { return nil }
return ImagePreviewItem(urls: previewImages, index: previewIndex)
},
set: { newValue in
if newValue == nil { previewIndex = nil }
}
)
}
private var sheetContainer: some View {
NavigationStack(path: $navigationPath) {
personSheetContent
.navigationDestination(for: OrderSourcePerson.self) { person in
leadSheetContent(person: person)
}
.navigationDestination(for: OrderSourceImagePreviewRoute.self) { route in
SimpleImagePreviewScreen(
imageUrls: route.urls,
startIndex: route.startIndex
)
}
}
.background(Color.white)
}
private var personSheetContent: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
Text("选择人员后,再选择该人员创建的带客单")
.font(.system(size: 13))
.foregroundStyle(OrderSourceColors.text666)
.padding(.bottom, 14)
if people.isEmpty {
Text("暂无合作获客员")
.font(.system(size: 14))
.foregroundStyle(OrderSourceColors.text666)
.frame(maxWidth: .infinity)
.padding(.vertical, 32)
} else {
LazyVStack(spacing: 10) {
ForEach(people) { person in
SourcePersonRow(
person: person,
selected: selection?.person.key == person.key,
onCall: callPhone,
onTap: {
onPersonClick(person)
navigationPath.append(person)
}
)
}
}
}
}
.padding(.horizontal, 16)
.padding(.top, 8)
.padding(.bottom, 24)
}
.navigationTitle("选择获客员")
.navigationBarTitleDisplayMode(.inline)
}
private func leadSheetContent(person: OrderSourcePerson) -> some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
if !person.phone.isEmpty {
HStack(spacing: 6) {
Text(person.phone)
.font(.system(size: 13))
.foregroundStyle(OrderSourceColors.primary)
.onTapGesture { callPhone(person.phone) }
AcquirerCallButton(phone: person.phone, onCall: callPhone)
}
.padding(.bottom, 14)
}
if leadsLoading {
ProgressView()
.tint(OrderSourceColors.primary)
.frame(maxWidth: .infinity)
.padding(.top, 32)
} else {
let leads = leadsForPerson(person)
if leads.isEmpty {
Text("暂无可绑定带客单")
.font(.system(size: 14))
.foregroundStyle(OrderSourceColors.text666)
.frame(maxWidth: .infinity)
.padding(.vertical, 24)
} else {
LazyVStack(spacing: 12) {
ForEach(leads) { lead in
SourceLeadCard(
lead: lead,
selected: selection?.person.key == person.key && selection?.lead.key == lead.key,
onCall: callPhone,
onImageTap: { images, index in
navigationPath.append(
OrderSourceImagePreviewRoute(urls: images, startIndex: index)
)
},
onSelect: {
onSelectionChange(OrderSourceSelection(person: person, lead: lead))
sheetVisible = false
}
)
}
}
}
}
}
.padding(.horizontal, 16)
.padding(.top, 8)
.padding(.bottom, 24)
}
.navigationTitle("\(person.name)创建的带客单")
.navigationBarTitleDisplayMode(.inline)
}
}
private struct ImagePreviewItem: Identifiable {
let urls: [String]
let index: Int
var id: String { "\(index)-\(urls.joined())" }
}
private struct OrderSourceImagePreviewRoute: Hashable {
let urls: [String]
let startIndex: Int
}