// // OrderListComponents.swift // suixinkan // // Created by Codex on 2026/6/29. // import SwiftUI /// 订单列表 UI 工具,集中状态色与通用展示逻辑。 enum OrderListUI { /// 返回订单状态对应的文字色和背景色。 static func statusColors(for status: Int) -> (text: Color, background: Color) { switch status { case 18: return (AppDesign.orderStatusPaidText, AppDesign.orderStatusPaidBackground) case 30: return (AppDesign.orderStatusCompletedText, AppDesign.orderStatusCompletedBackground) case 50: return (AppDesign.orderStatusRefundedText, AppDesign.orderStatusRefundedBackground) default: return (AppDesign.orderStatusOtherText, AppDesign.orderStatusOtherBackground) } } /// 掩码手机号,保留前三后四。 static func maskPhone(_ phone: String) -> String { let trimmed = phone.trimmingCharacters(in: .whitespacesAndNewlines) guard trimmed.count >= 7 else { return trimmed.isEmpty ? "--" : trimmed } return "\(trimmed.prefix(3))****\(trimmed.suffix(4))" } /// 付款时间展示,兼容 depositPayTime 兜底。 static func displayPayTime(payTime: String, depositPayTime: String) -> String { if payTime.isEmpty { return depositPayTime.isEmpty ? "--" : depositPayTime } if payTime.hasPrefix("1970") || payTime == "0" { return depositPayTime.isEmpty ? payTime : depositPayTime } return payTime } } /// 订单状态 Chip,对齐 Android OrderView 状态标签。 struct OrderStatusChip: View { let text: String let status: Int var body: some View { let colors = OrderListUI.statusColors(for: status) Text(text.isEmpty ? "--" : text) .font(.system(size: 14)) .foregroundStyle(colors.text) .padding(.horizontal, 4) .padding(.vertical, 2) .background(colors.background, in: RoundedRectangle(cornerRadius: 4)) } } /// 订单卡片信息行,标签在左、值在右。 struct OrderInfoRow: View { let title: String let value: String var valueColor: Color = .black var trailing: AnyView? init(title: String, value: String, valueColor: Color = .black, @ViewBuilder trailing: () -> some View = { EmptyView() }) { self.title = title self.value = value self.valueColor = valueColor let built = trailing() self.trailing = built is EmptyView ? nil : AnyView(built) } var body: some View { HStack(alignment: .center, spacing: 8) { Text(title) .font(.system(size: 14)) .foregroundStyle(AppDesign.orderLabelColor) Spacer(minLength: 8) if let trailing { trailing } else { Text(value) .font(.system(size: 14, weight: .medium)) .foregroundStyle(valueColor) .multilineTextAlignment(.trailing) .lineLimit(2) .minimumScaleFactor(0.75) } } } } /// 订单卡片操作按钮,对齐 Android AppButton 样式。 struct OrderActionButton: View { let title: String var textColor: Color = .white var backgroundColor: Color = AppDesign.primary var maxWidth: CGFloat? = .infinity let action: () -> Void var body: some View { Button(action: action) { Text(title) .font(.system(size: 16)) .foregroundStyle(textColor) .frame(maxWidth: maxWidth) .frame(height: 48) .background(backgroundColor, in: RoundedRectangle(cornerRadius: 8)) } .buttonStyle(.plain) } } /// 订单卡片拨号按钮。 struct OrderPhoneCallButton: View { let action: () -> Void var body: some View { Button(action: action) { Image(systemName: "phone.fill") .font(.system(size: 12)) .foregroundStyle(AppDesign.primary) .frame(width: 24, height: 24) .background(AppDesign.primarySoft, in: Circle()) } .buttonStyle(.plain) } } /// 需要剪辑修图单选区,对齐 Android ExclusiveChoiceButtons。 struct OrderRefinedChoiceSection: View { let currentSelection: Int let readOnly: Bool let onSelectionChange: (Int) -> Void var body: some View { HStack { Text("需要剪辑修图") .font(.system(size: 14)) .foregroundStyle(AppDesign.orderLabelColor) Spacer() if readOnly { Text(refinedDisplayText) .font(.system(size: 14, weight: .medium)) .foregroundStyle(.black) } else { HStack(spacing: 16) { refinedChoiceButton(title: "是", value: 1) refinedChoiceButton(title: "否", value: 2) } } } } private var refinedDisplayText: String { switch currentSelection { case 1: return "是" case 2: return "否" default: return "--" } } private func refinedChoiceButton(title: String, value: Int) -> some View { let selected = currentSelection == value return Button { onSelectionChange(value) } label: { HStack(spacing: 4) { ZStack { Circle() .stroke(AppDesign.primary, lineWidth: selected ? 0 : 1.5) .background(selected ? Circle().fill(AppDesign.primary) : nil) .frame(width: 20, height: 20) if selected { Image(systemName: "checkmark") .font(.system(size: 10, weight: .bold)) .foregroundStyle(.white) } } Text(title) .font(.system(size: 14)) .foregroundStyle(.black) } } .buttonStyle(.plain) } } /// 修图/视频数量赠送步进控件。 struct OrderGiftStepperRow: View { let title: String let valueText: String let editable: Bool let onTap: () -> Void var body: some View { HStack { Text(title) .font(.system(size: 14)) .foregroundStyle(AppDesign.orderLabelColor) Spacer() if editable { HStack(spacing: 8) { giftStepButton(symbol: "-", action: onTap) Text(valueText) .font(.system(size: 14, weight: .medium)) .foregroundStyle(.black) giftStepButton(symbol: "+", action: onTap) } } else { Text(valueText) .font(.system(size: 14, weight: .medium)) .foregroundStyle(.black) } } } private func giftStepButton(symbol: String, action: @escaping () -> Void) -> some View { Button(action: action) { Text(symbol) .font(.system(size: 16, weight: .bold)) .foregroundStyle(Color(hex: 0xE5E7EB)) .frame(width: 24, height: 24) .overlay(Circle().stroke(Color(hex: 0xE5E7EB), lineWidth: 1)) } .buttonStyle(.plain) } } /// 核销管理页横向 pill Tab。 struct WriteOffStatusTabBar: View { let filters: [OrderStatusFilter] let selectedStatus: Int let onSelect: (Int) -> Void var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(filters) { filter in let selected = selectedStatus == filter.id Button { onSelect(filter.id) } label: { Text(filter.title) .font(.system(size: 14)) .foregroundStyle(selected ? .white : AppDesign.orderLabelColor) .padding(.horizontal, 12) .frame(height: 32) .background( selected ? AppDesign.primary : Color(hex: 0xF3F4F6), in: RoundedRectangle(cornerRadius: 16) ) } .buttonStyle(.plain) } } .padding(.horizontal, 16) } } } /// 订单列表 `.searchable` 搜索行为,区分「输入中」与「已提交查询」,清空时重新拉全量。 enum OrderListSearchBehavior { /// 归一化搜索文本。 static func normalized(_ text: String) -> String { text.trimmingCharacters(in: .whitespacesAndNewlines) } /// 清空搜索框后是否需要重新请求(此前已提交过非空查询)。 static func shouldReloadAfterClear(newText: String, appliedQuery: String) -> Bool { normalized(newText).isEmpty && !appliedQuery.isEmpty } }