Implement orders tab aligned with Android, including scan verify flows.
Add role-based order/deposit lists, AVFoundation QR scanning, verify dialogs, and ViewModel tests so photographers, scenic admins, and store admins match Android behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
314
suixinkan/UI/Orders/OrderCardCell.swift
Normal file
314
suixinkan/UI/Orders/OrderCardCell.swift
Normal file
@ -0,0 +1,314 @@
|
||||
//
|
||||
// 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 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 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 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.text = order.orderTypeLabel
|
||||
typeChip.isHidden = order.orderTypeLabel.isEmpty
|
||||
|
||||
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
|
||||
)
|
||||
refundButton.isHidden = !showRefund
|
||||
verifyButton.isHidden = !showVerify
|
||||
actionStack.isHidden = !showRefund && !showVerify
|
||||
uploadTaskButton.isHidden = hideWriteActions || order.orderType != 19
|
||||
|
||||
orderSourceButton.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)
|
||||
}
|
||||
if let travel = order.photoTravel {
|
||||
addInfoRow(title: "修图张数", value: "\(travel.orderPhotoNum)")
|
||||
addInfoRow(title: "视频", value: "\(travel.orderVideoNum)")
|
||||
}
|
||||
if let remark = order.remark, !remark.isEmpty {
|
||||
addInfoRow(title: "备注", value: remark)
|
||||
}
|
||||
|
||||
let materials = order.multiTravel?.materialList ?? []
|
||||
historicalButton.isHidden = materials.isEmpty
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
cardView.backgroundColor = .white
|
||||
cardView.layer.cornerRadius = 12
|
||||
|
||||
orderNumberLabel.font = .systemFont(ofSize: 14)
|
||||
orderNumberLabel.textColor = .black
|
||||
createdAtLabel.font = .systemFont(ofSize: 12)
|
||||
createdAtLabel.textColor = UIColor(hex: 0x6B7280)
|
||||
typeChip.font = .systemFont(ofSize: 12)
|
||||
typeChip.textColor = AppColor.primary
|
||||
typeChip.backgroundColor = UIColor(hex: 0xEFF6FF)
|
||||
typeChip.layer.cornerRadius = 4
|
||||
typeChip.clipsToBounds = true
|
||||
typeChip.textAlignment = .center
|
||||
|
||||
configureActionButton(refundButton, title: "退款", color: UIColor(hex: 0xEF4444), bg: UIColor(hex: 0xFFE7E7))
|
||||
configureActionButton(verifyButton, title: "核销订单", color: .white, bg: UIColor(hex: 0x0073FF))
|
||||
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 = .systemFont(ofSize: 16)
|
||||
uploadTaskButton.backgroundColor = UIColor(hex: 0xEFF6FF)
|
||||
uploadTaskButton.layer.cornerRadius = 12
|
||||
uploadTaskButton.addTarget(self, action: #selector(uploadTaskTapped), for: .touchUpInside)
|
||||
|
||||
actionStack.axis = .horizontal
|
||||
actionStack.spacing = 12
|
||||
actionStack.distribution = .fillEqually
|
||||
actionStack.addArrangedSubview(refundButton)
|
||||
actionStack.addArrangedSubview(verifyButton)
|
||||
|
||||
infoStack.axis = .vertical
|
||||
infoStack.spacing = 6
|
||||
|
||||
cancelButton.setTitle("取消订单", for: .normal)
|
||||
cancelButton.setTitleColor(UIColor(hex: 0xEF4444), for: .normal)
|
||||
cancelButton.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
||||
|
||||
orderSourceButton.setTitleColor(AppColor.primary, for: .normal)
|
||||
orderSourceButton.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
orderSourceButton.contentHorizontalAlignment = .leading
|
||||
orderSourceButton.addTarget(self, action: #selector(orderSourceTapped), for: .touchUpInside)
|
||||
|
||||
historicalButton.setTitle("历史拍摄", for: .normal)
|
||||
historicalButton.setTitleColor(AppColor.primary, for: .normal)
|
||||
historicalButton.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
historicalButton.addTarget(self, action: #selector(historicalTapped), for: .touchUpInside)
|
||||
|
||||
refinedStack.axis = .horizontal
|
||||
refinedStack.spacing = 8
|
||||
refinedNeedButton.setTitle("需要精修", for: .normal)
|
||||
refinedNoButton.setTitle("无需精修", for: .normal)
|
||||
[refinedNeedButton, refinedNoButton].forEach {
|
||||
$0.titleLabel?.font = .systemFont(ofSize: 13)
|
||||
$0.layer.cornerRadius = 6
|
||||
$0.layer.borderWidth = 1
|
||||
}
|
||||
refinedNeedButton.addTarget(self, action: #selector(refinedNeedTapped), for: .touchUpInside)
|
||||
refinedNoButton.addTarget(self, action: #selector(refinedNoTapped), for: .touchUpInside)
|
||||
refinedStack.addArrangedSubview(refinedNeedButton)
|
||||
refinedStack.addArrangedSubview(refinedNoButton)
|
||||
|
||||
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.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 16, bottom: 12, right: 16))
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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: 16)
|
||||
button.backgroundColor = bg
|
||||
button.layer.cornerRadius = 8
|
||||
}
|
||||
|
||||
private func addInfoRow(title: String, value: String) {
|
||||
let row = UIStackView()
|
||||
row.axis = .horizontal
|
||||
row.distribution = .fillEqually
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = title
|
||||
titleLabel.font = .systemFont(ofSize: 13)
|
||||
titleLabel.textColor = UIColor(hex: 0x6B7280)
|
||||
let valueLabel = UILabel()
|
||||
valueLabel.text = value
|
||||
valueLabel.font = .systemFont(ofSize: 13)
|
||||
valueLabel.textColor = AppColor.text333
|
||||
valueLabel.textAlignment = .right
|
||||
row.addArrangedSubview(titleLabel)
|
||||
row.addArrangedSubview(valueLabel)
|
||||
infoStack.addArrangedSubview(row)
|
||||
}
|
||||
|
||||
private func addPhoneRow(phone: String, hidden: Bool) {
|
||||
let row = UIStackView()
|
||||
row.axis = .horizontal
|
||||
row.distribution = .fillEqually
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = "手机号"
|
||||
titleLabel.font = .systemFont(ofSize: 13)
|
||||
titleLabel.textColor = UIColor(hex: 0x6B7280)
|
||||
let valueButton = UIButton(type: .system)
|
||||
valueButton.setTitle(OrderPhoneMask.mask(phone), for: .normal)
|
||||
valueButton.setTitleColor(AppColor.primary, for: .normal)
|
||||
valueButton.titleLabel?.font = .systemFont(ofSize: 13)
|
||||
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 updateRefinedButtons(isRefined: Int, readOnly: Bool) {
|
||||
let selectedBorder = AppColor.primary.cgColor
|
||||
let normalBorder = UIColor(hex: 0xE5E7EB).cgColor
|
||||
refinedNeedButton.layer.borderColor = isRefined == 1 ? selectedBorder : normalBorder
|
||||
refinedNoButton.layer.borderColor = isRefined == 2 ? selectedBorder : normalBorder
|
||||
refinedNeedButton.isEnabled = !readOnly
|
||||
refinedNoButton.isEnabled = !readOnly
|
||||
}
|
||||
|
||||
@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 {
|
||||
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)"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user