新增 CooperationOrder 模块(双 Tab 列表、获客员绑定、主 Tab 扫码)、订单来源/带客单绑定及配套 API 测试;同步引入 roleCode 权限匹配与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
228 lines
8.7 KiB
Swift
228 lines
8.7 KiB
Swift
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
|
|
}
|