Rebuild order list UI to strictly match Android OrderView and DepositOrderCard.

Add OrderTokens and shared order components, restore filter bar pills, list loading states, and role-specific card layouts with deposit badge mapping tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 09:12:06 +08:00
parent 87696a4774
commit fe84a7e8f4
17 changed files with 1235 additions and 379 deletions

View File

@ -0,0 +1,44 @@
//
// OrderTokens.swift
// suixinkan
//
import CoreGraphics
import UIKit
/// Android `OrderListScreen` / `DepositOrderListScreen` inline
enum OrderTokens {
// MARK: - Colors
static let filterBackground = UIColor(hex: 0xF4F4F4)
static let searchPlaceholder = UIColor(hex: 0xB6BECA)
static let label563 = UIColor(hex: 0x4B5563)
static let timestamp = UIColor(hex: 0x6B7280)
static let orderNumber = UIColor.black
static let chipBackground = UIColor(hex: 0xEFF6FF)
static let phoneIconBackground = UIColor(hex: 0xEFF6FF)
static let giftBorder = UIColor(hex: 0xE5E7EB)
static let sourceBorder = UIColor(hex: 0xE6EAF0)
static let sourceBackground = UIColor(hex: 0xF9FAFC)
static let depositSuccess = UIColor(hex: 0x27AE60)
static let depositSuccessBackground = UIColor(hex: 0xE8F8F0)
static let depositRefunded = UIColor(hex: 0x6B7280)
static let depositRefundedBackground = UIColor(hex: 0xF3F4F6)
static let remarkDivider = UIColor(hex: 0xF4F4F4)
static let valueBlack = UIColor.black
// MARK: - Layout
static let searchHeight: CGFloat = 36
static let filterPillHeight: CGFloat = 35
static let actionButtonHeight: CGFloat = 48
static let cardRadius: CGFloat = 12
static let pillRadius: CGFloat = 4
static let cardPadding: CGFloat = 16
static let cardGap: CGFloat = 12
static let filterGap: CGFloat = 8
static let infoRowSpacing: CGFloat = 8
static let dividerHeight: CGFloat = 0.5
static let refundButtonWidthRatio: CGFloat = 0.3
}

View File

@ -18,23 +18,19 @@ final class AppStatusBadge: UIView {
case neutral
}
/// badge
enum OrderBadgeStyle {
case photographer
case deposit
}
private let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
AppFont.captionMedium.apply(to: label)
label.textAlignment = .center
addSubview(label)
label.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(
UIEdgeInsets(
top: AppSpacing.xxs,
left: AppSpacing.xs,
bottom: AppSpacing.xxs,
right: AppSpacing.xs
)
)
}
applyOrderBadgeStyle(.photographer)
layer.cornerRadius = AppRadius.xs
clipsToBounds = true
}
@ -44,6 +40,26 @@ final class AppStatusBadge: UIView {
fatalError("init(coder:) has not been implemented")
}
/// badge 14sp / 12sp
func applyOrderBadgeStyle(_ style: OrderBadgeStyle) {
switch style {
case .photographer:
label.font = .app(.bodyMedium)
label.snp.remakeConstraints { make in
make.edges.equalToSuperview().inset(
UIEdgeInsets(top: 0, left: AppSpacing.xxs, bottom: 0, right: AppSpacing.xxs)
)
}
case .deposit:
label.font = .app(.caption)
label.snp.remakeConstraints { make in
make.edges.equalToSuperview().inset(
UIEdgeInsets(top: 2, left: AppSpacing.xs, bottom: 2, right: AppSpacing.xs)
)
}
}
}
///
func apply(tone: Tone, text: String) {
label.text = text
@ -61,15 +77,16 @@ final class AppStatusBadge: UIView {
label.textColor = AppColor.danger
backgroundColor = AppColor.dangerBackground
case .neutral:
label.textColor = AppColor.textTabInactive
backgroundColor = AppColor.inputBackground
label.textColor = OrderTokens.depositRefunded
backgroundColor = OrderTokens.depositRefundedBackground
}
}
/// Android OrderView
/// Android `OrderView`
func applyOrderStatus(_ status: Int, text: String) {
applyOrderBadgeStyle(.photographer)
switch status {
case 18, 20:
case 18:
apply(tone: .info, text: text)
case 30:
apply(tone: .success, text: text)
@ -79,4 +96,21 @@ final class AppStatusBadge: UIView {
apply(tone: .warning, text: text)
}
}
/// Android `DepositOrderCard`
func applyDepositOrderStatus(_ status: Int, text: String) {
applyOrderBadgeStyle(.deposit)
switch status {
case 20:
apply(tone: .info, text: text)
case 30:
label.text = text
label.textColor = OrderTokens.depositSuccess
backgroundColor = OrderTokens.depositSuccessBackground
case 50:
apply(tone: .neutral, text: text)
default:
apply(tone: .warning, text: text)
}
}
}

View File

@ -10,29 +10,35 @@ struct DepositOrderCardActions {
var onRefund: (() -> Void)?
var onPartialRefund: (() -> Void)?
var onVerify: (() -> Void)?
var onCallPhone: (() -> Void)?
var onTap: (() -> Void)?
}
/// Android `DepositOrderListScreen`
/// Android `DepositOrderCard`
final class DepositOrderCardCell: UITableViewCell {
static let reuseIdentifier = "DepositOrderCardCell"
private let cardView = UIView()
private let projectLabel = UILabel()
private let statusBadge = OrderStatusBadgeView()
private let amountLabel = UILabel()
private let timeLabel = UILabel()
private let uidLabel = UILabel()
private let phoneLabel = UILabel()
private let refundButton = UIButton(type: .system)
private let partialRefundButton = UIButton(type: .system)
private let verifyButton = UIButton(type: .system)
private let contentStack = UIStackView()
private let headerRow = UIStackView()
private let orderNumberLabel = UILabel()
private let statusBadge = DepositOrderStatusBadgeView()
private let createdAtLabel = UILabel()
private let typeChip = OrderTypeChipView()
private let infoStack = UIStackView()
private let phoneRow = OrderPhoneRowView(style: .deposit)
private let actionRow = UIStackView()
private let refundButton = OrderActionButton(title: "退款", style: .destructiveLight)
private let partialRefundButton = OrderActionButton(title: "部分退款", style: .dangerFilled)
private let verifyButton = OrderActionButton(title: "核销", style: .primaryFilled)
private var actions = DepositOrderCardActions()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.backgroundColor = .clear
setupUI()
}
@ -43,98 +49,76 @@ final class DepositOrderCardCell: UITableViewCell {
func configure(item: StoreOrderItem, actions: DepositOrderCardActions) {
self.actions = actions
projectLabel.text = item.projectName.isEmpty ? item.orderTypeLabel : item.projectName
orderNumberLabel.text = "订单号: \(item.orderNumber)"
statusBadge.apply(status: item.orderStatus, text: item.orderStatusName)
amountLabel.text = "¥\(item.actualPayAmount)"
timeLabel.text = item.createdAt
uidLabel.text = "UID\(item.userId)"
phoneLabel.text = OrderPhoneMask.mask(item.phone)
createdAtLabel.text = item.createdAt
typeChip.apply(text: item.orderTypeLabel)
infoStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
addInfoRow(title: "项目名称", value: item.projectName)
addInfoRow(title: "订单金额", value: "¥ \(item.actualPayAmount)", primary: true)
addInfoRow(title: "付款时间", value: item.payTime.isEmpty ? "--" : item.payTime)
addInfoRow(title: "完成时间", value: item.completeTime.isEmpty ? "--" : item.completeTime)
addInfoRow(title: "用户 UID", value: String(item.userId))
infoStack.addArrangedSubview(phoneRow)
phoneRow.apply(phone: item.phone)
phoneRow.onTap = { [weak self] in self?.actions.onCallPhone?() }
let showVerify = item.orderType == StoreOrderType.offlineProduct && item.orderStatus == 20
verifyButton.isHidden = !showVerify
refundButton.isHidden = item.orderStatus != 20 && item.orderStatus != 30
partialRefundButton.isHidden = item.orderStatus != 20 && item.orderStatus != 30
}
private func setupUI() {
cardView.backgroundColor = .white
cardView.layer.cornerRadius = 12
cardView.backgroundColor = AppColor.cardBackground
cardView.layer.cornerRadius = OrderTokens.cardRadius
let tap = UITapGestureRecognizer(target: self, action: #selector(cardTapped))
cardView.addGestureRecognizer(tap)
projectLabel.font = .systemFont(ofSize: 16, weight: .medium)
amountLabel.font = .systemFont(ofSize: 16, weight: .semibold)
amountLabel.textColor = AppColor.primary
[timeLabel, uidLabel, phoneLabel].forEach {
$0.font = .systemFont(ofSize: 13)
$0.textColor = UIColor(hex: 0x6B7280)
}
orderNumberLabel.font = .app(.body)
orderNumberLabel.textColor = OrderTokens.orderNumber
createdAtLabel.font = .app(.caption)
createdAtLabel.textColor = OrderTokens.timestamp
configureActionButton(refundButton, title: "退款", color: UIColor(hex: 0xEF4444), bg: UIColor(hex: 0xFFE7E7))
configureActionButton(partialRefundButton, title: "部分退款", color: AppColor.primary, bg: UIColor(hex: 0xEFF6FF))
configureActionButton(verifyButton, title: "线下核销", color: .white, bg: UIColor(hex: 0x0073FF))
headerRow.axis = .horizontal
headerRow.alignment = .center
headerRow.distribution = .equalSpacing
headerRow.addArrangedSubview(orderNumberLabel)
headerRow.addArrangedSubview(statusBadge)
infoStack.axis = .vertical
infoStack.spacing = AppSpacing.xxs
actionRow.axis = .horizontal
actionRow.spacing = OrderTokens.cardGap
actionRow.distribution = .fillEqually
actionRow.addArrangedSubview(refundButton)
actionRow.addArrangedSubview(partialRefundButton)
refundButton.addTarget(self, action: #selector(refundTapped), for: .touchUpInside)
partialRefundButton.addTarget(self, action: #selector(partialRefundTapped), for: .touchUpInside)
verifyButton.addTarget(self, action: #selector(verifyTapped), for: .touchUpInside)
contentView.addSubview(cardView)
[projectLabel, statusBadge, amountLabel, timeLabel, uidLabel, phoneLabel, refundButton, partialRefundButton, verifyButton].forEach {
cardView.addSubview($0)
contentStack.axis = .vertical
contentStack.spacing = OrderTokens.cardGap
[headerRow, createdAtLabel, typeChip, infoStack, actionRow, verifyButton].forEach {
contentStack.addArrangedSubview($0)
}
contentView.addSubview(cardView)
cardView.addSubview(contentStack)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 16, bottom: 12, right: 16))
make.edges.equalToSuperview().inset(
UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: OrderTokens.cardGap, right: AppSpacing.md)
)
}
projectLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().inset(16)
}
statusBadge.snp.makeConstraints { make in
make.centerY.equalTo(projectLabel)
make.trailing.equalToSuperview().inset(16)
}
amountLabel.snp.makeConstraints { make in
make.top.equalTo(projectLabel.snp.bottom).offset(8)
make.leading.equalToSuperview().inset(16)
}
timeLabel.snp.makeConstraints { make in
make.top.equalTo(amountLabel.snp.bottom).offset(4)
make.leading.equalToSuperview().inset(16)
}
uidLabel.snp.makeConstraints { make in
make.top.equalTo(timeLabel.snp.bottom).offset(4)
make.leading.equalToSuperview().inset(16)
}
phoneLabel.snp.makeConstraints { make in
make.top.equalTo(uidLabel.snp.bottom).offset(4)
make.leading.equalToSuperview().inset(16)
}
refundButton.snp.makeConstraints { make in
make.top.equalTo(phoneLabel.snp.bottom).offset(12)
make.leading.equalToSuperview().inset(16)
make.height.equalTo(40)
make.width.equalTo(88)
make.bottom.equalToSuperview().inset(16)
}
partialRefundButton.snp.makeConstraints { make in
make.centerY.equalTo(refundButton)
make.leading.equalTo(refundButton.snp.trailing).offset(8)
make.height.equalTo(40)
make.width.equalTo(100)
}
verifyButton.snp.makeConstraints { make in
make.centerY.equalTo(refundButton)
make.trailing.equalToSuperview().inset(16)
make.height.equalTo(40)
make.width.equalTo(100)
contentStack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(OrderTokens.cardPadding)
}
}
private func configureActionButton(_ button: UIButton, title: String, color: UIColor, bg: UIColor) {
button.setTitle(title, for: .normal)
button.setTitleColor(color, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14)
button.backgroundColor = bg
button.layer.cornerRadius = 8
private func addInfoRow(title: String, value: String, primary: Bool = false) {
let row = OrderInfoRowView()
row.apply(title: title, value: value, usesPrimaryValue: primary)
infoStack.addArrangedSubview(row)
}
@objc private func refundTapped() { actions.onRefund?() }

View File

@ -22,27 +22,43 @@ struct OrderCardActions {
var onOrderSourceTap: (() -> Void)?
}
/// Android `OrderView`
/// Android `OrderView`
final class OrderCardCell: UITableViewCell {
static let reuseIdentifier = "OrderCardCell"
private let cardView = UIView()
private let contentStack = UIStackView()
private let headerRow = UIStackView()
private let orderNumberLabel = UILabel()
private let statusBadge = OrderStatusBadgeView()
private let createdAtLabel = UILabel()
private let typeChip = UILabel()
private let actionStack = UIStackView()
private let refundButton = UIButton(type: .system)
private let verifyButton = UIButton(type: .system)
private let uploadTaskButton = UIButton(type: .system)
private let infoStack = UIStackView()
private let cancelButton = UIButton(type: .system)
private let typeChip = OrderTypeChipView()
private let orderSourceContainer = UIView()
private let orderSourceButton = UIButton(type: .system)
private let historicalButton = UIButton(type: .system)
private let refinedStack = UIStackView()
private let refinedNeedButton = UIButton(type: .system)
private let refinedNoButton = UIButton(type: .system)
private let actionRow = UIStackView()
private let refundButton = OrderActionButton(title: "退款", style: .destructiveLight)
private let verifyButton = OrderActionButton(title: "核销订单", style: .primaryFilled)
private let uploadTaskButton = OrderActionButton(title: "上传任务", style: .primaryLight)
private let editOptionView = OrderEditOptionView()
private let refundAmountRow = OrderInfoRowView()
private let detailStack = UIStackView()
private let photoCountRow = UIStackView()
private let photoCountLabel = UILabel()
private let photoValueLabel = UILabel()
private let photoGiftStepper = OrderGiftStepperView()
private let videoCountRow = UIStackView()
private let videoCountLabel = UILabel()
private let videoValueLabel = UILabel()
private let videoGiftStepper = OrderGiftStepperView()
private let remarkSection = UIStackView()
private let remarkDivider = UIView()
private let remarkTitleLabel = UILabel()
private let remarkBodyLabel = UILabel()
private let historicalPhotosView = OrderHistoricalPhotosView()
private let cancelButton = OrderActionButton(title: "取消订单", style: .primaryFilled)
private var refundWidthConstraint: Constraint?
private var actions = OrderCardActions()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@ -58,249 +74,266 @@ final class OrderCardCell: UITableViewCell {
fatalError("init(coder:) has not been implemented")
}
func configure(order: OrderEntity, hideWriteActions: Bool, actions: OrderCardActions, showOrderSource: Bool, sourceText: String?) {
func configure(
order: OrderEntity,
hideWriteActions: Bool,
actions: OrderCardActions,
showOrderSource: Bool,
sourceText: String?
) {
self.actions = actions
orderNumberLabel.text = "订单号:\(order.orderNumber)"
statusBadge.apply(status: order.orderStatus, text: order.orderStatusName)
createdAtLabel.text = order.createdAt.isEmpty ? "--" : order.createdAt
typeChip.text = order.orderTypeLabel
typeChip.isHidden = order.orderTypeLabel.isEmpty
typeChip.apply(text: order.orderTypeLabel)
let showRefund = !hideWriteActions && (order.orderStatus == 18 || order.orderStatus == 30) && order.orderType != 19
let showVerify = !hideWriteActions && (
(order.photoTravel?.needCheckIn == true && order.orderStatus != 30) || order.orderType == 19
)
actionRow.isHidden = !showRefund && !showVerify
refundButton.isHidden = !showRefund
verifyButton.isHidden = !showVerify
actionStack.isHidden = !showRefund && !showVerify
updateRefundWidth(showVerify: showVerify && showRefund)
uploadTaskButton.isHidden = hideWriteActions || order.orderType != 19
orderSourceButton.isHidden = !showOrderSource
orderSourceContainer.isHidden = !showOrderSource
orderSourceButton.setTitle(sourceText ?? "选择订单来源", for: .normal)
let showRefined = order.orderStatus == 18 || order.orderStatus == 30
refinedStack.isHidden = !showRefined
updateRefinedButtons(isRefined: order.isRefined, readOnly: hideWriteActions)
cancelButton.isHidden = hideWriteActions || order.photoTravel?.canCancelOrder != true
infoStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
addInfoRow(title: "付款金额", value: "¥\(order.actualPayAmount)")
if !order.payTime.isEmpty { addInfoRow(title: "付款时间", value: order.payTime) }
if !order.completeTime.isEmpty { addInfoRow(title: "完成时间", value: order.completeTime) }
if !order.payTypeName.isEmpty { addInfoRow(title: "付款方式", value: order.payTypeName) }
addInfoRow(title: "UID", value: String(order.userId))
addPhoneRow(phone: order.phone, hidden: hideWriteActions)
if let travel = order.photoTravel, !travel.checkInTime.isEmpty {
addInfoRow(title: "核销时间", value: travel.checkInTime)
let showEdit = (order.orderStatus == 18 || order.orderStatus == 30) && order.orderType != 19
editOptionView.isHidden = !showEdit
if showEdit {
editOptionView.apply(isRefined: order.isRefined, readOnly: hideWriteActions)
}
if let travel = order.photoTravel {
addInfoRow(title: "修图张数", value: "\(travel.orderPhotoNum)")
addInfoRow(title: "视频", value: "\(travel.orderVideoNum)")
let showRefundAmount = order.orderStatus == 50
&& !order.actualRefundAmount.isEmpty
&& order.actualRefundAmount != "0"
refundAmountRow.isHidden = !showRefundAmount
if showRefundAmount {
refundAmountRow.apply(title: "退款金额", value: "¥\(order.actualRefundAmount)", usesPrimaryValue: true)
}
rebuildDetailRows(order: order, hideWriteActions: hideWriteActions)
configureTravelRows(order: order, hideWriteActions: hideWriteActions)
if let remark = order.remark, !remark.isEmpty {
addInfoRow(title: "备注", value: remark)
remarkSection.isHidden = false
remarkBodyLabel.text = remark
} else {
remarkSection.isHidden = true
}
let materials = order.multiTravel?.materialList ?? []
historicalButton.isHidden = materials.isEmpty
historicalPhotosView.isHidden = materials.isEmpty
if !materials.isEmpty {
historicalPhotosView.apply(materials: materials)
}
cancelButton.isHidden = hideWriteActions || order.photoTravel?.canCancelOrder != true
}
private func setupUI() {
cardView.backgroundColor = AppColor.cardBackground
cardView.layer.cornerRadius = AppRadius.lg
cardView.layer.cornerRadius = OrderTokens.cardRadius
orderNumberLabel.font = .app(.body)
orderNumberLabel.textColor = AppColor.textPrimary
orderNumberLabel.textColor = OrderTokens.orderNumber
createdAtLabel.font = .app(.caption)
createdAtLabel.textColor = AppColor.textSecondary
typeChip.font = .app(.caption)
typeChip.textColor = AppColor.primary
typeChip.backgroundColor = AppColor.primaryLight
typeChip.layer.cornerRadius = AppRadius.xs
typeChip.clipsToBounds = true
typeChip.textAlignment = .center
createdAtLabel.textColor = OrderTokens.timestamp
configureActionButton(refundButton, title: "退款", color: AppColor.danger, bg: AppColor.dangerBackground)
configureActionButton(verifyButton, title: "核销订单", color: .white, bg: AppColor.primary)
refundButton.addTarget(self, action: #selector(refundTapped), for: .touchUpInside)
verifyButton.addTarget(self, action: #selector(verifyTapped), for: .touchUpInside)
uploadTaskButton.setTitle("上传任务", for: .normal)
uploadTaskButton.setTitleColor(AppColor.primary, for: .normal)
uploadTaskButton.titleLabel?.font = .app(.subtitle)
uploadTaskButton.backgroundColor = AppColor.primaryLight
uploadTaskButton.layer.cornerRadius = AppRadius.lg
uploadTaskButton.addTarget(self, action: #selector(uploadTaskTapped), for: .touchUpInside)
actionStack.axis = .horizontal
actionStack.spacing = AppSpacing.sm
actionStack.distribution = .fillEqually
actionStack.addArrangedSubview(refundButton)
actionStack.addArrangedSubview(verifyButton)
infoStack.axis = .vertical
infoStack.spacing = 6
cancelButton.setTitle("取消订单", for: .normal)
cancelButton.setTitleColor(AppColor.danger, for: .normal)
cancelButton.titleLabel?.font = .app(.body)
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
headerRow.axis = .horizontal
headerRow.alignment = .center
headerRow.distribution = .equalSpacing
headerRow.addArrangedSubview(orderNumberLabel)
headerRow.addArrangedSubview(statusBadge)
orderSourceContainer.backgroundColor = OrderTokens.sourceBackground
orderSourceContainer.layer.cornerRadius = AppRadius.sm
orderSourceContainer.layer.borderWidth = 1
orderSourceContainer.layer.borderColor = OrderTokens.sourceBorder.cgColor
orderSourceButton.setTitleColor(AppColor.primary, for: .normal)
orderSourceButton.titleLabel?.font = .app(.body)
orderSourceButton.contentHorizontalAlignment = .leading
orderSourceButton.addTarget(self, action: #selector(orderSourceTapped), for: .touchUpInside)
historicalButton.setTitle("历史拍摄", for: .normal)
historicalButton.setTitleColor(AppColor.primary, for: .normal)
historicalButton.titleLabel?.font = .app(.body)
historicalButton.addTarget(self, action: #selector(historicalTapped), for: .touchUpInside)
refinedStack.axis = .horizontal
refinedStack.spacing = AppSpacing.xs
refinedNeedButton.setTitle("需要精修", for: .normal)
refinedNoButton.setTitle("无需精修", for: .normal)
[refinedNeedButton, refinedNoButton].forEach {
$0.titleLabel?.font = .app(.body)
$0.layer.cornerRadius = AppRadius.sm
$0.layer.borderWidth = 1
orderSourceContainer.addSubview(orderSourceButton)
orderSourceButton.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(AppSpacing.sm)
}
refinedNeedButton.addTarget(self, action: #selector(refinedNeedTapped), for: .touchUpInside)
refinedNoButton.addTarget(self, action: #selector(refinedNoTapped), for: .touchUpInside)
refinedStack.addArrangedSubview(refinedNeedButton)
refinedStack.addArrangedSubview(refinedNoButton)
actionRow.axis = .horizontal
actionRow.spacing = OrderTokens.cardGap
actionRow.alignment = .fill
actionRow.addArrangedSubview(refundButton)
actionRow.addArrangedSubview(verifyButton)
refundButton.addTarget(self, action: #selector(refundTapped), for: .touchUpInside)
verifyButton.addTarget(self, action: #selector(verifyTapped), for: .touchUpInside)
uploadTaskButton.addTarget(self, action: #selector(uploadTaskTapped), for: .touchUpInside)
editOptionView.onSelectionChange = { [weak self] value in
self?.actions.onRefinedChanged?(value)
}
detailStack.axis = .vertical
detailStack.spacing = OrderTokens.infoRowSpacing
configureTravelRow(photoCountRow, titleLabel: photoCountLabel, valueLabel: photoValueLabel, title: "修图张数", stepper: photoGiftStepper)
configureTravelRow(videoCountRow, titleLabel: videoCountLabel, valueLabel: videoValueLabel, title: "视频剪辑", stepper: videoGiftStepper)
photoGiftStepper.onMinusTap = { [weak self] in self?.actions.onSendGift?() }
photoGiftStepper.onPlusTap = { [weak self] in self?.actions.onSendGift?() }
videoGiftStepper.onMinusTap = { [weak self] in self?.actions.onSendGift?() }
videoGiftStepper.onPlusTap = { [weak self] in self?.actions.onSendGift?() }
remarkDivider.backgroundColor = OrderTokens.remarkDivider
remarkTitleLabel.text = "备注信息:"
remarkTitleLabel.font = .app(.body)
remarkTitleLabel.textColor = OrderTokens.label563
remarkBodyLabel.font = .app(.body)
remarkBodyLabel.textColor = OrderTokens.label563
remarkBodyLabel.numberOfLines = 0
remarkSection.axis = .vertical
remarkSection.spacing = 6
remarkSection.addArrangedSubview(remarkDivider)
remarkSection.addArrangedSubview(remarkTitleLabel)
remarkSection.addArrangedSubview(remarkBodyLabel)
remarkDivider.snp.makeConstraints { make in
make.height.equalTo(1)
}
historicalPhotosView.onTap = { [weak self] in self?.actions.onHistoricalPhoto?() }
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
contentStack.axis = .vertical
contentStack.spacing = OrderTokens.cardGap
[
headerRow,
createdAtLabel,
typeChip,
orderSourceContainer,
actionRow,
uploadTaskButton,
editOptionView,
refundAmountRow,
detailStack,
photoCountRow,
videoCountRow,
remarkSection,
historicalPhotosView,
cancelButton,
].forEach { contentStack.addArrangedSubview($0) }
contentView.addSubview(cardView)
cardView.addSubview(orderNumberLabel)
cardView.addSubview(statusBadge)
cardView.addSubview(createdAtLabel)
cardView.addSubview(typeChip)
cardView.addSubview(orderSourceButton)
cardView.addSubview(actionStack)
cardView.addSubview(uploadTaskButton)
cardView.addSubview(refinedStack)
cardView.addSubview(infoStack)
cardView.addSubview(historicalButton)
cardView.addSubview(cancelButton)
cardView.addSubview(contentStack)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: AppSpacing.sm, right: AppSpacing.md))
make.edges.equalToSuperview().inset(
UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: OrderTokens.cardGap, right: AppSpacing.md)
)
}
orderNumberLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().inset(16)
}
statusBadge.snp.makeConstraints { make in
make.centerY.equalTo(orderNumberLabel)
make.trailing.equalToSuperview().inset(16)
}
createdAtLabel.snp.makeConstraints { make in
make.top.equalTo(orderNumberLabel.snp.bottom).offset(4)
make.leading.equalToSuperview().inset(16)
}
typeChip.snp.makeConstraints { make in
make.top.equalTo(createdAtLabel.snp.bottom).offset(4)
make.leading.equalToSuperview().inset(16)
make.height.equalTo(20)
make.width.greaterThanOrEqualTo(40)
}
orderSourceButton.snp.makeConstraints { make in
make.top.equalTo(typeChip.snp.bottom).offset(8)
make.leading.trailing.equalToSuperview().inset(16)
}
actionStack.snp.makeConstraints { make in
make.top.equalTo(orderSourceButton.snp.bottom).offset(12)
make.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(48)
}
uploadTaskButton.snp.makeConstraints { make in
make.top.equalTo(actionStack.snp.bottom).offset(12)
make.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(48)
}
refinedStack.snp.makeConstraints { make in
make.top.equalTo(uploadTaskButton.snp.bottom).offset(12)
make.leading.equalToSuperview().inset(16)
}
infoStack.snp.makeConstraints { make in
make.top.equalTo(refinedStack.snp.bottom).offset(12)
make.leading.trailing.equalToSuperview().inset(16)
}
historicalButton.snp.makeConstraints { make in
make.top.equalTo(infoStack.snp.bottom).offset(8)
make.leading.equalToSuperview().inset(16)
}
cancelButton.snp.makeConstraints { make in
make.top.equalTo(historicalButton.snp.bottom).offset(12)
make.leading.equalToSuperview().inset(16)
make.bottom.equalToSuperview().inset(16)
contentStack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(OrderTokens.cardPadding)
}
}
private func configureActionButton(_ button: UIButton, title: String, color: UIColor, bg: UIColor) {
button.setTitle(title, for: .normal)
button.setTitleColor(color, for: .normal)
button.titleLabel?.font = .app(.subtitle)
button.backgroundColor = bg
button.layer.cornerRadius = AppRadius.sm
}
private func addInfoRow(title: String, value: String) {
let row = UIStackView()
private func configureTravelRow(
_ row: UIStackView,
titleLabel: UILabel,
valueLabel: UILabel,
title: String,
stepper: OrderGiftStepperView
) {
row.axis = .horizontal
row.distribution = .fillEqually
let titleLabel = UILabel()
row.alignment = .center
row.distribution = .equalSpacing
titleLabel.text = title
titleLabel.font = .app(.body)
titleLabel.textColor = AppColor.textSecondary
let valueLabel = UILabel()
valueLabel.text = value
valueLabel.font = .app(.body)
valueLabel.textColor = AppColor.textPrimary
valueLabel.textAlignment = .right
titleLabel.textColor = OrderTokens.label563
valueLabel.font = .systemFont(ofSize: 14, weight: .medium)
valueLabel.textColor = OrderTokens.valueBlack
row.addArrangedSubview(titleLabel)
row.addArrangedSubview(stepper)
row.addArrangedSubview(valueLabel)
infoStack.addArrangedSubview(row)
valueLabel.isHidden = true
}
private func addPhoneRow(phone: String, hidden: Bool) {
let row = UIStackView()
row.axis = .horizontal
row.distribution = .fillEqually
let titleLabel = UILabel()
titleLabel.text = "手机号"
titleLabel.font = .app(.body)
titleLabel.textColor = AppColor.textSecondary
let valueButton = UIButton(type: .system)
valueButton.setTitle(OrderPhoneMask.mask(phone), for: .normal)
valueButton.setTitleColor(AppColor.primary, for: .normal)
valueButton.titleLabel?.font = .app(.body)
valueButton.contentHorizontalAlignment = .trailing
valueButton.isHidden = hidden || phone.isEmpty
valueButton.addTarget(self, action: #selector(callTapped), for: .touchUpInside)
row.addArrangedSubview(titleLabel)
row.addArrangedSubview(valueButton)
infoStack.addArrangedSubview(row)
private func updateRefundWidth(showVerify: Bool) {
refundWidthConstraint?.deactivate()
if showVerify {
refundButton.snp.makeConstraints { make in
refundWidthConstraint = make.width.equalTo(actionRow.snp.width).multipliedBy(OrderTokens.refundButtonWidthRatio).constraint
}
}
}
private func updateRefinedButtons(isRefined: Int, readOnly: Bool) {
let selectedBorder = AppColor.primary.cgColor
let normalBorder = AppColor.border.cgColor
refinedNeedButton.layer.borderColor = isRefined == 1 ? selectedBorder : normalBorder
refinedNoButton.layer.borderColor = isRefined == 2 ? selectedBorder : normalBorder
refinedNeedButton.isEnabled = !readOnly
refinedNoButton.isEnabled = !readOnly
private func rebuildDetailRows(order: OrderEntity, hideWriteActions: Bool) {
detailStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
addDetailRow(title: "付款金额", value: "¥\(order.actualPayAmount)", primary: true)
addDetailRow(title: "付款时间", value: resolvedPayTime(order))
addDetailRow(title: "完成时间", value: order.completeTime.isEmpty ? "--" : order.completeTime)
if !order.payTypeName.isEmpty {
addDetailRow(title: "付款方式", value: order.payTypeName)
}
addDetailRow(title: "UID", value: String(order.userId))
let phoneRow = OrderPhoneRowView(style: .photographer)
phoneRow.apply(phone: order.phone, hidden: hideWriteActions)
phoneRow.onTap = { [weak self] in self?.actions.onCallPhone?() }
detailStack.addArrangedSubview(phoneRow)
if !order.projectName.isEmpty {
addDetailRow(title: "关联项目", value: order.projectName)
}
if let travel = order.photoTravel, !travel.checkInTime.isEmpty {
addDetailRow(title: "核销时间", value: travel.checkInTime)
}
}
private func configureTravelRows(order: OrderEntity, hideWriteActions: Bool) {
let showTravel = order.photoTravel != nil && order.orderType != 19 && order.orderType != 14
photoCountRow.isHidden = !showTravel
videoCountRow.isHidden = !showTravel
guard showTravel, let travel = order.photoTravel else { return }
let canGift = travel.canGiftRetouch && !hideWriteActions
let photoText = "\(travel.orderPhotoNum + travel.retouchGiftPhotoNum)"
let videoText = "\(travel.orderVideoNum + travel.retouchGiftVideoNum)"
photoGiftStepper.isHidden = !canGift
videoGiftStepper.isHidden = !canGift
photoValueLabel.isHidden = canGift
videoValueLabel.isHidden = canGift
if canGift {
photoGiftStepper.apply(text: photoText)
videoGiftStepper.apply(text: videoText)
} else {
photoValueLabel.text = photoText
videoValueLabel.text = videoText
}
}
private func addDetailRow(title: String, value: String, primary: Bool = false) {
let row = OrderInfoRowView()
row.apply(title: title, value: value, usesPrimaryValue: primary)
detailStack.addArrangedSubview(row)
}
private func resolvedPayTime(_ order: OrderEntity) -> String {
let payTime = order.payTime
if payTime.isEmpty { return order.depositPayTime.isEmpty ? "--" : order.depositPayTime }
if payTime.hasPrefix("1970") || payTime == "0" {
return order.depositPayTime.isEmpty ? "--" : order.depositPayTime
}
return payTime
}
@objc private func refundTapped() { actions.onRefund?() }
@objc private func verifyTapped() { actions.onVerify?() }
@objc private func uploadTaskTapped() { actions.onUploadTask?() }
@objc private func cancelTapped() { actions.onCancel?() }
@objc private func callTapped() { actions.onCallPhone?() }
@objc private func orderSourceTapped() { actions.onOrderSourceTap?() }
@objc private func historicalTapped() { actions.onHistoricalPhoto?() }
@objc private func refinedNeedTapped() { actions.onRefinedChanged?(1) }
@objc private func refinedNoTapped() { actions.onRefinedChanged?(2) }
}
enum OrderPhoneMask {

View File

@ -6,7 +6,7 @@
import SnapKit
import UIKit
/// + +
/// + + Android Header
final class OrderFilterBarView: UIView {
var onSearchChanged: ((String) -> Void)?
@ -14,16 +14,18 @@ final class OrderFilterBarView: UIView {
var onFilterSelected: ((Int, Int) -> Void)?
var onDateRangeTap: (() -> Void)?
private let searchContainer = UIView()
private let searchIconView = UIImageView(image: UIImage(systemName: "magnifyingglass"))
private let searchField = UITextField()
private let filterButton = UIButton(type: .system)
private let dateButton = UIButton(type: .system)
private let divider = UIView()
private let filterPill = OrderFilterPillControl()
private let datePill = OrderFilterPillControl()
private var isStoreMode = false
private var currentFilterLabel = "全部"
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
backgroundColor = AppColor.cardBackground
setupUI()
}
@ -34,95 +36,102 @@ final class OrderFilterBarView: UIView {
func configure(isStoreMode: Bool, filterLabel: String, dateRangeText: String, searchText: String) {
self.isStoreMode = isStoreMode
currentFilterLabel = filterLabel
searchField.placeholder = isStoreMode ? "输入游客手机号" : "输入手机号搜索"
searchField.attributedPlaceholder = NSAttributedString(
string: searchField.placeholder ?? "",
attributes: [.foregroundColor: OrderTokens.searchPlaceholder]
)
searchField.text = searchText
filterButton.setTitle("\(filterLabel)", for: .normal)
dateButton.setTitle(dateRangeText, for: .normal)
filterPill.pillTitle = filterLabel
datePill.pillTitle = dateRangeText
rebuildFilterMenu(selectedLabel: filterLabel)
}
private func setupUI() {
searchField.font = .systemFont(ofSize: 14)
searchContainer.backgroundColor = OrderTokens.filterBackground
searchContainer.layer.cornerRadius = OrderTokens.pillRadius
searchContainer.clipsToBounds = true
searchIconView.tintColor = .gray
searchIconView.contentMode = .scaleAspectFit
searchField.font = .app(.body)
searchField.textColor = AppColor.textPrimary
searchField.keyboardType = .phonePad
searchField.returnKeyType = .search
searchField.backgroundColor = UIColor(hex: 0xF4F4F4)
searchField.layer.cornerRadius = 4
searchField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 12))
searchField.leftViewMode = .always
searchField.addTarget(self, action: #selector(searchChanged), for: .editingChanged)
searchField.borderStyle = .none
searchField.backgroundColor = .clear
searchField.delegate = self
searchField.addTarget(self, action: #selector(searchChanged), for: .editingChanged)
filterButton.setTitleColor(AppColor.text333, for: .normal)
filterButton.titleLabel?.font = .systemFont(ofSize: 14)
filterButton.addTarget(self, action: #selector(filterTapped), for: .touchUpInside)
divider.backgroundColor = AppColor.border
dateButton.setTitleColor(AppColor.primary, for: .normal)
dateButton.titleLabel?.font = .systemFont(ofSize: 14)
dateButton.contentHorizontalAlignment = .trailing
dateButton.addTarget(self, action: #selector(dateTapped), for: .touchUpInside)
filterPill.showsMenuAsPrimaryAction = true
addSubview(searchField)
addSubview(filterButton)
addSubview(dateButton)
datePill.setTrailingSystemImage("calendar")
datePill.addTarget(self, action: #selector(dateTapped), for: .touchUpInside)
addSubview(searchContainer)
searchContainer.addSubview(searchIconView)
searchContainer.addSubview(searchField)
addSubview(divider)
addSubview(filterPill)
addSubview(datePill)
searchContainer.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(OrderTokens.searchHeight)
}
searchIconView.snp.makeConstraints { make in
make.leading.equalToSuperview().inset(AppSpacing.sm)
make.centerY.equalToSuperview()
make.width.height.equalTo(18)
}
searchField.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(36)
make.leading.equalTo(searchIconView.snp.trailing).offset(10)
make.trailing.equalToSuperview().inset(AppSpacing.sm)
make.centerY.equalToSuperview()
}
filterButton.snp.makeConstraints { make in
make.top.equalTo(searchField.snp.bottom).offset(12)
make.leading.equalToSuperview().inset(16)
make.bottom.equalToSuperview().inset(12)
divider.snp.makeConstraints { make in
make.top.equalTo(searchContainer.snp.bottom).offset(AppSpacing.md)
make.leading.trailing.equalToSuperview()
make.height.equalTo(OrderTokens.dividerHeight)
}
dateButton.snp.makeConstraints { make in
make.centerY.equalTo(filterButton)
make.trailing.equalToSuperview().inset(16)
filterPill.snp.makeConstraints { make in
make.top.equalTo(divider.snp.bottom).offset(AppSpacing.md)
make.leading.equalToSuperview().inset(AppSpacing.md)
make.bottom.equalToSuperview().inset(AppSpacing.md)
}
datePill.snp.makeConstraints { make in
make.top.bottom.width.equalTo(filterPill)
make.leading.equalTo(filterPill.snp.trailing).offset(OrderTokens.filterGap)
make.trailing.equalToSuperview().inset(AppSpacing.md)
}
}
private func rebuildFilterMenu(selectedLabel: String) {
let items: [(String, Int, Int)] = isStoreMode
? StoreOrderFilter.allCases.map { ($0.label, $0.tabIndex, 0) }
: PhotographerOrderFilter.allCases.map { ($0.label, $0.tabIndex, $0.rTabIndex) }
let actions = items.map { title, tab, rTab in
UIAction(title: title, state: title == selectedLabel ? .on : .off) { [weak self] _ in
guard let self else { return }
self.filterPill.pillTitle = title
self.rebuildFilterMenu(selectedLabel: title)
self.onFilterSelected?(tab, rTab)
}
}
filterPill.menu = UIMenu(title: "", options: .displayInline, children: actions)
}
@objc private func searchChanged() {
onSearchChanged?(searchField.text ?? "")
}
@objc private func filterTapped() {
if isStoreMode {
presentStoreFilterMenu()
} else {
presentPhotographerFilterMenu()
}
}
@objc private func dateTapped() {
onDateRangeTap?()
}
private func presentPhotographerFilterMenu() {
let actions: [(String, Int, Int)] = PhotographerOrderFilter.allCases.map {
($0.label, $0.tabIndex, $0.rTabIndex)
}
presentMenu(actions: actions)
}
private func presentStoreFilterMenu() {
let actions: [(String, Int, Int)] = StoreOrderFilter.allCases.map {
($0.label, $0.tabIndex, 0)
}
presentMenu(actions: actions)
}
private func presentMenu(actions: [(String, Int, Int)]) {
guard let presenter = window?.rootViewController?.presentedViewController ?? window?.rootViewController else { return }
let alert = UIAlertController(title: "筛选订单", message: nil, preferredStyle: .actionSheet)
actions.forEach { title, tab, rTab in
alert.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
self?.currentFilterLabel = title
self?.filterButton.setTitle("\(title)", for: .normal)
self?.onFilterSelected?(tab, rTab)
})
}
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
presenter.present(alert, animated: true)
}
}
extension OrderFilterBarView: UITextFieldDelegate {

View File

@ -6,39 +6,53 @@
import SnapKit
import UIKit
///
/// Android LazyColumn
final class OrderListView: UIView {
enum Mode {
case photographer
case store
}
var onRefresh: (() -> Void)?
var onLoadMore: ((Int) -> Void)?
let tableView = UITableView(frame: .zero, style: .plain)
private let refreshControl = UIRefreshControl()
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
private let emptyLabel = UILabel()
private let footerLabel = UILabel()
private let footerSpinner = UIActivityIndicatorView(style: .medium)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hex: 0xF5F5F5)
backgroundColor = AppColor.pageBackground
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.keyboardDismissMode = .onDrag
tableView.contentInset = UIEdgeInsets(top: OrderTokens.cardGap, left: 0, bottom: OrderTokens.cardGap, right: 0)
tableView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(refreshTriggered), for: .valueChanged)
footerLabel.font = .systemFont(ofSize: 12)
footerLabel.textColor = UIColor(hex: 0x9CA3AF)
loadingIndicator.hidesWhenStopped = true
loadingIndicator.color = AppColor.primary
emptyLabel.font = .app(.body)
emptyLabel.textColor = OrderTokens.label563
emptyLabel.text = "暂无订单"
emptyLabel.textAlignment = .center
footerLabel.font = .app(.body)
footerLabel.textColor = OrderTokens.label563
footerLabel.textAlignment = .center
footerLabel.text = "没有更多"
footerSpinner.hidesWhenStopped = true
footerSpinner.color = AppColor.primary
addSubview(tableView)
addSubview(loadingIndicator)
tableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
loadingIndicator.snp.makeConstraints { make in
make.center.equalToSuperview()
}
}
@available(*, unavailable)
@ -46,18 +60,54 @@ final class OrderListView: UIView {
fatalError("init(coder:) has not been implemented")
}
func apply(isRefreshing: Bool, canLoadMore: Bool, isEmpty: Bool) {
func apply(
isRefreshing: Bool,
canLoadMore: Bool,
isEmpty: Bool,
isLoading: Bool = false,
isLoadingMore: Bool = false
) {
if isRefreshing {
if !refreshControl.isRefreshing { refreshControl.beginRefreshing() }
} else {
refreshControl.endRefreshing()
}
footerLabel.text = isEmpty ? "暂无订单" : (canLoadMore ? "" : "没有更多")
tableView.tableFooterView = footerLabelFrame(isEmpty: isEmpty, canLoadMore: canLoadMore)
if isLoading, isEmpty {
loadingIndicator.startAnimating()
tableView.backgroundView = nil
tableView.tableFooterView = nil
} else {
loadingIndicator.stopAnimating()
if isEmpty {
tableView.backgroundView = emptyBackgroundView()
} else {
tableView.backgroundView = nil
}
tableView.tableFooterView = footerView(canLoadMore: canLoadMore, isLoadingMore: isLoadingMore, isEmpty: isEmpty)
}
}
private func footerLabelFrame(isEmpty: Bool, canLoadMore: Bool) -> UIView? {
guard !canLoadMore || isEmpty else { return nil }
private func emptyBackgroundView() -> UIView {
let container = UIView(frame: tableView.bounds)
emptyLabel.frame = container.bounds
emptyLabel.autoresizingMask = [.flexibleWidth, .flexibleHeight]
container.addSubview(emptyLabel)
return container
}
private func footerView(canLoadMore: Bool, isLoadingMore: Bool, isEmpty: Bool) -> UIView? {
guard !isEmpty else { return nil }
if isLoadingMore {
let container = UIView(frame: CGRect(x: 0, y: 0, width: bounds.width, height: 44))
footerSpinner.center = CGPoint(x: container.bounds.midX, y: container.bounds.midY)
footerSpinner.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
container.addSubview(footerSpinner)
footerSpinner.startAnimating()
return container
}
footerSpinner.stopAnimating()
guard !canLoadMore else { return nil }
footerLabel.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 44)
return footerLabel
}

View File

@ -146,7 +146,9 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
listView.apply(
isRefreshing: depositViewModel.isRefreshing,
canLoadMore: depositViewModel.canLoadMore,
isEmpty: depositViewModel.orderList.isEmpty
isEmpty: depositViewModel.orderList.isEmpty,
isLoading: depositViewModel.isRefreshing && depositViewModel.orderList.isEmpty,
isLoadingMore: depositViewModel.isLoadingMore
)
if let response = depositViewModel.writeOffResponse {
depositViewModel.dismissWriteOffDialog()
@ -162,7 +164,9 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
listView.apply(
isRefreshing: orderListViewModel.isRefreshing,
canLoadMore: orderListViewModel.canLoadMore,
isEmpty: orderListViewModel.orderList.isEmpty
isEmpty: orderListViewModel.orderList.isEmpty,
isLoading: orderListViewModel.isRefreshing && orderListViewModel.orderList.isEmpty,
isLoadingMore: orderListViewModel.isLoadingMore
)
if let response = orderListViewModel.signInResponse {
orderListViewModel.clearSignInResponse()
@ -186,11 +190,11 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
private func dateRangeText(start: Date?, end: Date?) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd"
formatter.dateFormat = "yyyy-MM-dd"
if let start, let end {
return "\(formatter.string(from: start))-\(formatter.string(from: end))"
}
return "全部"
return "时间筛选"
}
private func presentDateRangeSheet() {
@ -427,6 +431,10 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
Task { await self?.depositViewModel.processScanResult(result, api: self?.orderAPI ?? NetworkServices.shared.orderAPI) }
}
}
actions.onCallPhone = {
guard let url = URL(string: "tel://\(item.phone)") else { return }
UIApplication.shared.open(url)
}
actions.onTap = { [weak self] in
guard let self else { return }
guard self.depositViewModel.canOpenDetail(for: item) else {

View File

@ -0,0 +1,30 @@
//
// DepositOrderStatusBadgeView.swift
// suixinkan
//
import SnapKit
import UIKit
/// Android `DepositOrderCard` badge12sp
final class DepositOrderStatusBadgeView: UIView {
private let badge = AppStatusBadge()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(badge)
badge.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(status: Int, text: String) {
badge.applyDepositOrderStatus(status, text: text)
}
}

View File

@ -0,0 +1,49 @@
//
// OrderActionButton.swift
// suixinkan
//
import SnapKit
import UIKit
/// Android `AppButton`
final class OrderActionButton: UIButton {
enum Style {
case destructiveLight
case primaryFilled
case primaryLight
case dangerFilled
}
init(title: String, style: Style) {
super.init(frame: .zero)
setTitle(title, for: .normal)
titleLabel?.font = .app(.subtitle)
layer.cornerRadius = AppRadius.sm
clipsToBounds = true
switch style {
case .destructiveLight:
setTitleColor(AppColor.danger, for: .normal)
backgroundColor = AppColor.dangerBackground
case .primaryFilled:
setTitleColor(.white, for: .normal)
backgroundColor = AppColor.primary
case .primaryLight:
setTitleColor(AppColor.primary, for: .normal)
backgroundColor = OrderTokens.chipBackground
layer.cornerRadius = AppRadius.lg
case .dangerFilled:
setTitleColor(.white, for: .normal)
backgroundColor = AppColor.danger
}
snp.makeConstraints { make in
make.height.equalTo(OrderTokens.actionButtonHeight)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,131 @@
//
// OrderEditOptionView.swift
// suixinkan
//
import SnapKit
import UIKit
/// / Android `OrderEditOptionSection`
final class OrderEditOptionView: UIView {
var onSelectionChange: ((Int) -> Void)?
private let titleLabel = UILabel()
private let readOnlyLabel = UILabel()
private let yesButton = UIControl()
private let noButton = UIControl()
private let yesCircle = UIView()
private let noCircle = UIView()
private let yesLabel = UILabel()
private let noLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
titleLabel.text = "需要剪辑修图"
titleLabel.font = .app(.body)
titleLabel.textColor = OrderTokens.label563
readOnlyLabel.font = .systemFont(ofSize: 14, weight: .medium)
readOnlyLabel.textColor = OrderTokens.valueBlack
readOnlyLabel.isHidden = true
[yesLabel, noLabel].forEach {
$0.text = $0 === yesLabel ? "" : ""
$0.font = .app(.body)
$0.textColor = AppColor.textPrimary
}
[yesCircle, noCircle].forEach {
$0.layer.cornerRadius = 10
$0.layer.borderWidth = 1.5
$0.layer.borderColor = AppColor.primary.cgColor
$0.snp.makeConstraints { make in
make.width.height.equalTo(20)
}
}
configureChoiceButton(yesButton, circle: yesCircle, label: yesLabel, tag: 1)
configureChoiceButton(noButton, circle: noCircle, label: noLabel, tag: 2)
let choicesStack = UIStackView(arrangedSubviews: [yesButton, noButton])
choicesStack.axis = .horizontal
choicesStack.spacing = AppSpacing.md
addSubview(titleLabel)
addSubview(readOnlyLabel)
addSubview(choicesStack)
titleLabel.snp.makeConstraints { make in
make.leading.centerY.equalToSuperview()
}
readOnlyLabel.snp.makeConstraints { make in
make.trailing.centerY.equalToSuperview()
}
choicesStack.snp.makeConstraints { make in
make.trailing.centerY.equalToSuperview()
}
snp.makeConstraints { make in
make.height.greaterThanOrEqualTo(28)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(isRefined: Int, readOnly: Bool) {
readOnlyLabel.isHidden = !readOnly
yesButton.isHidden = readOnly
noButton.isHidden = readOnly
if readOnly {
readOnlyLabel.text = switch isRefined {
case 1: ""
case 2: ""
default: "--"
}
} else {
updateCircle(yesCircle, selected: isRefined == 1)
updateCircle(noCircle, selected: isRefined == 2)
}
}
private func configureChoiceButton(_ control: UIControl, circle: UIView, label: UILabel, tag: Int) {
control.tag = tag
control.addTarget(self, action: #selector(choiceTapped(_:)), for: .touchUpInside)
let row = UIStackView(arrangedSubviews: [circle, label])
row.axis = .horizontal
row.spacing = AppSpacing.xxs
row.isUserInteractionEnabled = false
control.addSubview(row)
row.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
private func updateCircle(_ circle: UIView, selected: Bool) {
if selected {
circle.backgroundColor = AppColor.primary
circle.layer.borderWidth = 0
let check = UIImageView(image: UIImage(systemName: "checkmark"))
check.tintColor = .white
check.tag = 999
circle.subviews.forEach { $0.removeFromSuperview() }
circle.addSubview(check)
check.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(12)
}
} else {
circle.backgroundColor = .clear
circle.layer.borderWidth = 1.5
circle.layer.borderColor = AppColor.primary.cgColor
circle.subviews.forEach { $0.removeFromSuperview() }
}
}
@objc private func choiceTapped(_ sender: UIControl) {
onSelectionChange?(sender.tag)
}
}

View File

@ -0,0 +1,66 @@
//
// OrderFilterPillControl.swift
// suixinkan
//
import SnapKit
import UIKit
/// pill Android /
final class OrderFilterPillControl: UIButton {
private let pillTitleLabel = UILabel()
private let trailingIconView = UIImageView()
var pillTitle: String = "" {
didSet { pillTitleLabel.text = pillTitle }
}
var showsTrailingIcon: Bool = false {
didSet { trailingIconView.isHidden = !showsTrailingIcon }
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = OrderTokens.filterBackground
layer.cornerRadius = OrderTokens.pillRadius
clipsToBounds = true
pillTitleLabel.font = .app(.body)
pillTitleLabel.textColor = OrderTokens.label563
pillTitleLabel.lineBreakMode = .byTruncatingTail
pillTitleLabel.isUserInteractionEnabled = false
trailingIconView.contentMode = .scaleAspectFit
trailingIconView.tintColor = OrderTokens.label563
trailingIconView.isHidden = true
trailingIconView.isUserInteractionEnabled = false
addSubview(pillTitleLabel)
addSubview(trailingIconView)
pillTitleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().inset(AppSpacing.sm)
make.centerY.equalToSuperview()
make.trailing.lessThanOrEqualTo(trailingIconView.snp.leading).offset(-AppSpacing.xxs)
}
trailingIconView.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(AppSpacing.sm)
make.centerY.equalToSuperview()
make.width.height.equalTo(15)
}
snp.makeConstraints { make in
make.height.equalTo(OrderTokens.filterPillHeight)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setTrailingSystemImage(_ name: String) {
trailingIconView.image = UIImage(systemName: name)
showsTrailingIcon = true
}
}

View File

@ -0,0 +1,61 @@
//
// OrderGiftStepperView.swift
// suixinkan
//
import SnapKit
import UIKit
/// /+/- Android
final class OrderGiftStepperView: UIView {
var onMinusTap: (() -> Void)?
var onPlusTap: (() -> Void)?
private let minusButton = UIButton(type: .system)
private let plusButton = UIButton(type: .system)
private let valueLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
[minusButton, plusButton].forEach { button in
button.layer.cornerRadius = 12
button.layer.borderWidth = 1
button.layer.borderColor = OrderTokens.giftBorder.cgColor
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
button.setTitleColor(OrderTokens.giftBorder, for: .normal)
button.snp.makeConstraints { make in
make.width.height.equalTo(24)
}
}
minusButton.setTitle("-", for: .normal)
plusButton.setTitle("+", for: .normal)
minusButton.addTarget(self, action: #selector(minusTapped), for: .touchUpInside)
plusButton.addTarget(self, action: #selector(plusTapped), for: .touchUpInside)
valueLabel.font = .systemFont(ofSize: 14, weight: .medium)
valueLabel.textColor = OrderTokens.valueBlack
valueLabel.textAlignment = .center
let stack = UIStackView(arrangedSubviews: [minusButton, valueLabel, plusButton])
stack.axis = .horizontal
stack.spacing = AppSpacing.xs
stack.alignment = .center
addSubview(stack)
stack.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(text: String) {
valueLabel.text = text
}
@objc private func minusTapped() { onMinusTap?() }
@objc private func plusTapped() { onPlusTap?() }
}

View File

@ -0,0 +1,109 @@
//
// OrderHistoricalPhotosView.swift
// suixinkan
//
import SnapKit
import UIKit
/// Android multi_travel
final class OrderHistoricalPhotosView: UIView {
var onTap: (() -> Void)?
private let titleLabel = UILabel()
private let divider = UIView()
private let photosStack = UIStackView()
private var imageViews: [UIImageView] = []
private var overlayLabel: UILabel?
override init(frame: CGRect) {
super.init(frame: frame)
divider.backgroundColor = OrderTokens.remarkDivider
titleLabel.text = "历史拍摄信息:"
titleLabel.font = .app(.body)
titleLabel.textColor = OrderTokens.label563
photosStack.axis = .horizontal
photosStack.spacing = AppSpacing.xs
photosStack.distribution = .fillEqually
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tap)
addSubview(divider)
addSubview(titleLabel)
addSubview(photosStack)
divider.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(1)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(divider.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview()
}
photosStack.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.xs)
make.leading.trailing.bottom.equalToSuperview()
make.height.equalTo(72)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(materials: [MultiTravelMaterialEntity]) {
isHidden = materials.isEmpty
photosStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
imageViews.removeAll()
overlayLabel?.removeFromSuperview()
overlayLabel = nil
let displayItems = Array(materials.prefix(3))
for (index, material) in displayItems.enumerated() {
let container = UIView()
container.backgroundColor = OrderTokens.filterBackground
container.layer.cornerRadius = AppRadius.sm
container.clipsToBounds = true
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
let thumbURL = material.coverUrl.isEmpty ? material.fileUrl : material.coverUrl
imageView.loadRemoteImage(urlString: thumbURL, placeholderColor: OrderTokens.filterBackground)
container.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
imageViews.append(imageView)
photosStack.addArrangedSubview(container)
if materials.count > 3, index == 2 {
let overlay = UIView()
overlay.backgroundColor = UIColor.black.withAlphaComponent(0.5)
let label = UILabel()
label.text = "查看更多"
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = .white
label.textAlignment = .center
overlay.addSubview(label)
label.snp.makeConstraints { make in
make.center.equalToSuperview()
}
container.addSubview(overlay)
overlay.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
overlayLabel = label
}
}
}
@objc private func handleTap() {
onTap?()
}
}

View File

@ -0,0 +1,46 @@
//
// OrderInfoRowView.swift
// suixinkan
//
import SnapKit
import UIKit
/// key-value Android InfoRow
final class OrderInfoRowView: UIView {
private let titleLabel = UILabel()
private let valueLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
titleLabel.font = .app(.body)
titleLabel.textColor = OrderTokens.label563
valueLabel.font = .systemFont(ofSize: 14, weight: .medium)
valueLabel.textColor = OrderTokens.valueBlack
valueLabel.textAlignment = .right
valueLabel.numberOfLines = 0
addSubview(titleLabel)
addSubview(valueLabel)
titleLabel.snp.makeConstraints { make in
make.leading.top.bottom.equalToSuperview()
make.width.lessThanOrEqualToSuperview().multipliedBy(0.45)
}
valueLabel.snp.makeConstraints { make in
make.trailing.top.bottom.equalToSuperview()
make.leading.greaterThanOrEqualTo(titleLabel.snp.trailing).offset(AppSpacing.xs)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(title: String, value: String, valueColor: UIColor = OrderTokens.valueBlack, usesPrimaryValue: Bool = false) {
titleLabel.text = "\(title)"
valueLabel.text = value
valueLabel.textColor = usesPrimaryValue ? AppColor.primary : valueColor
}
}

View File

@ -0,0 +1,101 @@
//
// OrderPhoneRowView.swift
// suixinkan
//
import SnapKit
import UIKit
/// / Android
final class OrderPhoneRowView: UIView {
enum Style {
case photographer
case deposit
}
var onTap: (() -> Void)?
private let style: Style
private let titleLabel = UILabel()
private let phoneLabel = UILabel()
private let phoneButton = UIButton(type: .system)
private let iconButton = UIButton(type: .system)
init(style: Style) {
self.style = style
super.init(frame: .zero)
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(phone: String, hidden: Bool = false) {
let masked = OrderPhoneMask.mask(phone)
phoneLabel.text = masked
isHidden = hidden || phone.isEmpty
}
private func setupUI() {
titleLabel.font = .app(.body)
titleLabel.textColor = OrderTokens.label563
titleLabel.text = "手机号:"
phoneLabel.font = .systemFont(ofSize: 14, weight: .medium)
phoneLabel.textAlignment = .right
phoneButton.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
iconButton.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
addSubview(titleLabel)
addSubview(phoneLabel)
addSubview(phoneButton)
addSubview(iconButton)
titleLabel.snp.makeConstraints { make in
make.leading.top.bottom.equalToSuperview()
}
switch style {
case .photographer:
phoneLabel.textColor = OrderTokens.valueBlack
iconButton.backgroundColor = OrderTokens.phoneIconBackground
iconButton.layer.cornerRadius = 12
iconButton.tintColor = AppColor.primary
iconButton.setImage(UIImage(systemName: "phone.fill"), for: .normal)
iconButton.snp.makeConstraints { make in
make.trailing.centerY.equalToSuperview()
make.width.height.equalTo(24)
}
phoneLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalTo(iconButton.snp.leading).offset(-7)
}
case .deposit:
phoneLabel.textColor = AppColor.primary
iconButton.tintColor = AppColor.primary
iconButton.setImage(UIImage(systemName: "phone.fill"), for: .normal)
iconButton.snp.makeConstraints { make in
make.trailing.centerY.equalToSuperview()
make.width.height.equalTo(16)
}
phoneLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalTo(iconButton.snp.leading).offset(-AppSpacing.xxs)
}
}
phoneButton.snp.makeConstraints { make in
make.leading.equalTo(phoneLabel)
make.trailing.equalTo(iconButton)
make.top.bottom.equalToSuperview()
}
}
@objc private func handleTap() {
onTap?()
}
}

View File

@ -0,0 +1,37 @@
//
// OrderTypeChipView.swift
// suixinkan
//
import SnapKit
import UIKit
/// chip
final class OrderTypeChipView: UIView {
private let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = OrderTokens.chipBackground
layer.cornerRadius = OrderTokens.pillRadius
clipsToBounds = true
label.font = .app(.caption)
label.textColor = AppColor.primary
addSubview(label)
label.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: AppSpacing.xxs, bottom: 0, right: AppSpacing.xxs))
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(text: String) {
label.text = text
isHidden = text.isEmpty
}
}

View File

@ -0,0 +1,64 @@
//
// OrderListUITokensTests.swift
// suixinkanTests
//
import UIKit
import XCTest
@testable import suixinkan
/// UI Token badge
final class OrderListUITokensTests: XCTestCase {
func testOrderTokensMatchAndroid() {
XCTAssertEqual(OrderTokens.filterBackground.hexRGB, 0xF4F4F4)
XCTAssertEqual(OrderTokens.label563.hexRGB, 0x4B5563)
XCTAssertEqual(OrderTokens.timestamp.hexRGB, 0x6B7280)
XCTAssertEqual(OrderTokens.depositSuccess.hexRGB, 0x27AE60)
XCTAssertEqual(OrderTokens.depositRefunded.hexRGB, 0x6B7280)
XCTAssertEqual(OrderTokens.searchHeight, 36)
XCTAssertEqual(OrderTokens.filterPillHeight, 35)
XCTAssertEqual(OrderTokens.actionButtonHeight, 48)
XCTAssertEqual(OrderTokens.cardRadius, 12)
}
func testPhotographerOrderStatusBadgeMapping() {
let badge = AppStatusBadge()
badge.applyOrderStatus(18, text: "已付定金")
badge.applyOrderStatus(30, text: "已完成")
badge.applyOrderStatus(50, text: "已退款")
XCTAssertFalse(badge.isHidden)
}
func testDepositOrderStatus50UsesNeutralGray() {
let badge = AppStatusBadge()
badge.applyDepositOrderStatus(50, text: "已退款")
XCTAssertEqual(badge.backgroundColor?.hexRGB, OrderTokens.depositRefundedBackground.hexRGB)
}
func testDepositOrderStatus30UsesDepositGreen() {
let badge = AppStatusBadge()
badge.applyDepositOrderStatus(30, text: "已完成")
XCTAssertEqual(badge.backgroundColor?.hexRGB, OrderTokens.depositSuccessBackground.hexRGB)
}
func testPhotographerStatus50UsesDangerNotGray() {
let badge = AppStatusBadge()
badge.applyOrderStatus(50, text: "已退款")
XCTAssertEqual(badge.backgroundColor?.hexRGB, AppColor.dangerBackground.hexRGB)
}
}
private extension UIColor {
var hexRGB: UInt {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
getRed(&red, green: &green, blue: &blue, alpha: &alpha)
let r = Int(round(red * 255))
let g = Int(round(green * 255))
let b = Int(round(blue * 255))
return UInt(r << 16 | g << 8 | b)
}
}