迁移合作订单完整能力,并统一 AppRoleCode 角色权限与首页菜单展示。
新增 CooperationOrder 模块(双 Tab 列表、获客员绑定、主 Tab 扫码)、订单来源/带客单绑定及配套 API 测试;同步引入 roleCode 权限匹配与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
227
suixinkan/Features/Orders/Views/OrderSourcePicker.swift
Normal file
227
suixinkan/Features/Orders/Views/OrderSourcePicker.swift
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user