Files
suixinkan_uikit/suixinkan/UI/Orders/OrderCardCell.swift
汉秋 2bea05b1d9 Add scenic selection and cooperation order flows aligned with Android.
Upgrade scenic picker with search, location sorting, permission apply/status, and blocking home dialog; add cooperation order module and order source picker improvements with unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 10:48:43 +08:00

342 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)?
var onOrderSourceImageTap: (([String], Int) -> 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 orderSourceEntryView = OrderSourceEntryView()
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,
sourceSelection: OrderSourceSelection?
) {
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
orderSourceEntryView.isHidden = !showOrderSource
if showOrderSource {
orderSourceEntryView.apply(selection: sourceSelection)
}
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)
orderSourceEntryView.addTarget(self, action: #selector(orderSourceTapped), for: .touchUpInside)
orderSourceEntryView.onImageTap = { [weak self] images, index in
self?.actions.onOrderSourceImageTap?(images, index)
}
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,
orderSourceEntryView,
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)"
}
}