Files
suixinkan_uikit/suixinkan/UI/Orders/OrderCardCell.swift
汉秋 fe84a7e8f4 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>
2026-07-07 09:12:06 +08:00

348 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// OrderCardCell.swift
// suixinkan
//
import SnapKit
import UIKit
struct OrderCardActions {
var onRefund: (() -> Void)?
var onVerify: (() -> Void)?
var onUploadTask: (() -> Void)?
var onCancel: (() -> Void)?
var onCallPhone: (() -> Void)?
var onGiftPhotoMinus: (() -> Void)?
var onGiftPhotoPlus: (() -> Void)?
var onGiftVideoMinus: (() -> Void)?
var onGiftVideoPlus: (() -> Void)?
var onSendGift: (() -> Void)?
var onRefinedChanged: ((Int) -> Void)?
var onHistoricalPhoto: (() -> Void)?
var onOrderSourceTap: (() -> Void)?
}
/// 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 = OrderTypeChipView()
private let orderSourceContainer = UIView()
private let orderSourceButton = 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?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.backgroundColor = .clear
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
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.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
updateRefundWidth(showVerify: showVerify && showRefund)
uploadTaskButton.isHidden = hideWriteActions || order.orderType != 19
orderSourceContainer.isHidden = !showOrderSource
orderSourceButton.setTitle(sourceText ?? "选择订单来源", for: .normal)
let showEdit = (order.orderStatus == 18 || order.orderStatus == 30) && order.orderType != 19
editOptionView.isHidden = !showEdit
if showEdit {
editOptionView.apply(isRefined: order.isRefined, readOnly: hideWriteActions)
}
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 {
remarkSection.isHidden = false
remarkBodyLabel.text = remark
} else {
remarkSection.isHidden = true
}
let materials = order.multiTravel?.materialList ?? []
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 = OrderTokens.cardRadius
orderNumberLabel.font = .app(.body)
orderNumberLabel.textColor = OrderTokens.orderNumber
createdAtLabel.font = .app(.caption)
createdAtLabel.textColor = OrderTokens.timestamp
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)
orderSourceContainer.addSubview(orderSourceButton)
orderSourceButton.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(AppSpacing.sm)
}
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(contentStack)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(
UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: OrderTokens.cardGap, right: AppSpacing.md)
)
}
contentStack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(OrderTokens.cardPadding)
}
}
private func configureTravelRow(
_ row: UIStackView,
titleLabel: UILabel,
valueLabel: UILabel,
title: String,
stepper: OrderGiftStepperView
) {
row.axis = .horizontal
row.alignment = .center
row.distribution = .equalSpacing
titleLabel.text = title
titleLabel.font = .app(.body)
titleLabel.textColor = OrderTokens.label563
valueLabel.font = .systemFont(ofSize: 14, weight: .medium)
valueLabel.textColor = OrderTokens.valueBlack
row.addArrangedSubview(titleLabel)
row.addArrangedSubview(stepper)
row.addArrangedSubview(valueLabel)
valueLabel.isHidden = true
}
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 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 orderSourceTapped() { actions.onOrderSourceTap?() }
}
enum OrderPhoneMask {
static func mask(_ phone: String) -> String {
let trimmed = phone.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.count >= 7 else { return trimmed }
let start = trimmed.prefix(3)
let end = trimmed.suffix(4)
return "\(start)****\(end)"
}
}