新增门店订单与核销管理列表,对齐 Android 订单能力并完善账号级缓存。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
276
suixinkan/Features/Orders/Views/OrderListComponents.swift
Normal file
276
suixinkan/Features/Orders/Views/OrderListComponents.swift
Normal file
@ -0,0 +1,276 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user