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>
342 lines
14 KiB
Swift
342 lines
14 KiB
Swift
//
|
||
// 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)"
|
||
}
|
||
}
|