// // CooperationOrderComponents.swift // suixinkan // // Created by Codex on 2026/6/29. // import SwiftUI import Kingfisher enum CooperationOrderColors { static let primary = Color(hex: 0x0073FF) static let text333 = Color(hex: 0x333333) static let text666 = Color(hex: 0x666666) static let text999 = Color(hex: 0x999999) static let searchPlaceholder = Color(hex: 0xB6BECA) static let searchBg = Color(hex: 0xF4F4F4) static let imageEmptyBg = Color(hex: 0xF7F8FA) static let imagePlaceholderBg = Color(hex: 0xF4F4F4) static let pageBg = Color(hex: 0xF5F7FA) } struct CooperationCard: View { @ViewBuilder let content: () -> Content var body: some View { content() .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background(.white, in: RoundedRectangle(cornerRadius: 10)) .shadow(color: Color.black.opacity(0.06), radius: 1, x: 0, y: 1) } } struct CooperationListInfoRow: View { let label: String let value: String var body: some View { HStack(alignment: .center, spacing: 0) { Text(label) .font(.system(size: 13)) .foregroundStyle(CooperationOrderColors.text999) Text(value) .font(.system(size: 13)) .foregroundStyle(CooperationOrderColors.text333) .lineLimit(1) .truncationMode(.tail) .padding(.leading, 14) .frame(maxWidth: .infinity, alignment: .leading) } } } struct CooperationTypeTag: View { let text: String var body: some View { Text(text) .font(.system(size: 12)) .foregroundStyle(CooperationOrderColors.primary) .padding(.horizontal, 8) .padding(.vertical, 4) .background(CooperationOrderColors.primary.opacity(0.09), in: RoundedRectangle(cornerRadius: 4)) } } struct CooperationOrderTabs: View { let selectedTab: Int let onTabSelected: (Int) -> Void private let tabs = ["获客员线索", "获客订单"] var body: some View { VStack(spacing: 0) { HStack(spacing: 0) { ForEach(Array(tabs.enumerated()), id: \.offset) { index, title in Button { onTabSelected(index) } label: { Text(title) .font(.system(size: 15, weight: selectedTab == index ? .bold : .regular)) .foregroundStyle(selectedTab == index ? CooperationOrderColors.primary : CooperationOrderColors.text666) .frame(maxWidth: .infinity) .frame(height: 48) } .buttonStyle(.plain) } } GeometryReader { geo in let tabWidth = geo.size.width / CGFloat(tabs.count) RoundedRectangle(cornerRadius: 1.5) .fill(CooperationOrderColors.primary) .frame(width: max(tabWidth - 68, 0), height: 3) .offset(x: tabWidth * CGFloat(selectedTab) + 34, y: 0) } .frame(height: 3) } .background(.white) } } struct CooperationOrderSearchBar: View { @Binding var searchText: String let onSearch: () -> Void var body: some View { HStack(spacing: 0) { HStack(spacing: 8) { Image(systemName: "magnifyingglass") .font(.system(size: 18)) .foregroundStyle(CooperationOrderColors.text999) TextField( "", text: $searchText, prompt: Text("名称搜索").foregroundColor(CooperationOrderColors.searchPlaceholder) ) .font(.system(size: 14)) .foregroundStyle(CooperationOrderColors.text333) .textInputAutocapitalization(.never) .autocorrectionDisabled(true) .submitLabel(.search) .onSubmit(onSearch) } .padding(.horizontal, 12) .frame(height: 38) .background(CooperationOrderColors.searchBg, in: RoundedRectangle(cornerRadius: 6)) } .padding(.horizontal, 14) .padding(.vertical, 12) .background(.white) } } struct CooperationImagePreviewItem: Identifiable { let urls: [String] let index: Int var id: String { "\(index)-\(urls.joined())" } } struct ReferralLeadCard: View { let lead: ReferralLead @State private var previewIndex: Int? @State private var previewImages: [String] = [] var body: some View { CooperationCard { VStack(alignment: .leading, spacing: 10) { HStack(alignment: .center, spacing: 0) { Text("用户手机号") .font(.system(size: 13)) .foregroundStyle(CooperationOrderColors.text999) Text(lead.displayPhone.isEmpty ? "—" : lead.displayPhone) .font(.system(size: 15, weight: .medium)) .foregroundStyle(CooperationOrderColors.text333) .padding(.leading, 14) Spacer(minLength: 8) Text(lead.createdAt) .font(.system(size: 11)) .foregroundStyle(CooperationOrderColors.text999) } Text("备注") .font(.system(size: 13)) .foregroundStyle(CooperationOrderColors.text999) Text(lead.remark.isEmpty ? "暂无备注" : lead.remark) .font(.system(size: 14)) .foregroundStyle(CooperationOrderColors.text333) .lineSpacing(7) Text("图片") .font(.system(size: 13)) .foregroundStyle(CooperationOrderColors.text999) ReferralLeadImageGrid(images: lead.displayImages) { images, index in previewImages = images previewIndex = index } } } .sheet(item: previewBinding) { item in SimpleImagePreviewSheet( imageUrls: item.urls, startIndex: item.index, onDismiss: { previewIndex = nil } ) } } private var previewBinding: Binding { Binding( get: { guard let previewIndex else { return nil } return CooperationImagePreviewItem(urls: previewImages, index: previewIndex) }, set: { newValue in if newValue == nil { previewIndex = nil } } ) } } struct ReferralLeadImageGrid: View { let images: [String] var onImageTap: ([String], Int) -> Void = { _, _ in } var body: some View { if images.isEmpty { ZStack { RoundedRectangle(cornerRadius: 6) .fill(CooperationOrderColors.imageEmptyBg) .frame(maxWidth: .infinity) .frame(height: 72) Text("暂无图片") .font(.system(size: 13)) .foregroundStyle(CooperationOrderColors.text999) } } else { HStack(spacing: 8) { ForEach(0..<3, id: \.self) { index in imageCell(at: index) } } } } /// 渲染单个网格位,先固定 1:1 容器再填充图片,避免 Kingfisher 按原图尺寸撑开导致格子大小不一。 @ViewBuilder private func imageCell(at index: Int) -> some View { Group { if index < images.count, let url = URL(string: images[index]) { RoundedRectangle(cornerRadius: 6) .fill(CooperationOrderColors.imagePlaceholderBg) .overlay { KFImage(url) .placeholder { CooperationOrderColors.imagePlaceholderBg } .resizable() .scaledToFill() } } else { Color.clear } } .aspectRatio(1, contentMode: .fit) .frame(maxWidth: .infinity) .clipShape(RoundedRectangle(cornerRadius: 6)) .contentShape(Rectangle()) .onTapGesture { guard index < images.count else { return } onImageTap(images, index) } } } struct AcquisitionOrderCard: View { let order: AcquisitionOrder var body: some View { CooperationCard { VStack(alignment: .leading, spacing: 10) { Text(order.projectName.isEmpty ? "—" : order.projectName) .font(.system(size: 16, weight: .bold)) .foregroundStyle(CooperationOrderColors.text333) .lineLimit(1) .truncationMode(.tail) HStack(alignment: .center, spacing: 8) { if !order.orderTypeLabel.isEmpty { CooperationTypeTag(text: order.orderTypeLabel) } Text("订单号 \(order.orderNumber)") .font(.system(size: 13)) .foregroundStyle(CooperationOrderColors.text666) .lineLimit(1) .truncationMode(.tail) } CooperationListInfoRow( label: "合作人员", value: order.displayPartner.isEmpty ? "暂未绑定" : order.displayPartner ) CooperationListInfoRow( label: "下单时间", value: order.createdAt.isEmpty ? "—" : order.createdAt ) HStack(alignment: .center, spacing: 0) { Text("客户 \(order.displayCustomerPhone.isEmpty ? "—" : order.displayCustomerPhone)") .font(.system(size: 12)) .foregroundStyle(CooperationOrderColors.text999) .frame(maxWidth: .infinity, alignment: .leading) Text("¥\(order.displayAmount.isEmpty ? "—" : order.displayAmount)") .font(.system(size: 17, weight: .bold)) .foregroundStyle(CooperationOrderColors.primary) } } } } } struct CooperativeSalerCard: View { let acquirer: CooperativeSaler let onDial: (String) -> Void let onEditRemark: () -> Void var body: some View { ZStack(alignment: .topTrailing) { HStack(alignment: .center, spacing: 0) { Image(systemName: "person.fill") .font(.system(size: 20)) .foregroundStyle(CooperationOrderColors.primary) .padding(10) .background(CooperationOrderColors.primary.opacity(0.08), in: RoundedRectangle(cornerRadius: 8)) VStack(alignment: .leading, spacing: 8) { AcquirerInfoRow( label: "获客员名称", value: acquirer.displayName.isEmpty ? "—" : acquirer.displayName, emphasize: true ) AcquirerPhoneInfoRow(phone: acquirer.displayPhone, onDial: onDial) AcquirerInfoRow( label: "绑定时间", value: acquirer.displayBindTime.isEmpty ? "—" : acquirer.displayBindTime ) } .padding(.leading, 12) .frame(maxWidth: .infinity, alignment: .leading) } .padding(.horizontal, 14) .padding(.vertical, 16) .frame(maxWidth: .infinity, alignment: .leading) .background(.white, in: RoundedRectangle(cornerRadius: 10)) .shadow(color: Color.black.opacity(0.06), radius: 1, x: 0, y: 1) Button("修改备注") { onEditRemark() } .font(.system(size: 13)) .foregroundStyle(CooperationOrderColors.primary) .padding(.top, 16) .padding(.trailing, 14) } } } struct AcquirerInfoRow: View { let label: String let value: String var emphasize: Bool = false var body: some View { HStack(alignment: .center, spacing: 0) { Text(label) .font(.system(size: 13)) .foregroundStyle(CooperationOrderColors.text999) Text(value) .font(.system(size: emphasize ? 15 : 13, weight: emphasize ? .bold : .regular)) .foregroundStyle(emphasize ? CooperationOrderColors.text333 : CooperationOrderColors.text666) .padding(.leading, 12) } .frame(maxWidth: .infinity, alignment: .leading) } } struct AcquirerPhoneInfoRow: View { let phone: String let onDial: (String) -> Void var body: some View { HStack(alignment: .center, spacing: 0) { Text("手机号") .font(.system(size: 13)) .foregroundStyle(CooperationOrderColors.text999) Text(maskAcquirerPhone(phone)) .font(.system(size: 13)) .foregroundStyle(CooperationOrderColors.text666) .padding(.leading, 12) Spacer(minLength: 8) if !phone.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { Button { onDial(phone) } label: { Image(systemName: "phone.fill") .font(.system(size: 15)) .foregroundStyle(CooperationOrderColors.primary) .frame(width: 30, height: 30) .background(Color(hex: 0xEFF6FF), in: Circle()) } .buttonStyle(.plain) } } .frame(maxWidth: .infinity, alignment: .leading) } } func maskAcquirerPhone(_ phone: String) -> String { let digits = phone.filter(\.isNumber) guard digits.count >= 7 else { return phone.isEmpty ? "—" : phone } return "\(digits.prefix(3))****\(digits.suffix(4))" }