重建订单列表 UI 并严格对齐 Android 订单卡片

This commit is contained in:
2026-07-07 09:12:06 +08:00
parent 28dc004110
commit 3bc017260f
17 changed files with 1235 additions and 379 deletions

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 {