277 lines
9.1 KiB
Swift
277 lines
9.1 KiB
Swift
//
|
||
// 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
|
||
}
|
||
}
|