Files
suixinkan_ios_new/suixinkan/Features/CooperationOrder/Views/CooperationOrderComponents.swift

402 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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<Content: View>: 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<CooperationImagePreviewItem?> {
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))"
}