离线或倒计时未进入提醒窗口时不再弹出超时提醒;全局 Loading 支持按需展示定位文案,提前提醒选项抽成共用组件。 Co-authored-by: Cursor <cursoragent@cursor.com>
377 lines
13 KiB
Swift
377 lines
13 KiB
Swift
import SwiftUI
|
|
import Kingfisher
|
|
|
|
enum OrderSourceColors {
|
|
static let primary = Color(hex: 0x0073FF)
|
|
static let text333 = Color(hex: 0x333333)
|
|
static let text563 = Color(hex: 0x4B5563)
|
|
static let text666 = Color(hex: 0x666666)
|
|
static let text999 = Color(hex: 0x999999)
|
|
static let border = Color(hex: 0xE6EAF0)
|
|
static let divider = Color(hex: 0xE9EDF3)
|
|
static let pageBg = Color(hex: 0xF5F7FA)
|
|
static let primarySoft = Color(hex: 0xEFF6FF)
|
|
static let imageEmptyBg = Color(hex: 0xF3F5F8)
|
|
static let entryBg = Color(hex: 0xF9FAFC)
|
|
}
|
|
|
|
struct AcquirerCallButton: View {
|
|
let phone: String
|
|
let onCall: (String) -> Void
|
|
|
|
var body: some View {
|
|
Button {
|
|
onCall(phone)
|
|
} label: {
|
|
Image(systemName: "phone.fill")
|
|
.font(.system(size: 12, weight: .semibold))
|
|
.foregroundStyle(OrderSourceColors.primary)
|
|
.frame(width: 32, height: 32)
|
|
.background(OrderSourceColors.primarySoft, in: Circle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityLabel("拨打\(phone)")
|
|
}
|
|
}
|
|
|
|
struct SourcePersonRow: View {
|
|
let person: OrderSourcePerson
|
|
let selected: Bool
|
|
let onCall: (String) -> Void
|
|
let onTap: () -> Void
|
|
|
|
var body: some View {
|
|
HStack(alignment: .center, spacing: 8) {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Text(person.name)
|
|
.font(.system(size: 16, weight: .medium))
|
|
.foregroundStyle(OrderSourceColors.text333)
|
|
if !person.phone.isEmpty {
|
|
Text(person.phone)
|
|
.font(.system(size: 14))
|
|
.foregroundStyle(OrderSourceColors.text666)
|
|
.padding(.top, 6)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
if !person.phone.isEmpty {
|
|
AcquirerCallButton(phone: person.phone, onCall: onCall)
|
|
}
|
|
|
|
Image(systemName: "chevron.right")
|
|
.font(.system(size: 12, weight: .semibold))
|
|
.foregroundStyle(OrderSourceColors.text666)
|
|
.frame(width: 20, height: 20)
|
|
}
|
|
.padding(.horizontal, 14)
|
|
.padding(.vertical, 14)
|
|
.background(
|
|
selected ? OrderSourceColors.primary.opacity(0.05) : Color.white,
|
|
in: RoundedRectangle(cornerRadius: 8)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.stroke(selected ? OrderSourceColors.primary : OrderSourceColors.border, lineWidth: 1)
|
|
)
|
|
.contentShape(RoundedRectangle(cornerRadius: 8))
|
|
.onTapGesture(perform: onTap)
|
|
}
|
|
}
|
|
|
|
struct SourceLeadContent: View {
|
|
/// 带客单缩略图固定三列等宽网格。
|
|
private static let leadImageColumns = Array(repeating: GridItem(.flexible()), count: 3)
|
|
|
|
let lead: OrderSourceLead
|
|
var compact = false
|
|
var enablePhoneCall = true
|
|
let onCall: (String) -> Void
|
|
let onImageTap: ([String], Int) -> Void
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: compact ? 6 : 10) {
|
|
if !lead.userPhone.isEmpty {
|
|
HStack(alignment: .center, spacing: 0) {
|
|
Text("客户手机号")
|
|
.font(.system(size: 12))
|
|
.foregroundStyle(OrderSourceColors.text666)
|
|
Text(lead.userPhone)
|
|
.font(.system(size: 13, weight: .medium))
|
|
.foregroundStyle(enablePhoneCall ? OrderSourceColors.primary : OrderSourceColors.text333)
|
|
.padding(.leading, 10)
|
|
.onTapGesture {
|
|
if enablePhoneCall {
|
|
onCall(lead.userPhone)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !lead.remark.isEmpty {
|
|
Text(lead.remark)
|
|
.font(.system(size: compact ? 12 : 13))
|
|
.foregroundStyle(OrderSourceColors.text333)
|
|
.lineSpacing(compact ? 4 : 6)
|
|
.lineLimit(compact ? 2 : 4)
|
|
}
|
|
|
|
if lead.images.isEmpty {
|
|
if !compact {
|
|
Text("暂无图片")
|
|
.font(.system(size: 12))
|
|
.foregroundStyle(OrderSourceColors.text666)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 16)
|
|
.background(OrderSourceColors.imageEmptyBg, in: RoundedRectangle(cornerRadius: 6))
|
|
}
|
|
} else {
|
|
LazyVGrid(columns: Self.leadImageColumns, spacing: 8) {
|
|
ForEach(0..<3, id: \.self) { index in
|
|
if index < min(lead.images.count, 3) {
|
|
leadThumbnail(imageUrl: lead.images[index], index: index)
|
|
} else {
|
|
Color.clear
|
|
.aspectRatio(1, contentMode: .fit)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func leadThumbnail(imageUrl: String, index: Int) -> some View {
|
|
Color.clear
|
|
.aspectRatio(1, contentMode: .fit)
|
|
.frame(maxWidth: .infinity)
|
|
.overlay {
|
|
leadThumbnailImage(imageUrl: imageUrl)
|
|
}
|
|
.clipShape(RoundedRectangle(cornerRadius: 6))
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
onImageTap(lead.images, index)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func leadThumbnailImage(imageUrl: String) -> some View {
|
|
if let url = URL(string: imageUrl), !imageUrl.isEmpty {
|
|
KFImage(url)
|
|
.placeholder { OrderSourceColors.imageEmptyBg }
|
|
.resizable()
|
|
.scaledToFill()
|
|
} else {
|
|
OrderSourceColors.imageEmptyBg
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SourceLeadCard: View {
|
|
let lead: OrderSourceLead
|
|
let selected: Bool
|
|
let onCall: (String) -> Void
|
|
let onImageTap: ([String], Int) -> Void
|
|
let onSelect: () -> Void
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
HStack(alignment: .center) {
|
|
Text("带客单")
|
|
.font(.system(size: 13, weight: .medium))
|
|
.foregroundStyle(OrderSourceColors.primary)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(OrderSourceColors.primary.opacity(0.08), in: RoundedRectangle(cornerRadius: 4))
|
|
|
|
Spacer()
|
|
|
|
if selected {
|
|
Image(systemName: "checkmark")
|
|
.font(.system(size: 14, weight: .bold))
|
|
.foregroundStyle(OrderSourceColors.primary)
|
|
.frame(width: 20, height: 20)
|
|
}
|
|
}
|
|
|
|
SourceLeadContent(
|
|
lead: lead,
|
|
compact: false,
|
|
enablePhoneCall: true,
|
|
onCall: onCall,
|
|
onImageTap: onImageTap
|
|
)
|
|
|
|
Text(selected ? "当前已选择" : "选择此带客单")
|
|
.font(.system(size: 14))
|
|
.foregroundStyle(selected ? OrderSourceColors.primary : .white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 10)
|
|
.background(
|
|
selected ? OrderSourceColors.primary.opacity(0.1) : OrderSourceColors.primary,
|
|
in: RoundedRectangle(cornerRadius: 6)
|
|
)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture(perform: onSelect)
|
|
}
|
|
.padding(12)
|
|
.background(
|
|
selected ? OrderSourceColors.primary.opacity(0.04) : Color.white,
|
|
in: RoundedRectangle(cornerRadius: 8)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.stroke(selected ? OrderSourceColors.primary : OrderSourceColors.border, lineWidth: 1)
|
|
)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture(perform: onSelect)
|
|
}
|
|
}
|
|
|
|
struct AcquirerSelectCard: View {
|
|
let name: String
|
|
let phone: String
|
|
let onTap: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: onTap) {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Text(name)
|
|
.font(.system(size: 16, weight: .bold))
|
|
.foregroundStyle(OrderSourceColors.text333)
|
|
if !phone.isEmpty {
|
|
Text(phone)
|
|
.font(.system(size: 13))
|
|
.foregroundStyle(OrderSourceColors.text666)
|
|
.padding(.top, 6)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(14)
|
|
.background(.white, in: RoundedRectangle(cornerRadius: 10))
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
struct ReferralOrderSelectCard: View {
|
|
let order: ReferralOrder
|
|
let selected: Bool
|
|
let onTap: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: onTap) {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("带客单号 \(order.displayReferralOrderNo)")
|
|
.font(.system(size: 15, weight: .bold))
|
|
.foregroundStyle(OrderSourceColors.text333)
|
|
|
|
if !order.statusLabel.isEmpty {
|
|
Text(order.statusLabel)
|
|
.font(.system(size: 13))
|
|
.foregroundStyle(OrderSourceColors.text666)
|
|
}
|
|
|
|
Text("用户 \(order.userMobile.isEmpty ? "—" : order.userMobile)")
|
|
.font(.system(size: 12))
|
|
.foregroundStyle(OrderSourceColors.text999)
|
|
|
|
if !order.remark.isEmpty {
|
|
Text(order.remark)
|
|
.font(.system(size: 13))
|
|
.foregroundStyle(OrderSourceColors.text666)
|
|
}
|
|
|
|
if !order.createdAt.isEmpty {
|
|
Text(order.createdAt)
|
|
.font(.system(size: 11))
|
|
.foregroundStyle(OrderSourceColors.text999)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(14)
|
|
.background(.white, in: RoundedRectangle(cornerRadius: 10))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.stroke(selected ? OrderSourceColors.primary : Color.clear, lineWidth: 1.5)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
struct SimpleImagePreviewContent: View {
|
|
let imageUrls: [String]
|
|
@Binding var currentIndex: Int
|
|
|
|
var body: some View {
|
|
TabView(selection: $currentIndex) {
|
|
ForEach(Array(imageUrls.enumerated()), id: \.offset) { index, urlString in
|
|
Group {
|
|
if let url = URL(string: urlString), !urlString.isEmpty {
|
|
KFImage(url)
|
|
.resizable()
|
|
.scaledToFit()
|
|
} else {
|
|
Text("无法加载图片")
|
|
.foregroundStyle(OrderSourceColors.text666)
|
|
}
|
|
}
|
|
.tag(index)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(Color.black)
|
|
}
|
|
}
|
|
.tabViewStyle(.page(indexDisplayMode: imageUrls.count > 1 ? .automatic : .never))
|
|
.background(Color.black.ignoresSafeArea())
|
|
}
|
|
}
|
|
|
|
struct SimpleImagePreviewSheet: View {
|
|
let imageUrls: [String]
|
|
let startIndex: Int
|
|
let onDismiss: () -> Void
|
|
|
|
@State private var currentIndex: Int
|
|
|
|
init(imageUrls: [String], startIndex: Int, onDismiss: @escaping () -> Void) {
|
|
self.imageUrls = imageUrls
|
|
self.startIndex = startIndex
|
|
self.onDismiss = onDismiss
|
|
_currentIndex = State(initialValue: startIndex)
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
SimpleImagePreviewContent(imageUrls: imageUrls, currentIndex: $currentIndex)
|
|
.navigationTitle("图片预览")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("关闭", action: onDismiss)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SimpleImagePreviewScreen: View {
|
|
let imageUrls: [String]
|
|
let startIndex: Int
|
|
|
|
@State private var currentIndex: Int
|
|
|
|
init(imageUrls: [String], startIndex: Int) {
|
|
self.imageUrls = imageUrls
|
|
self.startIndex = startIndex
|
|
_currentIndex = State(initialValue: startIndex)
|
|
}
|
|
|
|
var body: some View {
|
|
SimpleImagePreviewContent(imageUrls: imageUrls, currentIndex: $currentIndex)
|
|
.navigationTitle("图片预览")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
}
|